mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-11 07:43:28 +01:00
Create swift package
This commit is contained in:
60
Sources/SideStoreCore/Model/Account.swift
Normal file
60
Sources/SideStoreCore/Model/Account.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Account.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(Account)
|
||||
public class Account: NSManagedObject, Fetchable {
|
||||
public var localizedName: String {
|
||||
var components = PersonNameComponents()
|
||||
components.givenName = firstName
|
||||
components.familyName = lastName
|
||||
|
||||
let name = PersonNameComponentsFormatter.localizedString(from: components, style: .default)
|
||||
return name
|
||||
}
|
||||
|
||||
/* Properties */
|
||||
@NSManaged public var appleID: String
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
@NSManaged public var firstName: String
|
||||
@NSManaged public var lastName: String
|
||||
|
||||
@NSManaged public var isActiveAccount: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public var teams: Set<Team>
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(_ account: ALTAccount, context: NSManagedObjectContext) {
|
||||
super.init(entity: Account.entity(), insertInto: context)
|
||||
|
||||
update(account: account)
|
||||
}
|
||||
|
||||
public func update(account: ALTAccount) {
|
||||
appleID = account.appleID
|
||||
identifier = account.identifier
|
||||
|
||||
firstName = account.firstName
|
||||
lastName = account.lastName
|
||||
}
|
||||
}
|
||||
|
||||
public extension Account {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Account> {
|
||||
NSFetchRequest<Account>(entityName: "Account")
|
||||
}
|
||||
}
|
||||
47
Sources/SideStoreCore/Model/AppID.swift
Normal file
47
Sources/SideStoreCore/Model/AppID.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// AppID.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/27/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(AppID)
|
||||
public class AppID: NSManagedObject, Fetchable {
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public var bundleIdentifier: String
|
||||
@NSManaged public var features: [ALTFeature: Any]
|
||||
@NSManaged public var expirationDate: Date?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var team: Team?
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext) {
|
||||
super.init(entity: AppID.entity(), insertInto: context)
|
||||
|
||||
name = appID.name
|
||||
identifier = appID.identifier
|
||||
bundleIdentifier = appID.bundleIdentifier
|
||||
features = appID.features
|
||||
expirationDate = appID.expirationDate
|
||||
|
||||
self.team = team
|
||||
}
|
||||
}
|
||||
|
||||
public extension AppID {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppID> {
|
||||
NSFetchRequest<AppID>(entityName: "AppID")
|
||||
}
|
||||
}
|
||||
113
Sources/SideStoreCore/Model/AppPermission.swift
Normal file
113
Sources/SideStoreCore/Model/AppPermission.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// AppPermission.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
public extension ALTAppPermissionType {
|
||||
var localizedShortName: String? {
|
||||
switch self {
|
||||
case .photos: return NSLocalizedString("Photos", comment: "")
|
||||
case .backgroundAudio: return NSLocalizedString("Audio (BG)", comment: "")
|
||||
case .backgroundFetch: return NSLocalizedString("Fetch (BG)", comment: "")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var localizedName: String? {
|
||||
switch self {
|
||||
case .photos: return NSLocalizedString("Photos", comment: "")
|
||||
case .camera: return NSLocalizedString("Camera", comment: "")
|
||||
case .location: return NSLocalizedString("Location", comment: "")
|
||||
case .contacts: return NSLocalizedString("Contacts", comment: "")
|
||||
case .reminders: return NSLocalizedString("Reminders", comment: "")
|
||||
case .appleMusic: return NSLocalizedString("Apple Music", comment: "")
|
||||
case .microphone: return NSLocalizedString("Microphone", comment: "")
|
||||
case .speechRecognition: return NSLocalizedString("Speech Recognition", comment: "")
|
||||
case .backgroundAudio: return NSLocalizedString("Background Audio", comment: "")
|
||||
case .backgroundFetch: return NSLocalizedString("Background Fetch", comment: "")
|
||||
case .bluetooth: return NSLocalizedString("Bluetooth", comment: "")
|
||||
case .network: return NSLocalizedString("Network", comment: "")
|
||||
case .calendars: return NSLocalizedString("Calendars", comment: "")
|
||||
case .touchID: return NSLocalizedString("Touch ID", comment: "")
|
||||
case .faceID: return NSLocalizedString("Face ID", comment: "")
|
||||
case .siri: return NSLocalizedString("Siri", comment: "")
|
||||
case .motion: return NSLocalizedString("Motion", comment: "")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
switch self {
|
||||
case .photos: return UIImage(systemName: "photo.on.rectangle.angled")
|
||||
case .camera: return UIImage(systemName: "camera.fill")
|
||||
case .location: return UIImage(systemName: "location.fill")
|
||||
case .contacts: return UIImage(systemName: "person.2.fill")
|
||||
case .reminders: return UIImage(systemName: "checklist")
|
||||
case .appleMusic: return UIImage(systemName: "music.note")
|
||||
case .microphone: return UIImage(systemName: "mic.fill")
|
||||
case .speechRecognition: return UIImage(systemName: "waveform.and.mic")
|
||||
case .backgroundAudio: return UIImage(systemName: "speaker.fill")
|
||||
case .backgroundFetch: return UIImage(systemName: "square.and.arrow.down")
|
||||
case .bluetooth: return UIImage(systemName: "wave.3.right")
|
||||
case .network: return UIImage(systemName: "network")
|
||||
case .calendars: return UIImage(systemName: "calendar")
|
||||
case .touchID: return UIImage(systemName: "touchid")
|
||||
case .faceID: return UIImage(systemName: "faceid")
|
||||
case .siri: return UIImage(systemName: "mic.and.signal.meter.fill")
|
||||
case .motion: return UIImage(systemName: "figure.walk.motion")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AppPermission)
|
||||
public class AppPermission: NSManagedObject, Decodable, Fetchable {
|
||||
/* Properties */
|
||||
@NSManaged public var type: ALTAppPermissionType
|
||||
@NSManaged public var usageDescription: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var app: StoreApp!
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case usageDescription
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
super.init(entity: AppPermission.entity(), insertInto: context)
|
||||
|
||||
do {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
usageDescription = try container.decode(String.self, forKey: .usageDescription)
|
||||
|
||||
let rawType = try container.decode(String.self, forKey: .type)
|
||||
type = ALTAppPermissionType(rawValue: rawType)
|
||||
} catch {
|
||||
if let context = managedObjectContext {
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension AppPermission {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission> {
|
||||
NSFetchRequest<AppPermission>(entityName: "AppPermission")
|
||||
}
|
||||
}
|
||||
108
Sources/SideStoreCore/Model/AppVersion.swift
Normal file
108
Sources/SideStoreCore/Model/AppVersion.swift
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// AppVersion.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 8/18/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(AppVersion)
|
||||
public class AppVersion: NSManagedObject, Decodable, Fetchable {
|
||||
/* Properties */
|
||||
@NSManaged public var version: String
|
||||
@NSManaged public var date: Date
|
||||
@NSManaged public var localizedDescription: String?
|
||||
|
||||
@NSManaged public var downloadURL: URL
|
||||
@NSManaged public var size: Int64
|
||||
|
||||
@nonobjc public var minOSVersion: OperatingSystemVersion? {
|
||||
guard let osVersionString = _minOSVersion else { return nil }
|
||||
|
||||
let osVersion = OperatingSystemVersion(string: osVersionString)
|
||||
return osVersion
|
||||
}
|
||||
|
||||
@NSManaged @objc(minOSVersion) private var _minOSVersion: String?
|
||||
|
||||
@nonobjc public var maxOSVersion: OperatingSystemVersion? {
|
||||
guard let osVersionString = _maxOSVersion else { return nil }
|
||||
|
||||
let osVersion = OperatingSystemVersion(string: osVersionString)
|
||||
return osVersion
|
||||
}
|
||||
|
||||
@NSManaged @objc(maxOSVersion) private var _maxOSVersion: String?
|
||||
|
||||
@NSManaged public var appBundleID: String
|
||||
@NSManaged public var sourceID: String?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var app: StoreApp?
|
||||
@NSManaged public private(set) var latestVersionApp: StoreApp?
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case date
|
||||
case localizedDescription
|
||||
case downloadURL
|
||||
case size
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
super.init(entity: AppVersion.entity(), insertInto: context)
|
||||
|
||||
do {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
version = try container.decode(String.self, forKey: .version)
|
||||
date = try container.decode(Date.self, forKey: .date)
|
||||
localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
|
||||
|
||||
downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
size = try container.decode(Int64.self, forKey: .size)
|
||||
} catch {
|
||||
if let context = managedObjectContext {
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension AppVersion {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppVersion> {
|
||||
NSFetchRequest<AppVersion>(entityName: "AppVersion")
|
||||
}
|
||||
|
||||
class func makeAppVersion(
|
||||
version: String,
|
||||
date: Date,
|
||||
localizedDescription: String? = nil,
|
||||
downloadURL: URL,
|
||||
size: Int64,
|
||||
appBundleID: String,
|
||||
sourceID: String? = nil,
|
||||
in context: NSManagedObjectContext
|
||||
) -> AppVersion {
|
||||
let appVersion = AppVersion(context: context)
|
||||
appVersion.version = version
|
||||
appVersion.date = date
|
||||
appVersion.localizedDescription = localizedDescription
|
||||
appVersion.downloadURL = downloadURL
|
||||
appVersion.size = size
|
||||
appVersion.appBundleID = appBundleID
|
||||
appVersion.sourceID = sourceID
|
||||
|
||||
return appVersion
|
||||
}
|
||||
}
|
||||
366
Sources/SideStoreCore/Model/DatabaseManager.swift
Normal file
366
Sources/SideStoreCore/Model/DatabaseManager.swift
Normal file
@@ -0,0 +1,366 @@
|
||||
//
|
||||
// DatabaseManager.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
private extension CFNotificationName {
|
||||
static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString)
|
||||
}
|
||||
|
||||
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { _, _, _, _, _ in
|
||||
DatabaseManager.shared.receivedWillAccessDatabaseNotification()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
class func legacyDirectoryURL() -> URL {
|
||||
super.defaultDirectoryURL()
|
||||
}
|
||||
}
|
||||
|
||||
public class DatabaseManager {
|
||||
public static let shared = DatabaseManager()
|
||||
|
||||
public let persistentContainer: RSTPersistentContainer
|
||||
|
||||
public private(set) var isStarted = false
|
||||
|
||||
private var startCompletionHandlers = [(Error?) -> Void]()
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager")
|
||||
|
||||
private let coordinator = NSFileCoordinator()
|
||||
private let coordinatorQueue = OperationQueue()
|
||||
|
||||
private var ignoreWillAccessDatabaseNotification = false
|
||||
|
||||
private init() {
|
||||
persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
||||
persistentContainer.preferredMergePolicy = MergePolicy()
|
||||
|
||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager {
|
||||
func start(completionHandler: @escaping (Error?) -> Void) {
|
||||
func finish(_ error: Error?) {
|
||||
dispatchQueue.async {
|
||||
if error == nil {
|
||||
self.isStarted = true
|
||||
}
|
||||
|
||||
self.startCompletionHandlers.forEach { $0(error) }
|
||||
self.startCompletionHandlers.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchQueue.async {
|
||||
self.startCompletionHandlers.append(completionHandler)
|
||||
guard self.startCompletionHandlers.count == 1 else { return }
|
||||
|
||||
guard !self.isStarted else { return finish(nil) }
|
||||
|
||||
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
|
||||
self.ignoreWillAccessDatabaseNotification = true
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true)
|
||||
|
||||
self.migrateDatabaseToAppGroupIfNeeded { result in
|
||||
switch result {
|
||||
case let .failure(error): finish(error)
|
||||
case .success:
|
||||
self.persistentContainer.loadPersistentStores { _, error in
|
||||
guard error == nil else { return finish(error!) }
|
||||
|
||||
self.prepareDatabase { result in
|
||||
switch result {
|
||||
case let .failure(error): finish(error)
|
||||
case .success: finish(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signOut(completionHandler: @escaping (Error?) -> Void) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func purgeLoggedErrors(before date: Date? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
persistentContainer.performBackgroundTask { context in
|
||||
do {
|
||||
let predicate = date.map { NSPredicate(format: "%K <= %@", #keyPath(LoggedError.date), $0 as NSDate) }
|
||||
|
||||
let loggedErrors = LoggedError.all(satisfying: predicate, in: context, requestProperties: [\.returnsObjectsAsFaults: true])
|
||||
loggedErrors.forEach { context.delete($0) }
|
||||
|
||||
try context.save()
|
||||
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager {
|
||||
var viewContext: NSManagedObjectContext {
|
||||
persistentContainer.viewContext
|
||||
}
|
||||
|
||||
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? {
|
||||
guard let patreonAccountID = Keychain.shared.patreonAccountID else { return nil }
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(PatreonAccount.identifier), patreonAccountID)
|
||||
|
||||
let patreonAccount = PatreonAccount.first(satisfying: predicate, in: context)
|
||||
return patreonAccount
|
||||
}
|
||||
}
|
||||
|
||||
private extension DatabaseManager {
|
||||
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard !Bundle.isAppExtension else { return completionHandler(.success(())) }
|
||||
|
||||
let context = 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.latestVersion?.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDatabaseToAppGroupIfNeeded(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard UserDefaults.shared.requiresAppGroupMigration else { return completion(.success(())) }
|
||||
|
||||
func finish(_ result: Result<Void, Error>) {
|
||||
switch result {
|
||||
case let .failure(error): completion(.failure(error))
|
||||
case .success:
|
||||
UserDefaults.shared.requiresAppGroupMigration = false
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
let previousDatabaseURL = PersistentContainer.legacyDirectoryURL().appendingPathComponent("AltStore.sqlite")
|
||||
let databaseURL = PersistentContainer.defaultDirectoryURL().appendingPathComponent("AltStore.sqlite")
|
||||
|
||||
let previousAppsDirectoryURL = InstalledApp.legacyAppsDirectoryURL
|
||||
let appsDirectoryURL = InstalledApp.appsDirectoryURL
|
||||
|
||||
let databaseIntent = NSFileAccessIntent.writingIntent(with: databaseURL, options: [.forReplacing])
|
||||
let appsIntent = NSFileAccessIntent.writingIntent(with: appsDirectoryURL, options: [.forReplacing])
|
||||
|
||||
coordinator.coordinate(with: [databaseIntent, appsIntent], queue: coordinatorQueue) { error in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
let description = NSPersistentStoreDescription(url: previousDatabaseURL)
|
||||
|
||||
// Disable WAL to remove extra files automatically during migration.
|
||||
description.setOption(["journal_mode": "DELETE"] as NSDictionary, forKey: NSSQLitePragmasOption)
|
||||
|
||||
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.persistentContainer.managedObjectModel)
|
||||
|
||||
// Migrate database
|
||||
if FileManager.default.fileExists(atPath: previousDatabaseURL.path) {
|
||||
if FileManager.default.fileExists(atPath: databaseURL.path, isDirectory: nil) {
|
||||
try FileManager.default.removeItem(at: databaseURL)
|
||||
}
|
||||
|
||||
let previousDatabase = try persistentStoreCoordinator.addPersistentStore(ofType: description.type, configurationName: description.configuration, at: description.url, options: description.options)
|
||||
|
||||
// Pass nil options to prevent later error due to self.persistentContainer using WAL.
|
||||
try persistentStoreCoordinator.migratePersistentStore(previousDatabase, to: databaseURL, options: nil, withType: NSSQLiteStoreType)
|
||||
|
||||
try FileManager.default.removeItem(at: previousDatabaseURL)
|
||||
}
|
||||
|
||||
// Migrate apps
|
||||
if FileManager.default.fileExists(atPath: previousAppsDirectoryURL.path, isDirectory: nil) {
|
||||
_ = try FileManager.default.replaceItemAt(appsDirectoryURL, withItemAt: previousAppsDirectoryURL)
|
||||
}
|
||||
|
||||
finish(.success(()))
|
||||
} catch {
|
||||
print("Failed to migrate database to app group:", error)
|
||||
finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivedWillAccessDatabaseNotification() {
|
||||
defer { self.ignoreWillAccessDatabaseNotification = false }
|
||||
|
||||
// Ignore notifications sent by the current process.
|
||||
guard !ignoreWillAccessDatabaseNotification else { return }
|
||||
|
||||
exit(104)
|
||||
}
|
||||
}
|
||||
320
Sources/SideStoreCore/Model/InstalledApp.swift
Normal file
320
Sources/SideStoreCore/Model/InstalledApp.swift
Normal file
@@ -0,0 +1,320 @@
|
||||
//
|
||||
// InstalledApp.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
import SemanticVersion
|
||||
|
||||
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
|
||||
public let ALTActiveAppsLimit = 3
|
||||
|
||||
public protocol InstalledAppProtocol: Fetchable {
|
||||
var name: String { get }
|
||||
var bundleIdentifier: String { get }
|
||||
var resignedBundleIdentifier: String { get }
|
||||
var version: String { get }
|
||||
|
||||
var refreshedDate: Date { get }
|
||||
var expirationDate: Date { get }
|
||||
var installedDate: Date { get }
|
||||
}
|
||||
|
||||
@objc(InstalledApp)
|
||||
public class InstalledApp: NSManagedObject, InstalledAppProtocol {
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var bundleIdentifier: String
|
||||
@NSManaged public var resignedBundleIdentifier: String
|
||||
@NSManaged public var version: String
|
||||
|
||||
@NSManaged public var refreshedDate: Date
|
||||
@NSManaged public var expirationDate: Date
|
||||
@NSManaged public var installedDate: Date
|
||||
|
||||
@NSManaged public var isActive: Bool
|
||||
@NSManaged public var needsResign: Bool
|
||||
@NSManaged public var hasAlternateIcon: Bool
|
||||
|
||||
@NSManaged public var certificateSerialNumber: String?
|
||||
|
||||
/* Transient */
|
||||
@NSManaged public var isRefreshing: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public var storeApp: StoreApp?
|
||||
@NSManaged public var team: Team?
|
||||
@NSManaged public var appExtensions: Set<InstalledExtension>
|
||||
|
||||
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
||||
|
||||
public var isSideloaded: Bool {
|
||||
storeApp == nil
|
||||
}
|
||||
|
||||
@objc public var hasUpdate: Bool {
|
||||
if storeApp == nil { return false }
|
||||
if storeApp!.latestVersion == nil { return false }
|
||||
|
||||
let currentVersion = SemanticVersion(version)
|
||||
let latestVersion = SemanticVersion(storeApp!.latestVersion!.version)
|
||||
|
||||
if currentVersion == nil || latestVersion == nil {
|
||||
// One of the versions is not valid SemVer, fall back to comparing the version strings by character
|
||||
return version < storeApp!.latestVersion!.version
|
||||
}
|
||||
|
||||
return currentVersion! < latestVersion!
|
||||
}
|
||||
|
||||
public var appIDCount: Int {
|
||||
1 + appExtensions.count
|
||||
}
|
||||
|
||||
public var requiredActiveSlots: Int {
|
||||
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
|
||||
return requiredActiveSlots
|
||||
}
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext) {
|
||||
super.init(entity: InstalledApp.entity(), insertInto: context)
|
||||
|
||||
bundleIdentifier = originalBundleIdentifier
|
||||
|
||||
print("InstalledApp `self.bundleIdentifier`: \(bundleIdentifier)")
|
||||
|
||||
refreshedDate = Date()
|
||||
installedDate = Date()
|
||||
|
||||
expirationDate = refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||
|
||||
update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
|
||||
}
|
||||
|
||||
public func update(resignedApp: ALTApplication, certificateSerialNumber: String?) {
|
||||
name = resignedApp.name
|
||||
|
||||
resignedBundleIdentifier = resignedApp.bundleIdentifier
|
||||
version = resignedApp.version
|
||||
|
||||
self.certificateSerialNumber = certificateSerialNumber
|
||||
|
||||
if let provisioningProfile = resignedApp.provisioningProfile {
|
||||
update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(provisioningProfile: ALTProvisioningProfile) {
|
||||
refreshedDate = provisioningProfile.creationDate
|
||||
expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
|
||||
public func loadIcon(completion: @escaping (Result<UIImage?, Error>) -> Void) {
|
||||
let hasAlternateIcon = self.hasAlternateIcon
|
||||
let alternateIconURL = self.alternateIconURL
|
||||
let fileURL = self.fileURL
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do {
|
||||
if hasAlternateIcon,
|
||||
case let data = try Data(contentsOf: alternateIconURL),
|
||||
let icon = UIImage(data: data) {
|
||||
return completion(.success(icon))
|
||||
}
|
||||
|
||||
let application = ALTApplication(fileURL: fileURL)
|
||||
completion(.success(application?.icon))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstalledApp {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp> {
|
||||
NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
|
||||
}
|
||||
|
||||
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp> {
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES",
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate))
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
class func activeAppsFetchRequest() -> NSFetchRequest<InstalledApp> {
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
|
||||
print("Active Apps Fetch Request: \(String(describing: fetchRequest.predicate))")
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp? {
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
print("Fetch 'AltStore' Predicate: \(String(describing: predicate))")
|
||||
let altStore = InstalledApp.first(satisfying: predicate, in: context)
|
||||
return altStore
|
||||
}
|
||||
|
||||
class func fetchActiveApps(in context: NSManagedObjectContext) -> [InstalledApp] {
|
||||
let activeApps = InstalledApp.fetch(InstalledApp.activeAppsFetchRequest(), in: context)
|
||||
return activeApps
|
||||
}
|
||||
|
||||
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp] {
|
||||
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
|
||||
|
||||
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
// {
|
||||
// // No additional predicate
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
||||
// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
||||
// }
|
||||
|
||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||
in: context)
|
||||
|
||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context) {
|
||||
// Refresh AltStore last since it causes app to quit.
|
||||
installedApps.append(altStoreApp)
|
||||
}
|
||||
|
||||
return installedApps
|
||||
}
|
||||
|
||||
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp] {
|
||||
// Date 6 hours before now.
|
||||
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
||||
|
||||
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
|
||||
#keyPath(InstalledApp.isActive),
|
||||
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
print("Active Apps For Background Refresh 'AltStore' predicate: \(String(describing: predicate))")
|
||||
|
||||
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
// {
|
||||
// // No additional predicate
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
||||
// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
||||
// }
|
||||
|
||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||
in: context)
|
||||
|
||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date {
|
||||
// Refresh AltStore last since it may cause app to quit.
|
||||
installedApps.append(altStoreApp)
|
||||
}
|
||||
|
||||
return installedApps
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstalledApp {
|
||||
var openAppURL: URL {
|
||||
let openAppURL = URL(string: "altstore-" + bundleIdentifier + "://")!
|
||||
return openAppURL
|
||||
}
|
||||
|
||||
class func openAppURL(for app: AppProtocol) -> URL {
|
||||
let openAppURL = URL(string: "altstore-" + app.bundleIdentifier + "://")!
|
||||
return openAppURL
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstalledApp {
|
||||
class var appsDirectoryURL: URL {
|
||||
let baseDirectory = FileManager.default.altstoreSharedDirectory ?? FileManager.default.applicationSupportDirectory
|
||||
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
||||
|
||||
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) } catch { print("Creating App Directory Error: \(error)") }
|
||||
print("`appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
|
||||
return appsDirectoryURL
|
||||
}
|
||||
|
||||
class var legacyAppsDirectoryURL: URL {
|
||||
let baseDirectory = FileManager.default.applicationSupportDirectory
|
||||
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
||||
print("legacy `appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
|
||||
return appsDirectoryURL
|
||||
}
|
||||
|
||||
class func fileURL(for app: AppProtocol) -> URL {
|
||||
let appURL = directoryURL(for: app).appendingPathComponent("App.app")
|
||||
return appURL
|
||||
}
|
||||
|
||||
class func refreshedIPAURL(for app: AppProtocol) -> URL {
|
||||
let ipaURL = directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
|
||||
print("`ipaURL`: \(ipaURL.absoluteString)")
|
||||
return ipaURL
|
||||
}
|
||||
|
||||
class func directoryURL(for app: AppProtocol) -> URL {
|
||||
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.bundleIdentifier)
|
||||
|
||||
do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) } catch { print(error) }
|
||||
|
||||
return directoryURL
|
||||
}
|
||||
|
||||
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String {
|
||||
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
|
||||
return installedAppUTI
|
||||
}
|
||||
|
||||
class func installedBackupAppUTI(forBundleIdentifier bundleIdentifier: String) -> String {
|
||||
let installedBackupAppUTI = InstalledApp.installedAppUTI(forBundleIdentifier: bundleIdentifier) + ".backup"
|
||||
return installedBackupAppUTI
|
||||
}
|
||||
|
||||
class func alternateIconURL(for app: AppProtocol) -> URL {
|
||||
let installedBackupAppUTI = directoryURL(for: app).appendingPathComponent("AltIcon.png")
|
||||
return installedBackupAppUTI
|
||||
}
|
||||
|
||||
var directoryURL: URL {
|
||||
InstalledApp.directoryURL(for: self)
|
||||
}
|
||||
|
||||
var fileURL: URL {
|
||||
InstalledApp.fileURL(for: self)
|
||||
}
|
||||
|
||||
var refreshedIPAURL: URL {
|
||||
InstalledApp.refreshedIPAURL(for: self)
|
||||
}
|
||||
|
||||
var installedAppUTI: String {
|
||||
InstalledApp.installedAppUTI(forBundleIdentifier: resignedBundleIdentifier)
|
||||
}
|
||||
|
||||
var installedBackupAppUTI: String {
|
||||
InstalledApp.installedBackupAppUTI(forBundleIdentifier: resignedBundleIdentifier)
|
||||
}
|
||||
|
||||
var alternateIconURL: URL {
|
||||
InstalledApp.alternateIconURL(for: self)
|
||||
}
|
||||
}
|
||||
67
Sources/SideStoreCore/Model/InstalledExtension.swift
Normal file
67
Sources/SideStoreCore/Model/InstalledExtension.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// InstalledExtension.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/7/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(InstalledExtension)
|
||||
public class InstalledExtension: NSManagedObject, InstalledAppProtocol {
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var bundleIdentifier: String
|
||||
@NSManaged public var resignedBundleIdentifier: String
|
||||
@NSManaged public var version: String
|
||||
|
||||
@NSManaged public var refreshedDate: Date
|
||||
@NSManaged public var expirationDate: Date
|
||||
@NSManaged public var installedDate: Date
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public var parentApp: InstalledApp?
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext) {
|
||||
super.init(entity: InstalledExtension.entity(), insertInto: context)
|
||||
|
||||
bundleIdentifier = originalBundleIdentifier
|
||||
|
||||
refreshedDate = Date()
|
||||
installedDate = Date()
|
||||
|
||||
expirationDate = refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||
|
||||
update(resignedAppExtension: resignedAppExtension)
|
||||
}
|
||||
|
||||
public func update(resignedAppExtension: ALTApplication) {
|
||||
name = resignedAppExtension.name
|
||||
|
||||
resignedBundleIdentifier = resignedAppExtension.bundleIdentifier
|
||||
version = resignedAppExtension.version
|
||||
|
||||
if let provisioningProfile = resignedAppExtension.provisioningProfile {
|
||||
update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(provisioningProfile: ALTProvisioningProfile) {
|
||||
refreshedDate = provisioningProfile.creationDate
|
||||
expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstalledExtension {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension> {
|
||||
NSFetchRequest<InstalledExtension>(entityName: "InstalledExtension")
|
||||
}
|
||||
}
|
||||
117
Sources/SideStoreCore/Model/LoggedError.swift
Normal file
117
Sources/SideStoreCore/Model/LoggedError.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// LoggedError.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 9/6/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
public extension LoggedError {
|
||||
enum Operation: String {
|
||||
case install
|
||||
case update
|
||||
case refresh
|
||||
case activate
|
||||
case deactivate
|
||||
case backup
|
||||
case restore
|
||||
}
|
||||
}
|
||||
|
||||
@objc(LoggedError)
|
||||
public class LoggedError: NSManagedObject, Fetchable {
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var date: Date
|
||||
|
||||
@nonobjc public var operation: Operation? {
|
||||
guard let rawOperation = _operation else { return nil }
|
||||
|
||||
let operation = Operation(rawValue: rawOperation)
|
||||
return operation
|
||||
}
|
||||
|
||||
@NSManaged @objc(operation) private var _operation: String?
|
||||
|
||||
@NSManaged public private(set) var domain: String
|
||||
@NSManaged public private(set) var code: Int32
|
||||
@NSManaged public private(set) var userInfo: [String: Any]
|
||||
|
||||
@NSManaged public private(set) var appName: String
|
||||
@NSManaged public private(set) var appBundleID: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var storeApp: StoreApp?
|
||||
@NSManaged public private(set) var installedApp: InstalledApp?
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .long
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(error: Error, app: AppProtocol, date: Date = Date(), operation: Operation? = nil, context: NSManagedObjectContext) {
|
||||
super.init(entity: LoggedError.entity(), insertInto: context)
|
||||
|
||||
self.date = date
|
||||
_operation = operation?.rawValue
|
||||
|
||||
let nsError = error as NSError
|
||||
domain = nsError.domain
|
||||
code = Int32(nsError.code)
|
||||
userInfo = nsError.userInfo
|
||||
|
||||
appName = app.name
|
||||
appBundleID = app.bundleIdentifier
|
||||
|
||||
switch app {
|
||||
case let storeApp as StoreApp: self.storeApp = storeApp
|
||||
case let installedApp as InstalledApp: self.installedApp = installedApp
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension LoggedError {
|
||||
var app: AppProtocol {
|
||||
// `as AppProtocol` needed to fix "cannot convert AnyApp to StoreApp" compiler error with Xcode 14.
|
||||
let app = installedApp ?? storeApp ?? AnyApp(name: appName, bundleIdentifier: appBundleID, url: nil) as AppProtocol
|
||||
return app
|
||||
}
|
||||
|
||||
var error: Error {
|
||||
let nsError = NSError(domain: domain, code: Int(code), userInfo: userInfo)
|
||||
return nsError
|
||||
}
|
||||
|
||||
@objc
|
||||
var localizedDateString: String {
|
||||
let localizedDateString = LoggedError.dateFormatter.string(from: date)
|
||||
return localizedDateString
|
||||
}
|
||||
|
||||
var localizedFailure: String? {
|
||||
guard let operation = operation else { return nil }
|
||||
switch operation {
|
||||
case .install: return String(format: NSLocalizedString("Install %@ Failed", comment: ""), appName)
|
||||
case .update: return String(format: NSLocalizedString("Update %@ Failed", comment: ""), appName)
|
||||
case .refresh: return String(format: NSLocalizedString("Refresh %@ Failed", comment: ""), appName)
|
||||
case .activate: return String(format: NSLocalizedString("Activate %@ Failed", comment: ""), appName)
|
||||
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), appName)
|
||||
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), appName)
|
||||
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), appName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension LoggedError {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<LoggedError> {
|
||||
NSFetchRequest<LoggedError>(entityName: "LoggedError")
|
||||
}
|
||||
}
|
||||
32
Sources/SideStoreCore/Model/ManagedPatron.swift
Normal file
32
Sources/SideStoreCore/Model/ManagedPatron.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// ManagedPatron.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 4/18/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(ManagedPatron)
|
||||
public class ManagedPatron: NSManagedObject, Fetchable {
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(patron: Patron, context: NSManagedObjectContext) {
|
||||
super.init(entity: ManagedPatron.entity(), insertInto: context)
|
||||
|
||||
name = patron.name
|
||||
identifier = patron.identifier
|
||||
}
|
||||
}
|
||||
|
||||
public extension ManagedPatron {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<ManagedPatron> {
|
||||
NSFetchRequest<ManagedPatron>(entityName: "Patron")
|
||||
}
|
||||
}
|
||||
99
Sources/SideStoreCore/Model/MergePolicy.swift
Normal file
99
Sources/SideStoreCore/Model/MergePolicy.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// MergePolicy.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
import Roxas
|
||||
|
||||
open class MergePolicy: RSTRelationshipPreservingMergePolicy {
|
||||
override open func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws {
|
||||
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
||||
for conflict in conflicts {
|
||||
switch conflict.conflictingObjects.first {
|
||||
case is StoreApp where conflict.conflictingObjects.count == 2:
|
||||
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
|
||||
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
|
||||
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
|
||||
|
||||
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp {
|
||||
// Delete previous permissions (same as below).
|
||||
for permission in previousApp.permissions {
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
|
||||
// Delete previous versions (different than below).
|
||||
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil {
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
}
|
||||
|
||||
case is AppVersion where conflict.conflictingObjects.count == 2:
|
||||
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
|
||||
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
|
||||
|
||||
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
|
||||
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }),
|
||||
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion }) {
|
||||
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
|
||||
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown context-level conflict.
|
||||
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
||||
}
|
||||
}
|
||||
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for conflict in conflicts {
|
||||
switch conflict.databaseObject {
|
||||
case let databaseObject as StoreApp:
|
||||
// Delete previous permissions
|
||||
for permission in databaseObject.permissions {
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
|
||||
if let contextApp = conflict.conflictingObjects.first as? StoreApp {
|
||||
let contextVersions = Set(contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
|
||||
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version) {
|
||||
print("[ALTLog] Deleting cached app version: \(appVersion.appBundleID + "_" + appVersion.version), not in:", contextApp.versions.map { $0.appBundleID + "_" + $0.version })
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
}
|
||||
|
||||
case let databaseObject as Source:
|
||||
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
||||
|
||||
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
|
||||
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
|
||||
|
||||
for app in databaseObject.apps {
|
||||
if !bundleIdentifiers.contains(app.bundleIdentifier) {
|
||||
// No longer listed in Source, so remove it from database.
|
||||
app.managedObjectContext?.delete(app)
|
||||
}
|
||||
}
|
||||
|
||||
for newsItem in databaseObject.newsItems {
|
||||
if !newsItemIdentifiers.contains(newsItem.identifier) {
|
||||
// No longer listed in Source, so remove it from database.
|
||||
newsItem.managedObjectContext?.delete(newsItem)
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
}
|
||||
}
|
||||
84
Sources/SideStoreCore/Model/NewsItem.swift
Normal file
84
Sources/SideStoreCore/Model/NewsItem.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// NewsItem.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/29/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
@objc(NewsItem)
|
||||
public class NewsItem: NSManagedObject, Decodable, Fetchable {
|
||||
/* Properties */
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public var date: Date
|
||||
|
||||
@NSManaged public var title: String
|
||||
@NSManaged public var caption: String
|
||||
@NSManaged public var tintColor: UIColor
|
||||
@NSManaged public var sortIndex: Int32
|
||||
@NSManaged public var isSilent: Bool
|
||||
|
||||
@NSManaged public var imageURL: URL?
|
||||
@NSManaged public var externalURL: URL?
|
||||
|
||||
@NSManaged public var appID: String?
|
||||
@NSManaged public var sourceIdentifier: String?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public var storeApp: StoreApp?
|
||||
@NSManaged public var source: Source?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case identifier
|
||||
case date
|
||||
case title
|
||||
case caption
|
||||
case tintColor
|
||||
case imageURL
|
||||
case externalURL = "url"
|
||||
case appID
|
||||
case notify
|
||||
}
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
super.init(entity: NewsItem.entity(), insertInto: context)
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
identifier = try container.decode(String.self, forKey: .identifier)
|
||||
date = try container.decode(Date.self, forKey: .date)
|
||||
|
||||
title = try container.decode(String.self, forKey: .title)
|
||||
caption = try container.decode(String.self, forKey: .caption)
|
||||
|
||||
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor) {
|
||||
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
||||
}
|
||||
|
||||
self.tintColor = tintColor
|
||||
}
|
||||
|
||||
imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
|
||||
externalURL = try container.decodeIfPresent(URL.self, forKey: .externalURL)
|
||||
|
||||
appID = try container.decodeIfPresent(String.self, forKey: .appID)
|
||||
|
||||
let notify = try container.decodeIfPresent(Bool.self, forKey: .notify) ?? false
|
||||
isSilent = !notify
|
||||
}
|
||||
}
|
||||
|
||||
public extension NewsItem {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<NewsItem> {
|
||||
NSFetchRequest<NewsItem>(entityName: "NewsItem")
|
||||
}
|
||||
}
|
||||
65
Sources/SideStoreCore/Model/PatreonAccount.swift
Normal file
65
Sources/SideStoreCore/Model/PatreonAccount.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// PatreonAccount.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
extension PatreonAPI {
|
||||
struct AccountResponse: Decodable {
|
||||
struct Data: Decodable {
|
||||
struct Attributes: Decodable {
|
||||
var first_name: String?
|
||||
var full_name: String
|
||||
}
|
||||
|
||||
var id: String
|
||||
var attributes: Attributes
|
||||
}
|
||||
|
||||
var data: Data
|
||||
var included: [PatronResponse]?
|
||||
}
|
||||
}
|
||||
|
||||
@objc(PatreonAccount)
|
||||
public class PatreonAccount: NSManagedObject, Fetchable {
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var firstName: String?
|
||||
|
||||
@NSManaged public var isPatron: Bool
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(response: PatreonAPI.AccountResponse, context: NSManagedObjectContext) {
|
||||
super.init(entity: PatreonAccount.entity(), insertInto: context)
|
||||
|
||||
identifier = response.data.id
|
||||
name = response.data.attributes.full_name
|
||||
firstName = response.data.attributes.first_name
|
||||
|
||||
// if let patronResponse = response.included?.first
|
||||
// {
|
||||
// let patron = Patron(response: patronResponse)
|
||||
// self.isPatron = (patron.status == .active)
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// self.isPatron = false
|
||||
// }
|
||||
isPatron = true
|
||||
}
|
||||
}
|
||||
|
||||
public extension PatreonAccount {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<PatreonAccount> {
|
||||
NSFetchRequest<PatreonAccount>(entityName: "PatreonAccount")
|
||||
}
|
||||
}
|
||||
50
Sources/SideStoreCore/Model/RefreshAttempt.swift
Normal file
50
Sources/SideStoreCore/Model/RefreshAttempt.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// RefreshAttempt.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(RefreshAttempt)
|
||||
public class RefreshAttempt: NSManagedObject, Fetchable {
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public var date: Date
|
||||
|
||||
@NSManaged public var isSuccess: Bool
|
||||
@NSManaged public var errorDescription: String?
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(identifier: String, result: Result<[String: Result<InstalledApp, Error>], Error>, context: NSManagedObjectContext) {
|
||||
super.init(entity: RefreshAttempt.entity(), insertInto: context)
|
||||
|
||||
self.identifier = identifier
|
||||
date = Date()
|
||||
|
||||
do {
|
||||
let results = try result.get()
|
||||
|
||||
for (_, result) in results {
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
isSuccess = true
|
||||
errorDescription = nil
|
||||
} catch {
|
||||
isSuccess = false
|
||||
errorDescription = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension RefreshAttempt {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<RefreshAttempt> {
|
||||
NSFetchRequest<RefreshAttempt>(entityName: "RefreshAttempt")
|
||||
}
|
||||
}
|
||||
24
Sources/SideStoreCore/Model/SecureValueTransformer.swift
Normal file
24
Sources/SideStoreCore/Model/SecureValueTransformer.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// SecureValueTransformer.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/18/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(ALTSecureValueTransformer)
|
||||
public final class SecureValueTransformer: NSSecureUnarchiveFromDataTransformer {
|
||||
public static let name = NSValueTransformerName(rawValue: "ALTSecureValueTransformer")
|
||||
|
||||
override public static var allowedTopLevelClasses: [AnyClass] {
|
||||
let allowedClasses = super.allowedTopLevelClasses + [NSError.self]
|
||||
return allowedClasses
|
||||
}
|
||||
|
||||
public static func register() {
|
||||
let transformer = SecureValueTransformer()
|
||||
ValueTransformer.setValueTransformer(transformer, forName: name)
|
||||
}
|
||||
}
|
||||
284
Sources/SideStoreCore/Model/Source.swift
Normal file
284
Sources/SideStoreCore/Model/Source.swift
Normal file
@@ -0,0 +1,284 @@
|
||||
//
|
||||
// Source.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
public extension Source {
|
||||
#if ALPHA
|
||||
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
|
||||
#else
|
||||
static let altStoreIdentifier = Bundle.Info.appbundleIdentifier
|
||||
#endif
|
||||
|
||||
#if STAGING
|
||||
|
||||
#if ALPHA
|
||||
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
|
||||
#else
|
||||
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#if ALPHA
|
||||
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
|
||||
#else
|
||||
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
public struct AppPermissionFeed: Codable {
|
||||
let type: String // ALTAppPermissionType
|
||||
let usageDescription: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case usageDescription
|
||||
}
|
||||
}
|
||||
|
||||
public struct AppVersionFeed: Codable {
|
||||
/* Properties */
|
||||
let version: String
|
||||
let date: Date
|
||||
let localizedDescription: String?
|
||||
|
||||
let downloadURL: URL
|
||||
let size: Int64
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case date
|
||||
case localizedDescription
|
||||
case downloadURL
|
||||
case size
|
||||
}
|
||||
}
|
||||
|
||||
public struct PlatformURLFeed: Codable {
|
||||
/* Properties */
|
||||
let platform: Platform
|
||||
let downloadURL: URL
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case platform
|
||||
case downloadURL
|
||||
}
|
||||
}
|
||||
|
||||
public struct StoreAppFeed: Codable {
|
||||
let name: String
|
||||
let bundleIdentifier: String
|
||||
let subtitle: String?
|
||||
|
||||
let developerName: String
|
||||
let localizedDescription: String
|
||||
let size: Int64
|
||||
|
||||
let iconURL: URL
|
||||
let screenshotURLs: [URL]
|
||||
|
||||
let version: String
|
||||
let versionDate: Date
|
||||
let versionDescription: String?
|
||||
let downloadURL: URL
|
||||
let platformURLs: [PlatformURLFeed]?
|
||||
|
||||
let tintColor: String? // UIColor?
|
||||
let isBeta: Bool
|
||||
|
||||
// let source: Source?
|
||||
let appPermission: [AppPermissionFeed]
|
||||
let versions: [AppVersionFeed]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case bundleIdentifier
|
||||
case developerName
|
||||
case downloadURL
|
||||
case iconURL
|
||||
case isBeta = "beta"
|
||||
case localizedDescription
|
||||
case name
|
||||
case appPermission = "permissions"
|
||||
case platformURLs
|
||||
case screenshotURLs
|
||||
case size
|
||||
case subtitle
|
||||
case tintColor
|
||||
case version
|
||||
case versionDate
|
||||
case versionDescription
|
||||
case versions
|
||||
}
|
||||
}
|
||||
|
||||
public struct NewsItemFeed: Codable {
|
||||
let identifier: String
|
||||
let date: Date
|
||||
|
||||
let title: String
|
||||
let caption: String
|
||||
let tintColor: String // UIColor
|
||||
let notify: Bool
|
||||
|
||||
let imageURL: URL?
|
||||
let externalURL: URL?
|
||||
|
||||
let appID: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case identifier
|
||||
case date
|
||||
case title
|
||||
case caption
|
||||
case tintColor
|
||||
case imageURL
|
||||
case externalURL = "url"
|
||||
case appID
|
||||
case notify
|
||||
}
|
||||
}
|
||||
|
||||
public struct SourceJSON: Codable {
|
||||
let name: String
|
||||
let identifier: String
|
||||
let sourceURL: URL
|
||||
let userInfo: [String: String]? // [ALTSourceUserInfoKey:String]?
|
||||
let apps: [StoreAppFeed]
|
||||
let news: [NewsItemFeed]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case identifier
|
||||
case sourceURL
|
||||
case userInfo
|
||||
case apps
|
||||
case news
|
||||
}
|
||||
}
|
||||
|
||||
@objc(Source)
|
||||
public class Source: NSManagedObject, Fetchable, Decodable {
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public var sourceURL: URL
|
||||
|
||||
@NSManaged public var error: NSError?
|
||||
|
||||
/* Non-Core Data Properties */
|
||||
public var userInfo: [ALTSourceUserInfoKey: String]?
|
||||
|
||||
/* Relationships */
|
||||
@objc(apps) @NSManaged public private(set) var _apps: NSOrderedSet
|
||||
@objc(newsItems) @NSManaged public private(set) var _newsItems: NSOrderedSet
|
||||
|
||||
@nonobjc public var apps: [StoreApp] {
|
||||
get {
|
||||
_apps.array as! [StoreApp]
|
||||
}
|
||||
set {
|
||||
_apps = NSOrderedSet(array: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc public var newsItems: [NewsItem] {
|
||||
get {
|
||||
_newsItems.array as! [NewsItem]
|
||||
}
|
||||
set {
|
||||
_newsItems = NSOrderedSet(array: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case identifier
|
||||
case sourceURL
|
||||
case userInfo
|
||||
case apps
|
||||
case news
|
||||
}
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
guard let sourceURL = decoder.sourceURL else { preconditionFailure("Decoder must have non-nil sourceURL.") }
|
||||
|
||||
super.init(entity: Source.entity(), insertInto: context)
|
||||
|
||||
do {
|
||||
self.sourceURL = sourceURL
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
identifier = try container.decode(String.self, forKey: .identifier)
|
||||
|
||||
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
||||
|
||||
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
||||
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { a, _ in a })
|
||||
|
||||
for (index, app) in apps.enumerated() {
|
||||
app.sourceIdentifier = identifier
|
||||
app.sortIndex = Int32(index)
|
||||
}
|
||||
_apps = NSMutableOrderedSet(array: apps)
|
||||
|
||||
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||
for (index, item) in newsItems.enumerated() {
|
||||
item.sourceIdentifier = identifier
|
||||
item.sortIndex = Int32(index)
|
||||
}
|
||||
|
||||
for newsItem in newsItems {
|
||||
guard let appID = newsItem.appID else { continue }
|
||||
|
||||
if let storeApp = appsByID[appID] {
|
||||
newsItem.storeApp = storeApp
|
||||
} else {
|
||||
newsItem.storeApp = nil
|
||||
}
|
||||
}
|
||||
_newsItems = NSMutableOrderedSet(array: newsItems)
|
||||
} catch {
|
||||
if let context = managedObjectContext {
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Source {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> {
|
||||
NSFetchRequest<Source>(entityName: "Source")
|
||||
}
|
||||
|
||||
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source {
|
||||
let source = Source(context: context)
|
||||
source.name = "SideStore Offical"
|
||||
source.identifier = Source.altStoreIdentifier
|
||||
source.sourceURL = Source.altStoreSourceURL
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
class func fetchAltStoreSource(in context: NSManagedObjectContext) -> Source? {
|
||||
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
|
||||
return source
|
||||
}
|
||||
}
|
||||
342
Sources/SideStoreCore/Model/StoreApp.swift
Normal file
342
Sources/SideStoreCore/Model/StoreApp.swift
Normal file
@@ -0,0 +1,342 @@
|
||||
//
|
||||
// StoreApp.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
public extension StoreApp {
|
||||
#if ALPHA
|
||||
static let altstoreAppID = Bundle.main.Info.appbundleIdentifier
|
||||
#elseif BETA
|
||||
static let altstoreAppID = Bundle.main.Info.appbundleIdentifier
|
||||
#else
|
||||
static let altstoreAppID = Bundle.main.Info.appbundleIdentifier
|
||||
#endif
|
||||
|
||||
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
|
||||
}
|
||||
|
||||
@objc
|
||||
public enum Platform: UInt, Codable {
|
||||
case ios
|
||||
case tvos
|
||||
case macos
|
||||
}
|
||||
|
||||
@objc
|
||||
public final class PlatformURL: NSManagedObject, Decodable {
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var platform: Platform
|
||||
@NSManaged public private(set) var downloadURL: URL
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case platform
|
||||
case downloadURL
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
// Must initialize with context in order for child context saves to work correctly.
|
||||
super.init(entity: PlatformURL.entity(), insertInto: context)
|
||||
|
||||
do {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
platform = try container.decode(Platform.self, forKey: .platform)
|
||||
downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
} catch {
|
||||
if let context = managedObjectContext {
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PlatformURL: Comparable {
|
||||
public static func < (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
lhs.platform.rawValue < rhs.platform.rawValue
|
||||
}
|
||||
|
||||
public static func > (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
lhs.platform.rawValue > rhs.platform.rawValue
|
||||
}
|
||||
|
||||
public static func <= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
lhs.platform.rawValue <= rhs.platform.rawValue
|
||||
}
|
||||
|
||||
public static func >= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
lhs.platform.rawValue >= rhs.platform.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public typealias PlatformURLs = [PlatformURL]
|
||||
|
||||
@objc(StoreApp)
|
||||
public class StoreApp: NSManagedObject, Decodable, Fetchable {
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var name: String
|
||||
@NSManaged public private(set) var bundleIdentifier: String
|
||||
@NSManaged public private(set) var subtitle: String?
|
||||
|
||||
@NSManaged public private(set) var developerName: String
|
||||
@NSManaged public private(set) var localizedDescription: String
|
||||
@NSManaged @objc(size) internal var _size: Int32
|
||||
|
||||
@NSManaged public private(set) var iconURL: URL
|
||||
@NSManaged public private(set) var screenshotURLs: [URL]
|
||||
|
||||
@NSManaged @objc(version) internal var _version: String
|
||||
@NSManaged @objc(versionDate) internal var _versionDate: Date
|
||||
@NSManaged @objc(versionDescription) internal var _versionDescription: String?
|
||||
|
||||
@NSManaged @objc(downloadURL) internal var _downloadURL: URL
|
||||
@NSManaged public private(set) var platformURLs: PlatformURLs?
|
||||
|
||||
@NSManaged public private(set) var tintColor: UIColor?
|
||||
@NSManaged public private(set) var isBeta: Bool
|
||||
|
||||
@objc public internal(set) var sourceIdentifier: String? {
|
||||
get {
|
||||
willAccessValue(forKey: #keyPath(sourceIdentifier))
|
||||
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
|
||||
|
||||
let sourceIdentifier = primitiveSourceIdentifier
|
||||
return sourceIdentifier
|
||||
}
|
||||
set {
|
||||
willChangeValue(forKey: #keyPath(sourceIdentifier))
|
||||
primitiveSourceIdentifier = newValue
|
||||
didChangeValue(forKey: #keyPath(sourceIdentifier))
|
||||
|
||||
for version in versions {
|
||||
version.sourceID = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NSManaged private var primitiveSourceIdentifier: String?
|
||||
|
||||
@NSManaged public var sortIndex: Int32
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public var installedApp: InstalledApp?
|
||||
@NSManaged public var newsItems: Set<NewsItem>
|
||||
|
||||
@NSManaged @objc(source) public var _source: Source?
|
||||
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
|
||||
|
||||
@NSManaged public private(set) var latestVersion: AppVersion?
|
||||
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
||||
|
||||
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
||||
|
||||
@nonobjc public var source: Source? {
|
||||
set {
|
||||
_source = newValue
|
||||
sourceIdentifier = newValue?.identifier
|
||||
}
|
||||
get {
|
||||
_source
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc public var permissions: [AppPermission] {
|
||||
_permissions.array as! [AppPermission]
|
||||
}
|
||||
|
||||
@nonobjc public var versions: [AppVersion] {
|
||||
_versions.array as! [AppVersion]
|
||||
}
|
||||
|
||||
@nonobjc public var size: Int64? {
|
||||
guard let version = latestVersion else { return nil }
|
||||
return version.size
|
||||
}
|
||||
|
||||
@nonobjc public var version: String? {
|
||||
guard let version = latestVersion else { return nil }
|
||||
return version.version
|
||||
}
|
||||
|
||||
@nonobjc public var versionDescription: String? {
|
||||
guard let version = latestVersion else { return nil }
|
||||
return version.localizedDescription
|
||||
}
|
||||
|
||||
@nonobjc public var versionDate: Date? {
|
||||
guard let version = latestVersion else { return nil }
|
||||
return version.date
|
||||
}
|
||||
|
||||
@nonobjc public var downloadURL: URL? {
|
||||
guard let version = self.latestVersion else { return nil }
|
||||
return version.downloadURL
|
||||
}
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case bundleIdentifier
|
||||
case developerName
|
||||
case localizedDescription
|
||||
case version
|
||||
case versionDescription
|
||||
case versionDate
|
||||
case iconURL
|
||||
case screenshotURLs
|
||||
case downloadURL
|
||||
case platformURLs
|
||||
case tintColor
|
||||
case subtitle
|
||||
case permissions
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
case versions
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
// Must initialize with context in order for child context saves to work correctly.
|
||||
super.init(entity: StoreApp.entity(), insertInto: context)
|
||||
|
||||
do {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
|
||||
developerName = try container.decode(String.self, forKey: .developerName)
|
||||
localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
|
||||
|
||||
subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||
|
||||
iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||
screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
||||
|
||||
let downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL)
|
||||
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
|
||||
if let platformURLs = platformURLs {
|
||||
self.platformURLs = platformURLs
|
||||
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
|
||||
if let first = platformURLs.sorted().first {
|
||||
_downloadURL = first.downloadURL
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
|
||||
}
|
||||
|
||||
} else if let downloadURL = downloadURL {
|
||||
_downloadURL = downloadURL
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
|
||||
}
|
||||
|
||||
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor) {
|
||||
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
||||
}
|
||||
|
||||
self.tintColor = tintColor
|
||||
}
|
||||
|
||||
isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
|
||||
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
||||
_permissions = NSOrderedSet(array: permissions)
|
||||
|
||||
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions) {
|
||||
// TODO: Throw error if there isn't at least one version.
|
||||
|
||||
for version in versions {
|
||||
version.appBundleID = bundleIdentifier
|
||||
}
|
||||
|
||||
setVersions(versions)
|
||||
} else {
|
||||
let version = try container.decode(String.self, forKey: .version)
|
||||
let versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||
|
||||
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
let size = try container.decode(Int32.self, forKey: .size)
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: version,
|
||||
date: versionDate,
|
||||
localizedDescription: versionDescription,
|
||||
downloadURL: downloadURL,
|
||||
size: Int64(size),
|
||||
appBundleID: bundleIdentifier,
|
||||
in: context)
|
||||
setVersions([appVersion])
|
||||
}
|
||||
} catch {
|
||||
if let context = managedObjectContext {
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension StoreApp {
|
||||
func setVersions(_ versions: [AppVersion]) {
|
||||
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
|
||||
|
||||
self.latestVersion = latestVersion
|
||||
_versions = NSOrderedSet(array: versions)
|
||||
|
||||
// Preserve backwards compatibility by assigning legacy property values.
|
||||
_version = latestVersion.version
|
||||
_versionDate = latestVersion.date
|
||||
_versionDescription = latestVersion.localizedDescription
|
||||
_downloadURL = latestVersion.downloadURL
|
||||
_size = Int32(latestVersion.size)
|
||||
}
|
||||
}
|
||||
|
||||
public extension StoreApp {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp> {
|
||||
NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
||||
}
|
||||
|
||||
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp {
|
||||
let app = StoreApp(context: context)
|
||||
app.name = "SideStore"
|
||||
app.bundleIdentifier = StoreApp.altstoreAppID
|
||||
app.developerName = "Side Team"
|
||||
app.localizedDescription = "SideStore is an alternative App Store."
|
||||
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
|
||||
app.screenshotURLs = []
|
||||
app.sourceIdentifier = Source.altStoreIdentifier
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: "0.3.0",
|
||||
date: Date(),
|
||||
downloadURL: URL(string: "http://rileytestut.com")!,
|
||||
size: 0,
|
||||
appBundleID: app.bundleIdentifier,
|
||||
sourceID: Source.altStoreIdentifier,
|
||||
in: context)
|
||||
app.setVersions([appVersion])
|
||||
|
||||
print("makeAltStoreApp StoreApp: \(String(describing: app))")
|
||||
|
||||
#if BETA
|
||||
app.isBeta = true
|
||||
#endif
|
||||
|
||||
return app
|
||||
}
|
||||
}
|
||||
71
Sources/SideStoreCore/Model/Team.swift
Normal file
71
Sources/SideStoreCore/Model/Team.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Team.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
public extension ALTTeamType {
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .free: return NSLocalizedString("Free Developer Account", comment: "")
|
||||
case .individual: return NSLocalizedString("Developer", comment: "")
|
||||
case .organization: return NSLocalizedString("Organization", comment: "")
|
||||
case .unknown: fallthrough
|
||||
@unknown default: return NSLocalizedString("Unknown", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Team {
|
||||
static let maximumFreeAppIDs = 10
|
||||
}
|
||||
|
||||
@objc(Team)
|
||||
public class Team: NSManagedObject, Fetchable {
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public var type: ALTTeamType
|
||||
|
||||
@NSManaged public var isActiveTeam: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var account: Account!
|
||||
@NSManaged public var installedApps: Set<InstalledApp>
|
||||
@NSManaged public private(set) var appIDs: Set<AppID>
|
||||
|
||||
public var altTeam: ALTTeam?
|
||||
|
||||
override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(_ team: ALTTeam, account: Account, context: NSManagedObjectContext) {
|
||||
super.init(entity: Team.entity(), insertInto: context)
|
||||
|
||||
self.account = account
|
||||
|
||||
update(team: team)
|
||||
}
|
||||
|
||||
public func update(team: ALTTeam) {
|
||||
altTeam = team
|
||||
|
||||
name = team.name
|
||||
identifier = team.identifier
|
||||
type = team.type
|
||||
}
|
||||
}
|
||||
|
||||
public extension Team {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Team> {
|
||||
NSFetchRequest<Team>(entityName: "Team")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user