2019-05-20 21:24:53 +02:00
|
|
|
//
|
|
|
|
|
// DatabaseManager.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 5/20/19.
|
|
|
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import CoreData
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
import AltSign
|
2019-05-20 21:24:53 +02:00
|
|
|
import Roxas
|
|
|
|
|
|
2020-09-14 14:31:46 -07:00
|
|
|
private class PersistentContainer: RSTPersistentContainer
|
|
|
|
|
{
|
|
|
|
|
override class func defaultDirectoryURL() -> URL
|
|
|
|
|
{
|
|
|
|
|
guard let sharedDirectoryURL = FileManager.default.altstoreSharedDirectory else { return super.defaultDirectoryURL() }
|
|
|
|
|
|
|
|
|
|
let databaseDirectoryURL = sharedDirectoryURL.appendingPathComponent("Database")
|
|
|
|
|
try? FileManager.default.createDirectory(at: databaseDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
|
|
|
|
|
|
|
|
|
return databaseDirectoryURL
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-20 21:24:53 +02:00
|
|
|
public class DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
public static let shared = DatabaseManager()
|
|
|
|
|
|
|
|
|
|
public let persistentContainer: RSTPersistentContainer
|
|
|
|
|
|
|
|
|
|
public private(set) var isStarted = false
|
|
|
|
|
|
2019-07-31 13:24:59 -07:00
|
|
|
private var startCompletionHandlers = [(Error?) -> Void]()
|
2020-07-13 17:59:52 -07:00
|
|
|
private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager")
|
2019-07-31 13:24:59 -07:00
|
|
|
|
2019-05-20 21:24:53 +02:00
|
|
|
private init()
|
|
|
|
|
{
|
2020-09-14 14:31:46 -07:00
|
|
|
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
2019-07-24 12:23:54 -07:00
|
|
|
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
2019-05-20 21:24:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public extension DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
func start(completionHandler: @escaping (Error?) -> Void)
|
|
|
|
|
{
|
2019-07-31 13:24:59 -07:00
|
|
|
func finish(_ error: Error?)
|
|
|
|
|
{
|
2020-07-13 17:59:52 -07:00
|
|
|
self.dispatchQueue.async {
|
|
|
|
|
if error == nil
|
|
|
|
|
{
|
|
|
|
|
self.isStarted = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.startCompletionHandlers.forEach { $0(error) }
|
|
|
|
|
self.startCompletionHandlers.removeAll()
|
|
|
|
|
}
|
2019-07-31 13:24:59 -07:00
|
|
|
}
|
|
|
|
|
|
2020-07-13 17:59:52 -07:00
|
|
|
self.dispatchQueue.async {
|
|
|
|
|
self.startCompletionHandlers.append(completionHandler)
|
|
|
|
|
guard self.startCompletionHandlers.count == 1 else { return }
|
2019-05-20 21:24:53 +02:00
|
|
|
|
2020-07-13 17:59:52 -07:00
|
|
|
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: finish(nil)
|
|
|
|
|
}
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2019-05-20 21:24:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
2019-06-06 14:46:23 -07:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-20 21:24:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public extension DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
var viewContext: NSManagedObjectContext {
|
|
|
|
|
return self.persistentContainer.viewContext
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-06 14:46:23 -07:00
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public extension DatabaseManager
|
2019-06-06 14:46:23 -07:00
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
}
|
2019-08-28 11:13:22 -07:00
|
|
|
|
|
|
|
|
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
|
|
|
|
|
{
|
|
|
|
|
let patronAccount = PatreonAccount.first(in: context)
|
|
|
|
|
return patronAccount
|
|
|
|
|
}
|
2019-06-06 14:46:23 -07:00
|
|
|
}
|
2019-06-17 16:31:10 -07:00
|
|
|
|
|
|
|
|
private extension DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
|
|
|
{
|
2020-09-15 13:51:29 -07:00
|
|
|
guard !Bundle.isAppExtension() else { return completionHandler(.success(())) }
|
|
|
|
|
|
2019-07-31 13:24:59 -07:00
|
|
|
let context = self.persistentContainer.newBackgroundContext()
|
|
|
|
|
context.performAndWait {
|
2019-07-28 15:08:13 -07:00
|
|
|
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
|
2019-06-17 16:31:10 -07:00
|
|
|
|
2019-09-14 12:38:53 -07:00
|
|
|
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
|
|
|
|
|
|
2019-07-31 14:07:00 -07:00
|
|
|
let storeApp: StoreApp
|
2019-06-17 16:31:10 -07:00
|
|
|
|
2019-07-31 14:07:00 -07:00
|
|
|
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
|
2019-06-17 16:31:10 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
storeApp = app
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2019-07-31 14:07:00 -07:00
|
|
|
storeApp = StoreApp.makeAltStoreApp(in: context)
|
2019-07-28 15:08:13 -07:00
|
|
|
storeApp.version = localApp.version
|
2019-09-14 12:38:53 -07:00
|
|
|
storeApp.source = altStoreSource
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
2019-09-14 12:38:53 -07:00
|
|
|
|
2020-03-06 17:08:35 -08:00
|
|
|
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
|
2019-06-21 11:20:03 -07:00
|
|
|
let installedApp: InstalledApp
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
if let app = storeApp.installedApp
|
2019-06-17 16:31:10 -07:00
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
installedApp = app
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-03-06 17:08:35 -08:00
|
|
|
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
|
2019-07-28 15:08:13 -07:00
|
|
|
installedApp.storeApp = storeApp
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
|
|
|
|
|
2020-07-20 14:57:13 -07:00
|
|
|
/* 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
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
let fileURL = installedApp.fileURL
|
2020-06-07 09:49:29 -07:00
|
|
|
|
|
|
|
|
#if DEBUG
|
|
|
|
|
let replaceCachedApp = true
|
|
|
|
|
#else
|
|
|
|
|
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
if replaceCachedApp
|
2019-07-28 15:08:13 -07:00
|
|
|
{
|
2020-07-20 14:57:13 -07:00
|
|
|
func update(_ bundle: Bundle, bundleID: String) throws
|
|
|
|
|
{
|
|
|
|
|
let infoPlistURL = bundle.bundleURL.appendingPathComponent("Info.plist")
|
|
|
|
|
|
2020-06-10 14:58:25 -07:00
|
|
|
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
2020-07-20 14:57:13 -07:00
|
|
|
infoDictionary[kCFBundleIdentifierKey as String] = bundleID
|
|
|
|
|
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-21 16:35:08 -07:00
|
|
|
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
|
|
|
|
|
|
2020-07-20 14:57:13 -07:00
|
|
|
guard let appBundle = Bundle(url: temporaryFileURL) else { throw ALTError(.invalidApp) }
|
|
|
|
|
try update(appBundle, bundleID: StoreApp.altstoreAppID)
|
2019-09-21 16:35:08 -07:00
|
|
|
|
2020-07-20 14:57:13 -07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-21 16:35:08 -07:00
|
|
|
|
|
|
|
|
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
print("Failed to copy AltStore app bundle to its proper location.", error)
|
|
|
|
|
}
|
2019-07-28 15:08:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 14:43:19 -07:00
|
|
|
let cachedRefreshedDate = installedApp.refreshedDate
|
|
|
|
|
let cachedExpirationDate = installedApp.expirationDate
|
|
|
|
|
|
2019-09-21 16:35:08 -07:00
|
|
|
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
2020-03-06 17:08:35 -08:00
|
|
|
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
|
2019-06-17 16:31:10 -07:00
|
|
|
|
2020-03-11 14:43:19 -07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-17 16:31:10 -07:00
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
try context.save()
|
|
|
|
|
completionHandler(.success(()))
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|