@capgo/capacitor-updater 8.47.1 → 8.47.3
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 +41 -41
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +210 -49
- 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 +9 -5
- package/dist/esm/definitions.d.ts +19 -5
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +157 -35
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +59 -17
- package/package.json +1 -1
|
@@ -79,10 +79,16 @@ 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.3"
|
|
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"
|
|
86
|
+
static let autoUpdateModeOff = "off"
|
|
87
|
+
static let autoUpdateModeBackground = "atBackground"
|
|
88
|
+
static let autoUpdateModeInstall = "atInstall"
|
|
89
|
+
static let autoUpdateModeLaunch = "onLaunch"
|
|
90
|
+
static let autoUpdateModeAlways = "always"
|
|
91
|
+
static let autoUpdateModeOnlyDownload = "onlyDownload"
|
|
86
92
|
private let keepUrlPathFlagKey = "__capgo_keep_url_path_after_reload"
|
|
87
93
|
private let customIdDefaultsKey = "CapacitorUpdater.customId"
|
|
88
94
|
private let updateUrlDefaultsKey = "CapacitorUpdater.updateUrl"
|
|
@@ -102,6 +108,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
102
108
|
private var currentVersionNative: Version = "0.0.0"
|
|
103
109
|
private var currentBuildVersion: String = "0"
|
|
104
110
|
private var autoUpdate = false
|
|
111
|
+
private var autoUpdateMode = CapacitorUpdaterPlugin.autoUpdateModeOff
|
|
105
112
|
private var appReadyTimeout = 10000
|
|
106
113
|
private var appReadyCheck: DispatchWorkItem?
|
|
107
114
|
private var resetWhenUpdate = true
|
|
@@ -203,33 +210,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
203
210
|
keepUrlPathAfterReload = getConfig().getBoolean("keepUrlPathAfterReload", false)
|
|
204
211
|
syncKeepUrlPathFlag(enabled: keepUrlPathAfterReload)
|
|
205
212
|
|
|
206
|
-
// Handle directUpdate configuration - support string values and backward compatibility
|
|
207
|
-
if let directUpdateString = getConfig().getString("directUpdate") {
|
|
208
|
-
// Handle backward compatibility for boolean true
|
|
209
|
-
if directUpdateString == "true" {
|
|
210
|
-
directUpdateMode = "always"
|
|
211
|
-
directUpdate = true
|
|
212
|
-
} else {
|
|
213
|
-
directUpdateMode = directUpdateString
|
|
214
|
-
directUpdate = directUpdateString == "always" || directUpdateString == "atInstall" || directUpdateString == "onLaunch"
|
|
215
|
-
// Validate directUpdate value
|
|
216
|
-
if directUpdateString != "false" && directUpdateString != "always" && directUpdateString != "atInstall" && directUpdateString != "onLaunch" {
|
|
217
|
-
logger.error("Invalid directUpdate value: \"\(directUpdateString)\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\".")
|
|
218
|
-
directUpdateMode = "false"
|
|
219
|
-
directUpdate = false
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
let directUpdateBool = getConfig().getBoolean("directUpdate", false)
|
|
224
|
-
if directUpdateBool {
|
|
225
|
-
directUpdateMode = "always" // backward compatibility: true = always
|
|
226
|
-
directUpdate = true
|
|
227
|
-
} else {
|
|
228
|
-
directUpdateMode = "false"
|
|
229
|
-
directUpdate = false
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
213
|
autoSplashscreen = getConfig().getBoolean("autoSplashscreen", false)
|
|
234
214
|
autoSplashscreenLoader = getConfig().getBoolean("autoSplashscreenLoader", false)
|
|
235
215
|
let splashscreenTimeoutValue = getConfig().getInt("autoSplashscreenTimeout", 10000)
|
|
@@ -239,7 +219,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
239
219
|
updateUrl = storedUpdateUrl
|
|
240
220
|
logger.info("Loaded persisted updateUrl")
|
|
241
221
|
}
|
|
242
|
-
|
|
222
|
+
configureAutoUpdateModeFromConfig()
|
|
243
223
|
appReadyTimeout = max(1000, getConfig().getInt("appReadyTimeout", 10000)) // Minimum 1 second
|
|
244
224
|
implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
|
|
245
225
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
@@ -787,7 +767,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
787
767
|
let url = URL(string: urlString)
|
|
788
768
|
logger.info("Downloading \(String(describing: url))")
|
|
789
769
|
self.saveCallForAsyncHandling(call)
|
|
790
|
-
|
|
770
|
+
self.runBackgroundDownloadWork {
|
|
791
771
|
do {
|
|
792
772
|
let next: BundleInfo
|
|
793
773
|
if let manifestEntries = self.manifestEntries(from: manifestArray) {
|
|
@@ -2076,6 +2056,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2076
2056
|
}
|
|
2077
2057
|
|
|
2078
2058
|
private func shouldUseDirectUpdate() -> Bool {
|
|
2059
|
+
if !self.autoUpdate || self.autoUpdateMode == Self.autoUpdateModeOnlyDownload {
|
|
2060
|
+
return false
|
|
2061
|
+
}
|
|
2079
2062
|
if self.autoSplashscreenTimedOut {
|
|
2080
2063
|
return false
|
|
2081
2064
|
}
|
|
@@ -2102,6 +2085,99 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2102
2085
|
}
|
|
2103
2086
|
}
|
|
2104
2087
|
|
|
2088
|
+
private func configureAutoUpdateModeFromConfig() {
|
|
2089
|
+
if let configuredMode = getConfig().getString("autoUpdate"),
|
|
2090
|
+
configuredMode != "",
|
|
2091
|
+
configuredMode != "true",
|
|
2092
|
+
configuredMode != "false" {
|
|
2093
|
+
autoUpdateMode = Self.normalizedAutoUpdateMode(configuredMode)
|
|
2094
|
+
if autoUpdateMode != configuredMode {
|
|
2095
|
+
logger.error(
|
|
2096
|
+
"Invalid autoUpdate value: \"\(configuredMode)\". Supported values are: true, false, " +
|
|
2097
|
+
"\"off\", \"atBackground\", \"atInstall\", \"onLaunch\", \"always\", \"onlyDownload\". Defaulting to \"atBackground\"."
|
|
2098
|
+
)
|
|
2099
|
+
}
|
|
2100
|
+
} else {
|
|
2101
|
+
let configuredMode = getConfig().getString("autoUpdate")
|
|
2102
|
+
let enabled = configuredMode != nil ? configuredMode == "true" : getConfig().getBoolean("autoUpdate", true)
|
|
2103
|
+
autoUpdateMode = enabled
|
|
2104
|
+
? Self.autoUpdateModeForLegacyDirectUpdateMode(resolveLegacyDirectUpdateModeFromConfig())
|
|
2105
|
+
: Self.autoUpdateModeOff
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
autoUpdate = Self.isAutoUpdateModeEnabled(autoUpdateMode)
|
|
2109
|
+
directUpdateMode = Self.directUpdateModeForAutoUpdateMode(autoUpdateMode)
|
|
2110
|
+
directUpdate = Self.isDirectUpdateMode(directUpdateMode)
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
private func resolveLegacyDirectUpdateModeFromConfig() -> String {
|
|
2114
|
+
if let directUpdateString = getConfig().getString("directUpdate") {
|
|
2115
|
+
if directUpdateString == "true" {
|
|
2116
|
+
return Self.autoUpdateModeAlways
|
|
2117
|
+
}
|
|
2118
|
+
if directUpdateString == "false" || Self.isDirectUpdateMode(directUpdateString) {
|
|
2119
|
+
return directUpdateString
|
|
2120
|
+
}
|
|
2121
|
+
logger.error(
|
|
2122
|
+
"Invalid directUpdate value: \"\(directUpdateString)\". Supported values are: false, true, " +
|
|
2123
|
+
"\"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
2124
|
+
)
|
|
2125
|
+
return "false"
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
return getConfig().getBoolean("directUpdate", false) ? Self.autoUpdateModeAlways : "false"
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
static func normalizedAutoUpdateMode(_ value: String?) -> String {
|
|
2132
|
+
guard let value else {
|
|
2133
|
+
return autoUpdateModeBackground
|
|
2134
|
+
}
|
|
2135
|
+
switch value {
|
|
2136
|
+
case "false", autoUpdateModeOff:
|
|
2137
|
+
return autoUpdateModeOff
|
|
2138
|
+
case "true", autoUpdateModeBackground:
|
|
2139
|
+
return autoUpdateModeBackground
|
|
2140
|
+
case autoUpdateModeInstall, autoUpdateModeLaunch, autoUpdateModeAlways, autoUpdateModeOnlyDownload:
|
|
2141
|
+
return value
|
|
2142
|
+
default:
|
|
2143
|
+
return autoUpdateModeBackground
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
static func autoUpdateModeForLegacyDirectUpdateMode(_ directUpdateMode: String) -> String {
|
|
2148
|
+
switch directUpdateMode {
|
|
2149
|
+
case autoUpdateModeInstall, autoUpdateModeLaunch, autoUpdateModeAlways:
|
|
2150
|
+
return directUpdateMode
|
|
2151
|
+
default:
|
|
2152
|
+
return autoUpdateModeBackground
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
static func directUpdateModeForAutoUpdateMode(_ autoUpdateMode: String) -> String {
|
|
2157
|
+
switch autoUpdateMode {
|
|
2158
|
+
case autoUpdateModeInstall, autoUpdateModeLaunch, autoUpdateModeAlways:
|
|
2159
|
+
return autoUpdateMode
|
|
2160
|
+
default:
|
|
2161
|
+
return "false"
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
static func isAutoUpdateModeEnabled(_ autoUpdateMode: String) -> Bool {
|
|
2166
|
+
autoUpdateMode != autoUpdateModeOff
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
static func shouldAutoUpdateModeSetNextBundle(_ autoUpdateMode: String) -> Bool {
|
|
2170
|
+
isAutoUpdateModeEnabled(autoUpdateMode) && autoUpdateMode != autoUpdateModeOnlyDownload
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
static func isDirectUpdateMode(_ directUpdateMode: String) -> Bool {
|
|
2174
|
+
directUpdateMode == autoUpdateModeInstall || directUpdateMode == autoUpdateModeLaunch || directUpdateMode == autoUpdateModeAlways
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
private func shouldAutoSetNextBundle() -> Bool {
|
|
2178
|
+
Self.shouldAutoUpdateModeSetNextBundle(autoUpdateMode)
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2105
2181
|
static func shouldConsumeOnLaunchDirectUpdate(directUpdateMode: String, plannedDirectUpdate: Bool) -> Bool {
|
|
2106
2182
|
plannedDirectUpdate && directUpdateMode == "onLaunch"
|
|
2107
2183
|
}
|
|
@@ -2135,6 +2211,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2135
2211
|
|
|
2136
2212
|
func configureDirectUpdateModeForTesting(_ directUpdateMode: String, onLaunchDirectUpdateUsed: Bool = false) {
|
|
2137
2213
|
self.directUpdateMode = directUpdateMode
|
|
2214
|
+
self.autoUpdateMode = Self.autoUpdateModeForLegacyDirectUpdateMode(directUpdateMode)
|
|
2215
|
+
self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
|
|
2216
|
+
self.directUpdate = Self.isDirectUpdateMode(self.directUpdateMode)
|
|
2138
2217
|
self.setOnLaunchDirectUpdateUsed(onLaunchDirectUpdateUsed)
|
|
2139
2218
|
}
|
|
2140
2219
|
|
|
@@ -2142,6 +2221,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2142
2221
|
self.updateUrl = updateUrl
|
|
2143
2222
|
}
|
|
2144
2223
|
|
|
2224
|
+
func setAutoUpdateModeForTesting(_ autoUpdateMode: String) {
|
|
2225
|
+
self.autoUpdateMode = Self.normalizedAutoUpdateMode(autoUpdateMode)
|
|
2226
|
+
self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
|
|
2227
|
+
self.directUpdateMode = Self.directUpdateModeForAutoUpdateMode(self.autoUpdateMode)
|
|
2228
|
+
self.directUpdate = Self.isDirectUpdateMode(self.directUpdateMode)
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2145
2231
|
func setCurrentBuildVersionForTesting(_ currentBuildVersion: String) {
|
|
2146
2232
|
self.currentBuildVersion = currentBuildVersion
|
|
2147
2233
|
}
|
|
@@ -2237,7 +2323,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2237
2323
|
plannedDirectUpdate: Bool = false,
|
|
2238
2324
|
failureAction: String = "download_fail",
|
|
2239
2325
|
failureEvent: String = "downloadFailed",
|
|
2240
|
-
sendStats: Bool = true
|
|
2326
|
+
sendStats: Bool = true,
|
|
2327
|
+
notifyNoNeedUpdate: Bool = true
|
|
2241
2328
|
) {
|
|
2242
2329
|
// Clear download in progress flag - this is called at the end of every download attempt
|
|
2243
2330
|
// whether it succeeds, fails, or is skipped (e.g., already up to date)
|
|
@@ -2254,7 +2341,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2254
2341
|
}
|
|
2255
2342
|
self.notifyListeners(failureEvent, data: ["version": latestVersionName])
|
|
2256
2343
|
}
|
|
2257
|
-
|
|
2344
|
+
if notifyNoNeedUpdate {
|
|
2345
|
+
self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
|
|
2346
|
+
}
|
|
2258
2347
|
self.sendReadyToJs(current: current, msg: msg)
|
|
2259
2348
|
logger.info("endBackGroundTaskWithNotif \(msg) current: \(current.getVersionName()) latestVersionName: \(latestVersionName)")
|
|
2260
2349
|
self.endBackGroundTask()
|
|
@@ -2314,7 +2403,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2314
2403
|
downloadLock.unlock()
|
|
2315
2404
|
|
|
2316
2405
|
let plannedDirectUpdate = self.shouldUseDirectUpdate()
|
|
2317
|
-
let messageUpdate
|
|
2406
|
+
let messageUpdate: String
|
|
2407
|
+
if plannedDirectUpdate {
|
|
2408
|
+
messageUpdate = "Update will occur now."
|
|
2409
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
2410
|
+
messageUpdate = "Update will occur next time app moves to background."
|
|
2411
|
+
} else {
|
|
2412
|
+
messageUpdate = "Update will be downloaded and made available."
|
|
2413
|
+
}
|
|
2318
2414
|
guard let url = URL(string: self.updateUrl) else {
|
|
2319
2415
|
logger.error("Error no url or wrong format")
|
|
2320
2416
|
// Clear the flag if we return early
|
|
@@ -2358,7 +2454,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2358
2454
|
error: false,
|
|
2359
2455
|
plannedDirectUpdate: plannedDirectUpdate
|
|
2360
2456
|
)
|
|
2361
|
-
} else {
|
|
2457
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
2362
2458
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
2363
2459
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
|
|
2364
2460
|
}
|
|
@@ -2371,6 +2467,21 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2371
2467
|
error: false,
|
|
2372
2468
|
plannedDirectUpdate: plannedDirectUpdate
|
|
2373
2469
|
)
|
|
2470
|
+
} else {
|
|
2471
|
+
self.logger.info("autoUpdate is set to onlyDownload, builtin version will not be set as next bundle")
|
|
2472
|
+
let builtinUpdateAvailable = !current.isBuiltin()
|
|
2473
|
+
if builtinUpdateAvailable {
|
|
2474
|
+
let builtinBundle = self.implementation.getBundleInfo(id: BundleInfo.ID_BUILTIN)
|
|
2475
|
+
self.notifyListeners("updateAvailable", data: ["bundle": builtinBundle.toJSON()], retainUntilConsumed: true)
|
|
2476
|
+
}
|
|
2477
|
+
self.endBackGroundTaskWithNotif(
|
|
2478
|
+
msg: "Latest version is builtin, autoUpdate onlyDownload",
|
|
2479
|
+
latestVersionName: res.version,
|
|
2480
|
+
current: current,
|
|
2481
|
+
error: false,
|
|
2482
|
+
plannedDirectUpdate: plannedDirectUpdate,
|
|
2483
|
+
notifyNoNeedUpdate: !builtinUpdateAvailable
|
|
2484
|
+
)
|
|
2374
2485
|
}
|
|
2375
2486
|
return
|
|
2376
2487
|
}
|
|
@@ -2483,7 +2594,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2483
2594
|
plannedDirectUpdate: plannedDirectUpdate
|
|
2484
2595
|
)
|
|
2485
2596
|
}
|
|
2486
|
-
} else {
|
|
2597
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
2487
2598
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
2488
2599
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
2489
2600
|
}
|
|
@@ -2496,6 +2607,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2496
2607
|
error: false,
|
|
2497
2608
|
plannedDirectUpdate: plannedDirectUpdate
|
|
2498
2609
|
)
|
|
2610
|
+
} else {
|
|
2611
|
+
self.logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle")
|
|
2612
|
+
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()], retainUntilConsumed: true)
|
|
2613
|
+
self.endBackGroundTaskWithNotif(
|
|
2614
|
+
msg: "update downloaded, autoUpdate onlyDownload",
|
|
2615
|
+
latestVersionName: latestVersionName,
|
|
2616
|
+
current: current,
|
|
2617
|
+
error: false,
|
|
2618
|
+
plannedDirectUpdate: plannedDirectUpdate,
|
|
2619
|
+
notifyNoNeedUpdate: false
|
|
2620
|
+
)
|
|
2499
2621
|
}
|
|
2500
2622
|
return
|
|
2501
2623
|
} catch {
|
|
@@ -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)
|