1 //  Copyright © 2019 650 Industries. All rights reserved.
2 
3 // swiftlint:disable closure_body_length
4 // swiftlint:disable function_parameter_count
5 
6 import Foundation
7 
8 /**
9  * Subclass of AppLoader which handles downloading updates from a remote server.
10  */
11 public final class RemoteAppLoader: AppLoader {
12   private static let ErrorDomain = "EXUpdatesRemoteAppLoader"
13 
14   private let downloader: FileDownloader
15   private var remoteUpdateResponse: UpdateResponse?
16   private let completionQueue: DispatchQueue
17 
18   public required override init(
19     config: UpdatesConfig,
20     database: UpdatesDatabase,
21     directory: URL,
22     launchedUpdate: Update?,
23     completionQueue: DispatchQueue
24   ) {
25     self.downloader = FileDownloader(config: config)
26     self.completionQueue = completionQueue
27     super.init(config: config, database: database, directory: directory, launchedUpdate: launchedUpdate, completionQueue: completionQueue)
28   }
29 
30   override public func loadUpdate(
31     fromURL url: URL,
32     onUpdateResponse updateResponseBlockArg: @escaping AppLoaderUpdateResponseBlock,
33     asset assetBlockArg: @escaping AppLoaderAssetBlock,
34     success successBlockArg: @escaping AppLoaderSuccessBlock,
35     error errorBlockArg: @escaping AppLoaderErrorBlock
36   ) {
37     self.updateResponseBlock = updateResponseBlockArg
38     self.assetBlock = assetBlockArg
39     self.errorBlock = errorBlockArg
40 
41     self.successBlock = { [weak self] (updateResponse: UpdateResponse?) in
42       guard let strongSelf = self else {
43         successBlockArg(updateResponse)
44         return
45       }
46       // even if update is nil (meaning we didn't load a new update),
47       // we want to persist the header data from remoteUpdateResponse
48       if let remoteUpdateResponse = strongSelf.remoteUpdateResponse,
49         let responseHeaderData = remoteUpdateResponse.responseHeaderData {
50         strongSelf.database.databaseQueue.async {
51           do {
52             // swiftlint:disable:next force_unwrapping
53             try strongSelf.database.setMetadata(withResponseHeaderData: responseHeaderData, scopeKey: strongSelf.config.scopeKey!)
54             successBlockArg(updateResponse)
55           } catch {
56             NSLog("Error persisting header data to disk: %@", error.localizedDescription)
57             errorBlockArg(error)
58           }
59         }
60       } else {
61         successBlockArg(updateResponse)
62       }
63     }
64 
65     database.databaseQueue.async {
66       let embeddedUpdate = EmbeddedAppLoader.embeddedManifest(withConfig: self.config, database: self.database)
67       let extraHeaders = FileDownloader.extraHeadersForRemoteUpdateRequest(
68         withDatabase: self.database,
69         config: self.config,
70         launchedUpdate: self.launchedUpdate,
71         embeddedUpdate: embeddedUpdate
72       )
73       self.downloader.downloadRemoteUpdate(
74         fromURL: url,
75         withDatabase: self.database,
76         extraHeaders: extraHeaders
77       ) { updateResponse in
78         self.remoteUpdateResponse = updateResponse
79         self.startLoading(fromUpdateResponse: updateResponse)
80       } errorBlock: { error in
81         self.errorBlock.let { it in
82           it(error)
83         }
84       }
85     }
86   }
87 
88   override public func downloadAsset(_ asset: UpdateAsset) {
89     let urlOnDisk = self.directory.appendingPathComponent(asset.filename)
90 
91     FileDownloader.assetFilesQueue.async {
92       if FileManager.default.fileExists(atPath: urlOnDisk.path) {
93         // file already exists, we don't need to download it again
94         DispatchQueue.global().async {
95           self.handleAssetDownloadAlreadyExists(asset)
96         }
97       } else {
98         guard let assetUrl = asset.url else {
99           self.handleAssetDownload(
100             withError: NSError(
101               domain: RemoteAppLoader.ErrorDomain,
102               code: 1006,
103               userInfo: [
104                 NSLocalizedDescriptionKey: "Failed to download asset with no URL provided"
105               ]
106             ),
107             asset: asset
108           )
109           return
110         }
111 
112         self.downloader.downloadFile(
113           fromURL: assetUrl,
114           verifyingHash: asset.expectedHash,
115           toPath: urlOnDisk.path,
116           extraHeaders: asset.extraRequestHeaders ?? [:]
117         ) { data, response, _ in
118           DispatchQueue.global().async {
119             self.handleAssetDownload(withData: data, response: response, asset: asset)
120           }
121         } errorBlock: { error in
122           DispatchQueue.global().async {
123             self.handleAssetDownload(withError: error, asset: asset)
124           }
125         }
126       }
127     }
128   }
129 
130   static func processSuccessLoaderResult(
131     config: UpdatesConfig,
132     database: UpdatesDatabase,
133     selectionPolicy: SelectionPolicy,
134     launchedUpdate: Update?,
135     directory: URL,
136     loaderTaskQueue: DispatchQueue,
137     updateResponse: UpdateResponse?,
138     priorError: Error?,
139     onComplete: @escaping (_ updateToLaunch: Update?, _ error: Error?, _ didRollBackToEmbedded: Bool) -> Void
140   ) {
141     let updateBeingLaunched = updateResponse?.manifestUpdateResponsePart?.updateManifest
142 
143     if let rollBackDirective = updateResponse?.directiveUpdateResponsePart?.updateDirective as? RollBackToEmbeddedUpdateDirective {
144       self.processRollBackToEmbeddedDirective(
145         config: config,
146         database: database,
147         selectionPolicy: selectionPolicy,
148         launchedUpdate: launchedUpdate,
149         directory: directory,
150         loaderTaskQueue: loaderTaskQueue,
151         rollBackDirective: rollBackDirective,
152         manifestFilters: updateResponse?.responseHeaderData?.manifestFilters,
153         priorError: priorError,
154         onComplete: onComplete
155       )
156     } else {
157       onComplete(updateBeingLaunched, priorError, false)
158     }
159   }
160 
161   /**
162    * If directive is to roll-back to the embedded update and there is an embedded update,
163    * we need to update embedded update in the DB with the newer commitTime from the message so that
164    * the selection policy will choose it. That way future updates can continue to be applied
165    * over this roll back, but older ones won't.
166    * The embedded update is guaranteed to be in the DB from the earlier [EmbeddedAppLoader] call in this task.
167    */
168   private static func processRollBackToEmbeddedDirective(
169     config: UpdatesConfig,
170     database: UpdatesDatabase,
171     selectionPolicy: SelectionPolicy,
172     launchedUpdate: Update?,
173     directory: URL,
174     loaderTaskQueue: DispatchQueue,
175     rollBackDirective: RollBackToEmbeddedUpdateDirective,
176     manifestFilters: [String: Any]?,
177     priorError: Error?,
178     onComplete: @escaping (_ updateToLaunch: Update?, _ error: Error?, _ didRollBackToEmbedded: Bool) -> Void
179   ) {
180     if !config.hasEmbeddedUpdate {
181       onComplete(nil, priorError, false)
182       return
183     }
184 
185     guard let embeddedManifest = EmbeddedAppLoader.embeddedManifest(withConfig: config, database: database) else {
186       onComplete(nil, priorError, false)
187       return
188     }
189 
190     if !selectionPolicy.shouldLoadRollBackToEmbeddedDirective(
191       rollBackDirective,
192       withEmbeddedUpdate: embeddedManifest,
193       launchedUpdate: launchedUpdate,
194       filters: manifestFilters
195     ) {
196       onComplete(nil, priorError, false)
197       return
198     }
199 
200     // update the embedded update commit time in the in-memory embedded update since it is a singleton
201     embeddedManifest.commitTime = rollBackDirective.commitTime
202 
203     EmbeddedAppLoader(
204       config: config,
205       database: database,
206       directory: directory,
207       launchedUpdate: nil,
208       completionQueue: loaderTaskQueue
209     ).loadUpdateResponseFromEmbeddedManifest(
210       withCallback: { _ in
211         return true
212       }, asset: { _, _, _, _ in
213       }, success: { updateResponse in
214         do {
215           let update = updateResponse?.manifestUpdateResponsePart?.updateManifest
216           // do this synchronously as it is needed to launch, and we're already on a background dispatch queue so no UI will be blocked
217           try database.databaseQueue.sync {
218             // swiftlint:disable force_unwrapping
219             try database.setUpdateCommitTime(rollBackDirective.commitTime, onUpdate: update!)
220             // swiftlint:enable force_unwrapping
221           }
222           onComplete(update, priorError, true)
223         } catch {
224           onComplete(nil, error, false)
225         }
226       }, error: { embeddedLoaderError in
227         onComplete(nil, embeddedLoaderError, false)
228       }
229     )
230   }
231 }
232 
233 // swiftlint:enable function_parameter_count
234