Asks user to review permissions when installing/updating apps

When installing, all entitlements will be shown. When updating, only _added_ entitlements will be shown.
This commit is contained in:
Riley Testut
2023-12-07 17:15:08 -06:00
committed by Magesh K
parent d625b381d9
commit cf09843538
6 changed files with 558 additions and 71 deletions

View File

@@ -162,51 +162,11 @@ final class VerifyAppOperation: ResultOperation<Void>
Task<Void, Never> {
do
{
do
{
guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) }
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
// Verify permissions last in case user bypasses error.
try await self.verifyPermissions(of: app, match: appVersion)
}
catch let error as VerificationError where error.code == .undeclaredPermissions
{
#if !BETA
throw error
#endif
if let recommendedSources = UserDefaults.shared.recommendedSources, let sourceID = await self.context.$appVersion.sourceID
{
let isRecommended = recommendedSources.contains { $0.identifier == sourceID }
guard !isRecommended else {
// Don't enforce permission checking for Recommended Sources while 2.0 is in beta.
return self.finish(.success(()))
}
}
// While in beta, allow users to temporarily bypass permissions alert
// so source maintainers have time to update their sources.
guard let presentingViewController = self.context.presentingViewController else { throw error }
let message = NSLocalizedString("While AltStore 2.0 is in beta, you may choose to ignore this warning at your own risk until the source is updated.", comment: "")
let ignoreAction = await UIAlertAction(title: NSLocalizedString("Install Anyway", comment: ""), style: .destructive)
let viewPermissionsAction = await UIAlertAction(title: NSLocalizedString("View Permisions", comment: ""), style: .default)
while true
{
let action = try await presentingViewController.presentConfirmationAlert(title: error.errorFailureReason,
message: message,
actions: [ignoreAction, viewPermissionsAction])
guard action == viewPermissionsAction else { break } // break loop to continue with installation (unless we're viewing permissions).
await presentingViewController.presentAlert(title: NSLocalizedString("Undeclared Permissions", comment: ""), message: error.recoverySuggestion)
}
}
guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) }
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
try await self.verifyPermissions(of: app, match: appVersion)
self.finish(.success(()))
}
@@ -257,26 +217,45 @@ private extension VerifyAppOperation
guard let storeApp = await $appVersion.app else { throw OperationError.invalidParameters }
// Verify source permissions match first.
_ = try await self.verifyPermissions(of: app, match: storeApp)
let allPermissions = try await self.verifyPermissions(of: app, match: storeApp)
// TODO: Uncomment to verify added permissions.
// switch self.permissionsMode
// {
// case .none, .all: break
// case .added:
// let installedAppURL = InstalledApp.fileURL(for: app)
// guard let previousApp = ALTApplication(fileURL: installedAppURL) else { throw OperationError.appNotFound(name: app.name) }
//
// var previousEntitlements = Set(previousApp.entitlements.keys)
// for appExtension in previousApp.appExtensions
// {
// previousEntitlements.formUnion(appExtension.entitlements.keys)
// }
//
// // Make sure all entitlements already exist in previousApp.
// let addedEntitlements = Array(allPermissions.lazy.compactMap { $0 as? ALTEntitlement }.filter { !previousEntitlements.contains($0) })
// guard addedEntitlements.isEmpty else { throw VerificationError.addedPermissions(addedEntitlements, appVersion: appVersion) }
// }
guard #available(iOS 15, *) else {
// Only review downloaded app permissions on iOS 15 and above.
return
}
switch self.permissionsMode
{
case .none: break
case .all:
guard let presentingViewController = self.context.presentingViewController else { break } // Don't fail just because we can't show permissions.
let allEntitlements = allPermissions.compactMap { $0 as? ALTEntitlement }
if !allEntitlements.isEmpty
{
try await self.review(allEntitlements, for: app, mode: .all, presentingViewController: presentingViewController)
}
case .added:
let installedAppURL = InstalledApp.fileURL(for: app)
guard let previousApp = ALTApplication(fileURL: installedAppURL) else { throw OperationError.appNotFound(name: app.name) }
var previousEntitlements = Set(previousApp.entitlements.keys)
for appExtension in previousApp.appExtensions
{
previousEntitlements.formUnion(appExtension.entitlements.keys)
}
// Make sure all entitlements already exist in previousApp.
let addedEntitlements = Array(allPermissions.lazy.compactMap { $0 as? ALTEntitlement }.filter { !previousEntitlements.contains($0) })
if !addedEntitlements.isEmpty
{
// _DO_ throw error if there isn't a presentingViewController.
guard let presentingViewController = self.context.presentingViewController else { throw VerificationError.addedPermissions(addedEntitlements, appVersion: appVersion) }
try await self.review(addedEntitlements, for: app, mode: .added, presentingViewController: presentingViewController)
}
}
}
@discardableResult
@@ -345,11 +324,70 @@ private extension VerifyAppOperation
return true
}
guard missingPermissions.isEmpty else {
// There is at least one undeclared permission, so throw error.
throw VerificationError.undeclaredPermissions(missingPermissions, app: app)
do
{
guard missingPermissions.isEmpty else {
// There is at least one undeclared permission, so throw error.
throw VerificationError.undeclaredPermissions(missingPermissions, app: app)
}
}
catch let error as VerificationError where error.code == .undeclaredPermissions
{
#if !BETA
throw error
#endif
if let recommendedSources = UserDefaults.shared.recommendedSources, let (sourceID, sourceURL) = await $storeApp.perform({ $0.source.map { ($0.identifier, $0.sourceURL) } })
{
let normalizedSourceURL = try? sourceURL.normalized()
let isRecommended = recommendedSources.contains { $0.identifier == sourceID || (try? $0.sourceURL?.normalized()) == normalizedSourceURL }
guard !isRecommended else {
// Don't enforce permission checking for Recommended Sources while 2.0 is in beta.
return localPermissions
}
}
// While in beta, allow users to temporarily bypass permissions alert
// so source maintainers have time to update their sources.
guard let presentingViewController = self.context.presentingViewController else { throw error }
let message = NSLocalizedString("While AltStore 2.0 is in beta, you may choose to ignore this warning at your own risk until the source is updated.", comment: "")
let ignoreAction = await UIAlertAction(title: NSLocalizedString("Install Anyway", comment: ""), style: .destructive)
let viewPermissionsAction = await UIAlertAction(title: NSLocalizedString("View Permisions", comment: ""), style: .default)
while true
{
let action = try await presentingViewController.presentConfirmationAlert(title: error.errorFailureReason,
message: message,
actions: [ignoreAction, viewPermissionsAction])
guard action == viewPermissionsAction else { break } // break loop to continue with installation (unless we're viewing permissions).
await presentingViewController.presentAlert(title: NSLocalizedString("Undeclared Permissions", comment: ""), message: error.recoverySuggestion)
}
}
return localPermissions
}
@MainActor @available(iOS 15, *)
func review(_ permissions: [ALTEntitlement], for app: AppProtocol, mode: PermissionReviewMode, presentingViewController: UIViewController) async throws
{
let reviewPermissionsViewController = ReviewPermissionsViewController(app: app, permissions: permissions, mode: mode)
let navigationController = UINavigationController(rootViewController: reviewPermissionsViewController)
defer {
navigationController.dismiss(animated: true)
}
try await withCheckedThrowingContinuation { continuation in
reviewPermissionsViewController.completionHandler = { result in
continuation.resume(with: result)
}
presentingViewController.present(navigationController, animated: true)
}
}
}