Compare commits
806 Commits
1.3.1
...
fabianthde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2939919ddb | ||
|
|
38a1c7eef6 | ||
|
|
f6252c3a8b | ||
|
|
653d80b88e | ||
|
|
89609ad35c | ||
|
|
2211013e57 | ||
|
|
f206ee1406 | ||
|
|
00dc9b36af | ||
|
|
24146cef90 | ||
|
|
c46a50ec58 | ||
|
|
de7e909c01 | ||
|
|
fbc754d8b7 | ||
|
|
767d878051 | ||
|
|
132b140af2 | ||
|
|
df7d8871ff | ||
|
|
ca2398e4c7 | ||
|
|
b8f02d2152 | ||
|
|
e85876cd24 | ||
|
|
3f06a53058 | ||
|
|
4ee053a4f9 | ||
|
|
e5369524ce | ||
|
|
77465cebd0 | ||
|
|
f90bf3bfcf | ||
|
|
0000610b9d | ||
|
|
c7e095583d | ||
|
|
a725f3e9cc | ||
|
|
b5dea18073 | ||
|
|
b9b309e603 | ||
|
|
15f1be0aa8 | ||
|
|
ffd80ce0b4 | ||
|
|
350891ee2a | ||
|
|
5dec1cd561 | ||
|
|
c4d235d742 | ||
|
|
cdc6675dd5 | ||
|
|
85635bb26e | ||
|
|
3be0a4a89c | ||
|
|
47e47fb3cf | ||
|
|
48903034b6 | ||
|
|
6952218ee7 | ||
|
|
80146c1e03 | ||
|
|
642ae996c9 | ||
|
|
8040636aa5 | ||
|
|
731fcfaca7 | ||
|
|
708fb3fccd | ||
|
|
9f429fb068 | ||
|
|
29fc693f4d | ||
|
|
6f373ad305 | ||
|
|
c069d779d9 | ||
|
|
cd88970a22 | ||
|
|
6b6708e43c | ||
|
|
9206eeb9e3 | ||
|
|
080bbb3c51 | ||
|
|
ea2c862900 | ||
|
|
4fe72ea113 | ||
|
|
c486a62b50 | ||
|
|
3ce4451da4 | ||
|
|
294ba12391 | ||
|
|
4a3343fe61 | ||
|
|
d1e6ddd435 | ||
|
|
3e0379dc70 | ||
|
|
d99674f8bd | ||
|
|
ca7acc17da | ||
|
|
16a8bce102 | ||
|
|
ed2270ff46 | ||
|
|
45b6c3b338 | ||
|
|
84e2284f56 | ||
|
|
1c0d0be622 | ||
|
|
a9ce0f487d | ||
|
|
07533e0365 | ||
|
|
ee5ddd4264 | ||
|
|
f519d22d81 | ||
|
|
51ed87086a | ||
|
|
1ca3aa3cdb | ||
|
|
0178c63f6a | ||
|
|
8a97c409fa | ||
|
|
3dd0735305 | ||
|
|
536f775baa | ||
|
|
00f7a684a3 | ||
|
|
d79b166a6a | ||
|
|
b3d827f56a | ||
|
|
40bcef1dcb | ||
|
|
6146f1bdaa | ||
|
|
f5d82d9ef0 | ||
|
|
b2a29ae606 | ||
|
|
98ccba53a2 | ||
|
|
9bfda36647 | ||
|
|
5710cdf19c | ||
|
|
20cf54bfcd | ||
|
|
2ce639e750 | ||
|
|
b1ed413c4f | ||
|
|
b8c3060037 | ||
|
|
c3ea4940d7 | ||
|
|
40e1225b87 | ||
|
|
0c171122b2 | ||
|
|
6d0f4bb3da | ||
|
|
5e2cc6e20c | ||
|
|
99cb43bbea | ||
|
|
ca7d8277f7 | ||
|
|
337d26333e | ||
|
|
ebb64d255b | ||
|
|
7dcb199f68 | ||
|
|
4334e887de | ||
|
|
4e84dc4cc8 | ||
|
|
1a1ed072bf | ||
|
|
ae457f07c4 | ||
|
|
00095942c3 | ||
|
|
d1caa5fc21 | ||
|
|
813e2f97ac | ||
|
|
bcb5a90f5e | ||
|
|
020a1a3149 | ||
|
|
c4d649ec58 | ||
|
|
c02cf2c284 | ||
|
|
c30afd042e | ||
|
|
17640fe6cf | ||
|
|
2e4f6ee420 | ||
|
|
a3768d9221 | ||
|
|
80c3390363 | ||
|
|
a5e3869d8f | ||
|
|
aa7d7c2d02 | ||
|
|
015f205569 | ||
|
|
e59fb15926 | ||
|
|
173c585f2d | ||
|
|
6f8c27793e | ||
|
|
332b81c803 | ||
|
|
4b343b500d | ||
|
|
e87c537642 | ||
|
|
2e6300cce2 | ||
|
|
09514d15a6 | ||
|
|
0de23dcba0 | ||
|
|
bacb153151 | ||
|
|
a01aa299d8 | ||
|
|
44edbddbd8 | ||
|
|
79d677cf3c | ||
|
|
be39b6512f | ||
|
|
fcfeea35da | ||
|
|
7d0eb8c61e | ||
|
|
4d8438a6b6 | ||
|
|
f611244e35 | ||
|
|
546a978d3b | ||
|
|
70b23fb073 | ||
|
|
a56ca597d6 | ||
|
|
679e0228a8 | ||
|
|
e153394323 | ||
|
|
5bd1fcfcfd | ||
|
|
2a392ddc44 | ||
|
|
b5cb8bc0d9 | ||
|
|
fa170bcf98 | ||
|
|
7939d46949 | ||
|
|
ab9df8201a | ||
|
|
4a670ec091 | ||
|
|
10e57e59c4 | ||
|
|
b9ec43ef34 | ||
|
|
42197cd375 | ||
|
|
704852973b | ||
|
|
056b4200df | ||
|
|
250a7d8627 | ||
|
|
1ba51e161e | ||
|
|
32e58af896 | ||
|
|
312fa6fe76 | ||
|
|
afbe0837ba | ||
|
|
36ad2a720f | ||
|
|
901e3b14bb | ||
|
|
588d209f7b | ||
|
|
554c54e6be | ||
|
|
b0fac34ffc | ||
|
|
5ede9f7c6b | ||
|
|
c7254fd23e | ||
|
|
55fcea04af | ||
|
|
c212c0a6b2 | ||
|
|
a31fd6709a | ||
|
|
e367fd2b73 | ||
|
|
1ca67d0241 | ||
|
|
8ffa952ff9 | ||
|
|
da246fa30b | ||
|
|
13f306742e | ||
|
|
f3815dc45e | ||
|
|
d086254012 | ||
|
|
bc4d5ba097 | ||
|
|
c556783fe3 | ||
|
|
5fba4c12aa | ||
|
|
7e0dde3ece | ||
|
|
fc03e83531 | ||
|
|
4c441077c7 | ||
|
|
4a5ca81e9a | ||
|
|
75eebe8f8c | ||
|
|
271a8cdac5 | ||
|
|
25103c1188 | ||
|
|
d81058e606 | ||
|
|
693df54b3b | ||
|
|
ae6ed99dc4 | ||
|
|
14bd58e741 | ||
|
|
6d35a7a4ba | ||
|
|
46b0d1ceac | ||
|
|
67a66d2fcd | ||
|
|
43e90b57ea | ||
|
|
c80740e590 | ||
|
|
54ccb9611e | ||
|
|
8fcb897800 | ||
|
|
699eda5d1b | ||
|
|
d7d0a83550 | ||
|
|
e3c331c911 | ||
|
|
eda4dd6aec | ||
|
|
8ad7be474d | ||
|
|
a64435f155 | ||
|
|
fa160124d2 | ||
|
|
5765cb8330 | ||
|
|
f472b227bb | ||
|
|
d2b419c42e | ||
|
|
09d4de660f | ||
|
|
728dcd8523 | ||
|
|
93cf9bf6a9 | ||
|
|
50841f5e24 | ||
|
|
fc6d92d1fc | ||
|
|
7162a029bb | ||
|
|
d797ddd668 | ||
|
|
989e8c3aa6 | ||
|
|
08b79af242 | ||
|
|
0d2f346a30 | ||
|
|
39f1d5f5fd | ||
|
|
05008bb7f8 | ||
|
|
be90d6fc45 | ||
|
|
a1bcdf9924 | ||
|
|
b0e001393c | ||
|
|
2d08941f6a | ||
|
|
d0fef1f312 | ||
|
|
68342cb0d4 | ||
|
|
2b419212a7 | ||
|
|
b2cbc7e34d | ||
|
|
61247e575b | ||
|
|
31e18266d1 | ||
|
|
df8a8de889 | ||
|
|
8a037d6b29 | ||
|
|
47b555b98c | ||
|
|
0c2dae475e | ||
|
|
dc676d04d8 | ||
|
|
15b54bff50 | ||
|
|
47bd4b4c0b | ||
|
|
3c8b36ddfe | ||
|
|
608df3fddd | ||
|
|
c092c285ee | ||
|
|
93b745e379 | ||
|
|
c18db77ade | ||
|
|
2c0b167e6b | ||
|
|
313254d0c8 | ||
|
|
6f519c97d3 | ||
|
|
17a3e16b1d | ||
|
|
8199358088 | ||
|
|
412928eeaa | ||
|
|
51e1b935bd | ||
|
|
742b51e5e2 | ||
|
|
fdb5e2eebb | ||
|
|
0192f64cd2 | ||
|
|
193298ac87 | ||
|
|
a81cb81799 | ||
|
|
ad8a7fdc9b | ||
|
|
5440afcebe | ||
|
|
715d7e664c | ||
|
|
aa182cfa68 | ||
|
|
f92dd7a872 | ||
|
|
b02b9197d0 | ||
|
|
86d02be70c | ||
|
|
cb990978ee | ||
|
|
a103202c92 | ||
|
|
9d7b133037 | ||
|
|
f727f2a1a9 | ||
|
|
03034768d9 | ||
|
|
aed3e20e08 | ||
|
|
74bac6d986 | ||
|
|
7ebecc353a | ||
|
|
f0302b0d1e | ||
|
|
0b004ad089 | ||
|
|
c9001f068b | ||
|
|
96e0554aae | ||
|
|
31b4aadaba | ||
|
|
f46fa5392a | ||
|
|
3b6a17f193 | ||
|
|
aea77d3b8c | ||
|
|
7cfbe077db | ||
|
|
7bb620f941 | ||
|
|
5b0341a733 | ||
|
|
d99225da1f | ||
|
|
f279180a37 | ||
|
|
9a22018477 | ||
|
|
197119e56d | ||
|
|
dd055ddc5d | ||
|
|
94c3277245 | ||
|
|
fdaf402472 | ||
|
|
bce7764f75 | ||
|
|
70a258aae2 | ||
|
|
77cf00e8e4 | ||
|
|
de05579d1f | ||
|
|
c7d4b722d0 | ||
|
|
02b837c54b | ||
|
|
50e0e88cc2 | ||
|
|
c34245ff21 | ||
|
|
f1a8334f59 | ||
|
|
c8fc4ea500 | ||
|
|
39805bc103 | ||
|
|
2c615682df | ||
|
|
1257e4efac | ||
|
|
5e4a21087e | ||
|
|
2aaef99a54 | ||
|
|
161d3a795d | ||
|
|
9b671cb1a9 | ||
|
|
07d9a9f2c3 | ||
|
|
efabe7f536 | ||
|
|
82aead976e | ||
|
|
17be52c7b6 | ||
|
|
d0a196ec40 | ||
|
|
d484de185d | ||
|
|
96e4e7a4e8 | ||
|
|
4adb34b959 | ||
|
|
819bc12a68 | ||
|
|
eb23e5365f | ||
|
|
84e2faf8a8 | ||
|
|
84f58efc17 | ||
|
|
42254ee4a1 | ||
|
|
7c564aed7a | ||
|
|
8cdcb29274 | ||
|
|
403a369df9 | ||
|
|
5527912cd1 | ||
|
|
c8531dfe37 | ||
|
|
ed8bb2e5a1 | ||
|
|
dd66355488 | ||
|
|
fc3f83231c | ||
|
|
e70c712020 | ||
|
|
1b34aeaec4 | ||
|
|
2566bfa2ed | ||
|
|
ba06f2bbc6 | ||
|
|
1e4fe1680f | ||
|
|
2effb199a1 | ||
|
|
23c139320a | ||
|
|
f65eba606e | ||
|
|
7a2825da9a | ||
|
|
2975eddfe9 | ||
|
|
7f28eae954 | ||
|
|
789be5e942 | ||
|
|
85b114cdfd | ||
|
|
53d063e994 | ||
|
|
0ab081ccbc | ||
|
|
00ce9d64dc | ||
|
|
be20c024aa | ||
|
|
992fb9839a | ||
|
|
96ae2ee7ac | ||
|
|
379cecb08f | ||
|
|
9089b271b3 | ||
|
|
c9d522fad5 | ||
|
|
be80aa1512 | ||
|
|
c1d64a8027 | ||
|
|
1bc2aa9d38 | ||
|
|
e167ee104b | ||
|
|
43b85da314 | ||
|
|
b6c21c9766 | ||
|
|
874da8c8d6 | ||
|
|
989580d196 | ||
|
|
03ef54c37b | ||
|
|
ab56dda275 | ||
|
|
3a91f958e3 | ||
|
|
79913a0c9c | ||
|
|
d0fe64ecfa | ||
|
|
4da69685a1 | ||
|
|
bf560dd10d | ||
|
|
7ce76ee28d | ||
|
|
540e9bad29 | ||
|
|
22c2e2c4e5 | ||
|
|
f67d9dcdfa | ||
|
|
edfcadcbdc | ||
|
|
156bcc7d54 | ||
|
|
63ff912d76 | ||
|
|
6cbfaac4f4 | ||
|
|
6ad6e0d8c0 | ||
|
|
7c38bb03b9 | ||
|
|
5574172d99 | ||
|
|
bc8081ebae | ||
|
|
6ed6132c54 | ||
|
|
76c02c98d8 | ||
|
|
012a7885ff | ||
|
|
2b3d41d982 | ||
|
|
a56a48145b | ||
|
|
8dc097e23c | ||
|
|
0323520389 | ||
|
|
e1e395023d | ||
|
|
850214b103 | ||
|
|
02e9805482 | ||
|
|
0feae8402e | ||
|
|
042da53b54 | ||
|
|
aa1b2bace7 | ||
|
|
6b1b4d6015 | ||
|
|
ebeac417e5 | ||
|
|
be005616ea | ||
|
|
ec3a9b0615 | ||
|
|
0b3e651c4b | ||
|
|
426bdd3aa1 | ||
|
|
75e29d61f8 | ||
|
|
dfab283154 | ||
|
|
986c0d7edc | ||
|
|
918c44bc89 | ||
|
|
b8f680d74a | ||
|
|
76fcf6d545 | ||
|
|
c51d25c58b | ||
|
|
1efdba096c | ||
|
|
c4505b7c42 | ||
|
|
d5235bd40b | ||
|
|
cc3feb4843 | ||
|
|
353d105c04 | ||
|
|
070cb6c873 | ||
|
|
a066dda0f9 | ||
|
|
ac929c2603 | ||
|
|
9102402a18 | ||
|
|
54a0fc21d8 | ||
|
|
e2b8b7369e | ||
|
|
bcfbe515a4 | ||
|
|
46834ab5ce | ||
|
|
646000920f | ||
|
|
5ea83ccea1 | ||
|
|
03c6473685 | ||
|
|
d37890fac4 | ||
|
|
d5057ea8ea | ||
|
|
2cbebbe9b7 | ||
|
|
71b1885f74 | ||
|
|
2a8e3887ad | ||
|
|
2f92ce6bda | ||
|
|
9c58755317 | ||
|
|
9c1fe4d63b | ||
|
|
994d3c74fd | ||
|
|
a413c24b45 | ||
|
|
dc276a6393 | ||
|
|
cf6448845f | ||
|
|
b45c859861 | ||
|
|
fd81092392 | ||
|
|
26ef3073ae | ||
|
|
72a684a22f | ||
|
|
14529030be | ||
|
|
9570b797fd | ||
|
|
cdb5fb34dd | ||
|
|
ddff6a24f3 | ||
|
|
ae3c0acfc0 | ||
|
|
eef23ae49d | ||
|
|
2262f04fb3 | ||
|
|
b7a99ed508 | ||
|
|
38f68de3ea | ||
|
|
6b6f016189 | ||
|
|
82faa89912 | ||
|
|
dfd49de8d1 | ||
|
|
aa8dd80e54 | ||
|
|
751d9419ff | ||
|
|
643d7bf6fa | ||
|
|
afb20f79a9 | ||
|
|
6c2a83964b | ||
|
|
07daff261a | ||
|
|
8ddeb7f9fb | ||
|
|
1f7c089c70 | ||
|
|
f1f6852ab4 | ||
|
|
e5d66defbc | ||
|
|
29913c5b09 | ||
|
|
21d807d0c3 | ||
|
|
a6e5c32166 | ||
|
|
0b3e94b974 | ||
|
|
6db5aec672 | ||
|
|
73ff5fe9dc | ||
|
|
77694aac8e | ||
|
|
a04a27c1e3 | ||
|
|
7a547c70e3 | ||
|
|
b0abf0e7a5 | ||
|
|
48c49c6ec7 | ||
|
|
16564500e2 | ||
|
|
6f6b17b211 | ||
|
|
f1618ad9df | ||
|
|
947a14d6a2 | ||
|
|
f030ecd66f | ||
|
|
ec86fb77b0 | ||
|
|
cfa246adc5 | ||
|
|
ebb236e47c | ||
|
|
37b00d670b | ||
|
|
23516d0466 | ||
|
|
2b4f1ce1c2 | ||
|
|
c786858f17 | ||
|
|
a149cb231b | ||
|
|
3c9ef728e1 | ||
|
|
4257f58f96 | ||
|
|
ddfab31781 | ||
|
|
5e3e8f2809 | ||
|
|
fefa8b174d | ||
|
|
fb3d732a62 | ||
|
|
0658e323ae | ||
|
|
35046b33ff | ||
|
|
8f4c70c9cc | ||
|
|
c9bc14ab7f | ||
|
|
0f023905c8 | ||
|
|
61dc02514a | ||
|
|
590998fbaa | ||
|
|
7a1f631113 | ||
|
|
706229640f | ||
|
|
0397db51f7 | ||
|
|
e53928cf1e | ||
|
|
17b8fd6e6f | ||
|
|
03338b589c | ||
|
|
ac8560afd3 | ||
|
|
ef0cae6953 | ||
|
|
ed396b400d | ||
|
|
e6ef288a69 | ||
|
|
619d16ddd3 | ||
|
|
943fe79d3c | ||
|
|
310d4619b4 | ||
|
|
35e9d8752b | ||
|
|
aa057918ee | ||
|
|
5428ebf129 | ||
|
|
1384037430 | ||
|
|
c2bda2241c | ||
|
|
337d432fdd | ||
|
|
45d104fd2c | ||
|
|
00b2e25b01 | ||
|
|
1b16193e21 | ||
|
|
203aec2854 | ||
|
|
5997ac5424 | ||
|
|
b4f97aadf1 | ||
|
|
b7caaeb788 | ||
|
|
c3ca4fa8f3 | ||
|
|
d5563aafba | ||
|
|
c89c244225 | ||
|
|
08e540e12f | ||
|
|
2849eebb28 | ||
|
|
683307b9af | ||
|
|
5231ea1c1e | ||
|
|
35ae81c76c | ||
|
|
a4d7d94301 | ||
|
|
44b0092b44 | ||
|
|
c6b8f69ef2 | ||
|
|
eac35ef8f4 | ||
|
|
e9eee50b3e | ||
|
|
f7c797e0b0 | ||
|
|
f9d66e0a78 | ||
|
|
a9d1d6edf5 | ||
|
|
babb2c0856 | ||
|
|
0570f2cd5b | ||
|
|
9c72b7ae8f | ||
|
|
e4b0b153e5 | ||
|
|
3edd8d5ebe | ||
|
|
bf68a284bb | ||
|
|
978544ed3f | ||
|
|
98135bc5fd | ||
|
|
626924bc34 | ||
|
|
a0fd2b6d16 | ||
|
|
44c431e9e0 | ||
|
|
6852f892f0 | ||
|
|
ec1eaf00eb | ||
|
|
ae0aa7dc65 | ||
|
|
29f78c7429 | ||
|
|
cd8834e368 | ||
|
|
f3fc967710 | ||
|
|
7d93c64b5b | ||
|
|
d4b957db23 | ||
|
|
d9678855a0 | ||
|
|
65c01e3f6e | ||
|
|
6821cee443 | ||
|
|
15a12da321 | ||
|
|
009d064576 | ||
|
|
3a4e2d9f9b | ||
|
|
e7afa235f7 | ||
|
|
edc5bd5d21 | ||
|
|
c06b09e00c | ||
|
|
3eeba27191 | ||
|
|
c6d1a040a1 | ||
|
|
558a3fc865 | ||
|
|
e0b50ac80c | ||
|
|
07ef7ae18f | ||
|
|
d07bd33e06 | ||
|
|
1616ca1c34 | ||
|
|
52fe74fbea | ||
|
|
8857ccbf86 | ||
|
|
279a290b60 | ||
|
|
128a3fe2f2 | ||
|
|
c97acfc76c | ||
|
|
bc2dae1b21 | ||
|
|
983b8ebe38 | ||
|
|
b6ba4640de | ||
|
|
5214aaafe7 | ||
|
|
39713f95ea | ||
|
|
b88f56e185 | ||
|
|
248444c04d | ||
|
|
dbd27e6113 | ||
|
|
3f09a79645 | ||
|
|
98b3746b25 | ||
|
|
07f8c38820 | ||
|
|
8393c07601 | ||
|
|
22d7595357 | ||
|
|
2157d95c56 | ||
|
|
729b2a1f0d | ||
|
|
cbcd5fbd2c | ||
|
|
a3318b1253 | ||
|
|
e59c7e1124 | ||
|
|
8dc108030d | ||
|
|
6e4feecff0 | ||
|
|
77c085ef1a | ||
|
|
acc202031c | ||
|
|
2354f85998 | ||
|
|
e1a6bd3d53 | ||
|
|
1420cbd86e | ||
|
|
a3a69b5cbd | ||
|
|
b2ee7cfa2c | ||
|
|
84869af81a | ||
|
|
352fb1be73 | ||
|
|
7cfcab312c | ||
|
|
8889923111 | ||
|
|
49f5f96097 | ||
|
|
37aaf2cdb6 | ||
|
|
e632ad0d84 | ||
|
|
fc49bc25f3 | ||
|
|
95eeafa06b | ||
|
|
a2f531a460 | ||
|
|
689d61d7d1 | ||
|
|
1f6edd778b | ||
|
|
4abd4c2f7f | ||
|
|
3ad3fe5cce | ||
|
|
6c4931b0ba | ||
|
|
fc75ed730d | ||
|
|
a767762f49 | ||
|
|
699632caa7 | ||
|
|
e2ce2b3776 | ||
|
|
aedb3012a4 | ||
|
|
915eed3a69 | ||
|
|
f7a2c9f9f0 | ||
|
|
f8f26bfb40 | ||
|
|
2b53e3483a | ||
|
|
1ce9731465 | ||
|
|
3b45ab7f62 | ||
|
|
1948894502 | ||
|
|
bb3b039672 | ||
|
|
66ef234f02 | ||
|
|
a94a6b3f4b | ||
|
|
0d06e028cd | ||
|
|
5d441fd23a | ||
|
|
21a731987e | ||
|
|
831b8cab4d | ||
|
|
80f00e8927 | ||
|
|
5afffb38aa | ||
|
|
67da21ccfc | ||
|
|
f63e88d081 | ||
|
|
aa1bc25ac8 | ||
|
|
291c35c1b3 | ||
|
|
9412f4d24f | ||
|
|
fb3946aad5 | ||
|
|
fe871e0a30 | ||
|
|
f1349964d4 | ||
|
|
488e589943 | ||
|
|
791cad5e9c | ||
|
|
719cee9122 | ||
|
|
3c350e4671 | ||
|
|
2dc872392a | ||
|
|
1f8e16dce8 | ||
|
|
00e8b7c80e | ||
|
|
c8b4ce8d38 | ||
|
|
5000b43533 | ||
|
|
9c04ad846a | ||
|
|
788a77b280 | ||
|
|
8b01a8d67c | ||
|
|
7a0e9d5835 | ||
|
|
668ca66a04 | ||
|
|
546db3fa23 | ||
|
|
12f33c355a | ||
|
|
707c2db508 | ||
|
|
700046e693 | ||
|
|
b291f7b606 | ||
|
|
615d4fb35b | ||
|
|
acc2ca7caf | ||
|
|
cc1ff5b51d | ||
|
|
724f1fc22d | ||
|
|
af7fe484a2 | ||
|
|
361b84e3a1 | ||
|
|
226795eafd | ||
|
|
de174db1bc | ||
|
|
e54d309f39 | ||
|
|
50a5d56856 | ||
|
|
aaaf6ed38d | ||
|
|
8045a23531 | ||
|
|
5abf7a5a11 | ||
|
|
669c6f5bf4 | ||
|
|
9af9347e0c | ||
|
|
25f06cccf1 | ||
|
|
b0c36adedb | ||
|
|
88c8d5f0f8 | ||
|
|
26fe9ca72b | ||
|
|
d1b897e212 | ||
|
|
5df4169a1b | ||
|
|
80a39889ca | ||
|
|
f202e985db | ||
|
|
bfc2ea2c3a | ||
|
|
e506ceb25a | ||
|
|
671a12b89c | ||
|
|
8021ff8871 | ||
|
|
fb9b1a5c7d | ||
|
|
e70c51e36c | ||
|
|
8d2e3f92b5 | ||
|
|
0256079738 | ||
|
|
47d85b7bab | ||
|
|
cace7576e2 | ||
|
|
3d9417c071 | ||
|
|
f1a39e1a1f | ||
|
|
de925e7fea | ||
|
|
e75d184194 | ||
|
|
3def65f501 | ||
|
|
1d160aeeea | ||
|
|
846b2c16d1 | ||
|
|
a6c882e282 | ||
|
|
e03f881f07 | ||
|
|
89705469e1 | ||
|
|
3817f700b9 | ||
|
|
70a475ff5f | ||
|
|
4c3d33efdc | ||
|
|
b7564207b3 | ||
|
|
43395c4db5 | ||
|
|
012917f938 | ||
|
|
f02fcad3a0 | ||
|
|
a3a4af182d | ||
|
|
49d6e66745 | ||
|
|
ad33f6e1fb | ||
|
|
a0aaa680fd | ||
|
|
67166b4421 | ||
|
|
c0f3bd8bb7 | ||
|
|
7262a6a1a0 | ||
|
|
bcf02a4cfe | ||
|
|
cdcc5c941d | ||
|
|
eea409dd03 | ||
|
|
dc1fbe8f63 | ||
|
|
728a4b7123 | ||
|
|
56cf77be42 | ||
|
|
4e07831635 | ||
|
|
ad6bee7801 | ||
|
|
042ad856a9 | ||
|
|
7cace2cacb | ||
|
|
2b00ea5107 | ||
|
|
43be34fd34 | ||
|
|
4d9fad5d53 | ||
|
|
83622b68dc | ||
|
|
d6a33176e6 | ||
|
|
0d37ebd7fd | ||
|
|
5884c78b8e | ||
|
|
bef3eb3964 | ||
|
|
0be1be5769 | ||
|
|
db87d9ca7b | ||
|
|
186ad09ab3 | ||
|
|
fafec6c904 | ||
|
|
496aca642c | ||
|
|
cb4656722a | ||
|
|
70f897699c | ||
|
|
0b36214bb5 | ||
|
|
f9342acb30 | ||
|
|
f96de8d082 | ||
|
|
0bef37e91f | ||
|
|
a69d15f1b1 | ||
|
|
284f90ccd3 | ||
|
|
2411cca51f | ||
|
|
64f8983d29 | ||
|
|
540c9cc8af | ||
|
|
f564fc5190 | ||
|
|
fff128e1ce | ||
|
|
da2370d9ac | ||
|
|
17594a51d1 | ||
|
|
05dc365dff | ||
|
|
39b60a07d9 | ||
|
|
e0dea67380 | ||
|
|
8bd4e25b7f | ||
|
|
b3f2474456 | ||
|
|
60abb9ee07 | ||
|
|
4a893d3c80 | ||
|
|
de34e077ce | ||
|
|
2d87c396f1 | ||
|
|
19bf19350e | ||
|
|
d8f1dcb032 | ||
|
|
753fb740fe | ||
|
|
1582d1b143 | ||
|
|
c403d7c788 | ||
|
|
7c9d8bd90d | ||
|
|
7cbe921020 | ||
|
|
8354794c24 | ||
|
|
b25a0e46cb | ||
|
|
1b8b043290 | ||
|
|
a4d9188bc7 | ||
|
|
47cf59a1ad | ||
|
|
b9b2afa200 | ||
|
|
ea6861b1eb | ||
|
|
a0b5d6d8ae | ||
|
|
484742885f | ||
|
|
2fc19f6741 | ||
|
|
f5fc64be44 | ||
|
|
fe62d6f80f | ||
|
|
c5a97f6c25 | ||
|
|
2ae1ddb2d5 | ||
|
|
29dda98736 | ||
|
|
76008022e7 | ||
|
|
b4299c71fb | ||
|
|
25477422a9 | ||
|
|
cba98ddf57 | ||
|
|
0f9df5af8a | ||
|
|
41b57b7f5e | ||
|
|
bab1fcb7bc | ||
|
|
4f6e194b35 | ||
|
|
ba842ff718 | ||
|
|
88929a1e98 | ||
|
|
ce9c222402 | ||
|
|
f1e598b0b6 | ||
|
|
5c95f7727a | ||
|
|
fcbfe7d4df | ||
|
|
a5950617f1 | ||
|
|
6f7d230895 | ||
|
|
e7ef101f99 |
39
.editorconfig
Normal file
@@ -0,0 +1,39 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py}]
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
# Swift files
|
||||
[*.swift]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
# 4 space indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Indentation override for all JS under lib directory
|
||||
[lib/**.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @JoeMatt @lonkelle
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Bug Report
|
||||
description: Report a bug
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
assignees:
|
||||
- naturecodevoid
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
||||
|
||||
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: What is the bug and how did you discover it?
|
||||
placeholder: Please be clear and concise with your description.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-to-reproduce
|
||||
attributes:
|
||||
label: Instructions to reproduce
|
||||
description: Please include clear and consistent instructions for reproducing the bug to make it easier for us to fix it.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: What version of SideStore are you using?
|
||||
description: To retrieve this, go to `Settings` in the SideStore app and scroll down to the bottom.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: Other info
|
||||
description: If you have any other comments, other info that might be useful, or if you found a workaround, please put it here.
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# force issue template usage
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: Discord
|
||||
url: https://discord.gg/RgpFBX3Q3k
|
||||
about: If you need support, please go here first instead of making an issue!
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/SideStore/SideStore/discussions
|
||||
about: As an alternative to Discord, you can also make a new GitHub discussion.
|
||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Feature Request
|
||||
description: Suggest a feature
|
||||
title: "[FEATURE REQUEST] "
|
||||
labels: ["enhancement"]
|
||||
assignees:
|
||||
- naturecodevoid
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||
|
||||
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: What is the feature? How would it work?
|
||||
placeholder: Please be clear and concise with your description.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: use-cases
|
||||
attributes:
|
||||
label: Use cases
|
||||
description: Please include multiple use cases where this feature would be useful.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: If you have alternative ideas of how this feature could work, you can put them here.
|
||||
15
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
### Changes
|
||||
|
||||
<!-- Fill this list with what your PR changes. Example: -->
|
||||
- Fix bug
|
||||
- Change UI for QOL
|
||||
|
||||
<!-- If your PR is ready to be merged, you can remove this section. -->
|
||||
### Todo before merge
|
||||
|
||||
<!-- Example: -->
|
||||
- [x] Finish UI changes
|
||||
- [ ] Test
|
||||
|
||||
<!-- If your PR doesn't close an issue, you can remove the next line. -->
|
||||
Closes #1234
|
||||
22
.github/workflows/attach_build_products.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Add artifact links to pull request and related issues
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Pull Request SideStore build]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
artifacts-url-comments:
|
||||
name: add artifact links to pull request and related issues job
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: add artifact links to pull request and related issues step
|
||||
uses: tonyhallett/artifacts-url-comments@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
prefix: Builds for this Pull Request are available at
|
||||
suffix: Have a nice day.
|
||||
format: name
|
||||
addTo: pull
|
||||
# addTo: pullandissues
|
||||
96
.github/workflows/beta.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Beta SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore Beta
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new beta release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
28
.github/workflows/increase-nightly-build-num.sh
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Ensure we are in root directory
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
||||
}
|
||||
|
||||
if [ ! -f ".nightly-build-num" ]; then
|
||||
write
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_DATE=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||
LAST_BUILD_NUM=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||
|
||||
if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||
write
|
||||
else
|
||||
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
write
|
||||
fi
|
||||
|
||||
106
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Nightly SideStore build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore Nightly
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Cache .nightly-build-num
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .nightly-build-num
|
||||
key: nightly-build-num
|
||||
|
||||
- name: Increase nightly build number and set as version
|
||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to nightly release
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: "Nightly"
|
||||
tag: "nightly"
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases?q=beta).
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
- name: Reset cache for apps.sidestore.io/nightly
|
||||
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
||||
64
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Pull Request SideStore build
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Add PR suffix to version
|
||||
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||
env:
|
||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
93
.github/workflows/stable.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
name: Stable SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-12'
|
||||
version: '14.2'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new stable release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
20
.gitignore
vendored
@@ -8,7 +8,7 @@
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData
|
||||
|
||||
archive.xcarchive
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
@@ -27,4 +27,20 @@ xcuserdata
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.hmap
|
||||
/newrelic_agent.log
|
||||
/CodeSigning.xcconfig
|
||||
/.vscode
|
||||
|
||||
## AppCode specific
|
||||
.idea/
|
||||
|
||||
Payload/
|
||||
SideStore.ipa
|
||||
*.dSYM
|
||||
|
||||
Dependencies/.*-prebuilt-fetch-*
|
||||
Dependencies/minimuxer/*
|
||||
Dependencies/em_proxy/*
|
||||
!Dependencies/**/.gitkeep
|
||||
.nightly-build-num
|
||||
|
||||
11
.gitmodules
vendored
@@ -1,12 +1,9 @@
|
||||
[submodule "Dependencies/Roxas"]
|
||||
path = Dependencies/Roxas
|
||||
url = https://github.com/rileytestut/Roxas.git
|
||||
[submodule "Dependencies/AltSign"]
|
||||
path = Dependencies/AltSign
|
||||
url = https://github.com/rileytestut/AltSign.git
|
||||
[submodule "Dependencies/libimobiledevice"]
|
||||
path = Dependencies/libimobiledevice
|
||||
url = https://github.com/rileytestut/libimobiledevice.git
|
||||
url = https://github.com/libimobiledevice/libimobiledevice
|
||||
[submodule "Dependencies/libusbmuxd"]
|
||||
path = Dependencies/libusbmuxd
|
||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||
@@ -16,3 +13,9 @@
|
||||
[submodule "Dependencies/MarkdownAttributedString"]
|
||||
path = Dependencies/MarkdownAttributedString
|
||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||
[submodule "Dependencies/libimobiledevice-glue"]
|
||||
path = Dependencies/libimobiledevice-glue
|
||||
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||
[submodule "Dependencies/libfragmentzip"]
|
||||
path = Dependencies/libfragmentzip
|
||||
url = https://github.com/SideStore/libfragmentzip.git
|
||||
|
||||
3
AltBackup.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup
|
||||
@@ -2,7 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
121
AltBackup/AppDelegate.swift
Normal file
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
||||
|
||||
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
|
||||
|
||||
static let operationResultKey = "result"
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
private var currentBackupReturnURL: URL?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Override point for customization after application launch.
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
|
||||
|
||||
let viewController = ViewController()
|
||||
|
||||
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||
self.window?.rootViewController = viewController
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
{
|
||||
return self.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
func open(_ url: URL) -> Bool
|
||||
{
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let command = components.host?.lowercased() else { return false }
|
||||
|
||||
switch command
|
||||
{
|
||||
case "backup":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
self.currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
|
||||
|
||||
return true
|
||||
|
||||
case "restore":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
self.currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
|
||||
|
||||
return true
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func operationDidFinish(_ notification: Notification)
|
||||
{
|
||||
defer { self.currentBackupReturnURL = nil }
|
||||
|
||||
guard
|
||||
let returnURL = self.currentBackupReturnURL,
|
||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||
else { return }
|
||||
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
components.path = "/success"
|
||||
|
||||
case .failure(let error as NSError):
|
||||
components.path = "/failure"
|
||||
components.queryItems = ["errorDomain": error.domain,
|
||||
"errorCode": String(error.code),
|
||||
"errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) }
|
||||
}
|
||||
|
||||
guard let responseURL = components.url else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
||||
print("Sent response to app with success:", success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
AltBackup/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
38
AltBackup/Assets.xcassets/Background.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.518",
|
||||
"green" : "0.502",
|
||||
"red" : "0.004"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.404",
|
||||
"green" : "0.322",
|
||||
"red" : "0.008"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
AltBackup/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
20
AltBackup/Assets.xcassets/Text.colorset/Contents.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.750",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
293
AltBackup/BackupController.swift
Normal file
@@ -0,0 +1,293 @@
|
||||
//
|
||||
// BackupController.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/12/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ErrorUserInfoKey
|
||||
{
|
||||
static let sourceFile: String = "alt_sourceFile"
|
||||
static let sourceFileLine: String = "alt_sourceFileLine"
|
||||
}
|
||||
|
||||
extension Error
|
||||
{
|
||||
var sourceDescription: String? {
|
||||
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
|
||||
return nil
|
||||
}
|
||||
return "(\((sourceFile as NSString).lastPathComponent), Line \(sourceFileLine))"
|
||||
}
|
||||
}
|
||||
|
||||
struct BackupError: ALTLocalizedError
|
||||
{
|
||||
enum Code
|
||||
{
|
||||
case invalidBundleID
|
||||
case appGroupNotFound(String?)
|
||||
case randomError // Used for debugging.
|
||||
}
|
||||
|
||||
let code: Code
|
||||
|
||||
let sourceFile: String
|
||||
let sourceFileLine: Int
|
||||
|
||||
var failure: String?
|
||||
|
||||
var failureReason: String? {
|
||||
switch self.code
|
||||
{
|
||||
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||
case .appGroupNotFound(let appGroup):
|
||||
if let appGroup = appGroup
|
||||
{
|
||||
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||
}
|
||||
else
|
||||
{
|
||||
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||
}
|
||||
case .randomError: return NSLocalizedString("A random error occured.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var errorUserInfo: [String : Any] {
|
||||
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
|
||||
NSLocalizedFailureReasonErrorKey: self.failureReason,
|
||||
NSLocalizedFailureErrorKey: self.failure,
|
||||
ErrorUserInfoKey.sourceFile: self.sourceFile,
|
||||
ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
|
||||
return userInfo.compactMapValues { $0 }
|
||||
}
|
||||
|
||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
||||
{
|
||||
self.code = code
|
||||
self.failure = description
|
||||
self.sourceFile = file
|
||||
self.sourceFileLine = line
|
||||
}
|
||||
}
|
||||
|
||||
class BackupController: NSObject
|
||||
{
|
||||
private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
override init()
|
||||
{
|
||||
self.operationQueue.name = "AltBackup-BackupQueue"
|
||||
}
|
||||
|
||||
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
|
||||
// Use temporary directory to prevent messing up successful backup with incomplete one.
|
||||
let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString)
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
|
||||
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
|
||||
self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
|
||||
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: documentsDirectory.path)
|
||||
{
|
||||
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)")
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: libraryDirectory.path)
|
||||
{
|
||||
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
|
||||
}
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||
{
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup)
|
||||
|
||||
// There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories.
|
||||
try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles])
|
||||
}
|
||||
|
||||
// Replace previous backup with new backup.
|
||||
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
|
||||
|
||||
print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
|
||||
catch { print("Failed to remove temporary directory.", error) }
|
||||
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
|
||||
}
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) }
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
|
||||
self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App")
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
|
||||
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||
{
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
|
||||
}
|
||||
|
||||
let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup)
|
||||
try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL)
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackupController
|
||||
{
|
||||
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
|
||||
{
|
||||
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
|
||||
|
||||
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
|
||||
{
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
|
||||
{
|
||||
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
|
||||
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path)
|
||||
{
|
||||
do {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
catch CocoaError.fileWriteNoPermission where isDirectory {
|
||||
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
|
||||
continue
|
||||
}
|
||||
catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
print("Copied item from \(fileURL) to \(destinationURL)")
|
||||
}
|
||||
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
|
||||
// Ignore errors for /Documents/Inbox
|
||||
print("Failed to copy Inbox directory:", error)
|
||||
}
|
||||
catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
AltBackup/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="Background">
|
||||
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
67
AltBackup/Info.plist
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>ALTBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltBackup General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altbackup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
15
AltBackup/UIColor+AltBackup.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// UIColor+AltBackup.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor
|
||||
{
|
||||
static let altstoreBackground = UIColor(named: "Background")!
|
||||
static let altstoreText = UIColor(named: "Text")!
|
||||
}
|
||||
206
AltBackup/ViewController.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Bundle
|
||||
{
|
||||
var appName: String? {
|
||||
let appName =
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
|
||||
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
|
||||
return appName
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController
|
||||
{
|
||||
enum BackupOperation
|
||||
{
|
||||
case backup
|
||||
case restore
|
||||
}
|
||||
}
|
||||
|
||||
class ViewController: UIViewController
|
||||
{
|
||||
private let backupController = BackupController()
|
||||
|
||||
private var currentOperation: BackupOperation? {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var textLabel: UILabel!
|
||||
private var detailTextLabel: UILabel!
|
||||
private var activityIndicatorView: UIActivityIndicatorView!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||
{
|
||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = .altstoreBackground
|
||||
|
||||
self.textLabel = UILabel(frame: .zero)
|
||||
self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
|
||||
self.textLabel.textColor = .altstoreText
|
||||
self.textLabel.textAlignment = .center
|
||||
self.textLabel.numberOfLines = 0
|
||||
|
||||
self.detailTextLabel = UILabel(frame: .zero)
|
||||
self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
self.detailTextLabel.textColor = .altstoreText
|
||||
self.detailTextLabel.textAlignment = .center
|
||||
self.detailTextLabel.numberOfLines = 0
|
||||
|
||||
self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
|
||||
self.activityIndicatorView.color = .altstoreText
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
#if DEBUG
|
||||
let button1 = UIButton(type: .system)
|
||||
button1.setTitle("Backup", for: .normal)
|
||||
button1.setTitleColor(.white, for: .normal)
|
||||
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
|
||||
let button2 = UIButton(type: .system)
|
||||
button2.setTitle("Restore", for: .normal)
|
||||
button2.setTitleColor(.white, for: .normal)
|
||||
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||
#else
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
||||
#endif
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.spacing = 22
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
self.view.addSubview(stackView)
|
||||
|
||||
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
|
||||
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
|
||||
self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController
|
||||
{
|
||||
@objc func backup()
|
||||
{
|
||||
self.currentOperation = .backup
|
||||
|
||||
self.backupController.performBackup { (result) in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restore()
|
||||
{
|
||||
self.currentOperation = .restore
|
||||
|
||||
self.backupController.restoreBackup { (result) in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
switch self.currentOperation
|
||||
{
|
||||
case .backup:
|
||||
self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
case .restore:
|
||||
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
case .none:
|
||||
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||
|
||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||
|
||||
self.detailTextLabel.isHidden = false
|
||||
self.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController
|
||||
{
|
||||
func process(_ result: Result<Void, Error>, errorTitle: String)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error as NSError):
|
||||
let message: String
|
||||
|
||||
if let sourceDescription = error.sourceDescription
|
||||
{
|
||||
message = error.localizedDescription + "\n\n" + sourceDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
message = error.localizedDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didEnterBackground(_ notification: Notification)
|
||||
{
|
||||
// Reset UI once we've left app (but not before).
|
||||
self.currentOperation = nil
|
||||
}
|
||||
}
|
||||
59
AltDaemon/AltDaemon-Bridging-Header.h
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
// libproc
|
||||
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
|
||||
|
||||
// Security.framework
|
||||
CF_ENUM(uint32_t) {
|
||||
kSecCSInternalInformation = 1 << 0,
|
||||
kSecCSSigningInformation = 1 << 1,
|
||||
kSecCSRequirementInformation = 1 << 2,
|
||||
kSecCSDynamicInformation = 1 << 3,
|
||||
kSecCSContentInformation = 1 << 4,
|
||||
kSecCSSkipResourceDirectory = 1 << 5,
|
||||
kSecCSCalculateCMSDigest = 1 << 6,
|
||||
};
|
||||
|
||||
OSStatus SecStaticCodeCreateWithPath(CFURLRef path, uint32_t flags, void ** __nonnull CF_RETURNS_RETAINED staticCode);
|
||||
OSStatus SecCodeCopySigningInformation(void *code, uint32_t flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AKDevice : NSObject
|
||||
|
||||
@property (class, readonly) AKDevice *currentDevice;
|
||||
|
||||
@property (strong, readonly) NSString *serialNumber;
|
||||
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
|
||||
@property (strong, readonly) NSString *serverFriendlyDescription;
|
||||
|
||||
@end
|
||||
|
||||
@interface AKAppleIDSession : NSObject
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier;
|
||||
|
||||
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
|
||||
|
||||
@end
|
||||
|
||||
@interface LSApplicationWorkspace : NSObject
|
||||
|
||||
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
|
||||
|
||||
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
|
||||
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
22
AltDaemon/AltDaemon.entitlements
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>$(DEVELOPMENT_TEAM).$(ORG_IDENTIFIER).AltDaemon</string>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
<key>platform-application</key>
|
||||
<true/>
|
||||
<key>com.apple.authkit.client.private</key>
|
||||
<true/>
|
||||
<key>com.apple.private.mobileinstall.allowedSPI</key>
|
||||
<array>
|
||||
<string>Install</string>
|
||||
<string>Uninstall</string>
|
||||
<string>InstallForLaunchServices</string>
|
||||
<string>UninstallForLaunchServices</string>
|
||||
<string>InstallLocalProvisioned</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
65
AltDaemon/AnisetteDataManager.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// AnisetteDataManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
@objc var localUserID: String? {
|
||||
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
}
|
||||
}
|
||||
|
||||
struct AnisetteDataManager
|
||||
{
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
private let dateFormatter = ISO8601DateFormatter()
|
||||
|
||||
private init()
|
||||
{
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||
}
|
||||
|
||||
func requestAnisetteData() throws -> ALTAnisetteData
|
||||
{
|
||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
|
||||
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
|
||||
|
||||
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
|
||||
let headers = session.appleIDHeaders(for: request)
|
||||
|
||||
let device = akDevice.current
|
||||
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||
|
||||
var localUserID = UserDefaults.standard.localUserID
|
||||
if localUserID == nil
|
||||
{
|
||||
localUserID = UUID().uuidString
|
||||
UserDefaults.standard.localUserID = localUserID
|
||||
}
|
||||
|
||||
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
|
||||
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
|
||||
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
|
||||
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
|
||||
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
|
||||
deviceSerialNumber: device.serialNumber,
|
||||
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
|
||||
date: date,
|
||||
locale: .current,
|
||||
timeZone: .current)
|
||||
return anisetteData
|
||||
}
|
||||
}
|
||||
138
AltDaemon/AppManager.swift
Normal file
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// AppManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension URL
|
||||
{
|
||||
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
||||
}
|
||||
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
||||
}
|
||||
|
||||
struct AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
|
||||
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
||||
private let profilesQueue = OperationQueue()
|
||||
|
||||
private let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
private init()
|
||||
{
|
||||
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||
self.profilesQueue.qualityOfService = .userInitiated
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
|
||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
|
||||
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
|
||||
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
// Use memory mapping to reduce peak memory usage and stay within limit.
|
||||
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
||||
|
||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
for profile in profiles
|
||||
{
|
||||
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
||||
try profile.data.write(to: destinationURL, options: .atomic)
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||
|
||||
if bundleIdentifiers.contains(profile.bundleIdentifier)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
AltDaemon/DaemonRequestHandler.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// DaemonRequestHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
||||
connectionHandlers: [XPCConnectionHandler()])
|
||||
|
||||
extension DaemonConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct DaemonRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
||||
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||
{
|
||||
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
do
|
||||
{
|
||||
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
||||
|
||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
|
||||
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
||||
print("Installed app with result:", result)
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
AltDaemon/XPCConnectionHandler.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// XPCConnectionHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 9/14/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class XPCConnectionHandler: NSObject, ConnectionHandler
|
||||
{
|
||||
var connectionHandler: ((Connection) -> Void)?
|
||||
var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
||||
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
||||
|
||||
deinit
|
||||
{
|
||||
self.stopListening()
|
||||
}
|
||||
|
||||
func startListening()
|
||||
{
|
||||
for listener in self.listeners
|
||||
{
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening()
|
||||
{
|
||||
self.listeners.forEach { $0.suspend() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension XPCConnectionHandler
|
||||
{
|
||||
func disconnect(_ connection: Connection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
self.disconnectionHandler?(connection)
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCConnectionHandler: NSXPCListenerDelegate
|
||||
{
|
||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
|
||||
{
|
||||
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
||||
|
||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
||||
defer { pathBuffer.deallocate() }
|
||||
|
||||
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
|
||||
|
||||
let path = String(cString: pathBuffer)
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
|
||||
var code: UnsafeMutableRawPointer?
|
||||
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
|
||||
|
||||
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
var signingInfo: CFDictionary?
|
||||
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
|
||||
|
||||
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
// Only accept connections from AltStore.
|
||||
guard
|
||||
let codeSigningInfo = signingInfo as? [String: Any],
|
||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
||||
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
|
||||
else { return false }
|
||||
|
||||
let connection = XPCConnection(newConnection)
|
||||
newConnection.invalidationHandler = { [weak self, weak connection] in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.disconnect(connection)
|
||||
}
|
||||
|
||||
self.connectionHandler?(connection)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
14
AltDaemon/main.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// main.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/2/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
autoreleasepool {
|
||||
DaemonConnectionManager.shared.start()
|
||||
RunLoop.current.run()
|
||||
}
|
||||
10
AltDaemon/package/DEBIAN/control
Normal file
@@ -0,0 +1,10 @@
|
||||
Package: com.rileytestut.altdaemon
|
||||
Name: AltDaemon
|
||||
Depends:
|
||||
Version: 1.0
|
||||
Architecture: iphoneos-arm
|
||||
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
|
||||
Maintainer: Riley Testut
|
||||
Author: Riley Testut
|
||||
Homepage: https://altstore.io
|
||||
Section: System
|
||||
2
AltDaemon/package/DEBIAN/postinst
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
2
AltDaemon/package/DEBIAN/preinst
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1
|
||||
2
AltDaemon/package/DEBIAN/prerm
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.rileytestut.altdaemon</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/env</string>
|
||||
<string>_MSSafeMode=1</string>
|
||||
<string>_SafeMode=1</string>
|
||||
<string>/usr/bin/AltDaemon</string>
|
||||
</array>
|
||||
<key>UserName</key>
|
||||
<string>mobile</string>
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>cy:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
<key>lh:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
AltDaemon/package/usr/bin/AltDaemon
Executable file
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// Bundle+AltStore.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Bundle
|
||||
{
|
||||
struct Info
|
||||
{
|
||||
public static let deviceID = "ALTDeviceID"
|
||||
public static let serverID = "ALTServerID"
|
||||
public static let certificateID = "ALTCertificateID"
|
||||
public static let appGroups = "ALTAppGroups"
|
||||
|
||||
public static let urlTypes = "CFBundleURLTypes"
|
||||
public static let exportedUTIs = "UTExportedTypeDeclarations"
|
||||
}
|
||||
}
|
||||
|
||||
public extension Bundle
|
||||
{
|
||||
var infoPlistURL: URL {
|
||||
let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist")
|
||||
return infoPlistURL
|
||||
}
|
||||
|
||||
var provisioningProfileURL: URL {
|
||||
let infoPlistURL = self.bundleURL.appendingPathComponent("embedded.mobileprovision")
|
||||
return infoPlistURL
|
||||
}
|
||||
|
||||
var certificateURL: URL {
|
||||
let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12")
|
||||
return infoPlistURL
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// CodableServerError.swift
|
||||
// AltKit
|
||||
//
|
||||
// Created by Riley Testut on 3/5/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
|
||||
extension ALTServerError.Code: Codable {}
|
||||
|
||||
struct CodableServerError: Codable
|
||||
{
|
||||
var error: ALTServerError {
|
||||
return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
|
||||
}
|
||||
|
||||
private var errorCode: ALTServerError.Code
|
||||
private var userInfo: [String: String]?
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case errorCode
|
||||
case userInfo
|
||||
}
|
||||
|
||||
init(error: ALTServerError)
|
||||
{
|
||||
self.errorCode = error.code
|
||||
|
||||
let userInfo = error.userInfo.compactMapValues { $0 as? String }
|
||||
if !userInfo.isEmpty
|
||||
{
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let errorCode = try container.decode(Int.self, forKey: .errorCode)
|
||||
self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
|
||||
|
||||
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.error.code.rawValue, forKey: .errorCode)
|
||||
try container.encodeIfPresent(self.userInfo, forKey: .userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
//
|
||||
// NSError+ALTServerError.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer";
|
||||
NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation";
|
||||
|
||||
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
|
||||
NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdentifier";
|
||||
|
||||
@implementation NSError (ALTServerError)
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
[NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
|
||||
if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
|
||||
{
|
||||
return [error alt_localizedFailureReason];
|
||||
}
|
||||
|
||||
if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
|
||||
{
|
||||
return [error alt_localizedRecoverySuggestion];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}];
|
||||
}
|
||||
|
||||
- (nullable NSString *)alt_localizedFailureReason
|
||||
{
|
||||
switch ((ALTServerError)self.code)
|
||||
{
|
||||
case ALTServerErrorUnderlyingError:
|
||||
{
|
||||
NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey];
|
||||
if (underlyingErrorCode == nil)
|
||||
{
|
||||
return NSLocalizedString(@"An unknown error occured.", @"");
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:NSLocalizedString(@"Error code: %@", @""), underlyingErrorCode];
|
||||
}
|
||||
|
||||
case ALTServerErrorUnknown:
|
||||
return NSLocalizedString(@"An unknown error occured.", @"");
|
||||
|
||||
case ALTServerErrorConnectionFailed:
|
||||
return NSLocalizedString(@"Could not connect to AltServer.", @"");
|
||||
|
||||
case ALTServerErrorLostConnection:
|
||||
return NSLocalizedString(@"Lost connection to AltServer.", @"");
|
||||
|
||||
case ALTServerErrorDeviceNotFound:
|
||||
return NSLocalizedString(@"AltServer could not find this device.", @"");
|
||||
|
||||
case ALTServerErrorDeviceWriteFailed:
|
||||
return NSLocalizedString(@"Failed to write app data to device.", @"");
|
||||
|
||||
case ALTServerErrorInvalidRequest:
|
||||
return NSLocalizedString(@"AltServer received an invalid request.", @"");
|
||||
|
||||
case ALTServerErrorInvalidResponse:
|
||||
return NSLocalizedString(@"AltServer sent an invalid response.", @"");
|
||||
|
||||
case ALTServerErrorInvalidApp:
|
||||
return NSLocalizedString(@"The app is invalid.", @"");
|
||||
|
||||
case ALTServerErrorInstallationFailed:
|
||||
return NSLocalizedString(@"An error occured while installing the app.", @"");
|
||||
|
||||
case ALTServerErrorMaximumFreeAppLimitReached:
|
||||
return NSLocalizedString(@"Cannot activate more than 3 apps and app extensions.", @"");
|
||||
|
||||
case ALTServerErrorUnsupportediOSVersion:
|
||||
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
|
||||
|
||||
case ALTServerErrorUnknownRequest:
|
||||
return NSLocalizedString(@"AltServer does not support this request.", @"");
|
||||
|
||||
case ALTServerErrorUnknownResponse:
|
||||
return NSLocalizedString(@"Received an unknown response from AltServer.", @"");
|
||||
|
||||
case ALTServerErrorInvalidAnisetteData:
|
||||
return NSLocalizedString(@"Invalid anisette data.", @"");
|
||||
|
||||
case ALTServerErrorPluginNotFound:
|
||||
return NSLocalizedString(@"Could not connect to Mail plug-in.", @"");
|
||||
|
||||
case ALTServerErrorProfileNotFound:
|
||||
return [self profileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not find profile", "")];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSString *)alt_localizedRecoverySuggestion
|
||||
{
|
||||
switch ((ALTServerError)self.code)
|
||||
{
|
||||
case ALTServerErrorConnectionFailed:
|
||||
case ALTServerErrorDeviceNotFound:
|
||||
return NSLocalizedString(@"Make sure you have trusted this phone with your computer and WiFi sync is enabled.", @"");
|
||||
|
||||
case ALTServerErrorPluginNotFound:
|
||||
return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @"");
|
||||
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)profileErrorLocalizedDescriptionWithBaseDescription:(NSString *)baseDescription
|
||||
{
|
||||
NSString *localizedDescription = nil;
|
||||
|
||||
NSString *bundleID = self.userInfo[ALTProvisioningProfileBundleIDErrorKey];
|
||||
if (bundleID)
|
||||
{
|
||||
localizedDescription = [NSString stringWithFormat:@"%@ “%@”", baseDescription, bundleID];
|
||||
}
|
||||
else
|
||||
{
|
||||
localizedDescription = [NSString stringWithFormat:@"%@.", baseDescription];
|
||||
}
|
||||
|
||||
return localizedDescription;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// ALTPluginService.h
|
||||
// AltPlugin
|
||||
//
|
||||
// Created by Riley Testut on 11/14/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTPluginService : NSObject
|
||||
|
||||
+ (instancetype)sharedService;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,99 +0,0 @@
|
||||
//
|
||||
// ALTPluginService.m
|
||||
// AltPlugin
|
||||
//
|
||||
// Created by Riley Testut on 11/14/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTPluginService.h"
|
||||
|
||||
#import <dlfcn.h>
|
||||
|
||||
#import "ALTAnisetteData.h"
|
||||
|
||||
@import AppKit;
|
||||
|
||||
@interface AKAppleIDSession : NSObject
|
||||
- (id)appleIDHeadersForRequest:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface AKDevice
|
||||
+ (AKDevice *)currentDevice;
|
||||
- (NSString *)uniqueDeviceIdentifier;
|
||||
- (NSString *)serialNumber;
|
||||
- (NSString *)serverFriendlyDescription;
|
||||
@end
|
||||
|
||||
@interface ALTPluginService ()
|
||||
|
||||
@property (nonatomic, readonly) NSISO8601DateFormatter *dateFormatter;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ALTPluginService
|
||||
|
||||
+ (instancetype)sharedService
|
||||
{
|
||||
static ALTPluginService *_service = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_service = [[self alloc] init];
|
||||
});
|
||||
|
||||
return _service;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_dateFormatter = [[NSISO8601DateFormatter alloc] init];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
[[ALTPluginService sharedService] start];
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||
|
||||
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"com.rileytestut.AltServer.FetchAnisetteData" object:nil];
|
||||
}
|
||||
|
||||
- (void)receiveNotification:(NSNotification *)notification
|
||||
{
|
||||
NSString *requestUUID = notification.userInfo[@"requestUUID"];
|
||||
|
||||
NSMutableURLRequest* req = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:@"https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA"]];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
|
||||
AKAppleIDSession *session = [[NSClassFromString(@"AKAppleIDSession") alloc] initWithIdentifier:@"com.apple.gs.xcode.auth"];
|
||||
NSDictionary *headers = [session appleIDHeadersForRequest:req];
|
||||
|
||||
AKDevice *device = [NSClassFromString(@"AKDevice") currentDevice];
|
||||
NSDate *date = [self.dateFormatter dateFromString:headers[@"X-Apple-I-Client-Time"]];
|
||||
|
||||
ALTAnisetteData *anisetteData = [[NSClassFromString(@"ALTAnisetteData") alloc] initWithMachineID:headers[@"X-Apple-I-MD-M"]
|
||||
oneTimePassword:headers[@"X-Apple-I-MD"]
|
||||
localUserID:headers[@"X-Apple-I-MD-LU"]
|
||||
routingInfo:[headers[@"X-Apple-I-MD-RINFO"] longLongValue]
|
||||
deviceUniqueIdentifier:device.uniqueDeviceIdentifier
|
||||
deviceSerialNumber:device.serialNumber
|
||||
deviceDescription:device.serverFriendlyDescription
|
||||
date:date
|
||||
locale:[NSLocale currentLocale]
|
||||
timeZone:[NSTimeZone localTimeZone]];
|
||||
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:anisetteData requiringSecureCoding:YES error:nil];
|
||||
|
||||
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.rileytestut.AltServer.AnisetteDataResponse" object:nil userInfo:@{@"requestUUID": requestUUID, @"anisetteData": data} deliverImmediately:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>ALTPluginService</string>
|
||||
<key>Supported10.14PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
||||
</array>
|
||||
<key>Supported10.15PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,8 +0,0 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "ALTDeviceManager.h"
|
||||
#import "ALTWiredConnection.h"
|
||||
#import "ALTNotificationConnection.h"
|
||||
#import "AltKit.h"
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// AnisetteDataManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 11/16/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltKit
|
||||
|
||||
class AnisetteDataManager: NSObject
|
||||
{
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
private var anisetteDataCompletionHandlers: [String: (Result<ALTAnisetteData, Error>) -> Void] = [:]
|
||||
private var anisetteDataTimers: [String: Timer] = [:]
|
||||
|
||||
private override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
DistributedNotificationCenter.default().addObserver(self, selector: #selector(AnisetteDataManager.handleAnisetteDataResponse(_:)), name: Notification.Name("com.rileytestut.AltServer.AnisetteDataResponse"), object: nil)
|
||||
}
|
||||
|
||||
func requestAnisetteData(_ completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
||||
{
|
||||
let requestUUID = UUID().uuidString
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = completion
|
||||
|
||||
let timer = Timer(timeInterval: 1.0, repeats: false) { (timer) in
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.pluginNotFound)))
|
||||
}
|
||||
self.anisetteDataTimers[requestUUID] = timer
|
||||
|
||||
RunLoop.main.add(timer, forMode: .default)
|
||||
|
||||
DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AnisetteDataManager
|
||||
{
|
||||
@objc func handleAnisetteDataResponse(_ notification: Notification)
|
||||
{
|
||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
||||
|
||||
if
|
||||
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
|
||||
{
|
||||
if let range = anisetteData.deviceDescription.lowercased().range(of: "(com.apple.mail")
|
||||
{
|
||||
var adjustedDescription = anisetteData.deviceDescription[..<range.lowerBound]
|
||||
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
|
||||
|
||||
anisetteData.deviceDescription = String(adjustedDescription)
|
||||
}
|
||||
|
||||
self.finishRequest(forUUID: requestUUID, result: .success(anisetteData))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.invalidAnisetteData)))
|
||||
}
|
||||
}
|
||||
|
||||
func finishRequest(forUUID requestUUID: String, result: Result<ALTAnisetteData, Error>)
|
||||
{
|
||||
let completionHandler = self.anisetteDataCompletionHandlers[requestUUID]
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = nil
|
||||
|
||||
let timer = self.anisetteDataTimers[requestUUID]
|
||||
self.anisetteDataTimers[requestUUID] = nil
|
||||
|
||||
timer?.invalidate()
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
@@ -1,548 +0,0 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import UserNotifications
|
||||
|
||||
import AltSign
|
||||
|
||||
import LaunchAtLogin
|
||||
import STPrivilegedTask
|
||||
|
||||
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
|
||||
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
|
||||
|
||||
enum PluginError: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
case unknown
|
||||
case taskError(String)
|
||||
case taskErrorCode(Int)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
|
||||
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
|
||||
case .taskError(let output): return output
|
||||
case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private var statusItem: NSStatusItem?
|
||||
|
||||
private var connectedDevices = [ALTDevice]()
|
||||
|
||||
private weak var authenticationAlert: NSAlert?
|
||||
|
||||
@IBOutlet private var appMenu: NSMenu!
|
||||
@IBOutlet private var connectedDevicesMenu: NSMenu!
|
||||
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
|
||||
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
|
||||
|
||||
private weak var authenticationAppleIDTextField: NSTextField?
|
||||
private weak var authenticationPasswordTextField: NSSecureTextField?
|
||||
|
||||
private var isMailPluginInstalled: Bool {
|
||||
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
return isMailPluginInstalled
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification)
|
||||
{
|
||||
UserDefaults.standard.registerDefaults()
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
ConnectionManager.shared.start()
|
||||
ALTDeviceManager.shared.start()
|
||||
|
||||
let item = NSStatusBar.system.statusItem(withLength: -1)
|
||||
guard let button = item.button else { return }
|
||||
|
||||
button.image = NSImage(named: "MenuBarIcon")
|
||||
button.target = self
|
||||
button.action = #selector(AppDelegate.presentMenu)
|
||||
|
||||
self.statusItem = item
|
||||
|
||||
self.connectedDevicesMenu.delegate = self
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
if !UserDefaults.standard.didPresentInitialNotification
|
||||
{
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("AltServer Running", comment: "")
|
||||
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.didPresentInitialNotification = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification)
|
||||
{
|
||||
// Insert code here to tear down your application
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
@objc func presentMenu()
|
||||
{
|
||||
guard let button = self.statusItem?.button, let superview = button.superview, let window = button.window else { return }
|
||||
|
||||
self.connectedDevices = ALTDeviceManager.shared.availableDevices
|
||||
|
||||
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
|
||||
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
|
||||
|
||||
if self.isMailPluginInstalled
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in", comment: "")
|
||||
}
|
||||
|
||||
self.installMailPluginMenuItem.target = self
|
||||
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
|
||||
|
||||
let x = button.frame.origin.x
|
||||
let y = button.frame.origin.y - 5
|
||||
|
||||
let location = superview.convert(NSMakePoint(x, y), to: nil)
|
||||
|
||||
guard let event = NSEvent.mouseEvent(with: .leftMouseUp, location: location,
|
||||
modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil,
|
||||
eventNumber: 0, clickCount: 1, pressure: 0)
|
||||
else { return }
|
||||
|
||||
NSMenu.popUpContextMenu(self.appMenu, with: event, for: button)
|
||||
}
|
||||
|
||||
@objc func installAltStore(_ item: NSMenuItem)
|
||||
{
|
||||
guard case let index = self.connectedDevicesMenu.index(of: item), index != -1 else { return }
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
|
||||
|
||||
let textFieldSize = NSSize(width: 300, height: 22)
|
||||
|
||||
let appleIDTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
||||
appleIDTextField.delegate = self
|
||||
appleIDTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
appleIDTextField.placeholderString = NSLocalizedString("Apple ID", comment: "")
|
||||
alert.window.initialFirstResponder = appleIDTextField
|
||||
self.authenticationAppleIDTextField = appleIDTextField
|
||||
|
||||
let passwordTextField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
||||
passwordTextField.delegate = self
|
||||
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
passwordTextField.placeholderString = NSLocalizedString("Password", comment: "")
|
||||
self.authenticationPasswordTextField = passwordTextField
|
||||
|
||||
appleIDTextField.nextKeyView = passwordTextField
|
||||
|
||||
let stackView = NSStackView(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height * 2))
|
||||
stackView.orientation = .vertical
|
||||
stackView.distribution = .equalSpacing
|
||||
stackView.spacing = 0
|
||||
stackView.addArrangedSubview(appleIDTextField)
|
||||
stackView.addArrangedSubview(passwordTextField)
|
||||
alert.accessoryView = stackView
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Install", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
self.authenticationAlert = alert
|
||||
self.validate()
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { return }
|
||||
|
||||
let username = appleIDTextField.stringValue
|
||||
let password = passwordTextField.stringValue
|
||||
|
||||
let device = self.connectedDevices[index]
|
||||
|
||||
func install()
|
||||
{
|
||||
ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("Installation Succeeded", comment: "")
|
||||
content.body = String(format: NSLocalizedString("AltStore was successfully installed on %@.", comment: ""), device.name)
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||
// Ignore
|
||||
break
|
||||
|
||||
case .failure(let error as NSError):
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = NSLocalizedString("Installation Failed", comment: "")
|
||||
|
||||
if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? Error
|
||||
{
|
||||
alert.informativeText = underlyingError.localizedDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
alert.informativeText = error.localizedDescription
|
||||
}
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.isMailPluginInstalled
|
||||
{
|
||||
self.installMailPlugin { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(PluginError.cancelled): break
|
||||
case .failure(let error):
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
|
||||
alert.runModal()
|
||||
|
||||
install()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
install()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
|
||||
{
|
||||
if item.state == .on
|
||||
{
|
||||
item.state = .off
|
||||
}
|
||||
else
|
||||
{
|
||||
item.state = .on
|
||||
}
|
||||
|
||||
LaunchAtLogin.isEnabled.toggle()
|
||||
}
|
||||
|
||||
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
|
||||
{
|
||||
if self.isMailPluginInstalled
|
||||
{
|
||||
self.uninstallMailPlugin { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(PluginError.cancelled): break
|
||||
case .failure(let error):
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Uninstalled", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail for changes to take effect. You will not be able to use AltServer until the plug-in is reinstalled.", comment: "")
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.installMailPlugin { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(PluginError.cancelled): break
|
||||
case .failure(let error):
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { throw PluginError.cancelled }
|
||||
|
||||
self.downloadPlugin { (result) in
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
|
||||
// Ensure plug-in directory exists.
|
||||
let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path])
|
||||
|
||||
// Unzip AltPlugin to plug-ins directory.
|
||||
try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", pluginDirectoryURL.path], authorization: authorization)
|
||||
guard self.isMailPluginInstalled else { throw PluginError.unknown }
|
||||
|
||||
// Enable Mail plug-in preferences.
|
||||
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
|
||||
|
||||
print("Finished installing Mail plug-in!")
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(PluginError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
func downloadPlugin(completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let pluginURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!
|
||||
|
||||
let downloadTask = URLSession.shared.downloadTask(with: pluginURL) { (fileURL, response, error) in
|
||||
if let fileURL = fileURL
|
||||
{
|
||||
print("Downloaded plugin to URL:", fileURL)
|
||||
completionHandler(.success(fileURL))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(error ?? PluginError.unknown))
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func uninstallMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) }
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
{
|
||||
// Delete Mail plug-in from privileged directory.
|
||||
try self.run("rm", arguments: ["-rf", pluginURL.path])
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws
|
||||
{
|
||||
_ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef
|
||||
{
|
||||
return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false)
|
||||
}
|
||||
|
||||
func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef
|
||||
{
|
||||
var launchPath = "/usr/bin/" + program
|
||||
if !FileManager.default.fileExists(atPath: launchPath)
|
||||
{
|
||||
launchPath = "/bin/" + program
|
||||
}
|
||||
|
||||
print("Running program:", launchPath)
|
||||
|
||||
let task = STPrivilegedTask()
|
||||
task.launchPath = launchPath
|
||||
task.arguments = arguments
|
||||
task.freeAuthorizationWhenDone = freeAuthorization
|
||||
|
||||
let errorCode: OSStatus
|
||||
|
||||
if let authorization = authorization
|
||||
{
|
||||
errorCode = task.launch(withAuthorization: authorization)
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCode = task.launch()
|
||||
}
|
||||
|
||||
guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) }
|
||||
|
||||
task.waitUntilExit()
|
||||
|
||||
print("Exit code:", task.terminationStatus)
|
||||
|
||||
guard task.terminationStatus == 0 else {
|
||||
let outputData = task.outputFileHandle.readDataToEndOfFile()
|
||||
|
||||
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
|
||||
{
|
||||
throw PluginError.taskError(outputString)
|
||||
}
|
||||
|
||||
throw PluginError.taskErrorCode(Int(task.terminationStatus))
|
||||
}
|
||||
|
||||
guard let authorization = task.authorization else { throw PluginError.unknown }
|
||||
return authorization
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSMenuDelegate
|
||||
{
|
||||
func numberOfItems(in menu: NSMenu) -> Int
|
||||
{
|
||||
return self.connectedDevices.isEmpty ? 1 : self.connectedDevices.count
|
||||
}
|
||||
|
||||
func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool
|
||||
{
|
||||
if self.connectedDevices.isEmpty
|
||||
{
|
||||
item.title = NSLocalizedString("No Connected Devices", comment: "")
|
||||
item.isEnabled = false
|
||||
item.target = nil
|
||||
item.action = nil
|
||||
}
|
||||
else
|
||||
{
|
||||
let device = self.connectedDevices[index]
|
||||
item.title = device.name
|
||||
item.isEnabled = true
|
||||
item.target = self
|
||||
item.action = #selector(AppDelegate.installAltStore)
|
||||
item.tag = index
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSTextFieldDelegate
|
||||
{
|
||||
func controlTextDidChange(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
func controlTextDidEndEditing(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
private func validate()
|
||||
{
|
||||
guard
|
||||
let appleID = self.authenticationAppleIDTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
let password = self.authenticationPasswordTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
else { return }
|
||||
|
||||
if appleID.isEmpty || password.isEmpty
|
||||
{
|
||||
self.authenticationAlert?.buttons.first?.isEnabled = false
|
||||
}
|
||||
else
|
||||
{
|
||||
self.authenticationAlert?.buttons.first?.isEnabled = true
|
||||
}
|
||||
|
||||
self.authenticationAlert?.layout()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate
|
||||
{
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
|
||||
{
|
||||
completionHandler([.alert, .sound, .badge])
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@32-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@256-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@512-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MenuBar@19.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MenuBar@38.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,364 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" id="urc-xw-Dhc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zLd-d8-ghZ">
|
||||
<rect key="frame" x="0.0" y="25" width="300" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Apple ID" drawsBackground="YES" id="BXa-Re-rs3">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="QtW-r2-Vuh"/>
|
||||
<outlet property="nextKeyView" destination="9rp-Vx-rvB" id="bQY-qj-Sej"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9rp-Vx-rvB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="21"/>
|
||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Password" drawsBackground="YES" usesSingleLineMode="YES" id="xqJ-wt-DlP">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
</secureTextFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="qav-xj-izy"/>
|
||||
</connections>
|
||||
</secureTextField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="9rp-Vx-rvB" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="Eht-pU-Gyh"/>
|
||||
<constraint firstItem="zLd-d8-ghZ" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="mg7-Kq-abL"/>
|
||||
<constraint firstAttribute="width" constant="300" id="zqf-x6-BET"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="AltServer" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="appMenu" destination="uQy-DD-JDr" id="7cY-Ov-AOW"/>
|
||||
<outlet property="authenticationAppleIDTextField" destination="zLd-d8-ghZ" id="wW5-0J-zdq"/>
|
||||
<outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/>
|
||||
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/>
|
||||
<outlet property="installMailPluginMenuItem" destination="3CM-gV-X2G" id="lio-ha-z0S"/>
|
||||
<outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="AltServer" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="AltServer" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About AltServer" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Install AltStore" id="MJ8-Lt-SSV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Install AltStore" systemMenu="recentDocuments" id="KJ9-WY-pW1">
|
||||
<items>
|
||||
<menuItem title="No Connected Devices" id="N5N-3K-XuR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="DKG-yI-Ujv"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="VYb-BL-Zri"/>
|
||||
</connections>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="1ZZ-BB-xHy"/>
|
||||
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Install Mail Plug-in" id="3CM-gV-X2G" userLabel="Mail Plug-in">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="mVM-Nm-Zi9"/>
|
||||
<menuItem title="Check for Updates..." id="Tnq-gD-Eic" userLabel="Check for Updates">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="checkForUpdates:" target="Arf-IC-5eb" id="7JG-du-nr4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hmG-xg-qgm"/>
|
||||
<menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="AltServer Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// ALTNotificationConnection+Private.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTNotificationConnection.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/notification_proxy.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTNotificationConnection ()
|
||||
|
||||
@property (nonatomic, readonly) np_client_t client;
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// ALTNotificationConnection.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <AltSign/AltSign.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(NotificationConnection)
|
||||
@interface ALTNotificationConnection : NSObject
|
||||
|
||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
||||
|
||||
@property (nonatomic, copy, nullable) void (^receivedNotificationHandler)(CFNotificationName notification);
|
||||
|
||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications
|
||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)sendNotification:(CFNotificationName)notification
|
||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,93 +0,0 @@
|
||||
//
|
||||
// ALTNotificationConnection.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTNotificationConnection+Private.h"
|
||||
#import "AltKit.h"
|
||||
|
||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data);
|
||||
|
||||
@implementation ALTNotificationConnection
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_device = [device copy];
|
||||
_client = client;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
np_client_free(self.client);
|
||||
_client = nil;
|
||||
}
|
||||
|
||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
const char **notificationNames = (const char **)malloc((notifications.count + 1) * sizeof(char *));
|
||||
for (int i = 0; i < notifications.count; i++)
|
||||
{
|
||||
NSString *name = notifications[i];
|
||||
notificationNames[i] = name.UTF8String;
|
||||
}
|
||||
notificationNames[notifications.count] = NULL; // Must have terminating NULL entry.
|
||||
|
||||
np_error_t result = np_observe_notifications(self.client, notificationNames);
|
||||
if (result != NP_E_SUCCESS)
|
||||
{
|
||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
result = np_set_notify_callback(self.client, ALTDeviceReceivedNotification, (__bridge void *)self);
|
||||
if (result != NP_E_SUCCESS)
|
||||
{
|
||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
completionHandler(YES, nil);
|
||||
|
||||
free(notificationNames);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendNotification:(CFNotificationName)notification completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
np_error_t result = np_post_notification(self.client, [(__bridge NSString *)notification UTF8String]);
|
||||
if (result == NP_E_SUCCESS)
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data)
|
||||
{
|
||||
ALTNotificationConnection *connection = (__bridge ALTNotificationConnection *)user_data;
|
||||
|
||||
if (connection.receivedNotificationHandler)
|
||||
{
|
||||
connection.receivedNotificationHandler((__bridge CFNotificationName)@(notification));
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// ALTWiredConnection+Private.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTWiredConnection.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTWiredConnection ()
|
||||
|
||||
@property (nonatomic, readonly) idevice_connection_t connection;
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,101 +0,0 @@
|
||||
//
|
||||
// ALTWiredConnection.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTWiredConnection+Private.h"
|
||||
#import "AltKit.h"
|
||||
|
||||
@implementation ALTWiredConnection
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_device = [device copy];
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
idevice_disconnect(self.connection);
|
||||
_connection = nil;
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(NSError *error) = ^(NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Send Error: %@", error);
|
||||
completionHandler(NO, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
NSMutableData *mutableData = [data mutableCopy];
|
||||
while (mutableData.length > 0)
|
||||
{
|
||||
uint32_t sentBytes = 0;
|
||||
if (idevice_connection_send(self.connection, (const char *)mutableData.bytes, (int32_t)mutableData.length, &sentBytes) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
[mutableData replaceBytesInRange:NSMakeRange(0, sentBytes) withBytes:NULL length:0];
|
||||
}
|
||||
|
||||
finish(nil);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(NSData *data, NSError *error) = ^(NSData *data, NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Receive Data Error: %@", error);
|
||||
}
|
||||
|
||||
completionHandler(data, error);
|
||||
};
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
char bytes[4096];
|
||||
NSMutableData *receivedData = [NSMutableData dataWithCapacity:expectedSize];
|
||||
|
||||
while (receivedData.length < expectedSize)
|
||||
{
|
||||
uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length);
|
||||
|
||||
uint32_t receivedBytes = 0;
|
||||
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 0) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
NSData *data = [NSData dataWithBytesNoCopy:bytes length:receivedBytes freeWhenDone:NO];
|
||||
[receivedData appendData:data];
|
||||
}
|
||||
|
||||
finish(receivedData, nil);
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,231 +0,0 @@
|
||||
//
|
||||
// ClientConnection.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/9/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltSign
|
||||
|
||||
extension ClientConnection
|
||||
{
|
||||
enum Connection
|
||||
{
|
||||
case wireless(NWConnection)
|
||||
case wired(WiredConnection)
|
||||
}
|
||||
}
|
||||
|
||||
class ClientConnection
|
||||
{
|
||||
let connection: Connection
|
||||
|
||||
init(connection: Connection)
|
||||
{
|
||||
self.connection = connection
|
||||
}
|
||||
|
||||
func disconnect()
|
||||
{
|
||||
switch self.connection
|
||||
{
|
||||
case .wireless(let connection):
|
||||
switch connection.state
|
||||
{
|
||||
case .cancelled, .failed:
|
||||
print("Disconnecting from \(connection.endpoint)...")
|
||||
|
||||
default:
|
||||
// State update handler might call this method again.
|
||||
connection.cancel()
|
||||
}
|
||||
|
||||
case .wired(let connection):
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
func send<T: Encodable>(_ response: T, shouldDisconnect: Bool = false, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
func finish(_ result: Result<Void, ALTServerError>)
|
||||
{
|
||||
completionHandler(result)
|
||||
|
||||
if shouldDisconnect
|
||||
{
|
||||
// Add short delay to prevent us from dropping connection too quickly.
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
|
||||
self.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let data = try JSONEncoder().encode(response)
|
||||
let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) }
|
||||
|
||||
self.send(responseSize) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure: finish(.failure(.init(.lostConnection)))
|
||||
case .success:
|
||||
|
||||
self.send(data) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure: finish(.failure(.init(.lostConnection)))
|
||||
case .success: finish(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(.init(.invalidResponse)))
|
||||
}
|
||||
}
|
||||
|
||||
func receiveRequest(completionHandler: @escaping (Result<ServerRequest, ALTServerError>) -> Void)
|
||||
{
|
||||
let size = MemoryLayout<Int32>.size
|
||||
|
||||
print("Receiving request size")
|
||||
self.receiveData(expectedBytes: size) { (result) in
|
||||
do
|
||||
{
|
||||
let data = try result.get()
|
||||
|
||||
print("Receiving request...")
|
||||
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
|
||||
self.receiveData(expectedBytes: expectedBytes) { (result) in
|
||||
do
|
||||
{
|
||||
let data = try result.get()
|
||||
let request = try JSONDecoder().decode(ServerRequest.self, from: data)
|
||||
|
||||
print("Received installation request:", request)
|
||||
completionHandler(.success(request))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ data: Data, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
switch self.connection
|
||||
{
|
||||
case .wireless(let connection):
|
||||
connection.send(content: data, completion: .contentProcessed { (error) in
|
||||
if let error = error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
})
|
||||
|
||||
case .wired(let connection):
|
||||
connection.send(data) { (success, error) in
|
||||
if !success
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(.lostConnection)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receiveData(expectedBytes: Int, completionHandler: @escaping (Result<Data, Error>) -> Void)
|
||||
{
|
||||
func finish(data: Data?, error: Error?)
|
||||
{
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error)
|
||||
completionHandler(.success(data))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
|
||||
switch self.connection
|
||||
{
|
||||
case .wireless(let connection):
|
||||
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
|
||||
finish(data: data, error: error)
|
||||
}
|
||||
|
||||
case .wired(let connection):
|
||||
connection.receiveData(withExpectedSize: expectedBytes) { (data, error) in
|
||||
finish(data: data, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientConnection: CustomStringConvertible
|
||||
{
|
||||
var description: String {
|
||||
switch self.connection
|
||||
{
|
||||
case .wireless(let connection): return "\(connection.endpoint) (Wireless)"
|
||||
case .wired(let connection): return "\(connection.device.name) (Wired)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ClientConnection
|
||||
{
|
||||
func process(data: Data?, error: Error?) throws -> Data
|
||||
{
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let data = data else { throw error ?? ALTServerError(.unknown) }
|
||||
return data
|
||||
}
|
||||
catch let error as NWError
|
||||
{
|
||||
print("Error receiving data from connection \(connection)", error)
|
||||
|
||||
throw ALTServerError(.lostConnection)
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch let error as ALTServerError
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch
|
||||
{
|
||||
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,508 +0,0 @@
|
||||
//
|
||||
// ConnectionManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import AppKit
|
||||
|
||||
import AltKit
|
||||
|
||||
extension ALTServerError
|
||||
{
|
||||
init<E: Error>(_ error: E)
|
||||
{
|
||||
switch error
|
||||
{
|
||||
case let error as ALTServerError: self = error
|
||||
case is DecodingError: self = ALTServerError(.invalidRequest)
|
||||
case is EncodingError: self = ALTServerError(.invalidResponse)
|
||||
case let error as NSError:
|
||||
self = ALTServerError(.unknown, userInfo: error.userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConnectionManager
|
||||
{
|
||||
enum State
|
||||
{
|
||||
case notRunning
|
||||
case connecting
|
||||
case running(NWListener.Service)
|
||||
case failed(Swift.Error)
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionManager
|
||||
{
|
||||
static let shared = ConnectionManager()
|
||||
|
||||
var stateUpdateHandler: ((State) -> Void)?
|
||||
|
||||
private(set) var state: State = .notRunning {
|
||||
didSet {
|
||||
self.stateUpdateHandler?(self.state)
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var listener = self.makeListener()
|
||||
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility)
|
||||
|
||||
private var connections = [ClientConnection]()
|
||||
private var notificationConnections = [ALTDevice: NotificationConnection]()
|
||||
|
||||
private init()
|
||||
{
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
|
||||
}
|
||||
|
||||
func start()
|
||||
{
|
||||
switch self.state
|
||||
{
|
||||
case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
func stop()
|
||||
{
|
||||
switch self.state
|
||||
{
|
||||
case .running: self.listener.cancel()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect(_ connection: ClientConnection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
if let index = self.connections.firstIndex(where: { $0 === connection })
|
||||
{
|
||||
self.connections.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
{
|
||||
func makeListener() -> NWListener
|
||||
{
|
||||
let listener = try! NWListener(using: .tcp)
|
||||
|
||||
let service: NWListener.Service
|
||||
|
||||
if let serverID = UserDefaults.standard.serverID?.data(using: .utf8)
|
||||
{
|
||||
let txtDictionary = ["serverID": serverID]
|
||||
let txtData = NetService.data(fromTXTRecord: txtDictionary)
|
||||
|
||||
service = NWListener.Service(name: nil, type: ALTServerServiceType, domain: nil, txtRecord: txtData)
|
||||
}
|
||||
else
|
||||
{
|
||||
service = NWListener.Service(type: ALTServerServiceType)
|
||||
}
|
||||
|
||||
listener.service = service
|
||||
|
||||
listener.serviceRegistrationUpdateHandler = { (serviceChange) in
|
||||
switch serviceChange
|
||||
{
|
||||
case .add(.service(let name, let type, let domain, _)):
|
||||
let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
|
||||
self.state = .running(service)
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
listener.stateUpdateHandler = { (state) in
|
||||
switch state
|
||||
{
|
||||
case .ready: break
|
||||
case .waiting, .setup: self.state = .connecting
|
||||
case .cancelled: self.state = .notRunning
|
||||
case .failed(let error):
|
||||
self.state = .failed(error)
|
||||
self.start()
|
||||
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
listener.newConnectionHandler = { [weak self] (connection) in
|
||||
self?.prepare(connection)
|
||||
}
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
func prepare(_ connection: NWConnection)
|
||||
{
|
||||
let clientConnection = ClientConnection(connection: .wireless(connection))
|
||||
|
||||
guard !self.connections.contains(where: { $0 === clientConnection }) else { return }
|
||||
self.connections.append(clientConnection)
|
||||
|
||||
connection.stateUpdateHandler = { [weak self] (state) in
|
||||
switch state
|
||||
{
|
||||
case .setup, .preparing: break
|
||||
|
||||
case .ready:
|
||||
print("Connected to client:", connection.endpoint)
|
||||
self?.handleRequest(for: clientConnection)
|
||||
|
||||
case .waiting:
|
||||
print("Waiting for connection...")
|
||||
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(connection.endpoint).", error)
|
||||
self?.disconnect(clientConnection)
|
||||
|
||||
case .cancelled:
|
||||
self?.disconnect(clientConnection)
|
||||
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
{
|
||||
func startNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else { return }
|
||||
|
||||
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
|
||||
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.handle(notification, for: connection)
|
||||
}
|
||||
|
||||
self.notificationConnections[device] = connection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
guard let connection = self.notificationConnections[device] else { return }
|
||||
connection.disconnect()
|
||||
|
||||
self.notificationConnections[device] = nil
|
||||
}
|
||||
|
||||
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
|
||||
{
|
||||
switch notification
|
||||
{
|
||||
case .wiredServerConnectionAvailableRequest:
|
||||
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Error sending wired server connection response.", error)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Sent wired server connection available response!")
|
||||
}
|
||||
}
|
||||
|
||||
case .wiredServerConnectionStartRequest:
|
||||
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
|
||||
if let wiredConnection = wiredConnection
|
||||
{
|
||||
print("Started wired server connection!")
|
||||
|
||||
let clientConnection = ClientConnection(connection: .wired(wiredConnection))
|
||||
self.handleRequest(for: clientConnection)
|
||||
}
|
||||
else if let error = error
|
||||
{
|
||||
print("Error starting wired server connection.", error)
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
{
|
||||
func handleRequest(for connection: ClientConnection)
|
||||
{
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received initial request with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
let response = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent error response with result:", result)
|
||||
}
|
||||
|
||||
case .success(.anisetteData(let request)):
|
||||
self.handleAnisetteDataRequest(request, for: connection)
|
||||
|
||||
case .success(.prepareApp(let request)):
|
||||
self.handlePrepareAppRequest(request, for: connection)
|
||||
|
||||
case .success(.beginInstallation): break
|
||||
|
||||
case .success(.installProvisioningProfiles(let request)):
|
||||
self.handleInstallProvisioningProfilesRequest(request, for: connection)
|
||||
|
||||
case .success(.removeProvisioningProfiles(let request)):
|
||||
self.handleRemoveProvisioningProfilesRequest(request, for: connection)
|
||||
|
||||
case .success(.unknown):
|
||||
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent unknown request response with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: ClientConnection)
|
||||
{
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
let errorResponse = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(errorResponse, shouldDisconnect: true) { (result) in
|
||||
print("Sent anisette data error response with result:", result)
|
||||
}
|
||||
|
||||
case .success(let anisetteData):
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent anisette data response with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: ClientConnection)
|
||||
{
|
||||
var temporaryURL: URL?
|
||||
|
||||
func finish(_ result: Result<Void, ALTServerError>)
|
||||
{
|
||||
if let temporaryURL = temporaryURL
|
||||
{
|
||||
do { try FileManager.default.removeItem(at: temporaryURL) }
|
||||
catch { print("Failed to remove .ipa.", error) }
|
||||
}
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to process request from \(connection).", error)
|
||||
|
||||
let response = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent install app error response to \(connection) with result:", result)
|
||||
}
|
||||
|
||||
case .success:
|
||||
print("Processed request from \(connection).")
|
||||
|
||||
let response = InstallationProgressResponse(progress: 1.0)
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent install app response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.receiveApp(for: request, from: connection) { (result) in
|
||||
print("Received app with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let fileURL):
|
||||
temporaryURL = fileURL
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(.beginInstallation(let installRequest)):
|
||||
print("Installing to device \(request.udid)...")
|
||||
|
||||
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, activeProvisioningProfiles: installRequest.activeProfiles, connection: connection) { (result) in
|
||||
print("Installed to device with result:", result)
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success: finish(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
case .success:
|
||||
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent unknown request error response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receiveApp(for request: PrepareAppRequest, from connection: ClientConnection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
|
||||
{
|
||||
connection.receiveData(expectedBytes: request.contentSize) { (result) in
|
||||
do
|
||||
{
|
||||
print("Received app data!")
|
||||
|
||||
let data = try result.get()
|
||||
|
||||
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
|
||||
|
||||
print("Writing app data...")
|
||||
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
|
||||
try data.write(to: temporaryURL, options: .atomic)
|
||||
|
||||
print("Wrote app to URL:", temporaryURL)
|
||||
|
||||
completionHandler(.success(temporaryURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error processing app data:", error)
|
||||
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, activeProvisioningProfiles: Set<String>?, connection: ClientConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
|
||||
var isSending = false
|
||||
|
||||
var observation: NSKeyValueObservation?
|
||||
|
||||
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
|
||||
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
|
||||
|
||||
if let error = error.map({ $0 as? ALTServerError ?? ALTServerError(.unknown) })
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
|
||||
observation?.invalidate()
|
||||
observation = nil
|
||||
}
|
||||
|
||||
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
|
||||
serialQueue.async {
|
||||
guard !isSending else { return }
|
||||
isSending = true
|
||||
|
||||
print("Progress:", progress.fractionCompleted)
|
||||
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
|
||||
|
||||
connection.send(response) { (result) in
|
||||
serialQueue.async {
|
||||
isSending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: ClientConnection)
|
||||
{
|
||||
ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
|
||||
let errorResponse = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(errorResponse, shouldDisconnect: true) { (result) in
|
||||
print("Sent install profiles error response with result:", result)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent install profiles response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: ClientConnection)
|
||||
{
|
||||
ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
|
||||
let errorResponse = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(errorResponse, shouldDisconnect: true) { (result) in
|
||||
print("Sent remove profiles error response with result:", result)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent remove profiles error response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
{
|
||||
@objc func deviceDidConnect(_ notification: Notification)
|
||||
{
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.startNotificationConnection(to: device)
|
||||
}
|
||||
|
||||
@objc func deviceDidDisconnect(_ notification: Notification)
|
||||
{
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.stopNotificationConnection(to: device)
|
||||
}
|
||||
}
|
||||
@@ -1,600 +0,0 @@
|
||||
//
|
||||
// ALTDeviceManager+Installation.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 7/1/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import UserNotifications
|
||||
import ObjectiveC
|
||||
|
||||
#if STAGING
|
||||
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")!
|
||||
#else
|
||||
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
|
||||
#endif
|
||||
|
||||
enum InstallError: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
case noTeam
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .noTeam: return "You are not a member of any developer teams."
|
||||
case .missingPrivateKey: return "The developer certificate's private key could not be found."
|
||||
case .missingCertificate: return "The developer certificate could not be found."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ALTDeviceManager
|
||||
{
|
||||
func installAltStore(to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
func finish(_ error: Error?, title: String = "")
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
if let error = error
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
try? FileManager.default.removeItem(at: destinationDirectoryURL)
|
||||
}
|
||||
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
do
|
||||
{
|
||||
let anisetteData = try result.get()
|
||||
|
||||
self.authenticate(appleID: appleID, password: password, anisetteData: anisetteData) { (result) in
|
||||
do
|
||||
{
|
||||
let (account, session) = try result.get()
|
||||
|
||||
self.fetchTeam(for: account, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let team = try result.get()
|
||||
|
||||
self.register(device, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let device = try result.get()
|
||||
|
||||
self.fetchCertificate(for: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let certificate = try result.get()
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name)
|
||||
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
self.downloadApp { (result) in
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove downloaded .ipa.", error)
|
||||
}
|
||||
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
|
||||
|
||||
// Refresh anisette data to prevent session timeouts.
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
do
|
||||
{
|
||||
let anisetteData = try result.get()
|
||||
session.anisetteData = anisetteData
|
||||
|
||||
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
|
||||
self.updateFeatures(for: appID, app: application, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
|
||||
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
|
||||
finish(result.error, title: "Failed to Install AltStore")
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Provisioning Profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Update App ID")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Register App")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Refresh Anisette Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Download AltStore")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Certificate")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Register Device")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Team")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Authenticate")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Anisette Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadApp(completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let downloadTask = URLSession.shared.downloadTask(with: appURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
completionHandler(.success(fileURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void)
|
||||
{
|
||||
func handleVerificationCode(_ completionHandler: @escaping (String?) -> Void)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Two-Factor Authentication Enabled", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: "")
|
||||
|
||||
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 22))
|
||||
textField.delegate = self
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.placeholderString = NSLocalizedString("123456", comment: "")
|
||||
alert.accessoryView = textField
|
||||
alert.window.initialFirstResponder = textField
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
self.securityCodeAlert = alert
|
||||
self.securityCodeTextField = textField
|
||||
self.validate()
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
if response == .alertFirstButtonReturn
|
||||
{
|
||||
let code = textField.stringValue
|
||||
completionHandler(code)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, verificationHandler: handleVerificationCode) { (account, session, error) in
|
||||
if let account = account, let session = session
|
||||
{
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(error ?? ALTAppleAPIError(.unknown)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
|
||||
{
|
||||
func finish(_ result: Result<ALTTeam, Error>)
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success(let team):
|
||||
|
||||
var isCancelled = false
|
||||
|
||||
if team.type != .free
|
||||
{
|
||||
DispatchQueue.main.sync {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Installing AltStore will revoke your iOS development certificate.", comment: "")
|
||||
alert.informativeText = NSLocalizedString("""
|
||||
This will not affect apps you've submitted to the App Store, but may cause apps you've installed to your devices with Xcode to stop working until you reinstall them.
|
||||
|
||||
To prevent this from happening, feel free to try again with another Apple ID to install AltStore.
|
||||
""", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let buttonIndex = alert.runModal()
|
||||
if buttonIndex == NSApplication.ModalResponse.alertSecondButtonReturn
|
||||
{
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
if isCancelled
|
||||
{
|
||||
return completionHandler(.failure(InstallError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(.success(team))
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
||||
do
|
||||
{
|
||||
let teams = try Result(teams, error).get()
|
||||
|
||||
if let team = teams.first(where: { $0.type == .free })
|
||||
{
|
||||
return finish(.success(team))
|
||||
}
|
||||
else if let team = teams.first(where: { $0.type == .individual })
|
||||
{
|
||||
return finish(.success(team))
|
||||
}
|
||||
else if let team = teams.first
|
||||
{
|
||||
return finish(.success(team))
|
||||
}
|
||||
else
|
||||
{
|
||||
throw InstallError.noTeam
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
// Check if there is another AltStore certificate, which means AltStore has been installed with this Apple ID before.
|
||||
if certificates.contains(where: { $0.machineName?.starts(with: "AltStore") == true })
|
||||
{
|
||||
var isCancelled = false
|
||||
|
||||
DispatchQueue.main.sync {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("AltStore already installed on another device.", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Apps installed with AltStore on your other devices will stop working. Are you sure you want to continue?", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let buttonIndex = alert.runModal()
|
||||
if buttonIndex == NSApplication.ModalResponse.alertSecondButtonReturn
|
||||
{
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
if isCancelled
|
||||
{
|
||||
return completionHandler(.failure(InstallError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if let certificate = certificates.first
|
||||
{
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
self.fetchCertificate(for: team, session: session, completionHandler: completionHandler)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team, session: session) { (certificate, error) in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw InstallError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
||||
throw InstallError.missingCertificate
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerAppID(name appName: String, identifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let bundleID = "com.\(team.identifier).\(identifier)"
|
||||
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||
do
|
||||
{
|
||||
let appIDs = try Result(appIDs, error).get()
|
||||
|
||||
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||
return (feature, value)
|
||||
}
|
||||
|
||||
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
|
||||
|
||||
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
|
||||
{
|
||||
features[.appGroups] = true
|
||||
}
|
||||
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == device.identifier })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team, session: session) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ application: ALTApplication, to device: ALTDevice, team: ALTTeam, appID: ALTAppID, certificate: ALTCertificate, profile: ALTProvisioningProfile, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
let infoPlistURL = application.fileURL.appendingPathComponent("Info.plist")
|
||||
|
||||
guard var infoDictionary = NSDictionary(contentsOf: infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||
infoDictionary[Bundle.Info.deviceID] = device.identifier
|
||||
infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID
|
||||
infoDictionary[Bundle.Info.certificateID] = certificate.serialNumber
|
||||
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
||||
|
||||
if
|
||||
let machineIdentifier = certificate.machineIdentifier,
|
||||
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
|
||||
{
|
||||
let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12")
|
||||
try encryptedData.write(to: certificateURL, options: .atomic)
|
||||
}
|
||||
|
||||
let resigner = ALTSigner(team: team, certificate: certificate)
|
||||
resigner.signApp(at: application.fileURL, provisioningProfiles: [profile]) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
|
||||
let activeProfiles: Set<String>? = (team.type == .free) ? [profile.bundleIdentifier] : nil
|
||||
ALTDeviceManager.shared.installApp(at: application.fileURL, toDeviceWithUDID: device.identifier, activeProvisioningProfiles: activeProfiles) { (success, error) in
|
||||
completionHandler(Result(success, error))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to install app", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to install AltStore", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var securityCodeAlertKey = 0
|
||||
private var securityCodeTextFieldKey = 0
|
||||
|
||||
extension ALTDeviceManager: NSTextFieldDelegate
|
||||
{
|
||||
var securityCodeAlert: NSAlert? {
|
||||
get { return objc_getAssociatedObject(self, &securityCodeAlertKey) as? NSAlert }
|
||||
set { objc_setAssociatedObject(self, &securityCodeAlertKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
||||
}
|
||||
|
||||
var securityCodeTextField: NSTextField? {
|
||||
get { return objc_getAssociatedObject(self, &securityCodeTextFieldKey) as? NSTextField }
|
||||
set { objc_setAssociatedObject(self, &securityCodeTextFieldKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
||||
}
|
||||
|
||||
public func controlTextDidChange(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
public func controlTextDidEndEditing(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
private func validate()
|
||||
{
|
||||
guard let code = self.securityCodeTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) else { return }
|
||||
|
||||
if code.count == 6
|
||||
{
|
||||
self.securityCodeAlert?.buttons.first?.isEnabled = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.securityCodeAlert?.buttons.first?.isEnabled = false
|
||||
}
|
||||
|
||||
self.securityCodeAlert?.layout()
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// ALTDeviceManager.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AltSign/AltSign.h>
|
||||
|
||||
@class ALTWiredConnection;
|
||||
@class ALTNotificationConnection;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect);
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect);
|
||||
|
||||
@interface ALTDeviceManager : NSObject
|
||||
|
||||
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
|
||||
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *availableDevices;
|
||||
|
||||
- (void)start;
|
||||
|
||||
/* App Installation */
|
||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
/* Connections */
|
||||
- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
- (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// UserDefaults+AltServer.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 7/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UserDefaults
|
||||
{
|
||||
var serverID: String? {
|
||||
get {
|
||||
return self.string(forKey: "serverID")
|
||||
}
|
||||
set {
|
||||
self.set(newValue, forKey: "serverID")
|
||||
}
|
||||
}
|
||||
|
||||
var didPresentInitialNotification: Bool {
|
||||
get {
|
||||
return self.bool(forKey: "didPresentInitialNotification")
|
||||
}
|
||||
set {
|
||||
self.set(newValue, forKey: "didPresentInitialNotification")
|
||||
}
|
||||
}
|
||||
|
||||
func registerDefaults()
|
||||
{
|
||||
if self.serverID == nil
|
||||
{
|
||||
self.serverID = UUID().uuidString
|
||||
}
|
||||
}
|
||||
}
|
||||
3
AltStore.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:AltStore.xcodeproj">
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "altsign",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SideStore/AltSign",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "7e0e7edcf8fbc44ac1e35da3e9030a297aa18b84"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "appcenter-sdk-apple",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
|
||||
"state" : {
|
||||
"revision" : "8354a50fe01a7e54e196d3b5493b5ab53dd5866a",
|
||||
"version" : "4.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "asyncimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/fabianthdev/AsyncImage",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "018a4fffea025066d795ebb025c2769183f3fffb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "expandabletext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/fabianthdev/ExpandableText",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state" : {
|
||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "launchatlogin",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/LaunchAtLogin.git",
|
||||
"state" : {
|
||||
"revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "localconsole",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/duraidabdul/LocalConsole.git",
|
||||
"state" : {
|
||||
"revision" : "2c5d5e018acd4963fe6dfe858f6d6fecef7cbf2f",
|
||||
"version" : "1.12.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke.git",
|
||||
"state" : {
|
||||
"revision" : "9318d02a8a6d20af56505c9673261c1fd3b3aebe",
|
||||
"version" : "7.6.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "openssl",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/OpenSSL",
|
||||
"state" : {
|
||||
"revision" : "033fcb41dac96b1b6effa945ca1f9ade002370b2",
|
||||
"version" : "1.1.1501"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "plcrashreporter",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/microsoft/PLCrashReporter.git",
|
||||
"state" : {
|
||||
"revision" : "6b27393cad517c067dceea85fadf050e70c4ceaa",
|
||||
"version" : "1.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "reachability.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "semanticversion",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
|
||||
"state" : {
|
||||
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
|
||||
"version" : "0.3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sfsafesymbols",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||
"state" : {
|
||||
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
|
||||
"version" : "4.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
||||
"state" : {
|
||||
"revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "starscream",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/daltoniam/Starscream.git",
|
||||
"state" : {
|
||||
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
|
||||
"version" : "4.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "stprivilegedtask",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/JoeMatt/STPrivilegedTask.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
111
AltStore.xcodeproj/xcshareddata/xcschemes/AltDaemon.xcscheme
Normal file
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1150"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
|
||||
BuildableName = "libPods-AltDaemon.a"
|
||||
BlueprintName = "Pods-AltDaemon"
|
||||
ReferencedContainer = "container:Pods/Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "THEOS"
|
||||
value = "~/theos"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -14,26 +14,24 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E32E9B621EB87D7B000FEEE9"
|
||||
BuildableName = "LaunchAtLogin.framework"
|
||||
BlueprintName = "LaunchAtLogin"
|
||||
ReferencedContainer = "container:LaunchAtLogin.xcodeproj">
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
@@ -42,17 +40,22 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E32E9B621EB87D7B000FEEE9"
|
||||
BuildableName = "LaunchAtLogin.framework"
|
||||
BlueprintName = "LaunchAtLogin"
|
||||
ReferencedContainer = "container:LaunchAtLogin.xcodeproj">
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -60,18 +63,19 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E32E9B621EB87D7B000FEEE9"
|
||||
BuildableName = "LaunchAtLogin.framework"
|
||||
BlueprintName = "LaunchAtLogin"
|
||||
ReferencedContainer = "container:LaunchAtLogin.xcodeproj">
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
buildConfiguration = "Release">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -15,8 +15,8 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -29,17 +29,6 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -56,8 +45,8 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -67,8 +56,6 @@
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -81,8 +68,8 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
LastUpgradeVersion = "1230"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -14,9 +14,9 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||
BuildableName = "AltXPC.xpc"
|
||||
BlueprintName = "AltXPC"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -40,6 +40,16 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -50,9 +60,9 @@
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||
BuildableName = "AltXPC.xpc"
|
||||
BlueprintName = "AltXPC"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
16
AltStore.xcworkspace/contents.xcworkspacedata
generated
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "container:AltStore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/AltSign/AltSign.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -2,10 +2,7 @@
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "AltKit.h"
|
||||
|
||||
#import "ALTAppPermission.h"
|
||||
#import "ALTPatreonBenefitType.h"
|
||||
#import "ALTSourceUserInfoKey.h"
|
||||
|
||||
#import "NSAttributedString+Markdown.h"
|
||||
#import "ALTAppPatcher.h"
|
||||
|
||||
#include "fragmentzip.h"
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -8,17 +8,13 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
import AppCenter
|
||||
import AppCenterAnalytics
|
||||
import AppCenterCrashes
|
||||
|
||||
#if DEBUG
|
||||
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
|
||||
#elseif RELEASE
|
||||
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
|
||||
#else
|
||||
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
|
||||
#endif
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
|
||||
extension AnalyticsManager
|
||||
{
|
||||
@@ -75,7 +71,7 @@ extension AnalyticsManager
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsManager
|
||||
final class AnalyticsManager
|
||||
{
|
||||
static let shared = AnalyticsManager()
|
||||
|
||||
@@ -88,9 +84,9 @@ extension AnalyticsManager
|
||||
{
|
||||
func start()
|
||||
{
|
||||
MSAppCenter.start(appCenterAppSecret, withServices:[
|
||||
MSAnalytics.self,
|
||||
MSCrashes.self
|
||||
AppCenter.start(withAppSecret: appCenterAppSecret, services: [
|
||||
Analytics.self,
|
||||
Crashes.self
|
||||
])
|
||||
}
|
||||
|
||||
@@ -100,6 +96,6 @@ extension AnalyticsManager
|
||||
properties[item.key.rawValue] = item.value
|
||||
}
|
||||
|
||||
MSAnalytics.trackEvent(event.name, withProperties: properties)
|
||||
Analytics.trackEvent(event.name, withProperties: properties)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
@@ -24,7 +25,7 @@ extension AppContentViewController
|
||||
}
|
||||
}
|
||||
|
||||
class AppContentViewController: UITableViewController
|
||||
final class AppContentViewController: UITableViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
|
||||
@@ -79,10 +80,21 @@ class AppContentViewController: UITableViewController
|
||||
|
||||
self.subtitleLabel.text = self.app.subtitle
|
||||
self.descriptionTextView.text = self.app.localizedDescription
|
||||
self.versionDescriptionTextView.text = self.app.versionDescription
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
|
||||
|
||||
if let version = self.app.latestVersion
|
||||
{
|
||||
self.versionDescriptionTextView.text = version.localizedDescription
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.versionDescriptionTextView.text = nil
|
||||
self.versionLabel.text = nil
|
||||
self.versionDateLabel.text = nil
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
||||
}
|
||||
|
||||
self.descriptionTextView.maximumNumberOfLines = 5
|
||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
@@ -137,7 +149,8 @@ private extension AppContentViewController
|
||||
}
|
||||
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
|
||||
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image
|
||||
@@ -171,7 +184,8 @@ private extension AppContentViewController
|
||||
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||
let cell = cell as! PermissionCollectionViewCell
|
||||
cell.button.setImage(permission.type.icon, for: .normal)
|
||||
cell.textLabel.text = permission.type.localizedShortName
|
||||
cell.button.tintColor = .label
|
||||
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
|
||||
}
|
||||
|
||||
return dataSource
|
||||
@@ -207,10 +221,19 @@ extension AppContentViewController
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
{
|
||||
guard indexPath.row == Row.screenshots.rawValue else { return super.tableView(tableView, heightForRowAt: indexPath) }
|
||||
|
||||
guard let size = self.preferredScreenshotSize else { return 0.0 }
|
||||
return size.height
|
||||
switch Row.allCases[indexPath.row]
|
||||
{
|
||||
case .screenshots:
|
||||
guard let size = self.preferredScreenshotSize else { return 0.0 }
|
||||
return size.height
|
||||
|
||||
case .permissions:
|
||||
guard !self.app.permissions.isEmpty else { return 0.0 }
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class PermissionCollectionViewCell: UICollectionViewCell
|
||||
final class PermissionCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var button: UIButton!
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
@@ -29,7 +29,7 @@ class PermissionCollectionViewCell: UICollectionViewCell
|
||||
}
|
||||
}
|
||||
|
||||
class AppContentTableViewCell: UITableViewCell
|
||||
final class AppContentTableViewCell: UITableViewCell
|
||||
{
|
||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||
{
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
class AppViewController: UIViewController
|
||||
final class AppViewController: UIViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
|
||||
@@ -81,14 +82,14 @@ class AppViewController: UIViewController
|
||||
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
||||
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
|
||||
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||
self.bannerView.titleLabel.text = self.app.name
|
||||
self.bannerView.subtitleLabel.text = self.app.developerName
|
||||
self.bannerView.iconImageView.image = nil
|
||||
self.bannerView.iconImageView.tintColor = self.app.tintColor
|
||||
self.bannerView.button.tintColor = self.app.tintColor
|
||||
self.bannerView.betaBadgeView.isHidden = !self.app.isBeta
|
||||
self.bannerView.tintColor = self.app.tintColor
|
||||
|
||||
self.bannerView.configure(for: self.app)
|
||||
self.bannerView.accessibilityTraits.remove(.button)
|
||||
|
||||
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
|
||||
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||
@@ -187,6 +188,13 @@ class AppViewController: UIViewController
|
||||
|
||||
self.contentViewController = segue.destination as? AppContentViewController
|
||||
self.contentViewController.app = self.app
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
// Fix navigation bar + tab bar appearance on iOS 15.
|
||||
self.setContentScrollView(self.scrollView)
|
||||
self.navigationItem.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
@@ -344,7 +352,7 @@ class AppViewController: UIViewController
|
||||
|
||||
extension AppViewController
|
||||
{
|
||||
class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||
final class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||
{
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
@@ -376,10 +384,10 @@ private extension AppViewController
|
||||
button.progress = progress
|
||||
}
|
||||
|
||||
if Date() < self.app.versionDate
|
||||
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
|
||||
{
|
||||
self.bannerView.button.countdownDate = self.app.versionDate
|
||||
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
|
||||
self.bannerView.button.countdownDate = versionDate
|
||||
self.navigationBarDownloadButton.countdownDate = versionDate
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -490,7 +498,7 @@ extension AppViewController
|
||||
{
|
||||
guard self.app.installedApp == nil else { return }
|
||||
|
||||
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
let group = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
@@ -514,8 +522,8 @@ extension AppViewController
|
||||
}
|
||||
}
|
||||
|
||||
self.bannerView.button.progress = progress
|
||||
self.navigationBarDownloadButton.progress = progress
|
||||
self.bannerView.button.progress = group.progress
|
||||
self.navigationBarDownloadButton.progress = group.progress
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp)
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class PermissionPopoverViewController: UIViewController
|
||||
import AltStoreCore
|
||||
|
||||
final class PermissionPopoverViewController: UIViewController
|
||||
{
|
||||
var permission: AppPermission!
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class AppIDsViewController: UICollectionViewController
|
||||
final class AppIDsViewController: UICollectionViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
@@ -78,10 +79,11 @@ private extension AppIDsViewController
|
||||
|
||||
cell.bannerView.iconImageView.isHidden = true
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
cell.bannerView.betaBadgeView.isHidden = true
|
||||
|
||||
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||
|
||||
let attributedAccessibilityLabel = NSMutableAttributedString(string: appID.name + ". ")
|
||||
|
||||
if let expirationDate = appID.expirationDate
|
||||
{
|
||||
cell.bannerView.button.isHidden = false
|
||||
@@ -92,19 +94,16 @@ private extension AppIDsViewController
|
||||
let currentDate = Date()
|
||||
|
||||
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
|
||||
let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
|
||||
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
|
||||
|
||||
if numberOfDays == 1
|
||||
{
|
||||
cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
|
||||
}
|
||||
attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ")
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.isHidden = true
|
||||
cell.bannerView.button.isUserInteractionEnabled = true
|
||||
|
||||
cell.bannerView.buttonLabel.isHidden = true
|
||||
}
|
||||
|
||||
@@ -112,6 +111,18 @@ private extension AppIDsViewController
|
||||
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
|
||||
cell.bannerView.subtitleLabel.numberOfLines = 2
|
||||
|
||||
let attributedBundleIdentifier = NSMutableAttributedString(string: appID.bundleIdentifier.lowercased(), attributes: [.accessibilitySpeechPunctuation: true])
|
||||
|
||||
if let team = appID.team, let range = attributedBundleIdentifier.string.range(of: team.identifier.lowercased()), #available(iOS 13, *)
|
||||
{
|
||||
// Prefer to speak the team ID one character at a time.
|
||||
let nsRange = NSRange(range, in: attributedBundleIdentifier.string)
|
||||
attributedBundleIdentifier.addAttributes([.accessibilitySpeechSpellOut: true], range: nsRange)
|
||||
}
|
||||
|
||||
attributedAccessibilityLabel.append(attributedBundleIdentifier)
|
||||
cell.bannerView.accessibilityAttributedLabel = attributedAccessibilityLabel
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
}
|
||||
@@ -190,9 +201,9 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
||||
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
|
||||
{
|
||||
let text = NSLocalizedString("""
|
||||
Each app and app extension installed with AltStore must register an App ID with Apple. Apple limits free developer accounts to 10 App IDs at a time.
|
||||
Each app and app extension installed with SideStore must register an App ID with Apple. Apple limits non-developer Apple IDs to 10 App IDs at a time.
|
||||
|
||||
**App IDs can't be deleted**, but they do expire after one week. AltStore will automatically renew App IDs for all active apps once they've expired.
|
||||
**App IDs can't be deleted**, but they do expire after one week. SideStore will automatically renew App IDs for all active apps once they've expired.
|
||||
""", comment: "")
|
||||
|
||||
let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any])
|
||||
@@ -201,7 +212,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
||||
else
|
||||
{
|
||||
headerView.textLabel.text = NSLocalizedString("""
|
||||
Each app and app extension installed with AltStore must register an App ID with Apple.
|
||||
Each app and app extension installed with SideStore must register an App ID with Apple.
|
||||
|
||||
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
|
||||
""", comment: "")
|
||||
|
||||
18
AltStore/App/SideStoreUIApp.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// SideStoreUIApp.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SideStoreUIApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,72 +9,77 @@
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import AVFoundation
|
||||
import Intents
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltKit
|
||||
import Roxas
|
||||
|
||||
private enum RefreshError: LocalizedError
|
||||
{
|
||||
case noInstalledApps
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
|
||||
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
|
||||
|
||||
static func requestAppState(for appID: String) -> CFNotificationName
|
||||
{
|
||||
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
|
||||
return CFNotificationName(name as CFString)
|
||||
}
|
||||
|
||||
static func appIsRunning(for appID: String) -> CFNotificationName
|
||||
{
|
||||
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
|
||||
return CFNotificationName(name as CFString)
|
||||
}
|
||||
}
|
||||
|
||||
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||
{ (center, observer, name, object, userInfo) in
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let name = name else { return }
|
||||
appDelegate.receivedApplicationState(notification: name)
|
||||
}
|
||||
import EmotionalDamage
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
||||
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
|
||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
|
||||
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
|
||||
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
||||
|
||||
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
||||
|
||||
static let importAppDeepLinkURLKey = "fileURL"
|
||||
static let appBackupResultKey = "result"
|
||||
static let addSourceDeepLinkURLKey = "sourceURL"
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
private var runningApplications: Set<String>?
|
||||
private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing.
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var intentHandler: IntentHandler {
|
||||
get { _intentHandler as! IntentHandler }
|
||||
set { _intentHandler = newValue }
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var viewAppIntentHandler: ViewAppIntentHandler {
|
||||
get { _viewAppIntentHandler as! ViewAppIntentHandler }
|
||||
set { _viewAppIntentHandler = newValue }
|
||||
}
|
||||
|
||||
private lazy var _intentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return IntentHandler()
|
||||
}()
|
||||
|
||||
private lazy var _viewAppIntentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return ViewAppIntentHandler()
|
||||
}()
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Copy STDOUT and STDERR to the logging console
|
||||
_ = OutputCapturer.shared
|
||||
|
||||
// Register default settings before doing anything else.
|
||||
UserDefaults.registerDefaults()
|
||||
|
||||
DatabaseManager.shared.start { (error) in
|
||||
if let error = error
|
||||
{
|
||||
print("Failed to start DatabaseManager. Error:", error as Any)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Started DatabaseManager.")
|
||||
}
|
||||
}
|
||||
|
||||
AnalyticsManager.shared.start()
|
||||
|
||||
self.setTintColor()
|
||||
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
UserDefaults.standard.registerDefaults()
|
||||
SecureValueTransformer.register()
|
||||
|
||||
if UserDefaults.standard.firstLaunch == nil
|
||||
{
|
||||
@@ -95,21 +100,61 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication)
|
||||
{
|
||||
ServerManager.shared.stopDiscovering()
|
||||
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||
|
||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||
|
||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication)
|
||||
{
|
||||
AppManager.shared.update()
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
{
|
||||
return self.open(url)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
||||
{
|
||||
guard #available(iOS 14, *) else { return nil }
|
||||
|
||||
switch intent
|
||||
{
|
||||
case is RefreshAllIntent: return self.intentHandler
|
||||
case is ViewAppIntent: return self.viewAppIntentHandler
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension AppDelegate
|
||||
{
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
|
||||
{
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
||||
{
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
@@ -134,13 +179,63 @@ private extension AppDelegate
|
||||
else
|
||||
{
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let host = components.host, host.lowercased() == "patreon" else { return false }
|
||||
guard let host = components.host?.lowercased() else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
switch host
|
||||
{
|
||||
case "patreon":
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case "appbackupresponse":
|
||||
let result: Result<Void, Error>
|
||||
|
||||
switch url.path.lowercased()
|
||||
{
|
||||
case "/success": result = .success(())
|
||||
case "/failure":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
||||
guard
|
||||
let errorDomain = queryItems["errorDomain"],
|
||||
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
||||
let errorDescription = queryItems["errorDescription"]
|
||||
else { return false }
|
||||
|
||||
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||
result = .failure(error)
|
||||
|
||||
default: return false
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||
|
||||
return true
|
||||
|
||||
case "install":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case "source":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
default: return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,95 +272,92 @@ extension AppDelegate
|
||||
|
||||
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||
{
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
||||
{
|
||||
ServerManager.shared.startDiscovering()
|
||||
let threeHours: TimeInterval = 3 * 60 * 60
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||
|
||||
if !UserDefaults.standard.presentedLaunchReminderNotification
|
||||
{
|
||||
let threeHours: TimeInterval = 3 * 60 * 60
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||
content.body = NSLocalizedString("The more you open AltStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||
}
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||
}
|
||||
|
||||
let refreshIdentifier = UUID().uuidString
|
||||
|
||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||
|
||||
func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
|
||||
{
|
||||
// If finish is actually called, that means an error occured during installation.
|
||||
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
ServerManager.shared.stopDiscovering()
|
||||
self.scheduleFinishedRefreshingNotification(for: result, identifier: refreshIdentifier, delay: 0)
|
||||
}
|
||||
|
||||
taskCompletionHandler()
|
||||
|
||||
self.backgroundRefreshContext = nil
|
||||
}
|
||||
|
||||
if let error = taskResult.error
|
||||
{
|
||||
print("Error starting extended background task. Aborting.", error)
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
finish(.failure(error))
|
||||
taskCompletionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
if !DatabaseManager.shared.isStarted
|
||||
{
|
||||
DatabaseManager.shared.start() { (error) in
|
||||
if let error = error
|
||||
if error != nil
|
||||
{
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
finish(.failure(error))
|
||||
taskCompletionHandler()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||
self.performBackgroundFetch { (backgroundFetchResult) in
|
||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
||||
taskCompletionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||
self.performBackgroundFetch { (backgroundFetchResult) in
|
||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
||||
taskCompletionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||
{
|
||||
self.fetchSources { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||
case .success: backgroundFetchCompletionHandler(.newData)
|
||||
}
|
||||
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
refreshAppsCompletionHandler(.success([:]))
|
||||
}
|
||||
}
|
||||
|
||||
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
func refreshApps(identifier: String,
|
||||
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
|
||||
{
|
||||
var fetchSourcesResult: Result<Set<Source>, Error>?
|
||||
var serversResult: Result<Void, Error>?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
dispatchGroup.enter()
|
||||
|
||||
AppManager.shared.fetchSources() { (result) in
|
||||
fetchSourcesResult = result
|
||||
|
||||
do
|
||||
{
|
||||
let sources = try result.get()
|
||||
|
||||
guard let context = sources.first?.managedObjectContext else { return }
|
||||
let (sources, context) = try result.get()
|
||||
|
||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||
@@ -291,11 +383,11 @@ private extension AppDelegate
|
||||
for update in updates
|
||||
{
|
||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||
guard let storeApp = update.storeApp else { continue }
|
||||
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
|
||||
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
|
||||
content.sound = .default
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
@@ -315,7 +407,7 @@ private extension AppDelegate
|
||||
}
|
||||
else
|
||||
{
|
||||
content.title = NSLocalizedString("AltStore News", comment: "")
|
||||
content.title = NSLocalizedString("SideStore News", comment: "")
|
||||
}
|
||||
|
||||
content.body = newsItem.title
|
||||
@@ -328,223 +420,14 @@ private extension AppDelegate
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
||||
}
|
||||
|
||||
completionHandler(.success(sources))
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error fetching apps:", error)
|
||||
|
||||
fetchSourcesResult = .failure(error)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||
guard !installedApps.isEmpty else {
|
||||
serversResult = .success(())
|
||||
dispatchGroup.leave()
|
||||
|
||||
completionHandler(.failure(RefreshError.noInstalledApps))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.runningApplications = []
|
||||
self.backgroundRefreshContext = context
|
||||
|
||||
let identifiers = installedApps.compactMap { $0.bundleIdentifier }
|
||||
print("Apps to refresh:", identifiers)
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
|
||||
for identifier in identifiers
|
||||
{
|
||||
let appIsRunningNotification = CFNotificationName.appIsRunning(for: identifier)
|
||||
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
|
||||
|
||||
let requestAppStateNotification = CFNotificationName.requestAppState(for: identifier)
|
||||
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for three seconds to:
|
||||
// a) give us time to discover AltServers
|
||||
// b) give other processes a chance to respond to requestAppState notification
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
context.perform {
|
||||
if ServerManager.shared.discoveredServers.isEmpty
|
||||
{
|
||||
serversResult = .failure(ConnectionError.serverNotFound)
|
||||
}
|
||||
else
|
||||
{
|
||||
serversResult = .success(())
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
|
||||
let filteredApps = installedApps.filter { !(self.runningApplications?.contains($0.bundleIdentifier) ?? false) }
|
||||
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
|
||||
|
||||
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
|
||||
group.beginInstallationHandler = { (installedApp) in
|
||||
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
|
||||
|
||||
// We're starting to install AltStore, which means the app is about to quit.
|
||||
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
|
||||
// but if the app is still running, we cancel the notification.
|
||||
// Then, we schedule another notification and repeat the process.
|
||||
|
||||
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
|
||||
|
||||
if let error = group.context.error
|
||||
{
|
||||
self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier)
|
||||
}
|
||||
else
|
||||
{
|
||||
var results = group.results
|
||||
results[installedApp.bundleIdentifier] = .success(installedApp)
|
||||
|
||||
self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier)
|
||||
}
|
||||
}
|
||||
group.completionHandler = { (results) in
|
||||
completionHandler(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
guard let fetchSourcesResult = fetchSourcesResult else {
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
switch fetchSourcesResult
|
||||
{
|
||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||
case .success: backgroundFetchCompletionHandler(.newData)
|
||||
}
|
||||
|
||||
completionHandler(.success([:]))
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let fetchSourcesResult = fetchSourcesResult, let serversResult = serversResult else {
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
// Call completionHandler early to improve chances of refreshing in the background again.
|
||||
switch (fetchSourcesResult, serversResult)
|
||||
{
|
||||
case (.success, .success): backgroundFetchCompletionHandler(.newData)
|
||||
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
|
||||
case (.failure, _), (_, .failure): backgroundFetchCompletionHandler(.failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivedApplicationState(notification: CFNotificationName)
|
||||
{
|
||||
let baseName = String(CFNotificationName.appIsRunning.rawValue)
|
||||
|
||||
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
|
||||
self.runningApplications?.insert(appID)
|
||||
}
|
||||
|
||||
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, identifier: String, delay: TimeInterval = 5)
|
||||
{
|
||||
func scheduleFinishedRefreshingNotification()
|
||||
{
|
||||
self.cancelFinishedRefreshingNotification(identifier: identifier)
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
var shouldPresentAlert = true
|
||||
|
||||
do
|
||||
{
|
||||
let results = try result.get()
|
||||
shouldPresentAlert = !results.isEmpty
|
||||
|
||||
for (_, result) in results
|
||||
{
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
||||
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
||||
}
|
||||
catch ConnectionError.serverNotFound
|
||||
{
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
catch RefreshError.noInstalledApps
|
||||
{
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to refresh apps in background.", error)
|
||||
|
||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||
content.body = error.localizedDescription
|
||||
|
||||
shouldPresentAlert = true
|
||||
}
|
||||
|
||||
if shouldPresentAlert
|
||||
{
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
if delay > 0
|
||||
{
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests() { (requests) in
|
||||
// If app is still running at this point, we schedule another notification with same identifier.
|
||||
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
|
||||
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
|
||||
// and we should stop polling.
|
||||
guard requests.contains(where: { $0.identifier == identifier }) else { return }
|
||||
|
||||
scheduleFinishedRefreshingNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scheduleFinishedRefreshingNotification()
|
||||
|
||||
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
_ = RefreshAttempt(identifier: identifier, result: result, context: context)
|
||||
|
||||
do { try context.save() }
|
||||
catch { print("Failed to save refresh attempt.", error) }
|
||||
}
|
||||
}
|
||||
|
||||
func cancelFinishedRefreshingNotification(identifier: String)
|
||||
{
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15703"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -56,14 +56,14 @@
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to AltStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="333.5" height="41"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="332" height="41"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
|
||||
<rect key="frame" x="0.0" y="47" width="308.5" height="20.5"/>
|
||||
<rect key="frame" x="0.0" y="47" width="306.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -160,7 +160,7 @@
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
||||
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
@@ -210,6 +210,7 @@
|
||||
</constraints>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="zMn-DV-fpy"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
|
||||
@@ -229,7 +230,6 @@
|
||||
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
|
||||
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="zMn-DV-fpy"/>
|
||||
</view>
|
||||
<toolbarItems/>
|
||||
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="jCf-N4-xVD">
|
||||
@@ -281,13 +281,13 @@
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
|
||||
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch AltServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
|
||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave AltServer running in the background on your computer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -310,16 +310,16 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
|
||||
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
|
||||
<rect key="frame" x="79" y="17" width="264" height="61.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to WiFi" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
|
||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable iTunes WiFi Sync and connect to the same WiFi as AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -349,7 +349,7 @@
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from AltStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -372,7 +372,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
|
||||
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||
<rect key="frame" x="79" y="17" width="264" height="62"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||
@@ -380,8 +380,8 @@
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background when on same WiFi as AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
|
||||
<rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -393,7 +393,7 @@
|
||||
</subviews>
|
||||
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
||||
<rect key="frame" x="16" y="608" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
@@ -408,6 +408,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Zek-aC-HOO"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstItem="qZ9-AR-2zK" firstAttribute="top" secondItem="bp6-55-IG2" secondAttribute="bottom" id="3yt-cr-swd"/>
|
||||
@@ -418,7 +419,6 @@
|
||||
<constraint firstAttribute="bottomMargin" secondItem="qZ9-AR-2zK" secondAttribute="bottom" id="e8e-9l-Mkt"/>
|
||||
<constraint firstItem="qZ9-AR-2zK" firstAttribute="leading" secondItem="Otz-hn-WGS" secondAttribute="leadingMargin" id="t2b-3e-6ld"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Zek-aC-HOO"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="How it works" largeTitleDisplayMode="always" id="bCq-Jq-gf1"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
@@ -445,7 +445,7 @@
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
|
||||
<rect key="frame" x="16" y="570" width="343" height="89"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
@@ -460,7 +460,7 @@
|
||||
<action selector="refreshAltStore:" destination="aoK-yE-UVT" eventType="primaryActionTriggered" id="WQu-9b-Zgg"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
||||
<rect key="frame" x="0.0" y="59" width="343" height="30"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" title="Refresh Later">
|
||||
@@ -473,6 +473,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="iwE-xE-ziz"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstItem="fpO-Bf-gFY" firstAttribute="leading" secondItem="iwE-xE-ziz" secondAttribute="leading" id="A77-nX-Wg2"/>
|
||||
@@ -483,7 +484,6 @@
|
||||
<constraint firstItem="fpO-Bf-gFY" firstAttribute="top" secondItem="R83-kV-365" secondAttribute="top" id="oKo-10-7kD"/>
|
||||
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="iwE-xE-ziz"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
@@ -493,10 +493,72 @@
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2101.5999999999999" y="733.5832083958021"/>
|
||||
<point key="canvasLocation" x="2967" y="736"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<!--Select a Team-->
|
||||
<scene sceneID="ioQ-WB-CLJ">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
|
||||
<rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
|
||||
<rect key="frame" x="30" y="10" width="56.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf">
|
||||
<rect key="frame" x="30" y="33.5" width="70" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="0"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<sections/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="kOD-4P-a6L" id="OLE-fk-1MD"/>
|
||||
<outlet property="delegate" destination="kOD-4P-a6L" id="t9T-jO-TrR"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Select a Team" largeTitleDisplayMode="always" id="qxJ-Go-OPq"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1401" y="734"/>
|
||||
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
<resources>
|
||||
<namedColor name="Primary">
|
||||
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="SettingsBackground">
|
||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
@@ -504,5 +566,4 @@
|
||||
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
</document>
|
||||
|
||||
@@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
import AltSign
|
||||
|
||||
class AuthenticationViewController: UIViewController
|
||||
final class AuthenticationViewController: UIViewController
|
||||
{
|
||||
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
||||
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
||||
@@ -31,7 +31,7 @@ class AuthenticationViewController: UIViewController
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.signInButton.activityIndicatorView.style = .white
|
||||
self.signInButton.activityIndicatorView.style = .medium
|
||||
|
||||
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class InstructionsViewController: UIViewController
|
||||
final class InstructionsViewController: UIViewController
|
||||
{
|
||||
var completionHandler: (() -> Void)?
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AltSign
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
class RefreshAltStoreViewController: UIViewController
|
||||
final class RefreshAltStoreViewController: UIViewController
|
||||
{
|
||||
var context: AuthenticatedOperationContext!
|
||||
|
||||
@@ -27,7 +28,7 @@ class RefreshAltStoreViewController: UIViewController
|
||||
|
||||
self.placeholderView.detailTextLabel.textAlignment = .left
|
||||
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("AltStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including AltStore.\n\nTo prevent AltStore from expiring early, please refresh the app now. AltStore will quit once refreshing is complete.", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ private extension RefreshAltStoreViewController
|
||||
}
|
||||
|
||||
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
|
||||
let progress = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
|
||||
let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: self.completionHandler?(.success(()))
|
||||
@@ -57,7 +58,7 @@ private extension RefreshAltStoreViewController
|
||||
sender.progress = nil
|
||||
sender.isIndicatingActivity = false
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh AltStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
|
||||
refresh()
|
||||
}))
|
||||
@@ -70,7 +71,7 @@ private extension RefreshAltStoreViewController
|
||||
}
|
||||
}
|
||||
|
||||
sender.progress = progress
|
||||
sender.progress = group.progress
|
||||
}
|
||||
|
||||
refresh()
|
||||
|
||||
61
AltStore/Authentication/SelectTeamViewController.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// SelectTeamViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Megarushing on 4/26/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import MessageUI
|
||||
import Intents
|
||||
import IntentsUI
|
||||
|
||||
import AltSign
|
||||
|
||||
final class SelectTeamViewController: UITableViewController
|
||||
{
|
||||
public var teams: [ALTTeam]?
|
||||
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
|
||||
|
||||
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return teams?.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
return self.completionHandler!(.success((self.teams?[indexPath.row])!))
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "TeamCell", for: indexPath) as! InsetGroupTableViewCell
|
||||
|
||||
cell.textLabel?.text = self.teams?[indexPath.row].name
|
||||
cell.detailTextLabel?.text = self.teams?[indexPath.row].type.localizedDescription
|
||||
if indexPath.row == 0
|
||||
{
|
||||
cell.style = InsetGroupTableViewCell.Style.top
|
||||
} else if indexPath.row == self.tableView(self.tableView, numberOfRowsInSection: indexPath.section) - 1 {
|
||||
cell.style = InsetGroupTableViewCell.Style.bottom
|
||||
} else {
|
||||
cell.style = InsetGroupTableViewCell.Style.middle
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
"Teams"
|
||||
}
|
||||
|
||||
}
|
||||