@capgo/cordova-updater 8.1.1
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/LICENSE +373 -0
- package/README.md +82 -0
- package/dist/esm/definitions.d.ts +2684 -0
- package/dist/esm/definitions.js +109 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/exec.d.ts +4 -0
- package/dist/esm/exec.js +25 -0
- package/dist/esm/exec.js.map +1 -0
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +57 -0
- package/dist/esm/index.js +68 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +57 -0
- package/dist/esm/plugin.js +15 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/plugin.cjs.js +217 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +222 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +96 -0
- package/plugin.xml +207 -0
- package/src/android/CordovaUpdaterPlugin.java +5662 -0
- package/src/android/UpdaterPathHandler.java +99 -0
- package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
- package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
- package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
- package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
- package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
- package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
- package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
- package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
- package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
- package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
- package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
- package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
- package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
- package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
- package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
- package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
- package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
- package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
- package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
- package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
- package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
- package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
- package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
- package/src/android/capgo-cordova-updater.gradle +19 -0
- package/src/android/capgo-updater.xml +3 -0
- package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
- package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
- package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
- package/src/ios/AES.swift +216 -0
- package/src/ios/AppHealthTracker.swift +82 -0
- package/src/ios/BundleInfo.swift +185 -0
- package/src/ios/BundleStatus.swift +127 -0
- package/src/ios/CapgoRawRsa.swift +36 -0
- package/src/ios/CapgoUpdater.swift +3544 -0
- package/src/ios/CordovaCompat.swift +11 -0
- package/src/ios/CordovaPluginCall.swift +190 -0
- package/src/ios/CordovaUpdaterCommands.swift +270 -0
- package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
- package/src/ios/CryptoCipher.swift +326 -0
- package/src/ios/DelayCondition.swift +74 -0
- package/src/ios/DelayUntilNext.swift +30 -0
- package/src/ios/DelayUpdateUtils.swift +257 -0
- package/src/ios/DeviceIdHelper.swift +120 -0
- package/src/ios/Info.plist +28 -0
- package/src/ios/InternalUtils.swift +471 -0
- package/src/ios/Logger.swift +283 -0
- package/src/ios/RSA.swift +96 -0
- package/src/ios/ShakeMenu.swift +52 -0
- package/src/ios/UpdaterSchemeHandler.swift +81 -0
- package/src/ios/UserDefaultsExtension.swift +46 -0
- package/src/ios/WebViewStatsReporter.swift +303 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Foundation
|
|
8
|
+
import CryptoKit
|
|
9
|
+
|
|
10
|
+
public struct CryptoCipher {
|
|
11
|
+
private static var logger: Logger!
|
|
12
|
+
|
|
13
|
+
public static func setLogger(_ logger: Logger) {
|
|
14
|
+
self.logger = logger
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private static func hexStringToData(_ hex: String) -> Data? {
|
|
18
|
+
var data = Data()
|
|
19
|
+
var hexIterator = hex.makeIterator()
|
|
20
|
+
while let char1 = hexIterator.next(), let char2 = hexIterator.next() {
|
|
21
|
+
guard let byte = UInt8(String([char1, char2]), radix: 16) else {
|
|
22
|
+
return nil
|
|
23
|
+
}
|
|
24
|
+
data.append(byte)
|
|
25
|
+
}
|
|
26
|
+
return data
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static func isHexString(_ str: String) -> Bool {
|
|
30
|
+
let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
|
|
31
|
+
return str.unicodeScalars.allSatisfy { hexCharacterSet.contains($0) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public static func decryptChecksum(checksum: String, publicKey: String) throws -> String {
|
|
35
|
+
if publicKey.isEmpty {
|
|
36
|
+
logger.info("No encryption set (public key) ignored")
|
|
37
|
+
return checksum
|
|
38
|
+
}
|
|
39
|
+
do {
|
|
40
|
+
// Determine if input is hex or base64 encoded
|
|
41
|
+
// Hex strings only contain 0-9 and a-f, while base64 contains other characters
|
|
42
|
+
let checksumBytes: Data
|
|
43
|
+
let detectedFormat: String
|
|
44
|
+
if isHexString(checksum) {
|
|
45
|
+
// Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
|
|
46
|
+
guard let hexData = hexStringToData(checksum) else {
|
|
47
|
+
logger.error("Cannot decode checksum as hex")
|
|
48
|
+
logger.debug("Checksum value: \(checksum)")
|
|
49
|
+
throw CustomError.cannotDecode
|
|
50
|
+
}
|
|
51
|
+
checksumBytes = hexData
|
|
52
|
+
detectedFormat = "hex"
|
|
53
|
+
} else {
|
|
54
|
+
// TODO: remove backwards compatibility
|
|
55
|
+
// Base64 encoded (old format for backwards compatibility)
|
|
56
|
+
guard let base64Data = Data(base64Encoded: checksum) else {
|
|
57
|
+
logger.error("Cannot decode checksum as base64")
|
|
58
|
+
logger.debug("Checksum value: \(checksum)")
|
|
59
|
+
throw CustomError.cannotDecode
|
|
60
|
+
}
|
|
61
|
+
checksumBytes = base64Data
|
|
62
|
+
detectedFormat = "base64"
|
|
63
|
+
}
|
|
64
|
+
// swiftlint:disable:next line_length
|
|
65
|
+
logger.debug("Received checksum format: \(detectedFormat) (length: \(checksum.count) chars, \(checksumBytes.count) bytes)")
|
|
66
|
+
|
|
67
|
+
if checksumBytes.isEmpty {
|
|
68
|
+
logger.error("Decoded checksum is empty")
|
|
69
|
+
throw CustomError.cannotDecode
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// RSA-2048 encrypted data must be exactly 256 bytes
|
|
73
|
+
// If the checksum is not 256 bytes, the bundle was not encrypted properly
|
|
74
|
+
if checksumBytes.count != 256 {
|
|
75
|
+
// swiftlint:disable:next line_length
|
|
76
|
+
logger.error("Checksum is not RSA encrypted (size: \(checksumBytes.count) bytes, expected 256 for RSA-2048). Bundle must be uploaded with encryption when public key is configured.")
|
|
77
|
+
throw CustomError.cannotDecode
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
|
|
81
|
+
logger.error("The public key is not a valid RSA Public key")
|
|
82
|
+
throw CustomError.cannotDecode
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
guard let decryptedChecksum = rsaPublicKey.decrypt(data: checksumBytes) else {
|
|
86
|
+
logger.error("decryptChecksum fail")
|
|
87
|
+
throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Return as hex string to match calcChecksum output format
|
|
91
|
+
let result = decryptedChecksum.map { String(format: "%02x", $0) }.joined()
|
|
92
|
+
|
|
93
|
+
// Detect checksum algorithm based on length
|
|
94
|
+
let detectedAlgorithm: String
|
|
95
|
+
if decryptedChecksum.count == 32 {
|
|
96
|
+
detectedAlgorithm = "SHA-256"
|
|
97
|
+
} else if decryptedChecksum.count == 4 {
|
|
98
|
+
detectedAlgorithm = "CRC32 (deprecated)"
|
|
99
|
+
logger.error("CRC32 checksum detected - deprecated algorithm")
|
|
100
|
+
} else {
|
|
101
|
+
detectedAlgorithm = "unknown (\(decryptedChecksum.count) bytes)"
|
|
102
|
+
logger.error("Unknown checksum algorithm detected")
|
|
103
|
+
logger.debug("Byte count: \(decryptedChecksum.count), Expected: 32 (SHA-256)")
|
|
104
|
+
}
|
|
105
|
+
// swiftlint:disable:next line_length
|
|
106
|
+
logger.debug("Decrypted checksum: \(detectedAlgorithm) hex format (length: \(result.count) chars, \(decryptedChecksum.count) bytes)")
|
|
107
|
+
return result
|
|
108
|
+
} catch {
|
|
109
|
+
logger.error("Checksum decryption failed")
|
|
110
|
+
logger.debug("Error: \(error.localizedDescription)")
|
|
111
|
+
throw CustomError.cannotDecode
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Detect checksum algorithm based on hex string length.
|
|
116
|
+
/// SHA-256 = 64 hex chars (32 bytes)
|
|
117
|
+
/// CRC32 = 8 hex chars (4 bytes)
|
|
118
|
+
public static func detectChecksumAlgorithm(_ hexChecksum: String) -> String {
|
|
119
|
+
if hexChecksum.isEmpty {
|
|
120
|
+
return "empty"
|
|
121
|
+
}
|
|
122
|
+
let len = hexChecksum.count
|
|
123
|
+
if len == 64 {
|
|
124
|
+
return "SHA-256"
|
|
125
|
+
} else if len == 8 {
|
|
126
|
+
return "CRC32 (deprecated)"
|
|
127
|
+
} else {
|
|
128
|
+
return "unknown (\(len) hex chars)"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Log checksum info and warn if deprecated algorithm detected.
|
|
133
|
+
public static func logChecksumInfo(label: String, hexChecksum: String) {
|
|
134
|
+
let algorithm = detectChecksumAlgorithm(hexChecksum)
|
|
135
|
+
logger.debug("\(label): \(algorithm) hex format (length: \(hexChecksum.count) chars)")
|
|
136
|
+
if algorithm.contains("CRC32") {
|
|
137
|
+
logger.error("CRC32 checksum detected - deprecated algorithm")
|
|
138
|
+
} else if algorithm.contains("unknown") {
|
|
139
|
+
logger.error("Unknown checksum algorithm detected")
|
|
140
|
+
logger.debug("Char count: \(hexChecksum.count), Expected: 64 (SHA-256)")
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// 256 KiB: one size for checksum, copy, and decode.
|
|
145
|
+
/// 64 workers * 256 KiB = 16 MiB for one buffer; AES/Brotli hold two (~32 MiB).
|
|
146
|
+
static let ioBufferBytesValue = 256 * 1024
|
|
147
|
+
|
|
148
|
+
static func ioBufferBytes() -> Int {
|
|
149
|
+
return ioBufferBytesValue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static func checksumBufferBytes() -> Int {
|
|
153
|
+
return ioBufferBytesValue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static func copyBufferBytes() -> Int {
|
|
157
|
+
return ioBufferBytesValue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public static func calcChecksum(filePath: URL) -> String {
|
|
161
|
+
let bufferSize = checksumBufferBytes()
|
|
162
|
+
var sha256 = SHA256()
|
|
163
|
+
|
|
164
|
+
do {
|
|
165
|
+
let fileHandle: FileHandle
|
|
166
|
+
do {
|
|
167
|
+
fileHandle = try FileHandle(forReadingFrom: filePath)
|
|
168
|
+
} catch {
|
|
169
|
+
logger.error("Cannot open file for checksum calculation")
|
|
170
|
+
logger.debug("Path: \(filePath.path), Error: \(error)")
|
|
171
|
+
return ""
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
defer {
|
|
175
|
+
do {
|
|
176
|
+
try fileHandle.close()
|
|
177
|
+
} catch {
|
|
178
|
+
logger.error("Error closing file during checksum")
|
|
179
|
+
logger.debug("Error: \(error)")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
while autoreleasepool(invoking: {
|
|
184
|
+
let fileData: Data
|
|
185
|
+
do {
|
|
186
|
+
fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
|
|
187
|
+
} catch {
|
|
188
|
+
logger.error("Error reading file during checksum")
|
|
189
|
+
logger.debug("Error: \(error)")
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if fileData.count > 0 {
|
|
194
|
+
sha256.update(data: fileData)
|
|
195
|
+
return true // Continue
|
|
196
|
+
} else {
|
|
197
|
+
return false // End of file
|
|
198
|
+
}
|
|
199
|
+
}) {}
|
|
200
|
+
|
|
201
|
+
return hexString(from: sha256)
|
|
202
|
+
} catch {
|
|
203
|
+
logger.error("Cannot calculate checksum")
|
|
204
|
+
logger.debug("Path: \(filePath.path), Error: \(error)")
|
|
205
|
+
return ""
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
final class RunningChecksum {
|
|
210
|
+
private var sha256 = SHA256()
|
|
211
|
+
|
|
212
|
+
func update(_ data: Data) {
|
|
213
|
+
guard !data.isEmpty else {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
sha256.update(data: data)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func hex() -> String {
|
|
220
|
+
CryptoCipher.hexString(from: sha256)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static func hexString(from sha256: SHA256) -> String {
|
|
225
|
+
var copy = sha256
|
|
226
|
+
return copy.finalize().compactMap { String(format: "%02x", $0) }.joined()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static func shortPathKey(_ fileName: String) -> String {
|
|
230
|
+
var sha256 = SHA256()
|
|
231
|
+
sha256.update(data: Data(fileName.utf8))
|
|
232
|
+
return String(hexString(from: sha256).prefix(16))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
|
|
236
|
+
if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
|
|
237
|
+
logger.info("Encryption not set, no public key or session, ignored")
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if !publicKey.hasPrefix("-----BEGIN RSA PUBLIC KEY-----") {
|
|
242
|
+
logger.error("The public key is not a valid RSA Public key")
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
do {
|
|
247
|
+
guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
|
|
248
|
+
logger.error("The public key is not a valid RSA Public key")
|
|
249
|
+
throw CustomError.cannotDecode
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let sessionKeyComponents = sessionKey.components(separatedBy: ":")
|
|
253
|
+
let ivBase64 = sessionKeyComponents[0]
|
|
254
|
+
let encryptedKeyBase64 = sessionKeyComponents[1]
|
|
255
|
+
|
|
256
|
+
guard let ivData = Data(base64Encoded: ivBase64) else {
|
|
257
|
+
logger.error("Cannot decode sessionKey IV")
|
|
258
|
+
logger.debug("IV value: \(ivBase64)")
|
|
259
|
+
throw CustomError.cannotDecode
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if ivData.count != 16 {
|
|
263
|
+
logger.error("IV data has invalid length")
|
|
264
|
+
logger.debug("Length: \(ivData.count), Expected: 16")
|
|
265
|
+
throw CustomError.cannotDecode
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
guard let sessionKeyDataEncrypted = Data(base64Encoded: encryptedKeyBase64) else {
|
|
269
|
+
logger.error("Cannot decode sessionKey data")
|
|
270
|
+
logger.debug("Key value: \(encryptedKeyBase64)")
|
|
271
|
+
throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
|
|
275
|
+
logger.error("Failed to decrypt session key data")
|
|
276
|
+
throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if sessionKeyDataDecrypted.count != 16 {
|
|
280
|
+
logger.error("Decrypted session key has invalid length")
|
|
281
|
+
logger.debug("Length: \(sessionKeyDataDecrypted.count), Expected: 16")
|
|
282
|
+
throw NSError(domain: "Invalid decrypted session key", code: 5, userInfo: nil)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted, logger: logger)
|
|
286
|
+
|
|
287
|
+
let encryptedSize = (try FileManager.default.attributesOfItem(atPath: filePath.path)[.size] as? NSNumber)?.uint64Value ?? 0
|
|
288
|
+
if encryptedSize == 0 {
|
|
289
|
+
logger.error("Encrypted file data is empty")
|
|
290
|
+
throw NSError(domain: "Empty encrypted data", code: 6, userInfo: nil)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try aesPrivateKey.decrypt(from: filePath, to: filePath)
|
|
294
|
+
let decryptedSize = (try FileManager.default.attributesOfItem(atPath: filePath.path)[.size] as? NSNumber)?.uint64Value ?? 0
|
|
295
|
+
if decryptedSize == 0 {
|
|
296
|
+
logger.error("Decrypted data is empty")
|
|
297
|
+
throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
} catch {
|
|
301
|
+
logger.error("File decryption failed")
|
|
302
|
+
throw CustomError.cannotDecode
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// Get first 20 characters of the public key for identification
|
|
307
|
+
/// Returns 20-character string or empty string if key is invalid/empty
|
|
308
|
+
/// The first 12 chars are always "MIIBCgKCAQEA" for RSA 2048-bit keys,
|
|
309
|
+
/// so the unique part starts at character 13
|
|
310
|
+
public static func calcKeyId(publicKey: String) -> String {
|
|
311
|
+
if publicKey.isEmpty {
|
|
312
|
+
return ""
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Remove PEM headers and whitespace to get the raw key data
|
|
316
|
+
let cleanedKey = publicKey
|
|
317
|
+
.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
|
|
318
|
+
.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
|
|
319
|
+
.replacingOccurrences(of: "\n", with: "")
|
|
320
|
+
.replacingOccurrences(of: "\r", with: "")
|
|
321
|
+
.replacingOccurrences(of: " ", with: "")
|
|
322
|
+
|
|
323
|
+
// Return first 20 characters of the base64-encoded key
|
|
324
|
+
return String(cleanedKey.prefix(20))
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
//
|
|
8
|
+
// DelayCondition.swift
|
|
9
|
+
// Plugin
|
|
10
|
+
//
|
|
11
|
+
// Created by Luca Peruzzo on 12/09/22.
|
|
12
|
+
// Copyright © 2022 Capgo. All rights reserved.
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
private func delayUntilNextValue(value: String) -> DelayUntilNext {
|
|
18
|
+
switch value {
|
|
19
|
+
case "background": return .background
|
|
20
|
+
case "kill": return .kill
|
|
21
|
+
case "nativeVersion": return .nativeVersion
|
|
22
|
+
case "date": return .date
|
|
23
|
+
default:
|
|
24
|
+
return .background
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc public class DelayCondition: NSObject, Decodable, Encodable {
|
|
29
|
+
private let kind: DelayUntilNext
|
|
30
|
+
private let value: String?
|
|
31
|
+
|
|
32
|
+
convenience init(kind: String, value: String?) {
|
|
33
|
+
self.init(kind: delayUntilNextValue(value: kind), value: value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
init(kind: DelayUntilNext, value: String?) {
|
|
37
|
+
self.kind = kind
|
|
38
|
+
self.value = value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public required init(from decoder: Decoder) throws {
|
|
42
|
+
let values: KeyedDecodingContainer<DelayCondition.CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
|
43
|
+
kind = try values.decode(DelayUntilNext.self, forKey: .kind)
|
|
44
|
+
value = try values.decode(String.self, forKey: .value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
enum CodingKeys: String, CodingKey {
|
|
48
|
+
case kind, value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public func getKind() -> String {
|
|
52
|
+
return self.kind.description
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public func getValue() -> String? {
|
|
56
|
+
return self.value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public func toJSON() -> [String: String] {
|
|
60
|
+
return [
|
|
61
|
+
"kind": self.getKind(),
|
|
62
|
+
"value": self.getValue() ?? ""
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public static func == (lhs: DelayCondition, rhs: DelayCondition) -> Bool {
|
|
67
|
+
return lhs.getKind() == rhs.getKind() && lhs.getValue() == rhs.getValue()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public func toString() -> String {
|
|
71
|
+
return "{ \"kind\": \"\(self.getKind())\", \"value\": \"\(self.getValue() ?? "")\"}"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
//
|
|
8
|
+
// DelayUntilNext.swift
|
|
9
|
+
// Plugin
|
|
10
|
+
//
|
|
11
|
+
// Created by Luca Peruzzo on 12/09/22.
|
|
12
|
+
// Copyright © 2022 Capgo. All rights reserved.
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
enum DelayUntilNext: Decodable, Encodable, CustomStringConvertible {
|
|
17
|
+
case background
|
|
18
|
+
case kill
|
|
19
|
+
case nativeVersion
|
|
20
|
+
case date
|
|
21
|
+
|
|
22
|
+
var description: String {
|
|
23
|
+
switch self {
|
|
24
|
+
case .background: return "background"
|
|
25
|
+
case .kill: return "kill"
|
|
26
|
+
case .nativeVersion: return "nativeVersion"
|
|
27
|
+
case .date: return "date"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
//
|
|
8
|
+
// DelayUpdateUtils.swift
|
|
9
|
+
// Plugin
|
|
10
|
+
//
|
|
11
|
+
// Created by Auto-generated based on Android implementation
|
|
12
|
+
// Copyright © 2024 Capgo. All rights reserved.
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
import Version
|
|
17
|
+
|
|
18
|
+
public class DelayUpdateUtils {
|
|
19
|
+
|
|
20
|
+
// swiftlint:disable identifier_name
|
|
21
|
+
static let DELAY_CONDITION_PREFERENCES = "DELAY_CONDITION_PREFERENCES_CAPGO"
|
|
22
|
+
static let BACKGROUND_TIMESTAMP_KEY = "BACKGROUND_TIMESTAMP_KEY_CAPGO"
|
|
23
|
+
// swiftlint:enable identifier_name
|
|
24
|
+
private let logger: Logger
|
|
25
|
+
|
|
26
|
+
private let currentVersionNative: Version
|
|
27
|
+
|
|
28
|
+
public enum CancelDelaySource {
|
|
29
|
+
case killed
|
|
30
|
+
case background
|
|
31
|
+
case foreground
|
|
32
|
+
|
|
33
|
+
var description: String {
|
|
34
|
+
switch self {
|
|
35
|
+
case .killed: return "KILLED"
|
|
36
|
+
case .background: return "BACKGROUND"
|
|
37
|
+
case .foreground: return "FOREGROUND"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public init(currentVersionNative: Version, logger: Logger) {
|
|
43
|
+
self.currentVersionNative = currentVersionNative
|
|
44
|
+
self.logger = logger
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func checkCancelDelay(source: CancelDelaySource) {
|
|
48
|
+
let delayUpdatePreferences = UserDefaults.standard.string(
|
|
49
|
+
forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
|
|
50
|
+
let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).compactMap { obj in
|
|
51
|
+
guard let kind = obj.value(forKey: "kind") as? String else { return nil }
|
|
52
|
+
let value: String? = obj.value(forKey: "value") as? String
|
|
53
|
+
return DelayCondition(kind: kind, value: value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var delayConditionListToKeep: [DelayCondition] = []
|
|
57
|
+
var index = 0
|
|
58
|
+
|
|
59
|
+
for condition in delayConditionList {
|
|
60
|
+
let kind = condition.getKind()
|
|
61
|
+
let value = condition.getValue()
|
|
62
|
+
|
|
63
|
+
switch kind {
|
|
64
|
+
case "background":
|
|
65
|
+
if source == .foreground {
|
|
66
|
+
let backgroundedAt = getBackgroundTimestamp()
|
|
67
|
+
let now = Int64(Date().timeIntervalSince1970 * 1000) // Convert to milliseconds
|
|
68
|
+
let delta = max(0, now - backgroundedAt)
|
|
69
|
+
|
|
70
|
+
var longValue: Int64 = 0
|
|
71
|
+
if let value = value, !value.isEmpty {
|
|
72
|
+
longValue = Int64(value) ?? 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if delta > longValue {
|
|
76
|
+
// swiftlint:disable:next line_length
|
|
77
|
+
logger.info("Background condition (value: \(value ?? "")) deleted at index \(index). Delta: \(delta), longValue: \(longValue)")
|
|
78
|
+
} else {
|
|
79
|
+
delayConditionListToKeep.append(condition)
|
|
80
|
+
// swiftlint:disable:next line_length
|
|
81
|
+
logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
delayConditionListToKeep.append(condition)
|
|
85
|
+
// swiftlint:disable:next line_length
|
|
86
|
+
logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case "kill":
|
|
90
|
+
if source == .killed {
|
|
91
|
+
logger.info("Kill delay (value: \(value ?? "")) removed at index \(index) after app kill")
|
|
92
|
+
} else {
|
|
93
|
+
delayConditionListToKeep.append(condition)
|
|
94
|
+
// swiftlint:disable:next line_length
|
|
95
|
+
logger.info("Kill delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "date":
|
|
99
|
+
if let value = value, !value.isEmpty {
|
|
100
|
+
if let date = parseDateCondition(value) {
|
|
101
|
+
if Date() > date {
|
|
102
|
+
// swiftlint:disable:next line_length
|
|
103
|
+
logger.info("Date delay (value: \(value)) condition removed due to expired date at index \(index)")
|
|
104
|
+
} else {
|
|
105
|
+
delayConditionListToKeep.append(condition)
|
|
106
|
+
logger.info("Date delay (value: \(value)) kept at index \(index)")
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// swiftlint:disable:next line_length
|
|
110
|
+
logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index)")
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// swiftlint:disable:next line_length
|
|
114
|
+
logger.error("Date delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case "nativeVersion":
|
|
118
|
+
if let value = value, !value.isEmpty {
|
|
119
|
+
do {
|
|
120
|
+
let versionLimit = try Version(value)
|
|
121
|
+
if currentVersionNative >= versionLimit {
|
|
122
|
+
// swiftlint:disable:next line_length
|
|
123
|
+
logger.info("Native version delay (value: \(value)) condition removed due to above limit at index \(index)")
|
|
124
|
+
} else {
|
|
125
|
+
delayConditionListToKeep.append(condition)
|
|
126
|
+
logger.info("Native version delay (value: \(value)) kept at index \(index)")
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// swiftlint:disable:next line_length
|
|
130
|
+
logger.error("Native version delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// swiftlint:disable:next line_length
|
|
134
|
+
logger.error("Native version delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
logger.error("Unknown delay condition kind: \(kind) at index \(index)")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
index += 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if !delayConditionListToKeep.isEmpty {
|
|
145
|
+
let json = toJson(object: delayConditionListToKeep.map { $0.toJSON() })
|
|
146
|
+
_ = setMultiDelay(delayConditions: json)
|
|
147
|
+
} else {
|
|
148
|
+
// Clear all delay conditions if none are left to keep
|
|
149
|
+
_ = cancelDelay(source: "checkCancelDelay")
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public func setMultiDelay(delayConditions: String) -> Bool {
|
|
154
|
+
do {
|
|
155
|
+
UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
156
|
+
UserDefaults.standard.synchronize()
|
|
157
|
+
logger.info("Delay update saved")
|
|
158
|
+
return true
|
|
159
|
+
} catch {
|
|
160
|
+
logger.error("Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
|
|
166
|
+
do {
|
|
167
|
+
UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
168
|
+
UserDefaults.standard.synchronize()
|
|
169
|
+
logger.info("Background timestamp saved")
|
|
170
|
+
} catch {
|
|
171
|
+
logger.error("Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public func unsetBackgroundTimestamp() {
|
|
176
|
+
do {
|
|
177
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
178
|
+
UserDefaults.standard.synchronize()
|
|
179
|
+
logger.info("Background timestamp removed")
|
|
180
|
+
} catch {
|
|
181
|
+
// swiftlint:disable:next line_length
|
|
182
|
+
logger.error("Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func getBackgroundTimestamp() -> Int64 {
|
|
187
|
+
do {
|
|
188
|
+
let key = DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY
|
|
189
|
+
let timestamp = UserDefaults.standard.object(forKey: key) as? Int64 ?? 0
|
|
190
|
+
return timestamp
|
|
191
|
+
} catch {
|
|
192
|
+
logger.error("Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public func cancelDelay(source: String) -> Bool {
|
|
198
|
+
do {
|
|
199
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
200
|
+
UserDefaults.standard.synchronize()
|
|
201
|
+
logger.info("All delays canceled from \(source)")
|
|
202
|
+
return true
|
|
203
|
+
} catch {
|
|
204
|
+
logger.error("Failed to cancel update delay: \(error)")
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// MARK: - Helper methods
|
|
210
|
+
|
|
211
|
+
private func parseDateCondition(_ value: String) -> Date? {
|
|
212
|
+
let withFractionalSeconds = ISO8601DateFormatter()
|
|
213
|
+
withFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
214
|
+
if let date = withFractionalSeconds.date(from: value) {
|
|
215
|
+
return date
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let withoutFractionalSeconds = ISO8601DateFormatter()
|
|
219
|
+
withoutFractionalSeconds.formatOptions = [.withInternetDateTime]
|
|
220
|
+
if let date = withoutFractionalSeconds.date(from: value) {
|
|
221
|
+
return date
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Legacy fallback for strings without timezone.
|
|
225
|
+
for format in ["yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'HH:mm:ss"] {
|
|
226
|
+
let formatter = DateFormatter()
|
|
227
|
+
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
228
|
+
formatter.calendar = Calendar(identifier: .gregorian)
|
|
229
|
+
formatter.timeZone = .current
|
|
230
|
+
formatter.isLenient = false
|
|
231
|
+
formatter.dateFormat = format
|
|
232
|
+
if let date = formatter.date(from: value) {
|
|
233
|
+
return date
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return nil
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private func toJson(object: Any) -> String {
|
|
241
|
+
guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
|
|
242
|
+
return ""
|
|
243
|
+
}
|
|
244
|
+
return String(data: data, encoding: String.Encoding.utf8) ?? ""
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private func fromJsonArr(json: String) -> [NSObject] {
|
|
248
|
+
guard let jsonData = json.data(using: .utf8) else {
|
|
249
|
+
return []
|
|
250
|
+
}
|
|
251
|
+
let object = try? JSONSerialization.jsonObject(
|
|
252
|
+
with: jsonData,
|
|
253
|
+
options: .mutableContainers
|
|
254
|
+
) as? [NSObject]
|
|
255
|
+
return object ?? []
|
|
256
|
+
}
|
|
257
|
+
}
|