diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index c659971f..1a21be0f 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -991,7 +991,7 @@ private extension MyAppsViewController @objc func presentInactiveAppsAlert() { - let message: String + var message: String if UserDefaults.standard.activeAppLimitIncludesExtensions { @@ -1000,6 +1000,12 @@ private extension MyAppsViewController else { message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "") + + if UserDefaults.standard.ignoreActiveAppsLimit + { + message += "\n\n" + message += NSLocalizedString("If you're using the MacDirtyCow exploit to remove the 3-app limit, you can install up to 10 apps and app extensions instead.", comment: "") + } } let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: message, preferredStyle: .alert) diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index 1fd89ff4..509ea38e 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -245,7 +245,7 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1) if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion) { - UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit + UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit } else { diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 5415411e..90460b79 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -518,6 +518,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -603,6 +643,7 @@ + diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index 9888a521..f1ebef70 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -25,6 +25,7 @@ extension SettingsViewController case instructions case techyThings case credits + case macDirtyCow case debug } @@ -70,6 +71,7 @@ class SettingsViewController: UITableViewController @IBOutlet private var accountTypeLabel: UILabel! @IBOutlet private var backgroundRefreshSwitch: UISwitch! + @IBOutlet private var enforceThreeAppLimitSwitch: UISwitch! @IBOutlet private var versionLabel: UILabel! @@ -146,6 +148,7 @@ private extension SettingsViewController } self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled + self.enforceThreeAppLimitSwitch.isOn = !UserDefaults.standard.ignoreActiveAppsLimit if self.isViewLoaded { @@ -209,6 +212,16 @@ private extension SettingsViewController case .credits: settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "") + case .macDirtyCow: + if isHeader + { + settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("MACDIRTYCOW", comment: "") + } + else + { + settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("If you've removed the 3-sideloaded app limit via the MacDirtyCow exploit, disable this setting to sideload more than 3 apps at a time.", comment: "") + } + case .debug: settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "") } @@ -225,6 +238,20 @@ private extension SettingsViewController let size = settingsHeaderFooterView.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) return size.height } + + func isSectionHidden(_ section: Section) -> Bool + { + switch section + { + case .macDirtyCow: + let ios16_2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0) + + let isMacDirtyCowExploitSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2) + return !(isMacDirtyCowExploitSupported && UserDefaults.standard.isDebugModeEnabled) + + default: return false + } + } } private extension SettingsViewController @@ -279,6 +306,16 @@ private extension SettingsViewController UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn } + @IBAction func toggleEnforceThreeAppLimit(_ sender: UISwitch) + { + UserDefaults.standard.ignoreActiveAppsLimit = !sender.isOn + + if UserDefaults.standard.activeAppsLimit != nil + { + UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit + } + } + @available(iOS 14, *) @IBAction func addRefreshAppsShortcut() { @@ -376,6 +413,7 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return 0 case .signIn: return (self.activeTeam == nil) ? 1 : 0 case .account: return (self.activeTeam == nil) ? 0 : 3 case .appRefresh: return AppRefreshRow.allCases.count @@ -404,9 +442,10 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return nil case .signIn where self.activeTeam != nil: return nil case .account where self.activeTeam == nil: return nil - case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .debug: + case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug: let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView self.prepare(headerView, for: section, isHeader: true) return headerView @@ -420,8 +459,9 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return nil case .signIn where self.activeTeam != nil: return nil - case .signIn, .patreon, .appRefresh: + case .signIn, .patreon, .appRefresh, .macDirtyCow: let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView self.prepare(footerView, for: section, isHeader: false) return footerView @@ -435,9 +475,10 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return 1.0 case .signIn where self.activeTeam != nil: return 1.0 case .account where self.activeTeam == nil: return 1.0 - case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .debug: + case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug: let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true) return height @@ -450,9 +491,10 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return 1.0 case .signIn where self.activeTeam != nil: return 1.0 case .account where self.activeTeam == nil: return 1.0 - case .signIn, .patreon, .appRefresh: + case .signIn, .patreon, .appRefresh, .macDirtyCow: let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false) return height @@ -520,7 +562,7 @@ extension SettingsViewController case .refreshAttempts: break } - case .account, .patreon, .instructions, .techyThings: break + case .account, .patreon, .instructions, .techyThings, .macDirtyCow: break } } } diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift index 2b0ac0f4..0f3c512f 100644 --- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift +++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift @@ -41,6 +41,7 @@ public extension UserDefaults @NSManaged var trustedSourceIDs: [String]? + @nonobjc var activeAppsLimit: Int? { get { return self._activeAppsLimit?.intValue @@ -58,6 +59,8 @@ public extension UserDefaults } @NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber? + @NSManaged var ignoreActiveAppsLimit: Bool + class func registerDefaults() { let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0) @@ -72,7 +75,8 @@ public extension UserDefaults #keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported, #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions, #keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing, - #keyPath(UserDefaults.requiresAppGroupMigration): true + #keyPath(UserDefaults.requiresAppGroupMigration): true, + #keyPath(UserDefaults.ignoreActiveAppsLimit): false, ] UserDefaults.standard.register(defaults: defaults) diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift index b23926df..9511e15c 100644 --- a/AltStoreCore/Model/InstalledApp.swift +++ b/AltStoreCore/Model/InstalledApp.swift @@ -11,8 +11,22 @@ import CoreData import AltSign -// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1. -public let ALTActiveAppsLimit = 3 +extension InstalledApp +{ + public static var freeAccountActiveAppsLimit: Int { + if UserDefaults.standard.ignoreActiveAppsLimit + { + // MacDirtyCow exploit allows users to remove 3-app limit, so return 10 to match App ID limit per-week. + // Don't return nil because that implies there is no limit, which isn't quite true due to App ID limit. + return 10 + } + else + { + // Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1. + return 3 + } + } +} public protocol InstalledAppProtocol: Fetchable { diff --git a/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift b/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift index 880ae8fe..9c01f15f 100644 --- a/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift +++ b/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift @@ -50,7 +50,7 @@ class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy // We can assume there is an active app limit, // but will confirm next time user authenticates. - UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit + UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit } return NSNumber(value: isActive)