@capgo/capacitor-updater 8.47.2 → 8.47.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +200 -42
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +36 -14
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +21 -2
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +9 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +213 -44
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +59 -17
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +4 -2
- package/package.json +1 -1
|
@@ -79,7 +79,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
79
79
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
80
80
|
]
|
|
81
81
|
public var implementation = CapgoUpdater()
|
|
82
|
-
private let pluginVersion: String = "8.47.
|
|
82
|
+
private let pluginVersion: String = "8.47.4"
|
|
83
83
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
84
84
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
85
85
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -101,7 +101,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
101
101
|
private let previewPreviousShakeChannelSelectorDefaultsKey = "CapacitorUpdater.previewPreviousShakeChannelSelector"
|
|
102
102
|
private let previewPreviousNextBundleDefaultsKey = "CapacitorUpdater.previewPreviousNextBundle"
|
|
103
103
|
private let previewPreviousAppIdDefaultsKey = "CapacitorUpdater.previewPreviousAppId"
|
|
104
|
+
private let previewPreviousDefaultChannelDefaultsKey = "CapacitorUpdater.previewPreviousDefaultChannel"
|
|
105
|
+
private let previewPreviousDefaultChannelWasSetDefaultsKey = "CapacitorUpdater.previewPreviousDefaultChannelWasSet"
|
|
104
106
|
private let previewAppIdDefaultsKey = "CapacitorUpdater.previewAppId"
|
|
107
|
+
private let previewPayloadUrlDefaultsKey = "CapacitorUpdater.previewPayloadUrl"
|
|
105
108
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
106
109
|
private var updateUrl = ""
|
|
107
110
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
@@ -749,6 +752,62 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
749
752
|
return manifestEntries
|
|
750
753
|
}
|
|
751
754
|
|
|
755
|
+
private struct PreviewPayload: Decodable {
|
|
756
|
+
let version: String?
|
|
757
|
+
let url: String?
|
|
758
|
+
let checksum: String?
|
|
759
|
+
let sessionKey: String?
|
|
760
|
+
let manifest: [ManifestEntry]?
|
|
761
|
+
let message: String?
|
|
762
|
+
let error: String?
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private func makePreviewError(_ message: String) -> NSError {
|
|
766
|
+
NSError(domain: "CapacitorUpdaterPreview", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
770
|
+
guard let url = URL(string: urlString) else {
|
|
771
|
+
throw makePreviewError("Invalid download URL")
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
var checksum = rawChecksum
|
|
775
|
+
let next: BundleInfo
|
|
776
|
+
if let manifestEntries = manifestEntries {
|
|
777
|
+
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
778
|
+
} else {
|
|
779
|
+
next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if self.implementation.publicKey != "" && checksum == "" {
|
|
783
|
+
self.logger.error("Public key present but no checksum provided")
|
|
784
|
+
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
785
|
+
let id = next.getId()
|
|
786
|
+
let resDel = self.implementation.delete(id: id)
|
|
787
|
+
if !resDel {
|
|
788
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
789
|
+
}
|
|
790
|
+
throw ObjectSavableError.checksum
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
794
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
795
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
796
|
+
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
797
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
798
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
799
|
+
let id = next.getId()
|
|
800
|
+
let resDel = self.implementation.delete(id: id)
|
|
801
|
+
if !resDel {
|
|
802
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
803
|
+
}
|
|
804
|
+
throw ObjectSavableError.checksum
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
808
|
+
return next
|
|
809
|
+
}
|
|
810
|
+
|
|
752
811
|
@objc func download(_ call: CAPPluginCall) {
|
|
753
812
|
guard let urlString = call.getString("url") else {
|
|
754
813
|
logger.error("Download called without url")
|
|
@@ -762,57 +821,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
762
821
|
}
|
|
763
822
|
|
|
764
823
|
let sessionKey = call.getString("sessionKey", "")
|
|
765
|
-
|
|
824
|
+
let checksum = call.getString("checksum", "")
|
|
766
825
|
let manifestArray = call.getArray("manifest")
|
|
767
|
-
|
|
768
|
-
logger.info("Downloading \(String(describing: url))")
|
|
826
|
+
logger.info("Downloading \(urlString)")
|
|
769
827
|
self.saveCallForAsyncHandling(call)
|
|
770
828
|
self.runBackgroundDownloadWork {
|
|
771
829
|
do {
|
|
772
|
-
let next
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if self.implementation.publicKey != "" && checksum == "" {
|
|
780
|
-
self.logger.error("Public key present but no checksum provided")
|
|
781
|
-
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
782
|
-
let id = next.getId()
|
|
783
|
-
let resDel = self.implementation.delete(id: id)
|
|
784
|
-
if !resDel {
|
|
785
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
786
|
-
}
|
|
787
|
-
throw ObjectSavableError.checksum
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
791
|
-
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
792
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
793
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
794
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
795
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
796
|
-
let id = next.getId()
|
|
797
|
-
let resDel = self.implementation.delete(id: id)
|
|
798
|
-
if !resDel {
|
|
799
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
800
|
-
}
|
|
801
|
-
throw ObjectSavableError.checksum
|
|
802
|
-
} else {
|
|
803
|
-
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
804
|
-
}
|
|
830
|
+
let next = try self.downloadBundle(
|
|
831
|
+
urlString: urlString,
|
|
832
|
+
version: version,
|
|
833
|
+
sessionKey: sessionKey,
|
|
834
|
+
checksum: checksum,
|
|
835
|
+
manifestEntries: self.manifestEntries(from: manifestArray)
|
|
836
|
+
)
|
|
805
837
|
var updateAvailablePayload: JSObject = [:]
|
|
806
838
|
updateAvailablePayload["bundle"] = self.bundlePayload(next)
|
|
807
839
|
self.notifyListenersOnMain("updateAvailable", data: updateAvailablePayload)
|
|
808
840
|
self.resolveCall(call, data: next.toJSON())
|
|
809
841
|
} catch {
|
|
810
|
-
self.logger.error("Failed to download from: \(
|
|
842
|
+
self.logger.error("Failed to download from: \(urlString) \(error.localizedDescription)")
|
|
811
843
|
var downloadFailedPayload: JSObject = [:]
|
|
812
844
|
downloadFailedPayload["version"] = version
|
|
813
845
|
self.notifyListenersOnMain("downloadFailed", data: downloadFailedPayload)
|
|
814
846
|
self.implementation.sendStats(action: "download_fail")
|
|
815
|
-
self.rejectCall(call, message: "Failed to download from: \(
|
|
847
|
+
self.rejectCall(call, message: "Failed to download from: \(urlString) - \(error.localizedDescription)")
|
|
816
848
|
}
|
|
817
849
|
}
|
|
818
850
|
}
|
|
@@ -989,6 +1021,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
989
1021
|
return
|
|
990
1022
|
}
|
|
991
1023
|
let previewAppId = self.normalizedPreviewAppId(call.getString("appId"))
|
|
1024
|
+
let rawPayloadUrl = call.getString("payloadUrl")
|
|
1025
|
+
let previewPayloadUrl = self.normalizedPreviewPayloadUrl(rawPayloadUrl)
|
|
1026
|
+
if let rawPayloadUrl = rawPayloadUrl, !rawPayloadUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, previewPayloadUrl == nil {
|
|
1027
|
+
logger.error("startPreviewSession called with invalid payloadUrl")
|
|
1028
|
+
call.reject("Invalid preview payloadUrl")
|
|
1029
|
+
return
|
|
1030
|
+
}
|
|
992
1031
|
|
|
993
1032
|
if !self.previewSessionEnabled {
|
|
994
1033
|
let current = self.implementation.getCurrentBundle()
|
|
@@ -1007,6 +1046,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1007
1046
|
}
|
|
1008
1047
|
|
|
1009
1048
|
UserDefaults.standard.set(self.implementation.appId, forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1049
|
+
if let previousDefaultChannel = UserDefaults.standard.object(forKey: self.defaultChannelDefaultsKey) as? String {
|
|
1050
|
+
UserDefaults.standard.set(previousDefaultChannel, forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1051
|
+
UserDefaults.standard.set(true, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1052
|
+
} else {
|
|
1053
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1054
|
+
UserDefaults.standard.set(false, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1055
|
+
}
|
|
1010
1056
|
UserDefaults.standard.set(self.shakeMenuEnabled, forKey: self.previewPreviousShakeMenuDefaultsKey)
|
|
1011
1057
|
UserDefaults.standard.set(self.shakeChannelSelectorEnabled, forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
1012
1058
|
logger.info("Preview session started with fallback bundle: \(current.toString())")
|
|
@@ -1018,6 +1064,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1018
1064
|
logger.info("Preview session using appId: \(previewAppId)")
|
|
1019
1065
|
}
|
|
1020
1066
|
|
|
1067
|
+
if let previewPayloadUrl = previewPayloadUrl {
|
|
1068
|
+
UserDefaults.standard.set(previewPayloadUrl.absoluteString, forKey: self.previewPayloadUrlDefaultsKey)
|
|
1069
|
+
logger.info("Preview session using payload URL")
|
|
1070
|
+
} else {
|
|
1071
|
+
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1021
1074
|
self.previewSessionEnabled = true
|
|
1022
1075
|
self.previewSessionAlertPending = true
|
|
1023
1076
|
self.implementation.previewSession = true
|
|
@@ -1030,14 +1083,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1030
1083
|
|
|
1031
1084
|
func leavePreviewSessionFromShakeMenu() -> Bool {
|
|
1032
1085
|
let previewBundle = self.implementation.getCurrentBundle()
|
|
1033
|
-
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
1034
1086
|
|
|
1035
1087
|
let didReset = self.resetToPreviewFallbackBundle()
|
|
1036
1088
|
guard didReset else {
|
|
1037
1089
|
return false
|
|
1038
1090
|
}
|
|
1039
1091
|
|
|
1040
|
-
_ = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
1041
1092
|
let previewFallbackBundle = self.implementation.getPreviewFallbackBundle()
|
|
1042
1093
|
self.endPreviewSession()
|
|
1043
1094
|
let restoredNextBundle = self.implementation.getNextBundle()
|
|
@@ -1050,7 +1101,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1050
1101
|
}
|
|
1051
1102
|
|
|
1052
1103
|
func reloadPreviewSessionFromShakeMenu() -> Bool {
|
|
1053
|
-
self.
|
|
1104
|
+
if let payloadUrl = self.storedPreviewPayloadUrl() {
|
|
1105
|
+
return self.refreshPreviewSessionFromPayloadUrl(payloadUrl)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return self._reload()
|
|
1054
1109
|
}
|
|
1055
1110
|
|
|
1056
1111
|
func hasActivePreviewSession() -> Bool {
|
|
@@ -1088,6 +1143,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1088
1143
|
?? getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
1089
1144
|
self.restorePreviewPreviousNextBundle()
|
|
1090
1145
|
self.restorePreviewPreviousAppId()
|
|
1146
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1091
1147
|
|
|
1092
1148
|
self.previewSessionEnabled = false
|
|
1093
1149
|
self.previewSessionAlertPending = false
|
|
@@ -1117,6 +1173,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1117
1173
|
|
|
1118
1174
|
self.restorePreviewPreviousNextBundle()
|
|
1119
1175
|
self.restorePreviewPreviousAppId()
|
|
1176
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1120
1177
|
self.previewSessionEnabled = false
|
|
1121
1178
|
self.previewSessionAlertPending = false
|
|
1122
1179
|
self.implementation.previewSession = false
|
|
@@ -1132,7 +1189,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1132
1189
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
1133
1190
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
1134
1191
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1192
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1193
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1135
1194
|
UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
|
|
1195
|
+
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
1136
1196
|
UserDefaults.standard.synchronize()
|
|
1137
1197
|
}
|
|
1138
1198
|
|
|
@@ -1145,6 +1205,23 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1145
1205
|
logger.info("Restored appId after preview: \(previousAppId)")
|
|
1146
1206
|
}
|
|
1147
1207
|
|
|
1208
|
+
private func restorePreviewPreviousDefaultChannel() {
|
|
1209
|
+
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
1210
|
+
let hadPreviousDefaultChannel = UserDefaults.standard.object(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey) as? Bool ?? false
|
|
1211
|
+
|
|
1212
|
+
guard hadPreviousDefaultChannel,
|
|
1213
|
+
let previousDefaultChannel = UserDefaults.standard.string(forKey: self.previewPreviousDefaultChannelDefaultsKey) else {
|
|
1214
|
+
UserDefaults.standard.removeObject(forKey: self.defaultChannelDefaultsKey)
|
|
1215
|
+
self.implementation.defaultChannel = configDefaultChannel
|
|
1216
|
+
logger.info("Restored defaultChannel after preview to config value")
|
|
1217
|
+
return
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
UserDefaults.standard.set(previousDefaultChannel, forKey: self.defaultChannelDefaultsKey)
|
|
1221
|
+
self.implementation.defaultChannel = previousDefaultChannel
|
|
1222
|
+
logger.info("Restored defaultChannel after preview")
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1148
1225
|
private func normalizedPreviewAppId(_ rawAppId: String?) -> String? {
|
|
1149
1226
|
guard let rawAppId else {
|
|
1150
1227
|
return nil
|
|
@@ -1163,6 +1240,99 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1163
1240
|
return appId
|
|
1164
1241
|
}
|
|
1165
1242
|
|
|
1243
|
+
private func normalizedPreviewPayloadUrl(_ rawPayloadUrl: String?) -> URL? {
|
|
1244
|
+
guard let rawPayloadUrl else {
|
|
1245
|
+
return nil
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let payloadUrl = rawPayloadUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1249
|
+
guard !payloadUrl.isEmpty,
|
|
1250
|
+
let url = URL(string: payloadUrl),
|
|
1251
|
+
url.scheme == "https" || url.scheme == "http" else {
|
|
1252
|
+
return nil
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return url
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
private func storedPreviewPayloadUrl() -> URL? {
|
|
1259
|
+
normalizedPreviewPayloadUrl(UserDefaults.standard.string(forKey: self.previewPayloadUrlDefaultsKey))
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private func fetchPreviewPayload(_ payloadUrl: URL) throws -> PreviewPayload {
|
|
1263
|
+
var request = URLRequest(url: payloadUrl)
|
|
1264
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
1265
|
+
|
|
1266
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
1267
|
+
var responseData: Data?
|
|
1268
|
+
var response: URLResponse?
|
|
1269
|
+
var responseError: Error?
|
|
1270
|
+
|
|
1271
|
+
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
|
|
1272
|
+
responseData = data
|
|
1273
|
+
response = urlResponse
|
|
1274
|
+
responseError = error
|
|
1275
|
+
semaphore.signal()
|
|
1276
|
+
}.resume()
|
|
1277
|
+
|
|
1278
|
+
if semaphore.wait(timeout: .now() + 60) == .timedOut {
|
|
1279
|
+
throw makePreviewError("Preview payload request timed out")
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if let responseError = responseError {
|
|
1283
|
+
throw responseError
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
let data = responseData ?? Data()
|
|
1287
|
+
if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
|
|
1288
|
+
if let payload = try? JSONDecoder().decode(PreviewPayload.self, from: data) {
|
|
1289
|
+
throw makePreviewError(payload.message ?? payload.error ?? "Preview payload request failed with HTTP \(httpResponse.statusCode)")
|
|
1290
|
+
}
|
|
1291
|
+
let message = String(data: data, encoding: .utf8) ?? "Preview payload request failed with HTTP \(httpResponse.statusCode)"
|
|
1292
|
+
throw makePreviewError(message)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
return try JSONDecoder().decode(PreviewPayload.self, from: data)
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
private func refreshPreviewSessionFromPayloadUrl(_ payloadUrl: URL) -> Bool {
|
|
1299
|
+
do {
|
|
1300
|
+
let payload = try self.fetchPreviewPayload(payloadUrl)
|
|
1301
|
+
guard let version = payload.version, !version.isEmpty else {
|
|
1302
|
+
throw makePreviewError("Preview payload is missing a version")
|
|
1303
|
+
}
|
|
1304
|
+
guard payload.url != nil || payload.manifest?.isEmpty == false else {
|
|
1305
|
+
throw makePreviewError("Preview payload is missing download information")
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
let current = self.implementation.getCurrentBundle()
|
|
1309
|
+
if current.getVersionName() == version {
|
|
1310
|
+
self.logger.info("Preview payload unchanged, reloading current bundle")
|
|
1311
|
+
return self._reload()
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
let next = try self.downloadBundle(
|
|
1315
|
+
// Fallback URL is only provided when payload.url is missing; when manifestEntries is present,
|
|
1316
|
+
// downloadBundle routes through downloadManifest and ignores urlString.
|
|
1317
|
+
urlString: payload.url ?? "https://404.capgo.app/no.zip",
|
|
1318
|
+
version: version,
|
|
1319
|
+
sessionKey: payload.sessionKey ?? "",
|
|
1320
|
+
checksum: payload.checksum ?? "",
|
|
1321
|
+
manifestEntries: payload.manifest
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
guard self.implementation.set(id: next.getId()) else {
|
|
1325
|
+
throw makePreviewError("Downloaded preview bundle cannot be applied")
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
self.notifyBundleSet(next)
|
|
1329
|
+
return self._reload()
|
|
1330
|
+
} catch {
|
|
1331
|
+
self.logger.error("Could not refresh preview session: \(error.localizedDescription)")
|
|
1332
|
+
return false
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1166
1336
|
private func clearPreviewSessionForNativeBuildChange() {
|
|
1167
1337
|
guard self.previewSessionEnabled || self.implementation.getPreviewFallbackBundle() != nil else {
|
|
1168
1338
|
return
|
|
@@ -1174,10 +1344,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1174
1344
|
self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
1175
1345
|
self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
1176
1346
|
self.restorePreviewPreviousAppId()
|
|
1347
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1177
1348
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
1178
1349
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1179
|
-
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
1180
|
-
_ = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
1181
1350
|
self.clearPreviewSessionPreferences()
|
|
1182
1351
|
}
|
|
1183
1352
|
|
|
@@ -97,6 +97,43 @@ import UIKit
|
|
|
97
97
|
let timedOut: Bool
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
enum SecurePathError: Error {
|
|
101
|
+
case emptyPath
|
|
102
|
+
case windowsPath
|
|
103
|
+
case absolutePath
|
|
104
|
+
case pathTraversal
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static func resolvePathInsideDirectory(baseDirectory: URL, relativePath: String) throws -> URL {
|
|
108
|
+
if relativePath.isEmpty {
|
|
109
|
+
throw SecurePathError.emptyPath
|
|
110
|
+
}
|
|
111
|
+
if relativePath.contains("\\") || relativePath.contains("\0") {
|
|
112
|
+
throw SecurePathError.windowsPath
|
|
113
|
+
}
|
|
114
|
+
if (relativePath as NSString).isAbsolutePath {
|
|
115
|
+
throw SecurePathError.absolutePath
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let canonicalBase = baseDirectory.standardizedFileURL
|
|
119
|
+
let canonicalBasePath = canonicalBase.path
|
|
120
|
+
let normalizedBasePath = canonicalBasePath.hasSuffix("/") ? canonicalBasePath : "\(canonicalBasePath)/"
|
|
121
|
+
let canonicalTarget = canonicalBase.appendingPathComponent(relativePath).standardizedFileURL
|
|
122
|
+
let canonicalTargetPath = canonicalTarget.path
|
|
123
|
+
|
|
124
|
+
if canonicalTargetPath != canonicalBasePath && !canonicalTargetPath.hasPrefix(normalizedBasePath) {
|
|
125
|
+
throw SecurePathError.pathTraversal
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return canonicalTarget
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static func resolveManifestTargetPath(baseDirectory: URL, fileName: String) throws -> URL {
|
|
132
|
+
let isBrotli = fileName.hasSuffix(".br")
|
|
133
|
+
let targetFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
134
|
+
return try resolvePathInsideDirectory(baseDirectory: baseDirectory, relativePath: targetFileName)
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
private func isTimedOutError(_ error: Error?) -> Bool {
|
|
101
138
|
guard let nsError = error as NSError? else {
|
|
102
139
|
return false
|
|
@@ -491,21 +528,15 @@ import UIKit
|
|
|
491
528
|
}
|
|
492
529
|
}
|
|
493
530
|
|
|
494
|
-
private func
|
|
495
|
-
|
|
496
|
-
|
|
531
|
+
private func resolveZipEntry(path: String, destUnZip: URL) throws -> URL {
|
|
532
|
+
do {
|
|
533
|
+
return try Self.resolvePathInsideDirectory(baseDirectory: destUnZip, relativePath: path)
|
|
534
|
+
} catch SecurePathError.windowsPath {
|
|
497
535
|
logger.error("Unzip failed: Windows path not supported")
|
|
498
536
|
logger.debug("Invalid path: \(path)")
|
|
499
537
|
self.sendStats(action: "windows_path_fail")
|
|
500
538
|
throw CustomError.cannotUnzip
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Check for path traversal
|
|
504
|
-
let fileURL = destUnZip.appendingPathComponent(path)
|
|
505
|
-
let canonicalPath = fileURL.standardizedFileURL.path
|
|
506
|
-
let canonicalDir = destUnZip.standardizedFileURL.path
|
|
507
|
-
|
|
508
|
-
if !canonicalPath.hasPrefix(canonicalDir) {
|
|
539
|
+
} catch {
|
|
509
540
|
self.sendStats(action: "canonical_path_fail")
|
|
510
541
|
throw CustomError.cannotUnzip
|
|
511
542
|
}
|
|
@@ -596,10 +627,7 @@ import UIKit
|
|
|
596
627
|
|
|
597
628
|
do {
|
|
598
629
|
for entry in archive {
|
|
599
|
-
|
|
600
|
-
try validateZipEntry(path: entry.path, destUnZip: destUnZip)
|
|
601
|
-
|
|
602
|
-
let destPath = destUnZip.appendingPathComponent(entry.path)
|
|
630
|
+
let destPath = try resolveZipEntry(path: entry.path, destUnZip: destUnZip)
|
|
603
631
|
|
|
604
632
|
if entry.type == .directory {
|
|
605
633
|
try FileManager.default.createDirectory(at: destPath, withIntermediateDirectories: true, attributes: nil)
|
|
@@ -1100,8 +1128,22 @@ import UIKit
|
|
|
1100
1128
|
let legacyCacheFilePath: URL? = isBrotli ? cacheFolder.appendingPathComponent("\(finalFileHash)_\(fileNameWithoutPath)") : nil
|
|
1101
1129
|
|
|
1102
1130
|
let destFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
1103
|
-
let destFilePath
|
|
1104
|
-
let builtinFilePath
|
|
1131
|
+
let destFilePath: URL
|
|
1132
|
+
let builtinFilePath: URL
|
|
1133
|
+
do {
|
|
1134
|
+
destFilePath = try Self.resolveManifestTargetPath(baseDirectory: destFolder, fileName: fileName)
|
|
1135
|
+
builtinFilePath = try Self.resolvePathInsideDirectory(baseDirectory: builtinFolder, relativePath: fileName)
|
|
1136
|
+
} catch {
|
|
1137
|
+
logger.error("Invalid manifest file path: \(fileName)")
|
|
1138
|
+
self.sendStats(action: "manifest_path_fail", versionName: "\(version):\(fileName)")
|
|
1139
|
+
errorLock.lock()
|
|
1140
|
+
if downloadError == nil {
|
|
1141
|
+
downloadError = error
|
|
1142
|
+
}
|
|
1143
|
+
errorLock.unlock()
|
|
1144
|
+
hasError.value = true
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1105
1147
|
|
|
1106
1148
|
// Create parent directories synchronously (before operations start)
|
|
1107
1149
|
try? FileManager.default.createDirectory(at: destFilePath.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
|
|
@@ -81,9 +81,11 @@ extension UIWindow {
|
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
alertShake.addAction(UIAlertAction(title: reloadButtonTitle, style: .default) { _ in
|
|
84
|
-
DispatchQueue.
|
|
84
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
85
85
|
if !plugin.reloadPreviewSessionFromShakeMenu() {
|
|
86
|
-
|
|
86
|
+
DispatchQueue.main.async {
|
|
87
|
+
self.showError(message: "Could not reload the test app.", plugin: plugin)
|
|
88
|
+
}
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
})
|