Updates SourceError.blocked recovery suggestion to list installed/blocked apps

If source is already added, the error message will list all installed apps from the source.

If adding source for first time, the error message will mention exactly which apps have been blocked from the source (if provided).
This commit is contained in:
Riley Testut
2023-05-16 15:39:38 -05:00
committed by Magesh K
parent 177d453491
commit 5a2f32704c
8 changed files with 202 additions and 86 deletions

View File

@@ -369,6 +369,7 @@
D586D39B28EF58B0000E101F /* AltTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586D39A28EF58B0000E101F /* AltTests.swift */; }; D586D39B28EF58B0000E101F /* AltTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586D39A28EF58B0000E101F /* AltTests.swift */; };
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; }; D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; };
D5893F802A1419E800E767CD /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */; }; D5893F802A1419E800E767CD /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */; };
D5893F822A141E4900E767CD /* KnownSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F812A141E4900E767CD /* KnownSource.swift */; };
D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; }; D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; };
D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */; }; D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */; };
D59162AD29BA616A005CBF47 /* SourceHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */; }; D59162AD29BA616A005CBF47 /* SourceHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */; };
@@ -931,6 +932,7 @@
D586D39A28EF58B0000E101F /* AltTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltTests.swift; sourceTree = "<group>"; }; D586D39A28EF58B0000E101F /* AltTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltTests.swift; sourceTree = "<group>"; };
D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = "<group>"; }; D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = "<group>"; };
D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; }; D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
D5893F812A141E4900E767CD /* KnownSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnownSource.swift; sourceTree = "<group>"; };
D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceHeaderView.swift; sourceTree = "<group>"; }; D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceHeaderView.swift; sourceTree = "<group>"; };
D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SourceHeaderView.xib; sourceTree = "<group>"; }; D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SourceHeaderView.xib; sourceTree = "<group>"; };
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBarAppearance+TintColor.swift"; sourceTree = "<group>"; }; D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBarAppearance+TintColor.swift"; sourceTree = "<group>"; };
@@ -1224,6 +1226,7 @@
BF41B807233433C100C593A3 /* LoadingState.swift */, BF41B807233433C100C593A3 /* LoadingState.swift */,
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */, D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */,
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */, D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */,
D5893F812A141E4900E767CD /* KnownSource.swift */,
); );
path = Types; path = Types;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2722,6 +2725,7 @@
D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */, D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */,
D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */, D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */,
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
D5893F822A141E4900E767CD /* KnownSource.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */, D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */,

View File

@@ -369,8 +369,8 @@ extension AppManager
// sure there isn't already a source with this identifier. // sure there isn't already a source with this identifier.
let sourceExists = try await fetchedSource.isAdded let sourceExists = try await fetchedSource.isAdded
// This is just a sanity check, so pass nil for previousSourceName to keep code simple. // This is just a sanity check, so pass nil for existingSource to keep code simple.
guard !sourceExists else { throw SourceError.duplicate(source, previousSourceName: nil) } guard !sourceExists else { throw SourceError.duplicate(source, existingSource: nil) }
try await context.performAsync { try await context.performAsync {
try context.save() try context.save()
@@ -498,7 +498,7 @@ extension AppManager
} }
@discardableResult @discardableResult
func updateKnownSources(completionHandler: @escaping (Result<([UpdateKnownSourcesOperation.Source], [UpdateKnownSourcesOperation.Source]), Error>) -> Void) -> UpdateKnownSourcesOperation func updateKnownSources(completionHandler: @escaping (Result<([KnownSource], [KnownSource]), Error>) -> Void) -> UpdateKnownSourcesOperation
{ {
let updateKnownSourcesOperation = UpdateKnownSourcesOperation() let updateKnownSourcesOperation = UpdateKnownSourcesOperation()
updateKnownSourcesOperation.resultHandler = completionHandler updateKnownSourcesOperation.resultHandler = completionHandler

View File

@@ -29,9 +29,9 @@ extension SourceError
static func duplicateBundleID(_ bundleID: String, source: Source) -> SourceError { SourceError(code: .duplicateBundleID, source: source, bundleID: bundleID) } static func duplicateBundleID(_ bundleID: String, source: Source) -> SourceError { SourceError(code: .duplicateBundleID, source: source, bundleID: bundleID) }
static func duplicateVersion(_ version: String, for app: StoreApp, source: Source) -> SourceError { SourceError(code: .duplicateVersion, source: source, app: app, version: version) } static func duplicateVersion(_ version: String, for app: StoreApp, source: Source) -> SourceError { SourceError(code: .duplicateVersion, source: source, app: app, version: version) }
static func blocked(_ source: Source) -> SourceError { SourceError(code: .blocked, source: source) } static func blocked(_ source: Source, bundleIDs: [String]?, existingSource: Source?) -> SourceError { SourceError(code: .blocked, source: source, existingSource: existingSource, bundleIDs: bundleIDs) }
static func changedID(_ identifier: String, previousID: String, source: Source) -> SourceError { SourceError(code: .changedID, source: source, sourceID: identifier, previousSourceID: previousID) } static func changedID(_ identifier: String, previousID: String, source: Source) -> SourceError { SourceError(code: .changedID, source: source, sourceID: identifier, previousSourceID: previousID) }
static func duplicate(_ source: Source, previousSourceName: String?) -> SourceError { SourceError(code: .duplicate, source: source, previousSourceName: previousSourceName) } static func duplicate(_ source: Source, existingSource: Source?) -> SourceError { SourceError(code: .duplicate, source: source, existingSource: existingSource) }
static func missingPermissionUsageDescription(for permission: any ALTAppPermission, app: StoreApp, source: Source) -> SourceError { static func missingPermissionUsageDescription(for permission: any ALTAppPermission, app: StoreApp, source: Source) -> SourceError {
SourceError(code: .missingPermissionUsageDescription, source: source, app: app, permission: permission) SourceError(code: .missingPermissionUsageDescription, source: source, app: app, permission: permission)
@@ -45,12 +45,13 @@ struct SourceError: ALTLocalizedError
var errorFailure: String? var errorFailure: String?
@Managed var source: Source @Managed var source: Source
@Managed var app: StoreApp? @Managed var app: StoreApp?
var bundleID: String? @Managed var existingSource: Source?
var version: String? var version: String?
var bundleID: String?
@UserInfoValue var previousSourceName: String? var bundleIDs: [String]?
// Store in userInfo so they can be viewed from Error Log. // Store in userInfo so they can be viewed from Error Log.
@UserInfoValue var sourceID: String? @UserInfoValue var sourceID: String?
@UserInfoValue var previousSourceID: String? @UserInfoValue var previousSourceID: String?
@@ -97,9 +98,9 @@ struct SourceError: ALTLocalizedError
case .duplicate: case .duplicate:
let baseMessage = String(format: NSLocalizedString("A source with the identifier '%@' already exists", comment: ""), self.$source.identifier) let baseMessage = String(format: NSLocalizedString("A source with the identifier '%@' already exists", comment: ""), self.$source.identifier)
guard let previousSourceName else { return baseMessage + "." } guard let existingSourceName = self.$existingSource.name else { return baseMessage + "." }
let failureReason = baseMessage + " (“\(previousSourceName)”)." let failureReason = baseMessage + " (“\(existingSourceName)”)."
return failureReason return failureReason
case .missingPermissionUsageDescription: case .missingPermissionUsageDescription:
@@ -117,13 +118,75 @@ struct SourceError: ALTLocalizedError
var recoverySuggestion: String? { var recoverySuggestion: String? {
switch self.code switch self.code
{ {
case .blocked: return NSLocalizedString("For your protection, please remove the source and uninstall all apps downloaded from it.", comment: "") case .blocked:
if self.existingSource != nil
{
// Source already added, so tell them to remove it + any installed apps.
let baseMessage = NSLocalizedString("For your protection, please remove the source and uninstall", comment: "")
if let blockedAppNames = self.blockedAppNames
{
let recoverySuggestion = baseMessage + " " + NSLocalizedString("the following apps:", comment: "") + "\n\n" + blockedAppNames.joined(separator: "\n")
return recoverySuggestion
}
else
{
let recoverySuggestion = baseMessage + " " + NSLocalizedString("all apps downloaded from it.", comment: "")
return recoverySuggestion
}
}
else
{
// Source is not already added, so no need to tell users to remove it.
// Instead, we just list all affected apps (if provided).
guard let blockedAppNames else { return nil }
let recoverySuggestion = NSLocalizedString("The following apps have been flagged:", comment: "") + "\n\n" + blockedAppNames.joined(separator: "\n")
return recoverySuggestion
}
case .changedID: return NSLocalizedString("A source cannot change its identifier once added. This source can no longer be updated.", comment: "") case .changedID: return NSLocalizedString("A source cannot change its identifier once added. This source can no longer be updated.", comment: "")
case .duplicate: case .duplicate:
let failureReason = NSLocalizedString("Please remove the existing source in order to add this one.", comment: "") let recoverySuggestion = NSLocalizedString("Please remove the existing source in order to add this one.", comment: "")
return failureReason return recoverySuggestion
default: return nil default: return nil
} }
} }
} }
private extension SourceError
{
var blockedAppNames: [String]? {
let blockedAppNames: [String]?
if let existingSource
{
// Blocked apps = all installed apps from this source.
blockedAppNames = self.$existingSource.perform { _ in
let storeApps = existingSource.apps.lazy.filter { $0.installedApp != nil }
guard !storeApps.isEmpty else { return nil }
let appNames = storeApps.map { "\($0.name) (\($0.bundleIdentifier))" }
return Array(appNames)
}
}
else if let bundleIDs
{
// Blocked apps = explicitly listed bundleIDs in blocked source JSON entry.
blockedAppNames = self.$source.perform { source in
bundleIDs.compactMap { (bundleID) in
guard let storeApp = source._apps.lazy.compactMap({ $0 as? StoreApp }).first(where: { $0.bundleIdentifier == bundleID }) else { return nil }
return "\(storeApp.name) (\(storeApp.bundleIdentifier))"
}
}
}
else
{
blockedAppNames = nil
}
let sortedNames = blockedAppNames?.sorted { $0.localizedCompare($1) == .orderedAscending }
return sortedNames
}
}

View File

@@ -58,6 +58,28 @@ final class FetchSourceOperation: ResultOperation<Source>
{ {
super.main() super.main()
if let source = self.source
{
// Check if source is blocked before fetching it.
do
{
try self.managedObjectContext.performAndWait {
// Source must be from self.managedObjectContext
let source = self.managedObjectContext.object(with: source.objectID) as! Source
try self.verifySourceNotBlocked(source, response: nil)
}
}
catch
{
self.managedObjectContext.perform {
self.finish(.failure(error))
}
return
}
}
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(withParent: self.managedObjectContext) let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(withParent: self.managedObjectContext)
@@ -129,21 +151,7 @@ private extension FetchSourceOperation
{ {
func verify(_ source: Source, response: URLResponse) throws func verify(_ source: Source, response: URLResponse) throws
{ {
if let blockedSourceIDs = UserDefaults.shared.blockedSourceIDs try self.verifySourceNotBlocked(source, response: response)
{
guard !blockedSourceIDs.contains(source.identifier) else { throw SourceError.blocked(source) }
}
if let blockedSourceURLs = UserDefaults.shared.blockedSourceURLs
{
guard !blockedSourceURLs.contains(source.sourceURL) else { throw SourceError.blocked(source) }
if let responseURL = response.url
{
// responseURL may differ from sourceURL (e.g. due to redirects), so double-check it's also not blocked.
guard !blockedSourceURLs.contains(responseURL) else { throw SourceError.blocked(source) }
}
}
var bundleIDs = Set<String>() var bundleIDs = Set<String>()
for app in source.apps for app in source.apps
@@ -175,4 +183,25 @@ private extension FetchSourceOperation
guard source.identifier == previousSourceID else { throw SourceError.changedID(source.identifier, previousID: previousSourceID, source: source) } guard source.identifier == previousSourceID else { throw SourceError.changedID(source.identifier, previousID: previousSourceID, source: source) }
} }
} }
func verifySourceNotBlocked(_ source: Source, response: URLResponse?) throws
{
guard let blockedSources = UserDefaults.shared.blockedSources else { return }
for blockedSource in blockedSources
{
guard
source.identifier != blockedSource.identifier,
source.sourceURL.absoluteString.lowercased() != blockedSource.sourceURL?.absoluteString.lowercased()
else { throw SourceError.blocked(source, bundleIDs: blockedSource.bundleIDs, existingSource: self.source) }
if let responseURL = response?.url
{
// responseURL may differ from source.sourceURL (e.g. due to redirects), so double-check it's also not blocked.
guard responseURL.absoluteString.lowercased() != blockedSource.sourceURL?.absoluteString.lowercased() else {
throw SourceError.blocked(source, bundleIDs: blockedSource.bundleIDs, existingSource: self.source)
}
}
}
}
} }

View File

@@ -19,22 +19,16 @@ private extension URL
extension UpdateKnownSourcesOperation extension UpdateKnownSourcesOperation
{ {
struct Source: Decodable
{
var identifier: String
var sourceURL: URL?
}
private struct Response: Decodable private struct Response: Decodable
{ {
var version: Int var version: Int
var trusted: [Source] var trusted: [KnownSource]?
var blocked: [Source]? var blocked: [KnownSource]?
} }
} }
class UpdateKnownSourcesOperation: ResultOperation<([UpdateKnownSourcesOperation.Source], [UpdateKnownSourcesOperation.Source])> class UpdateKnownSourcesOperation: ResultOperation<([KnownSource], [KnownSource])>
{ {
override func main() override func main()
{ {
@@ -54,17 +48,11 @@ class UpdateKnownSourcesOperation: ResultOperation<([UpdateKnownSourcesOperation
guard let data = data else { throw error! } guard let data = data else { throw error! }
let response = try Foundation.JSONDecoder().decode(Response.self, from: data) let response = try Foundation.JSONDecoder().decode(Response.self, from: data)
let sources = (trusted: response.trusted, blocked: response.blocked ?? []) let sources = (trusted: response.trusted ?? [], blocked: response.blocked ?? [])
// Cache trusted sources // Cache sources
let trustedSourceIDs = Set(sources.trusted.map { $0.identifier }) UserDefaults.shared.trustedSources = sources.trusted
UserDefaults.shared.trustedSourceIDs = trustedSourceIDs UserDefaults.shared.blockedSources = sources.blocked
// Cache blocked sources
let blockedSourceIDs = Set(sources.blocked.map { $0.identifier })
let blockedSourceURLs = Set(sources.blocked.compactMap { $0.sourceURL })
UserDefaults.shared.blockedSourceIDs = blockedSourceIDs
UserDefaults.shared.blockedSourceURLs = blockedSourceURLs
self.finish(.success(sources)) self.finish(.success(sources))
} }

View File

@@ -293,7 +293,7 @@ private extension SourcesViewController
let predicate = NSPredicate(format: "%K == %@", #keyPath(Source.identifier), $source.identifier) let predicate = NSPredicate(format: "%K == %@", #keyPath(Source.identifier), $source.identifier)
if let existingSource = Source.first(satisfying: predicate, in: backgroundContext) if let existingSource = Source.first(satisfying: predicate, in: backgroundContext)
{ {
throw SourceError.duplicate(source, previousSourceName: existingSource.name) throw SourceError.duplicate(source, existingSource: existingSource)
} }
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@@ -0,0 +1,69 @@
//
// KnownSource.swift
// AltStore
//
// Created by Riley Testut on 5/16/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
struct KnownSource: Decodable
{
var identifier: String
var sourceURL: URL?
var bundleIDs: [String]?
}
private extension KnownSource
{
var dictionaryRepresentation: [String: Any] {
let dictionary: [String: Any?] = [
KnownSource.CodingKeys.identifier.stringValue: identifier,
KnownSource.CodingKeys.sourceURL.stringValue: self.sourceURL?.absoluteString,
KnownSource.CodingKeys.bundleIDs.stringValue: self.bundleIDs
]
return dictionary.compactMapValues { $0 }
}
init?(dictionary: [String: Any])
{
guard let identifier = dictionary[CodingKeys.identifier.stringValue] as? String else { return nil }
self.identifier = identifier
if let sourceURLString = dictionary[CodingKeys.sourceURL.stringValue] as? String
{
self.sourceURL = URL(string: sourceURLString)
}
let bundleIDs = dictionary[CodingKeys.bundleIDs.stringValue] as? [String]
self.bundleIDs = bundleIDs
}
}
extension UserDefaults
{
// Cache trusted sources just in case we need to check whether source is trusted or not.
@nonobjc var trustedSources: [KnownSource]? {
get {
guard let sources = _trustedSources?.compactMap({ KnownSource(dictionary: $0) }) else { return nil }
return sources
}
set {
_trustedSources = newValue?.map { $0.dictionaryRepresentation }
}
}
@NSManaged @objc(trustedSources) private var _trustedSources: [[String: Any]]?
@nonobjc var blockedSources: [KnownSource]? {
get {
guard let sources = _blockedSources?.compactMap({ KnownSource(dictionary: $0) }) else { return nil }
return sources
}
set {
_blockedSources = newValue?.map { $0.dictionaryRepresentation }
}
}
@NSManaged @objc(blockedSources) private var _blockedSources: [[String: Any]]?
}

View File

@@ -118,40 +118,3 @@ public extension UserDefaults
} }
} }
} }
public extension UserDefaults
{
// Cache trustedSourceIDs just in case we need to check whether source is trusted or not.
@nonobjc var trustedSourceIDs: Set<String>? {
get {
guard let sourceIDs = _trustedSourceIDs else { return nil }
return Set(sourceIDs)
}
set {
_trustedSourceIDs = newValue?.map { $0 }
}
}
@NSManaged @objc(trustedSourceIDs) private var _trustedSourceIDs: [String]?
@nonobjc var blockedSourceIDs: Set<String>? {
get {
guard let sourceIDs = _blockedSourceIDs else { return nil }
return Set(sourceIDs)
}
set {
_blockedSourceIDs = newValue?.map { $0 }
}
}
@NSManaged @objc(blockedSourceIDs) private var _blockedSourceIDs: [String]?
@nonobjc var blockedSourceURLs: Set<URL>? {
get {
guard let sourceURLs = _blockedSourceURLs?.compactMap({ URL(string: $0) }) else { return nil }
return Set(sourceURLs)
}
set {
_blockedSourceURLs = newValue?.map { $0.absoluteString }
}
}
@NSManaged @objc(blockedSourceURLs) private var _blockedSourceURLs: [String]?
}