Files
SideStore/AltStore/Model/DatabaseManager.swift
osy 3def65f501 Preserve device specific keys in Info.plist
Apple's Info.plist support platform and device specific keys to augment existing
keys. For example `UISupportedInterfaceOrientations~ipad` replaces
`UISupportedInterfaceOrientations` when running on an iPad.

By using Bundle.infoDictionary, Apple will pre-process the Info.plist and replace
any key with its device specific variant. Since AltStore does not support iPad,
this will strip out any iPad specific keys for the installing app.

We add an extension Bundle.completeInfoDictionary that will return the original
de-serialized dictionary including all the device specific keys.

See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html#//apple_ref/doc/uid/TP40009254-SW9

# Conflicts:
#	AltKit/Extensions/Bundle+AltStore.swift
#	AltStore/Model/DatabaseManager.swift
2020-08-31 12:47:11 -07:00

274 lines
10 KiB
Swift

//
// DatabaseManager.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import AltSign
import Roxas
public class DatabaseManager
{
public static let shared = DatabaseManager()
public let persistentContainer: RSTPersistentContainer
public private(set) var isStarted = false
private var startCompletionHandlers = [(Error?) -> Void]()
private init()
{
self.persistentContainer = RSTPersistentContainer(name: "AltStore")
self.persistentContainer.preferredMergePolicy = MergePolicy()
}
}
public extension DatabaseManager
{
func start(completionHandler: @escaping (Error?) -> Void)
{
self.startCompletionHandlers.append(completionHandler)
guard self.startCompletionHandlers.count == 1 else { return }
func finish(_ error: Error?)
{
self.startCompletionHandlers.forEach { $0(error) }
self.startCompletionHandlers.removeAll()
}
guard !self.isStarted else { return finish(nil) }
self.persistentContainer.loadPersistentStores { (description, error) in
guard error == nil else { return finish(error!) }
self.prepareDatabase() { (result) in
switch result
{
case .failure(let error):
finish(error)
case .success:
self.isStarted = true
finish(nil)
}
}
}
}
func signOut(completionHandler: @escaping (Error?) -> Void)
{
self.persistentContainer.performBackgroundTask { (context) in
if let account = self.activeAccount(in: context)
{
account.isActiveAccount = false
}
if let team = self.activeTeam(in: context)
{
team.isActiveTeam = false
}
do
{
try context.save()
Keychain.shared.reset()
completionHandler(nil)
}
catch
{
print("Failed to save when signing out.", error)
completionHandler(error)
}
}
}
}
public extension DatabaseManager
{
var viewContext: NSManagedObjectContext {
return self.persistentContainer.viewContext
}
}
extension DatabaseManager
{
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
{
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
let activeAccount = Account.first(satisfying: predicate, in: context)
return activeAccount
}
func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team?
{
let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
let activeTeam = Team.first(satisfying: predicate, in: context)
return activeTeam
}
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
{
let patronAccount = PatreonAccount.first(in: context)
return patronAccount
}
}
private extension DatabaseManager
{
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let context = self.persistentContainer.newBackgroundContext()
context.performAndWait {
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
let altStoreSource: Source
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
{
altStoreSource = source
}
else
{
altStoreSource = Source.makeAltStoreSource(in: context)
}
// Make sure to always update source URL to be current.
altStoreSource.sourceURL = Source.altStoreSourceURL
let storeApp: StoreApp
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
{
storeApp = app
}
else
{
storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.version = localApp.version
storeApp.source = altStoreSource
}
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
let installedApp: InstalledApp
if let app = storeApp.installedApp
{
installedApp = app
}
else
{
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
installedApp.storeApp = storeApp
}
/* App Extensions */
var installedExtensions = Set<InstalledExtension>()
for appExtension in localApp.appExtensions
{
let resignedBundleID = appExtension.bundleIdentifier
let originalBundleID = resignedBundleID.replacingOccurrences(of: localApp.bundleIdentifier, with: StoreApp.altstoreAppID)
let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
{
installedExtension = appExtension
}
else
{
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: context)
}
installedExtension.update(resignedAppExtension: appExtension)
installedExtensions.insert(installedExtension)
}
installedApp.appExtensions = installedExtensions
let fileURL = installedApp.fileURL
#if DEBUG
let replaceCachedApp = true
#else
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
#endif
if replaceCachedApp
{
func update(_ bundle: Bundle, bundleID: String) throws
{
let infoPlistURL = bundle.bundleURL.appendingPathComponent("Info.plist")
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = bundleID
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
}
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
do
{
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
guard let appBundle = Bundle(url: temporaryFileURL) else { throw ALTError(.invalidApp) }
try update(appBundle, bundleID: StoreApp.altstoreAppID)
if let tempApp = ALTApplication(fileURL: temporaryFileURL)
{
for appExtension in tempApp.appExtensions
{
guard let extensionBundle = Bundle(url: appExtension.fileURL) else { throw ALTError(.invalidApp) }
guard let installedExtension = installedExtensions.first(where: { $0.resignedBundleIdentifier == appExtension.bundleIdentifier }) else { throw ALTError(.invalidApp) }
try update(extensionBundle, bundleID: installedExtension.bundleIdentifier)
}
}
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
}
catch
{
print("Failed to copy AltStore app bundle to its proper location.", error)
}
}
}
let cachedRefreshedDate = installedApp.refreshedDate
let cachedExpirationDate = installedApp.expirationDate
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
if installedApp.refreshedDate < cachedRefreshedDate
{
// Embedded provisioning profile has a creation date older than our refreshed date.
// This most likely means we've refreshed the app since then, and profile is now outdated,
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
installedApp.refreshedDate = cachedRefreshedDate
installedApp.expirationDate = cachedExpirationDate
}
do
{
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
}