diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a5d86cfd..d2089d17 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @JoeMatt @lonkelle +* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index d1a6d1e5..9db97ffd 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -3,6 +3,7 @@ on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0 + workflow_dispatch: jobs: build: diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index f4dfb79d..41d03e40 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -332,6 +332,7 @@ D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */; }; D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; }; D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; }; + D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; }; D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; }; D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; }; D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */; }; @@ -839,6 +840,7 @@ D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogViewController.swift; sourceTree = ""; }; D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = ""; }; D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = ""; }; + D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = ""; }; D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = ""; }; D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 10.xcdatamodel"; sourceTree = ""; }; D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore9ToAltStore10.xcmappingmodel; sourceTree = ""; }; @@ -1697,6 +1699,7 @@ D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */, D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */, D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */, + D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */, BF7B44062725A4B8005288A4 /* Patch App */, ); path = Operations; @@ -2504,6 +2507,7 @@ BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */, BF41B808233433C100C593A3 /* LoadingState.swift in Sources */, BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */, + D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */, D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */, B39F16132918D7C5002E9404 /* Consts.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, diff --git a/AltStore/App IDs/AppIDsViewController.swift b/AltStore/App IDs/AppIDsViewController.swift index 1cd764b4..8030e6c9 100644 --- a/AltStore/App IDs/AppIDsViewController.swift +++ b/AltStore/App IDs/AppIDsViewController.swift @@ -91,13 +91,18 @@ private extension AppIDsViewController cell.bannerView.buttonLabel.isHidden = false - let currentDate = Date() + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .full + formatter.includesApproximationPhrase = false + formatter.includesTimeRemainingPhrase = false + formatter.allowedUnits = [.minute, .hour, .day] + formatter.maximumUnitCount = 1 - let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate) - let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays)) - cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal) + cell.bannerView.button.setTitle(formatter.string(from: Date(), to: expirationDate)?.uppercased(), for: .normal) - attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ") + formatter.includesTimeRemainingPhrase = true + + attributedAccessibilityLabel.mutableString.append((formatter.string(from: Date(), to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " ") } else { @@ -206,7 +211,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout **App IDs can't be deleted**, but they do expire after one week. SideStore will automatically renew App IDs for all active apps once they've expired. """, comment: "") - let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any]) + let attributedText = NSAttributedString(markdownRepresentation: text) headerView.textLabel.attributedText = attributedText } else diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 97a7816a..4df3fd8c 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -307,6 +307,16 @@ extension AppManager presentingViewController.present(alertController, animated: true, completion: nil) } } + + func clearAppCache(completion: @escaping (Result) -> Void) + { + let clearAppCacheOperation = ClearAppCacheOperation() + clearAppCacheOperation.resultHandler = { result in + completion(result) + } + + self.run([clearAppCacheOperation], context: nil) + } } extension AppManager @@ -754,6 +764,12 @@ extension AppManager let progress = self.refreshProgress[app.bundleIdentifier] return progress } + + func isActivelyManagingApp(withBundleID bundleID: String) -> Bool + { + let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID) + return isActivelyManaging + } } extension AppManager @@ -808,12 +824,6 @@ private extension AppManager } } - func isActivelyManagingApp(withBundleID bundleID: String) -> Bool - { - let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID) - return isActivelyManaging - } - @discardableResult private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) -> RefreshGroup { diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 57de2c8e..a5eac9ac 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -329,21 +329,25 @@ private extension MyAppsViewController let currentDate = Date() let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate) - let numberOfDaysText: String - if numberOfDays == 1 - { - numberOfDaysText = NSLocalizedString("1 day", comment: "") - } - else - { - numberOfDaysText = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays)) - } + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .full + formatter.includesApproximationPhrase = false + formatter.includesTimeRemainingPhrase = false + + formatter.allowedUnits = [.day, .hour, .minute] + + formatter.maximumUnitCount = 1 + + + + cell.bannerView.button.setTitle(formatter.string(from: currentDate, to: installedApp.expirationDate)?.uppercased(), for: .normal) - cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name) - - cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText) + + formatter.includesTimeRemainingPhrase = true + + cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " " // Make sure refresh button is correct size. cell.layoutIfNeeded() diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index af510de2..6cecb5b0 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -12,6 +12,7 @@ import Network import AltStoreCore import AltSign +import minimuxer enum AuthenticationError: LocalizedError { @@ -593,7 +594,7 @@ private extension AuthenticationOperation func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { - guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { + guard let udid = fetch_udid()?.toString() else { return completionHandler(.failure(OperationError.unknownUDID)) } diff --git a/AltStore/Operations/ClearAppCacheOperation.swift b/AltStore/Operations/ClearAppCacheOperation.swift new file mode 100644 index 00000000..879858c1 --- /dev/null +++ b/AltStore/Operations/ClearAppCacheOperation.swift @@ -0,0 +1,208 @@ +// +// ClearAppCacheOperation.swift +// AltStore +// +// Created by Riley Testut on 9/27/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import Foundation +import AltStoreCore +/* +struct BatchError: ALTLocalizedError +{ + + enum Code: Int, ALTErrorCode + { + typealias Error = BatchError + + case batchError + } + + var code: Code = .batchError + var underlyingErrors: [Error] + + var errorTitle: String? + var errorFailure: String? + + init(errors: [Error]) + { + self.underlyingErrors = errors + } + + var errorFailureReason: String { + guard !self.underlyingErrors.isEmpty else { return NSLocalizedString("An unknown error occured.", comment: "") } + + let errorMessages = self.underlyingErrors.map { $0.localizedDescription } + + let message = errorMessages.joined(separator: "\n\n") + return message + } +} +*/ +@objc(ClearAppCacheOperation) +class ClearAppCacheOperation: ResultOperation +{ + private let coordinator = NSFileCoordinator() + private let coordinatorQueue = OperationQueue() + + override init() + { + self.coordinatorQueue.name = "AltStore - ClearAppCacheOperation Queue" + } + + override func main() + { + super.main() + + var allErrors = [Error]() + + self.clearTemporaryDirectory { result in + switch result + { + //case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors) + case .failure(let error): allErrors.append(error) + case .success: break + } + + self.removeUninstalledAppBackupDirectories { result in + switch result + { + //case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors) + case .failure(let error): allErrors.append(error) + case .success: break + } + + if allErrors.isEmpty + { + self.finish(.success(())) + } + else + { + self.finish(.failure(OperationError.cacheClearError(errors: allErrors.map({ error in + return error.localizedDescription + })))) + } + } + } + } +} + +private extension ClearAppCacheOperation +{ + func clearTemporaryDirectory(completion: @escaping (Result) -> Void) + { + let intent = NSFileAccessIntent.writingIntent(with: FileManager.default.temporaryDirectory, options: [.forDeleting]) + self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in + do + { + if let error + { + throw error + } + + let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, + includingPropertiesForKeys: [], + options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles]) + var errors = [Error]() + + for fileURL in fileURLs + { + do + { + print("[ALTLog] Removing item from temporary directory:", fileURL.lastPathComponent) + try FileManager.default.removeItem(at: fileURL) + } + catch + { + print("[ALTLog] Failed to remove \(fileURL.lastPathComponent) from temporary directory.", error) + errors.append(error) + } + } + + if !errors.isEmpty + { + completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in + return error.localizedDescription + })))) + } + else + { + completion(.success(())) + } + } + catch + { + completion(.failure(error)) + } + } + } + + func removeUninstalledAppBackupDirectories(completion: @escaping (Result) -> Void) + { + guard let backupsDirectory = FileManager.default.appBackupsDirectory else { return completion(.failure(OperationError.missingAppGroup)) } + + DatabaseManager.shared.persistentContainer.performBackgroundTask { context in + let installedAppBundleIDs = Set(InstalledApp.all(in: context).map { $0.bundleIdentifier }) + + let intent = NSFileAccessIntent.writingIntent(with: backupsDirectory, options: [.forDeleting]) + self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in + do + { + if let error + { + throw error + } + + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: intent.url.path, isDirectory: &isDirectory), isDirectory.boolValue else { + completion(.success(())) + return + } + + let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, + includingPropertiesForKeys: [.isDirectoryKey, .nameKey], + options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles]) + var errors = [Error]() + + + for backupDirectory in fileURLs + { + do + { + let resourceValues = try backupDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey]) + guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue } + + if isDirectory && !installedAppBundleIDs.contains(bundleID) && !AppManager.shared.isActivelyManagingApp(withBundleID: bundleID) + { + print("[ALTLog] Removing backup directory for uninstalled app:", bundleID) + try FileManager.default.removeItem(at: backupDirectory) + } + } + catch + { + print("[ALTLog] Failed to remove app backup directory:", error) + errors.append(error) + } + } + + if !errors.isEmpty + { + completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in + return error.localizedDescription + })))) + } + else + { + completion(.success(())) + } + } + catch + { + print("[ALTLog] Failed to remove app backup directory:", error) + completion(.failure(error)) + } + } + } + } +} diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index 95cd1d28..4f33f6ed 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -40,6 +40,8 @@ enum OperationError: LocalizedError case provisioningError(result: String, message: String?) case anisetteV3Error(message: String) + case cacheClearError(errors: [String]) + var failureReason: String? { switch self { case .unknown: return NSLocalizedString("An unknown error occured.", comment: "") @@ -60,6 +62,7 @@ enum OperationError: LocalizedError case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message) case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "") case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message) + case .cacheClearError(let errors): return String(format: NSLocalizedString("An error occurred while clearing cache: %@", comment: ""), errors.joined(separator: "\n")) } } diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index 28f180d8..343f22e1 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -11,6 +11,7 @@ import Roxas import AltStoreCore import AltSign +import minimuxer @objc(ResignAppOperation) final class ResignAppOperation: ResultOperation @@ -181,7 +182,7 @@ private extension ResignAppOperation if app.isAltStoreApp { - guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID } + guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID } guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID } additionalValues[Bundle.Info.devicePairingString] = pairingFileString additionalValues[Bundle.Info.deviceID] = udid @@ -202,7 +203,7 @@ private extension ResignAppOperation // The embedded certificate + certificate identifier are already in app bundle, no need to update them. } } - else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String + else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = fetch_udid()?.toString() as? String { // There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID. additionalValues[Bundle.Info.deviceID] = udid diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 6885dbe0..33fc1873 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -21,7 +21,7 @@