mirror of
https://github.com/SideStore/SideStore.git
synced 2026-05-13 04:45:40 +02:00
Merge branch 'develop' into users/junepark678/altsign-fixes
This commit is contained in:
@@ -715,9 +715,10 @@ private extension AuthenticationOperation
|
||||
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
|
||||
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
|
||||
|
||||
#if DEBUG
|
||||
completionHandler(false)
|
||||
#else
|
||||
// #if DEBUG && targetEnvironment(simulator)
|
||||
// completionHandler(false)
|
||||
// #else
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let context = AuthenticatedOperationContext(context: self.context)
|
||||
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
||||
@@ -733,7 +734,7 @@ private extension AuthenticationOperation
|
||||
completionHandler(false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,14 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
|
||||
// enable minimuxer console logging only if enabled in settings
|
||||
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
||||
|
||||
try minimuxer.startWithLogger(
|
||||
try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")),
|
||||
documentsDirectory,
|
||||
isMinimuxerConsoleLoggingEnabled
|
||||
)
|
||||
} catch {
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
|
||||
@@ -53,38 +53,37 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||
guard let installedApp = self.context.installedApp else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
|
||||
}
|
||||
if #available(iOS 17, *) {
|
||||
let sideJITenabled = UserDefaults.standard.sidejitenable
|
||||
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||
|
||||
if sideJITenabled {
|
||||
installedApp.managedObjectContext?.perform {
|
||||
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case .invalidURL, .errorConnecting:
|
||||
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
||||
case .deviceNotFound:
|
||||
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
||||
case .other(let message):
|
||||
if let startRange = message.range(of: "<p>"),
|
||||
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
||||
let pContent = message[startRange.upperBound..<endRange.lowerBound]
|
||||
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
|
||||
print(message + " + " + String(pContent))
|
||||
} else {
|
||||
print(message)
|
||||
self.finish(.failure(OperationError.SideJITIssue(error: message)))
|
||||
}
|
||||
|
||||
let userdefaults = UserDefaults.standard
|
||||
|
||||
if #available(iOS 17, *), userdefaults.sidejitenable {
|
||||
let SideJITIP = userdefaults.textInputSideJITServerurl ?? "http://sidejitserver._http._tcp.local:8080"
|
||||
installedApp.managedObjectContext?.perform {
|
||||
enableJITSideJITServer(serverURL: URL(string: SideJITIP)!, installedApp: installedApp) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case .invalidURL, .errorConnecting:
|
||||
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
||||
case .deviceNotFound:
|
||||
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
||||
case .other(let message):
|
||||
if let startRange = message.range(of: "<p>"),
|
||||
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
||||
let pContent = message[startRange.upperBound..<endRange.lowerBound]
|
||||
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
|
||||
print(message + " + " + String(pContent))
|
||||
} else {
|
||||
print(message)
|
||||
self.finish(.failure(OperationError.SideJITIssue(error: message)))
|
||||
}
|
||||
case .success():
|
||||
self.finish(.success(()))
|
||||
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
|
||||
}
|
||||
case .success():
|
||||
self.finish(.success(()))
|
||||
print("JIT Enabled Successfully :3 (code made by Stossy11!)")
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
installedApp.managedObjectContext?.perform {
|
||||
@@ -107,48 +106,40 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
|
||||
func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
|
||||
guard let udid = fetch_udid()?.toString() else {
|
||||
completion(.failure(.other("Unable to get UDID")))
|
||||
return
|
||||
}
|
||||
|
||||
var SJSURL = serverurl
|
||||
let serverURLWithUDID = serverURL.appendingPathComponent(udid)
|
||||
let fullURL = serverURLWithUDID.appendingPathComponent(installedApp.resignedBundleIdentifier)
|
||||
|
||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||
}
|
||||
|
||||
if !SJSURL.hasPrefix("http") {
|
||||
completion(.failure(.invalidURL))
|
||||
return
|
||||
}
|
||||
|
||||
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
|
||||
|
||||
let url = URL(string: fullurl)!
|
||||
|
||||
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
|
||||
let task = URLSession.shared.dataTask(with: fullURL) { (data, response, error) in
|
||||
if let error = error {
|
||||
completion(.failure(.errorConnecting))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
|
||||
guard let data = data, let dataString = String(data: data, encoding: .utf8) else {
|
||||
return
|
||||
}
|
||||
|
||||
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
|
||||
if dataString == "Enabled JIT for '\(installedApp.name)'!" {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "JIT Successfully Enabled"
|
||||
content.subtitle = "JIT Enabled For \(installedapp.name)"
|
||||
content.sound = UNNotificationSound.default
|
||||
|
||||
content.subtitle = "JIT Enabled For \(installedApp.name)"
|
||||
content.sound = .default
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
|
||||
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
completion(.success(()))
|
||||
} else {
|
||||
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
|
||||
let errorType: SideJITServerErrorType = dataString == "Could not find device!"
|
||||
? .deviceNotFound
|
||||
: .other(dataString)
|
||||
completion(.failure(errorType))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ extension OperationError
|
||||
case anisetteV3Error//(message: String)
|
||||
case cacheClearError//(errors: [String])
|
||||
case noWiFi
|
||||
|
||||
case invalidOperationContext
|
||||
}
|
||||
|
||||
static var cancelled: CancellationError { CancellationError() }
|
||||
@@ -130,6 +132,10 @@ extension OperationError
|
||||
OperationError(code: .invalidParameters, failureReason: message)
|
||||
}
|
||||
|
||||
static func invalidOperationContext(_ message: String? = nil) -> OperationError {
|
||||
OperationError(code: .invalidOperationContext, failureReason: message)
|
||||
}
|
||||
|
||||
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||
}
|
||||
@@ -232,7 +238,10 @@ struct OperationError: ALTLocalizedError {
|
||||
|
||||
case .invalidParameters:
|
||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
||||
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
|
||||
return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message)
|
||||
case .invalidOperationContext:
|
||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
||||
return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message)
|
||||
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
|
||||
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
|
||||
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
|
||||
|
||||
@@ -14,6 +14,8 @@ import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
class ANISETTE_VERBOSITY: Operation {} // dummy tag iface
|
||||
|
||||
@objc(FetchAnisetteDataOperation)
|
||||
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
|
||||
{
|
||||
@@ -58,7 +60,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
UserDefaults.standard.menuAnisetteURL = urlString
|
||||
let url = URL(string: urlString)
|
||||
self.url = url
|
||||
print("Anisette URL: \(self.url!.absoluteString)")
|
||||
self.printOut("Anisette URL: \(self.url!.absoluteString)")
|
||||
|
||||
if let identifier = Keychain.shared.identifier,
|
||||
let adiPb = Keychain.shared.adiPb {
|
||||
@@ -107,7 +109,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
guard let url = URL(string: currentServerUrlString) else {
|
||||
// Invalid URL, skip to next
|
||||
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
|
||||
print(errmsg)
|
||||
self.printOut(errmsg)
|
||||
showToast(viewContext: viewContext, message: errmsg)
|
||||
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
||||
return
|
||||
@@ -118,7 +120,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
if success {
|
||||
// If the server is reachable, return the URL
|
||||
let okmsg = "Found working server: \(url.absoluteString)"
|
||||
print(okmsg)
|
||||
self.printOut(okmsg)
|
||||
if(currentIndex > 0){
|
||||
// notify user if available server is different the user-specified one
|
||||
self.showToast(viewContext: viewContext, message: okmsg)
|
||||
@@ -127,7 +129,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
} else {
|
||||
// If not, try the next URL
|
||||
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
|
||||
print(errmsg)
|
||||
self.printOut(errmsg)
|
||||
self.showToast(viewContext: viewContext, message: errmsg)
|
||||
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
||||
}
|
||||
@@ -170,10 +172,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
if v3 {
|
||||
if json["result"] == "GetHeadersError" {
|
||||
let message = json["message"]
|
||||
print("Error getting V3 headers: \(message ?? "no message")")
|
||||
self.printOut("Error getting V3 headers: \(message ?? "no message")")
|
||||
if let message = message,
|
||||
message.contains("-45061") {
|
||||
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
||||
self.printOut("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
||||
Keychain.shared.adiPb = nil
|
||||
return provision()
|
||||
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
|
||||
@@ -214,16 +216,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
|
||||
if let response = response,
|
||||
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
|
||||
print("Implementation-Version: \(version)")
|
||||
} else { print("No Implementation-Version header") }
|
||||
self.printOut("Implementation-Version: \(version)")
|
||||
} else { self.printOut("No Implementation-Version header") }
|
||||
|
||||
print("Anisette used: \(formattedJSON)")
|
||||
print("Original JSON: \(json)")
|
||||
self.printOut("Anisette used: \(formattedJSON)")
|
||||
self.printOut("Original JSON: \(json)")
|
||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||
print("Anisette is valid!")
|
||||
self.printOut("Anisette is valid!")
|
||||
self.finish(.success(anisette))
|
||||
} else {
|
||||
print("Anisette is invalid!!!!")
|
||||
self.printOut("Anisette is invalid!!!!")
|
||||
if v3 {
|
||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||
} else {
|
||||
@@ -242,22 +244,22 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
// MARK: - V1
|
||||
|
||||
func handleV1() {
|
||||
print("Server is V1")
|
||||
self.printOut("Server is V1")
|
||||
|
||||
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
|
||||
print("Server has already been trusted, fetching anisette")
|
||||
self.printOut("Server has already been trusted, fetching anisette")
|
||||
return self.fetchAnisetteV1()
|
||||
}
|
||||
|
||||
print("Alerting user about outdated server")
|
||||
self.printOut("Alerting user about outdated server")
|
||||
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
|
||||
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
|
||||
print("Fetching anisette via V1")
|
||||
self.printOut("Fetching anisette via V1")
|
||||
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
|
||||
self.fetchAnisetteV1()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
|
||||
print("Cancelled anisette operation")
|
||||
self.printOut("Cancelled anisette operation")
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
}))
|
||||
|
||||
@@ -273,14 +275,14 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
}
|
||||
|
||||
func fetchAnisetteV1() {
|
||||
print("Fetching anisette V1")
|
||||
self.printOut("Fetching anisette V1")
|
||||
URLSession.shared.dataTask(with: self.url!) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
|
||||
|
||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.printOut("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
@@ -290,7 +292,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
|
||||
func provision() {
|
||||
fetchClientInfo {
|
||||
print("Getting provisioning URLs")
|
||||
self.printOut("Getting provisioning URLs")
|
||||
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
|
||||
request.httpMethod = "GET"
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
@@ -302,12 +304,12 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
let endProvisioningURL = URL(string: endProvisioningString) {
|
||||
self.startProvisioningURL = startProvisioningURL
|
||||
self.endProvisioningURL = endProvisioningURL
|
||||
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
||||
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
||||
print("Starting a provisioning session")
|
||||
self.printOut("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
||||
self.printOut("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
||||
self.printOut("Starting a provisioning session")
|
||||
self.startProvisioningSession()
|
||||
} else {
|
||||
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
self.printOut("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
@@ -329,19 +331,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
|
||||
guard let result = json["result"] as? String else {
|
||||
print("The server didn't give us a result")
|
||||
self.printOut("The server didn't give us a result")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
|
||||
return
|
||||
}
|
||||
print("Received result: \(result)")
|
||||
self.printOut("Received result: \(result)")
|
||||
switch result {
|
||||
case "GiveIdentifier":
|
||||
print("Giving identifier")
|
||||
self.printOut("Giving identifier")
|
||||
client.json(["identifier": Keychain.shared.identifier!])
|
||||
|
||||
case "GiveStartProvisioningData":
|
||||
print("Getting start provisioning data")
|
||||
self.printOut("Getting start provisioning data")
|
||||
let body = [
|
||||
"Header": [String: Any](),
|
||||
"Request": [String: Any](),
|
||||
@@ -353,19 +355,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let spim = plist["Response"]?["spim"] as? String {
|
||||
print("Giving start provisioning data")
|
||||
self.printOut("Giving start provisioning data")
|
||||
client.json(["spim": spim])
|
||||
} else {
|
||||
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
self.printOut("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "GiveEndProvisioningData":
|
||||
print("Getting end provisioning data")
|
||||
self.printOut("Getting end provisioning data")
|
||||
guard let cpim = json["cpim"] as? String else {
|
||||
print("The server didn't give us a cpim")
|
||||
self.printOut("The server didn't give us a cpim")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
|
||||
return
|
||||
@@ -384,20 +386,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let ptm = plist["Response"]?["ptm"] as? String,
|
||||
let tk = plist["Response"]?["tk"] as? String {
|
||||
print("Giving end provisioning data")
|
||||
self.printOut("Giving end provisioning data")
|
||||
client.json(["ptm": ptm, "tk": tk])
|
||||
} else {
|
||||
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
self.printOut("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "ProvisioningSuccess":
|
||||
print("Provisioning succeeded!")
|
||||
self.printOut("Provisioning succeeded!")
|
||||
client.disconnect(closeCode: 0)
|
||||
guard let adiPb = json["adi_pb"] as? String else {
|
||||
print("The server didn't give us an adi.pb file")
|
||||
self.printOut("The server didn't give us an adi.pb file")
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
|
||||
return
|
||||
}
|
||||
@@ -406,27 +408,27 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
|
||||
default:
|
||||
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
|
||||
print("Failing because of \(result)")
|
||||
self.printOut("Failing because of \(result)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch let error as NSError {
|
||||
print("Failed to handle text: \(error.localizedDescription)")
|
||||
self.printOut("Failed to handle text: \(error.localizedDescription)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
|
||||
}
|
||||
|
||||
case .connected:
|
||||
print("Connected")
|
||||
self.printOut("Connected")
|
||||
|
||||
case .disconnected(let string, let code):
|
||||
print("Disconnected: \(code); \(string)")
|
||||
self.printOut("Disconnected: \(code); \(string)")
|
||||
|
||||
case .error(let error):
|
||||
print("Got error: \(String(describing: error))")
|
||||
self.printOut("Got error: \(String(describing: error))")
|
||||
|
||||
default:
|
||||
print("Unknown event: \(event)")
|
||||
self.printOut("Unknown event: \(event)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,10 +462,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
self.mdLu != nil &&
|
||||
self.deviceId != nil &&
|
||||
Keychain.shared.identifier != nil {
|
||||
print("Skipping client_info fetch since all the properties we need aren't nil")
|
||||
self.printOut("Skipping client_info fetch since all the properties we need aren't nil")
|
||||
return callback()
|
||||
}
|
||||
print("Trying to get client_info")
|
||||
self.printOut("Trying to get client_info")
|
||||
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
|
||||
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
|
||||
do {
|
||||
@@ -473,20 +475,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
if let clientInfo = json["client_info"] {
|
||||
print("Server is V3")
|
||||
self.printOut("Server is V3")
|
||||
|
||||
self.clientInfo = clientInfo
|
||||
self.userAgent = json["user_agent"]!
|
||||
print("Client-Info: \(self.clientInfo!)")
|
||||
print("User-Agent: \(self.userAgent!)")
|
||||
self.printOut("Client-Info: \(self.clientInfo!)")
|
||||
self.printOut("User-Agent: \(self.userAgent!)")
|
||||
|
||||
if Keychain.shared.identifier == nil {
|
||||
print("Generating identifier")
|
||||
self.printOut("Generating identifier")
|
||||
var bytes = [Int8](repeating: 0, count: 16)
|
||||
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||
|
||||
if status != errSecSuccess {
|
||||
print("ERROR GENERATING IDENTIFIER!!! \(status)")
|
||||
self.printOut("ERROR GENERATING IDENTIFIER!!! \(status)")
|
||||
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
|
||||
}
|
||||
|
||||
@@ -495,16 +497,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
|
||||
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
|
||||
self.mdLu = decoded.sha256().hexEncodedString()
|
||||
print("X-Apple-I-MD-LU: \(self.mdLu!)")
|
||||
self.printOut("X-Apple-I-MD-LU: \(self.mdLu!)")
|
||||
let uuid: UUID = decoded.object()
|
||||
self.deviceId = uuid.uuidString.uppercased()
|
||||
print("X-Mme-Device-Id: \(self.deviceId!)")
|
||||
self.printOut("X-Mme-Device-Id: \(self.deviceId!)")
|
||||
|
||||
callback()
|
||||
} else { self.handleV1() }
|
||||
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.printOut("Failed to load: \(error.localizedDescription)")
|
||||
self.handleV1()
|
||||
}
|
||||
}.resume()
|
||||
@@ -512,7 +514,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
|
||||
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||
fetchClientInfo {
|
||||
print("Fetching anisette V3")
|
||||
self.printOut("Fetching anisette V3")
|
||||
let url = UserDefaults.standard.menuAnisetteURL
|
||||
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||
request.httpMethod = "POST"
|
||||
@@ -527,12 +529,21 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
|
||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.printOut("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func printOut(_ text: String?){
|
||||
let isInternalLoggingEnabled = OperationsLoggingControl.getFromDatabase(for: ANISETTE_VERBOSITY.self)
|
||||
if(isInternalLoggingEnabled){
|
||||
// logging enabled, so log it
|
||||
text.map{ _ in print(text!) } ?? print()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WebSocketClient {
|
||||
|
||||
@@ -13,15 +13,16 @@ import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(FetchProvisioningProfilesOperation)
|
||||
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
var additionalEntitlements: [ALTEntitlement: Any]?
|
||||
|
||||
private let appGroupsLock = NSLock()
|
||||
internal let appGroupsLock = NSLock()
|
||||
|
||||
init(context: AppOperationContext)
|
||||
// this class is abstract or shouldn't be instantiated outside, use the subclasses
|
||||
fileprivate init(context: AppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
@@ -40,11 +41,13 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let team = self.context.team,
|
||||
let session = self.context.session
|
||||
else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) }
|
||||
guard let team = self.context.team,
|
||||
let session = self.context.session else {
|
||||
|
||||
return self.finish(.failure(
|
||||
OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))
|
||||
)
|
||||
}
|
||||
|
||||
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
|
||||
|
||||
@@ -120,7 +123,11 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
|
||||
|
||||
extension FetchProvisioningProfilesOperation
|
||||
{
|
||||
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
private func prepareProvisioningProfile(for app: ALTApplication,
|
||||
parentApp: ALTApplication?,
|
||||
team: ALTTeam,
|
||||
session: ALTAppleAPISession, c
|
||||
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
|
||||
@@ -134,19 +141,21 @@ extension FetchProvisioningProfilesOperation
|
||||
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
|
||||
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
|
||||
|
||||
// #if DEBUG
|
||||
//
|
||||
// if app.isAltStoreApp
|
||||
// {
|
||||
// // Use legacy bundle ID format for AltStore.
|
||||
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||
// }
|
||||
//
|
||||
// #else
|
||||
// TODO: @mahee96: Try to keep the debug build and release build operations similar, refactor later with proper reasoning
|
||||
// for now, restricted it to debug on simulator only
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
|
||||
if app.isAltStoreApp
|
||||
{
|
||||
// Use legacy bundle ID format for AltStore.
|
||||
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
if teamsMatch
|
||||
{
|
||||
@@ -160,7 +169,7 @@ extension FetchProvisioningProfilesOperation
|
||||
preferredBundleID = nil
|
||||
}
|
||||
|
||||
// #endif
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -211,35 +220,22 @@ extension FetchProvisioningProfilesOperation
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update features
|
||||
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update app groups
|
||||
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Fetch Provisioning Profile
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//process
|
||||
self.fetchProvisioningProfile(
|
||||
for: appID, team: team, session: session, completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
private func registerAppID(for application: ALTApplication,
|
||||
name: String,
|
||||
bundleIdentifier: String,
|
||||
team: ALTTeam,
|
||||
session: ALTAppleAPISession,
|
||||
completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||
do
|
||||
@@ -333,7 +329,81 @@ extension FetchProvisioningProfilesOperation
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
internal func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||
switch Result(profile, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
|
||||
// Delete existing profile
|
||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure:
|
||||
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
|
||||
// So instead, we just return the fetched profile from above.
|
||||
completionHandler(.success(profile))
|
||||
|
||||
case .success:
|
||||
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
|
||||
|
||||
// Fetch new provisioning profile
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
|
||||
override init(context: AppOperationContext)
|
||||
{
|
||||
super.init(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
|
||||
override init(context: AppOperationContext)
|
||||
{
|
||||
super.init(context: context)
|
||||
}
|
||||
|
||||
// modify Operations are allowed for the app groups and other stuffs
|
||||
func fetchProvisioningProfile(appID: ALTAppID,
|
||||
for app: ALTApplication,
|
||||
team: ALTTeam,
|
||||
session: ALTAppleAPISession,
|
||||
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
|
||||
// Update features
|
||||
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update app groups
|
||||
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Fetch Provisioning Profile
|
||||
super.fetchProvisioningProfile(for: appID, team: team, session: session, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
var entitlements = app.entitlements
|
||||
for (key, value) in additionalEntitlements ?? [:]
|
||||
@@ -412,7 +482,7 @@ extension FetchProvisioningProfilesOperation
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
private func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
var entitlements = app.entitlements
|
||||
for (key, value) in additionalEntitlements ?? [:]
|
||||
@@ -511,7 +581,7 @@ extension FetchProvisioningProfilesOperation
|
||||
Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).")
|
||||
groups.append(group)
|
||||
|
||||
case .failure(let error):
|
||||
case .failure(let error):
|
||||
Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||
errors.append(error)
|
||||
}
|
||||
@@ -547,34 +617,4 @@ extension FetchProvisioningProfilesOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||
switch Result(profile, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
|
||||
// Delete existing profile
|
||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure:
|
||||
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
|
||||
// So instead, we just return the fetched profile from above.
|
||||
completionHandler(.success(profile))
|
||||
|
||||
case .success:
|
||||
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
|
||||
|
||||
// Fetch new provisioning profile
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,14 @@ class ResultOperation<ResultType>: Operation
|
||||
result = .failure(error)
|
||||
}
|
||||
|
||||
// diagnostics logging
|
||||
let resultStatus = String(describing: result).prefix("success".count).uppercased()
|
||||
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
|
||||
" Result: \(result)\n")
|
||||
// Diagnostics: perform verbose logging of the operations only if enabled (so as to not flood console logs)
|
||||
let isLoggingEnabledForThisOperation = OperationsLoggingControl.getFromDatabase(for: type(of: self))
|
||||
if UserDefaults.standard.isVerboseOperationsLoggingEnabled && isLoggingEnabledForThisOperation {
|
||||
// diagnostics logging
|
||||
let resultStatus = String(describing: result).prefix("success".count).uppercased()
|
||||
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
|
||||
" Result: \(result)\n")
|
||||
}
|
||||
|
||||
self.resultHandler?(result)
|
||||
|
||||
|
||||
@@ -58,17 +58,18 @@ final class RemoveAppBackupOperation: ResultOperation<Void>
|
||||
}
|
||||
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
// When debugging, it's expected that app groups don't match, so ignore.
|
||||
self.finish(.success(()))
|
||||
|
||||
#else
|
||||
// TODO: @mahee96: Find out why should in debug builds the app-groups is not expected to match
|
||||
// #if DEBUG
|
||||
//
|
||||
// // When debugging, it's expected that app groups don't match, so ignore.
|
||||
// self.finish(.success(()))
|
||||
//
|
||||
// #else
|
||||
|
||||
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||
self.finish(.failure(error))
|
||||
|
||||
#endif
|
||||
// #endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
219
AltStore/Operations/RemoveAppExtensionsOperation.swift
Normal file
219
AltStore/Operations/RemoveAppExtensionsOperation.swift
Normal file
@@ -0,0 +1,219 @@
|
||||
//
|
||||
// RefreshAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 2/27/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
import AltSign
|
||||
|
||||
@objc(RemoveAppExtensionsOperation)
|
||||
final class RemoveAppExtensionsOperation: ResultOperation<Void>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
let localAppExtensions: Set<ALTApplication>?
|
||||
|
||||
init(context: AppOperationContext, localAppExtensions: Set<ALTApplication>?)
|
||||
{
|
||||
self.context = context
|
||||
self.localAppExtensions = localAppExtensions
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let targetAppBundle = context.app else {
|
||||
return self.finish(.failure(
|
||||
OperationError.invalidParameters("RemoveAppExtensionsOperation: context.app is nil")
|
||||
))
|
||||
}
|
||||
|
||||
self.removeAppExtensions(from: targetAppBundle,
|
||||
localAppExtensions: localAppExtensions,
|
||||
extensions: targetAppBundle.appExtensions,
|
||||
context.authenticatedContext.presentingViewController)
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static func removeExtensions(from extensions: Set<ALTApplication>) throws {
|
||||
for appExtension in extensions {
|
||||
print("Deleting extension \(appExtension.bundleIdentifier)")
|
||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func removeAppExtensions(from targetAppBundle: ALTApplication,
|
||||
localAppExtensions: Set<ALTApplication>?,
|
||||
extensions: Set<ALTApplication>,
|
||||
_ presentingViewController: UIViewController?)
|
||||
{
|
||||
|
||||
// target App Bundle doesn't contain extensions so don't bother
|
||||
guard !targetAppBundle.appExtensions.isEmpty else {
|
||||
return self.finish(.success(()))
|
||||
}
|
||||
|
||||
// process extensionsInfo
|
||||
let excessExtensions = processExtensionsInfo(from: targetAppBundle, localAppExtensions: localAppExtensions)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let presentingViewController: UIViewController = presentingViewController,
|
||||
presentingViewController.viewIfLoaded?.window != nil else {
|
||||
// background mode: remove only the excess extensions automatically for re-installs
|
||||
// keep all extensions for fresh install (localAppBundle = nil)
|
||||
return self.backgroundModeExtensionsCleanup(excessExtensions: excessExtensions)
|
||||
}
|
||||
|
||||
// present prompt to the user if we have a view context
|
||||
let alertController = self.createAlertDialog(from: targetAppBundle, extensions: extensions, presentingViewController)
|
||||
presentingViewController.present(alertController, animated: true){
|
||||
|
||||
// if for any reason the view wasn't presented, then just signal that as error
|
||||
if presentingViewController.presentedViewController == nil {
|
||||
let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." +
|
||||
"\nDid you move to different screen or background after starting the operation?"
|
||||
self.finish(.failure(
|
||||
OperationError.invalidOperationContext(errMsg)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createAlertDialog(from targetAppBundle: ALTApplication,
|
||||
extensions: Set<ALTApplication>,
|
||||
_ presentingViewController: UIViewController) -> UIAlertController
|
||||
{
|
||||
|
||||
/// Foreground prompt:
|
||||
let firstSentence: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
|
||||
}
|
||||
|
||||
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
|
||||
|
||||
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||
self.finish(.success(()))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||
do {
|
||||
try Self.removeExtensions(from: targetAppBundle.appExtensions)
|
||||
return self.finish(.success(()))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
|
||||
|
||||
|
||||
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
|
||||
do {
|
||||
try Self.removeExtensions(from: Set(selection))
|
||||
return self.finish(.success(()))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
let suiview = popoverContentController.view!
|
||||
suiview.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
popoverContentController.modalPresentationStyle = .popover
|
||||
|
||||
if let popoverPresentationController = popoverContentController.popoverPresentationController {
|
||||
popoverPresentationController.sourceView = presentingViewController.view
|
||||
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
|
||||
popoverPresentationController.delegate = popoverContentController
|
||||
|
||||
DispatchQueue.main.async {
|
||||
presentingViewController.present(popoverContentController, animated: true)
|
||||
}
|
||||
}else{
|
||||
self.finish(.failure(
|
||||
OperationError.invalidParameters("RemoveAppExtensionsOperation: popoverContentController.popoverPresentationController is nil"))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return alertController
|
||||
}
|
||||
|
||||
struct ExtensionsInfo{
|
||||
let excessInTarget: Set<ALTApplication>
|
||||
let necessaryInExisting: Set<ALTApplication>
|
||||
}
|
||||
|
||||
private func processExtensionsInfo(from targetAppBundle: ALTApplication,
|
||||
localAppExtensions: Set<ALTApplication>?) -> Set<ALTApplication>
|
||||
{
|
||||
//App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match
|
||||
let targetAppEx: Set<ALTApplication> = targetAppBundle.appExtensions
|
||||
let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier}
|
||||
|
||||
guard let extensionsInExistingApp = localAppExtensions else {
|
||||
let diagnosticsMsg = "RemoveAppExtensionsOperation: ExistingApp is nil, Hence keeping all app extensions from targetAppBundle"
|
||||
+ "RemoveAppExtensionsOperation: ExistingAppEx: nil; targetAppBundleEx: \(targetAppExNames)"
|
||||
print(diagnosticsMsg)
|
||||
return Set() // nothing is excess since we are keeping all, so returning empty
|
||||
}
|
||||
|
||||
let existingAppEx: Set<ALTApplication> = extensionsInExistingApp
|
||||
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
|
||||
|
||||
let excessExtensionsInTargetApp = targetAppEx.filter{
|
||||
!(existingAppExNames.contains($0.bundleIdentifier))
|
||||
}
|
||||
|
||||
let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty
|
||||
let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in localAppBundle and targetAppBundle are matching: \(isMatching)\n"
|
||||
+ "RemoveAppExtensionsOperation: \nlocalAppBundleEx: \(existingAppExNames); \ntargetAppBundleEx: \(String(describing: targetAppExNames))\n"
|
||||
print(diagnosticsMsg)
|
||||
|
||||
return excessExtensionsInTargetApp
|
||||
}
|
||||
|
||||
private func backgroundModeExtensionsCleanup(excessExtensions: Set<ALTApplication>) {
|
||||
// perform silent extensions cleanup for those that aren't already present in existing app
|
||||
print("\n Performing background mode Extensions removal \n")
|
||||
print("RemoveAppExtensionsOperation: Excess Extensions In TargetAppBundle: \(excessExtensions.map{$0.bundleIdentifier})")
|
||||
|
||||
do {
|
||||
try Self.removeExtensions(from: excessExtensions)
|
||||
return self.finish(.success(()))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,6 +244,7 @@ private extension ResignAppOperation
|
||||
{
|
||||
for case let fileURL as URL in enumerator
|
||||
{
|
||||
// for both sim and device, in debug mode builds, remove the tests bundles (if any)
|
||||
#if DEBUG
|
||||
guard !fileURL.lastPathComponent.lowercased().contains(".xctest") else {
|
||||
// Remove embedded XCTest (+ dSYM) bundle from copied app bundle.
|
||||
|
||||
Reference in New Issue
Block a user