2019-06-17 14:49:23 -07:00
//
2019-07-30 17:00:04 -07:00
// F e t c h S o u r c e O p e r a t i o n . s w i f t
2019-06-17 14:49:23 -07:00
// A l t S t o r e
//
2019-07-30 17:00:04 -07:00
// C r e a t e d b y R i l e y T e s t u t o n 7 / 3 0 / 1 9 .
2019-06-17 14:49:23 -07:00
// C o p y r i g h t © 2 0 1 9 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
2020-03-24 13:27:44 -07:00
import CoreData
2020-09-03 16:39:08 -07:00
import AltStoreCore
2019-06-17 14:49:23 -07:00
import Roxas
2022-11-22 13:02:19 -06:00
extension SourceError
{
enum Code : Int , ALTErrorCode
{
typealias Error = SourceError
case unsupported
case duplicateBundleID
2022-11-23 19:14:20 -06:00
case duplicateVersion
2023-05-11 18:17:26 -05:00
case changedID
2023-05-11 18:51:09 -05:00
case duplicate
2023-05-15 15:29:51 -05:00
case missingPermissionUsageDescription
2022-11-22 13:02:19 -06:00
}
static func unsupported ( _ source : Source ) -> SourceError { SourceError ( code : . unsupported , source : source ) }
2022-11-23 19:14:20 -06:00
static func duplicateBundleID ( _ bundleID : String , source : Source ) -> SourceError { SourceError ( code : . duplicateBundleID , source : source , bundleID : bundleID ) }
static func duplicateVersion ( _ version : String , for app : StoreApp , source : Source ) -> SourceError { SourceError ( code : . duplicateVersion , source : source , app : app , version : version ) }
2023-05-11 18:17:26 -05:00
static func changedID ( _ identifier : String , previousID : String , source : Source ) -> SourceError { SourceError ( code : . changedID , source : source , sourceID : identifier , previousSourceID : previousID ) }
2023-05-11 18:51:09 -05:00
static func duplicate ( _ source : Source , previousSourceName : String ? ) -> SourceError { SourceError ( code : . duplicate , source : source , previousSourceName : previousSourceName ) }
2023-05-15 15:29:51 -05:00
static func missingPermissionUsageDescription ( for permission : any ALTAppPermission , app : StoreApp , source : Source ) -> SourceError {
SourceError ( code : . missingPermissionUsageDescription , source : source , app : app , permission : permission )
}
2022-11-22 13:02:19 -06:00
}
struct SourceError : ALTLocalizedError
{
2022-11-23 19:14:20 -06:00
let code : Code
2022-11-22 13:02:19 -06:00
var errorTitle : String ?
var errorFailure : String ?
@ Managed var source : Source
2022-11-23 19:14:20 -06:00
@ Managed var app : StoreApp ?
var bundleID : String ?
var version : String ?
2022-11-22 13:02:19 -06:00
2023-05-11 18:51:09 -05:00
@ UserInfoValue var previousSourceName : String ?
2023-05-11 18:17:26 -05:00
// S t o r e i n u s e r I n f o s o t h e y c a n b e v i e w e d f r o m E r r o r L o g .
@ UserInfoValue var sourceID : String ?
@ UserInfoValue var previousSourceID : String ?
2023-05-15 15:29:51 -05:00
@ UserInfoValue
var permission : ( any ALTAppPermission ) ?
2022-11-22 13:02:19 -06:00
var errorFailureReason : String {
switch self . code
{
case . unsupported : return String ( format : NSLocalizedString ( " The source “%@” is not supported by this version of AltStore. " , comment : " " ) , self . $ source . name )
case . duplicateBundleID :
2022-11-23 19:14:20 -06:00
let bundleIDFragment = self . bundleID . map { String ( format : NSLocalizedString ( " the bundle identifier %@ " , comment : " " ) , $0 ) } ? ? NSLocalizedString ( " the same bundle identifier " , comment : " " )
2022-11-22 13:02:19 -06:00
let failureReason = String ( format : NSLocalizedString ( " The source “%@” contains multiple apps with %@. " , comment : " " ) , self . $ source . name , bundleIDFragment )
return failureReason
2022-11-23 19:14:20 -06:00
case . duplicateVersion :
var versionFragment = NSLocalizedString ( " duplicate versions " , comment : " " )
if let version
{
versionFragment += " ( \( version ) ) "
}
let appFragment : String
if let name = self . $ app . name , let bundleID = self . $ app . bundleIdentifier
{
appFragment = name + " ( \( bundleID ) ) "
}
else
{
appFragment = NSLocalizedString ( " one or more apps " , comment : " " )
}
let failureReason = String ( format : NSLocalizedString ( " The source “%@” contains %@ for %@. " , comment : " " ) , self . $ source . name , versionFragment , appFragment )
return failureReason
2023-05-11 18:17:26 -05:00
case . changedID :
let failureReason = String ( format : NSLocalizedString ( " The identifier of the source “%@” has changed. " , comment : " " ) , self . $ source . name )
return failureReason
2023-05-11 18:51:09 -05:00
case . duplicate :
let baseMessage = String ( format : NSLocalizedString ( " A source with the identifier '%@' already exists " , comment : " " ) , self . $ source . identifier )
guard let previousSourceName else { return baseMessage + " . " }
let failureReason = baseMessage + " (“ \( previousSourceName ) ”). "
return failureReason
2023-05-15 15:29:51 -05:00
case . missingPermissionUsageDescription :
let appName = self . $ app . name ? ? String ( format : NSLocalizedString ( " an app in source “%@” " , comment : " " ) , self . $ source . name )
guard let permission else {
return String ( format : NSLocalizedString ( " A permission for %@ is missing a usage description. " , comment : " " ) , appName )
}
let permissionType = permission . type . localizedName ? ? NSLocalizedString ( " Permission " , comment : " " )
let failureReason = String ( format : NSLocalizedString ( " The %@ '%@' for %@ is missing a usage description. " , comment : " " ) , permissionType . lowercased ( ) , permission . rawValue , appName )
return failureReason
2023-05-11 18:17:26 -05:00
}
}
var recoverySuggestion : String ? {
switch self . code
{
case . changedID : return NSLocalizedString ( " A source cannot change its identifier once added. This source can no longer be updated. " , comment : " " )
2023-05-11 18:51:09 -05:00
case . duplicate :
let failureReason = NSLocalizedString ( " Please remove the existing source in order to add this one. " , comment : " " )
return failureReason
2023-05-11 18:17:26 -05:00
default : return nil
2022-11-22 13:02:19 -06:00
}
}
}
2019-07-30 17:00:04 -07:00
@objc ( FetchSourceOperation )
2023-01-04 09:52:12 -05:00
final class FetchSourceOperation : ResultOperation < Source >
2019-06-17 14:49:23 -07:00
{
2019-07-30 17:00:04 -07:00
let sourceURL : URL
2020-03-24 13:27:44 -07:00
let managedObjectContext : NSManagedObjectContext
2019-07-30 17:00:04 -07:00
2023-05-11 18:17:26 -05:00
// N o n - n i l w h e n u p d a t i n g a n e x i s t i n g s o u r c e .
@ Managed
private var source : Source ?
2019-09-19 11:27:38 -07:00
private let session : URLSession
2019-06-17 14:49:23 -07:00
2019-09-07 15:37:08 -07:00
private lazy var dateFormatter : ISO8601DateFormatter = {
let dateFormatter = ISO8601DateFormatter ( )
2019-06-17 14:49:23 -07:00
return dateFormatter
} ( )
2023-05-11 18:17:26 -05:00
// N e w s o u r c e
convenience init ( sourceURL : URL , managedObjectContext : NSManagedObjectContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( ) )
{
self . init ( sourceURL : sourceURL , source : nil , managedObjectContext : managedObjectContext )
}
// E x i s t i n g s o u r c e
convenience init ( source : Source , managedObjectContext : NSManagedObjectContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( ) )
{
self . init ( sourceURL : source . sourceURL , source : source , managedObjectContext : managedObjectContext )
}
private init ( sourceURL : URL , source : Source ? , managedObjectContext : NSManagedObjectContext )
2019-07-30 17:00:04 -07:00
{
self . sourceURL = sourceURL
2020-03-24 13:27:44 -07:00
self . managedObjectContext = managedObjectContext
2023-05-11 18:17:26 -05:00
self . source = source
2019-09-19 11:27:38 -07:00
let configuration = URLSessionConfiguration . default
configuration . requestCachePolicy = . reloadIgnoringLocalCacheData
configuration . urlCache = nil
self . session = URLSession ( configuration : configuration )
2019-07-30 17:00:04 -07:00
}
2019-06-17 14:49:23 -07:00
override func main ( )
{
super . main ( )
2019-07-30 17:00:04 -07:00
let dataTask = self . session . dataTask ( with : self . sourceURL ) { ( data , response , error ) in
2020-08-27 16:23:50 -07:00
let childContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( withParent : self . managedObjectContext )
childContext . mergePolicy = NSOverwriteMergePolicy
childContext . perform {
2019-06-17 14:49:23 -07:00
do
{
let ( data , _ ) = try Result ( ( data , response ) , error ) . get ( )
2020-09-03 16:39:08 -07:00
let decoder = AltStoreCore . JSONDecoder ( )
2019-09-07 15:37:08 -07:00
decoder . dateDecodingStrategy = . custom ( { ( decoder ) -> Date in
let container = try decoder . singleValueContainer ( )
let text = try container . decode ( String . self )
// F u l l I S O 8 6 0 1 F o r m a t .
self . dateFormatter . formatOptions = [ . withFullDate , . withFullTime , . withTimeZone ]
if let date = self . dateFormatter . date ( from : text )
{
return date
}
// J u s t d a t e p o r t i o n o f I S O 8 6 0 1 .
self . dateFormatter . formatOptions = [ . withFullDate ]
if let date = self . dateFormatter . date ( from : text )
{
return date
}
throw DecodingError . dataCorruptedError ( in : container , debugDescription : " Date is in invalid format. " )
} )
2020-08-27 16:23:50 -07:00
decoder . managedObjectContext = childContext
2020-03-24 13:27:44 -07:00
decoder . sourceURL = self . sourceURL
2019-06-17 14:49:23 -07:00
2019-07-30 17:00:04 -07:00
let source = try decoder . decode ( Source . self , from : data )
2020-08-27 16:23:50 -07:00
let identifier = source . identifier
2019-11-04 13:38:54 -08:00
2022-11-22 13:02:19 -06:00
try self . verify ( source )
2020-08-27 16:23:50 -07:00
try childContext . save ( )
self . managedObjectContext . perform {
if let source = Source . first ( satisfying : NSPredicate ( format : " %K == %@ " , # keyPath ( Source . identifier ) , identifier ) , in : self . managedObjectContext )
{
self . finish ( . success ( source ) )
}
else
{
self . finish ( . failure ( OperationError . noSources ) )
}
}
2019-06-17 14:49:23 -07:00
}
catch
{
2020-08-27 16:23:50 -07:00
self . managedObjectContext . perform {
self . finish ( . failure ( error ) )
}
2019-06-17 14:49:23 -07:00
}
}
}
self . progress . addChild ( dataTask . progress , withPendingUnitCount : 1 )
dataTask . resume ( )
}
}
2022-11-22 13:02:19 -06:00
private extension FetchSourceOperation
{
func verify ( _ source : Source ) throws
{
#if ! BETA
if let trustedSourceIDs = UserDefaults . shared . trustedSourceIDs
{
guard trustedSourceIDs . contains ( source . identifier ) || source . identifier = = Source . altStoreIdentifier else { throw SourceError ( code : . unsupported , source : source ) }
}
#endif
var bundleIDs = Set < String > ( )
for app in source . apps
{
guard ! bundleIDs . contains ( app . bundleIdentifier ) else { throw SourceError . duplicateBundleID ( app . bundleIdentifier , source : source ) }
bundleIDs . insert ( app . bundleIdentifier )
2022-11-23 19:14:20 -06:00
var versions = Set < String > ( )
for version in app . versions
{
guard ! versions . contains ( version . version ) else { throw SourceError . duplicateVersion ( version . version , for : app , source : source ) }
versions . insert ( version . version )
}
2023-05-15 15:29:51 -05:00
for permission in app . permissions
{
switch permission . type
{
case . privacy , . backgroundMode :
guard permission . usageDescription != nil else { throw SourceError . missingPermissionUsageDescription ( for : permission . permission , app : app , source : source ) }
default : break
}
}
2022-11-22 13:02:19 -06:00
}
2023-05-11 18:17:26 -05:00
if let previousSourceID = self . $ source . identifier
{
guard source . identifier = = previousSourceID else { throw SourceError . changedID ( source . identifier , previousID : previousSourceID , source : source ) }
}
2022-11-22 13:02:19 -06:00
}
}