@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,283 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os.log
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
public class Logger {
|
|
6
|
+
public enum LogLevel: Int {
|
|
7
|
+
case silent = 0
|
|
8
|
+
case error
|
|
9
|
+
case warn
|
|
10
|
+
case info
|
|
11
|
+
case debug
|
|
12
|
+
|
|
13
|
+
public static subscript(_ str: String) -> LogLevel? {
|
|
14
|
+
var index = 0
|
|
15
|
+
|
|
16
|
+
while let item = LogLevel(rawValue: index) {
|
|
17
|
+
if str == "\(item)" {
|
|
18
|
+
return item
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
index += 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return nil
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public func asOSLogType() -> OSLogType {
|
|
28
|
+
switch self {
|
|
29
|
+
case .debug:
|
|
30
|
+
return OSLogType.debug
|
|
31
|
+
case .info:
|
|
32
|
+
return OSLogType.info
|
|
33
|
+
case .warn:
|
|
34
|
+
return OSLogType.default
|
|
35
|
+
case .error:
|
|
36
|
+
return OSLogType.error
|
|
37
|
+
case .silent:
|
|
38
|
+
return OSLogType.info // Compiler wants this, it will never be used
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
public func asString() -> String {
|
|
42
|
+
switch self {
|
|
43
|
+
case .debug:
|
|
44
|
+
return "debug"
|
|
45
|
+
case .info:
|
|
46
|
+
return "info"
|
|
47
|
+
case .warn:
|
|
48
|
+
return "warn"
|
|
49
|
+
case .error:
|
|
50
|
+
return "error"
|
|
51
|
+
case .silent:
|
|
52
|
+
return "info"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public struct Options {
|
|
58
|
+
public var level: LogLevel
|
|
59
|
+
public var labels: [String: String]
|
|
60
|
+
public var useSyslog: Bool
|
|
61
|
+
|
|
62
|
+
public init(level: LogLevel = LogLevel.info, labels: [String: String] = [:], useSyslog: Bool = false) {
|
|
63
|
+
self.level = level
|
|
64
|
+
self.labels = labels
|
|
65
|
+
self.useSyslog = useSyslog
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private var _labels: [LogLevel: String] = [
|
|
70
|
+
LogLevel.silent: "",
|
|
71
|
+
LogLevel.error: "🔴",
|
|
72
|
+
LogLevel.warn: "🟠",
|
|
73
|
+
LogLevel.info: "🟢",
|
|
74
|
+
LogLevel.debug: "🔎"
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
public var labels: [String: String] {
|
|
78
|
+
get {
|
|
79
|
+
var result: [String: String] = [:]
|
|
80
|
+
|
|
81
|
+
for (level, label) in _labels {
|
|
82
|
+
result[String(describing: level)] = label
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
set {
|
|
89
|
+
for (level, label) in newValue {
|
|
90
|
+
if let logLevel = LogLevel[level] {
|
|
91
|
+
_labels[logLevel] = label
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public var level = LogLevel.info
|
|
98
|
+
|
|
99
|
+
public var levelName: String {
|
|
100
|
+
get {
|
|
101
|
+
String(describing: level)
|
|
102
|
+
}
|
|
103
|
+
set {
|
|
104
|
+
if let newLevel = LogLevel[newValue] {
|
|
105
|
+
level = newLevel
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private var _tag = ""
|
|
111
|
+
|
|
112
|
+
public var tag: String {
|
|
113
|
+
get {
|
|
114
|
+
_tag
|
|
115
|
+
}
|
|
116
|
+
set {
|
|
117
|
+
if !newValue.isEmpty {
|
|
118
|
+
_tag = newValue
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private let kDefaultTimerLabel = "default"
|
|
124
|
+
private var timers: [String: Date] = [:]
|
|
125
|
+
public var useSyslog = false
|
|
126
|
+
public var webView: WKWebView?
|
|
127
|
+
|
|
128
|
+
public func setWebView(webView: WKWebView) {
|
|
129
|
+
self.webView = webView
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public init(
|
|
133
|
+
withTag tag: String,
|
|
134
|
+
options: Options? = nil
|
|
135
|
+
) {
|
|
136
|
+
self.tag = tag
|
|
137
|
+
if let options = options {
|
|
138
|
+
self.level = options.level
|
|
139
|
+
self.labels = options.labels
|
|
140
|
+
self.useSyslog = options.useSyslog
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public func error(_ message: String) {
|
|
145
|
+
log(atLevel: LogLevel.error, message: message)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public func warn(_ message: String) {
|
|
149
|
+
log(atLevel: LogLevel.warn, message: message)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public func info(_ message: String) {
|
|
153
|
+
log(atLevel: LogLevel.info, message: message)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public func log(_ message: String) {
|
|
157
|
+
log(atLevel: LogLevel.info, message: message)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public func debug(_ message: String) {
|
|
161
|
+
log(atLevel: LogLevel.debug, message: message)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public func dir(_ value: Any?) {
|
|
165
|
+
// Check the log level here to avoid conversion to string
|
|
166
|
+
if canLog(atLevel: LogLevel.info) {
|
|
167
|
+
log(atLevel: LogLevel.info, message: String(describing: value))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public func log(atLevel level: LogLevel, message: String) {
|
|
172
|
+
// This will never fail, but we have to keep swift happy
|
|
173
|
+
if let label = _labels[level] {
|
|
174
|
+
log(atLevel: level, label: label, tag: tag, message: message)
|
|
175
|
+
if let webView = self.webView {
|
|
176
|
+
DispatchQueue.main.async {
|
|
177
|
+
let combined = "\(label) \(self.tag) : \(message)"
|
|
178
|
+
let jsArg = self.toJSStringLiteral(combined)
|
|
179
|
+
webView.evaluateJavaScript("console.\(level.asString())(\(jsArg))", completionHandler: nil)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func toJSStringLiteral(_ value: String) -> String {
|
|
186
|
+
// Prefer JSON encoding to produce a valid JS string literal
|
|
187
|
+
if let data = try? JSONEncoder().encode(value),
|
|
188
|
+
let encoded = String(data: data, encoding: .utf8) {
|
|
189
|
+
return encoded
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Fallback manual escaping (unlikely to be used)
|
|
193
|
+
var escaped = value
|
|
194
|
+
escaped = escaped.replacingOccurrences(of: "\\", with: "\\\\")
|
|
195
|
+
escaped = escaped.replacingOccurrences(of: "\"", with: "\\\"")
|
|
196
|
+
escaped = escaped.replacingOccurrences(of: "'", with: "\\'")
|
|
197
|
+
escaped = escaped.replacingOccurrences(of: "\n", with: "\\n")
|
|
198
|
+
escaped = escaped.replacingOccurrences(of: "\r", with: "\\r")
|
|
199
|
+
escaped = escaped.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
|
|
200
|
+
escaped = escaped.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
|
|
201
|
+
return "\"\(escaped)\""
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public func log(atLevel level: LogLevel, label: String?, tag: String, message: String) {
|
|
205
|
+
// This will never fail, but we have to keep swift happy
|
|
206
|
+
if let label = label ?? _labels[level] {
|
|
207
|
+
print(atLevel: level, label: label, tag: tag, message: message)
|
|
208
|
+
// eval
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private func canLog(atLevel level: LogLevel) -> Bool {
|
|
213
|
+
self.level.rawValue >= level.rawValue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private func print(atLevel level: LogLevel, label: String, tag: String, message: String) {
|
|
217
|
+
guard canLog(atLevel: level) else {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var msg = message
|
|
222
|
+
|
|
223
|
+
if !label.isEmpty {
|
|
224
|
+
// If the label is ASCII, put it after the tag, otherwise before.
|
|
225
|
+
if label[label.startIndex].isASCII {
|
|
226
|
+
msg = "[\(tag)] \(label): \(message)"
|
|
227
|
+
} else {
|
|
228
|
+
msg = "\(label) [\(tag)]: \(message)"
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
msg = "[\(tag)]: \(message)"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if useSyslog {
|
|
235
|
+
os_log("%{public}@", type: level.asOSLogType(), msg)
|
|
236
|
+
} else {
|
|
237
|
+
Swift.print(msg)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public func time(_ label: String?) {
|
|
242
|
+
timers[label ?? ""] = Date()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
public func timeLog(_ label: String?) {
|
|
246
|
+
if let timer = timers[label ?? ""] {
|
|
247
|
+
info(formatTimeInterval(timer.timeIntervalSinceNow))
|
|
248
|
+
} else {
|
|
249
|
+
warn("timer \(label ?? kDefaultTimerLabel) does not exist")
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
public func timeEnd(_ label: String?) {
|
|
254
|
+
timeLog(label)
|
|
255
|
+
timers.removeValue(forKey: label ?? kDefaultTimerLabel)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private func formatTimeInterval(_ interval: TimeInterval) -> String {
|
|
259
|
+
let int = Int(interval)
|
|
260
|
+
let millis = Int(((1 + interval.remainder(dividingBy: 1)) * 1000).rounded())
|
|
261
|
+
let seconds = int % 60
|
|
262
|
+
let minutes = (int / 60) % 60
|
|
263
|
+
let hours = (int / 3600)
|
|
264
|
+
|
|
265
|
+
if seconds < 1 {
|
|
266
|
+
return "\(millis)ms"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if minutes < 1 {
|
|
270
|
+
return "\(seconds).\(String(format: "%0.3d", millis))s"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if hours < 1 {
|
|
274
|
+
return "\(minutes):\(String(format: "%0.2d", seconds)).\(String(format: "%0.3d", millis)) (min:sec.ms)"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return "\(hours):\(String(format: "%0.2d", minutes)):\(String(format: "%0.2d", seconds)) (hr:min:sec)"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public func trace() {
|
|
281
|
+
info(String(format: "%@", Thread.callStackSymbols))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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 Security
|
|
9
|
+
import os.log
|
|
10
|
+
|
|
11
|
+
/// Capgo v2 uses Node's privateEncrypt with PKCS#1 v1.5 type-1 padding for
|
|
12
|
+
/// checksums and session keys. Recover the signed payload using Apple's RSA
|
|
13
|
+
/// implementation instead of custom modular exponentiation.
|
|
14
|
+
public struct RSAPublicKey {
|
|
15
|
+
private let key: SecKey
|
|
16
|
+
private static let keyLock = NSLock()
|
|
17
|
+
private static let log = OSLog(subsystem: "app.capgo.cordova.updater", category: "RSA")
|
|
18
|
+
// One public key only: bounded storage, invalid/rotated keys never fall back
|
|
19
|
+
// to a cached key with different material. SecKey is immutable after import.
|
|
20
|
+
private static var cachedKey: (der: Data, key: SecKey)?
|
|
21
|
+
|
|
22
|
+
public static func load(rsaPublicKey: String) -> RSAPublicKey? {
|
|
23
|
+
guard let der = pemToDer(rsaPublicKey) else {
|
|
24
|
+
os_log("RSA key import failed: invalid PEM/base64", log: Self.log, type: .error)
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
keyLock.lock()
|
|
29
|
+
defer { keyLock.unlock() }
|
|
30
|
+
if let cached = cachedKey, cached.der == der {
|
|
31
|
+
return RSAPublicKey(key: cached.key)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let attributes: [CFString: Any] = [
|
|
35
|
+
kSecAttrKeyType: kSecAttrKeyTypeRSA,
|
|
36
|
+
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
|
37
|
+
]
|
|
38
|
+
var error: Unmanaged<CFError>?
|
|
39
|
+
guard let key = SecKeyCreateWithData(der as CFData, attributes as CFDictionary, &error) else {
|
|
40
|
+
os_log(
|
|
41
|
+
"RSA key import failed: %{public}@",
|
|
42
|
+
log: Self.log,
|
|
43
|
+
type: .error,
|
|
44
|
+
String(describing: error?.takeRetainedValue())
|
|
45
|
+
)
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
guard CapgoRawRsa.supportsCapgoRecovery(key: key) else {
|
|
49
|
+
os_log(
|
|
50
|
+
"RSA key import failed: unsupported key (Capgo requires RSA-2048 with raw encryption)",
|
|
51
|
+
log: Self.log,
|
|
52
|
+
type: .error
|
|
53
|
+
)
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
56
|
+
cachedKey = (der, key)
|
|
57
|
+
return RSAPublicKey(key: key)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public func decrypt(data: Data) -> Data? {
|
|
61
|
+
let blockSize = SecKeyGetBlockSize(key)
|
|
62
|
+
guard data.count == blockSize else { return nil }
|
|
63
|
+
// Raw public-key encryption is the same c^e mod n operation as Node's
|
|
64
|
+
// publicDecrypt. Do NOT use encryption PKCS#1 padding here: the recovered
|
|
65
|
+
// block already contains signature (type-1) padding, checked below.
|
|
66
|
+
guard let recovered = CapgoRawRsa.recoverSignatureBlock(key: key, data: data) else {
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
return Self.unpadSignature(recovered, blockSize: blockSize)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static func unpadSignature(_ block: Data, blockSize: Int = 256) -> Data? {
|
|
73
|
+
let bytes = [UInt8](block)
|
|
74
|
+
// RFC 8017: 00 01, at least eight FF bytes, 00, then the payload.
|
|
75
|
+
guard bytes.count == blockSize, bytes[0] == 0, bytes[1] == 1 else { return nil }
|
|
76
|
+
var separator = 2
|
|
77
|
+
while separator < bytes.count && bytes[separator] == 0xff {
|
|
78
|
+
separator += 1
|
|
79
|
+
}
|
|
80
|
+
guard separator >= 10, separator < bytes.count - 1, bytes[separator] == 0 else {
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
83
|
+
return Data(bytes[(separator + 1)...])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static func pemToDer(_ pem: String) -> Data? {
|
|
87
|
+
let encoded = pem
|
|
88
|
+
.replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
|
|
89
|
+
.replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
|
|
90
|
+
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
91
|
+
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
92
|
+
.replacingOccurrences(of: "\\n", with: "")
|
|
93
|
+
.components(separatedBy: .whitespacesAndNewlines).joined()
|
|
94
|
+
return Data(base64Encoded: encoded)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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 UIKit
|
|
8
|
+
|
|
9
|
+
/// Cordova shake menu stub. Full preview/channel shake UI is planned for a follow-up release.
|
|
10
|
+
final class ShakeMenu {
|
|
11
|
+
private let logger: Logger
|
|
12
|
+
|
|
13
|
+
init(plugin: CordovaUpdaterPlugin, viewController: UIViewController?, logger: Logger, gesture: String) {
|
|
14
|
+
self.logger = logger
|
|
15
|
+
logger.debug("ShakeMenu: gesture \(gesture) registered (Cordova stub)")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func usesGesture(_ gesture: String) -> Bool {
|
|
19
|
+
false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func stop() {
|
|
23
|
+
// no-op
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
extension CordovaUpdaterPlugin {
|
|
28
|
+
func syncShakeMenuGestureRecognizer() {
|
|
29
|
+
// Cordova stub: gesture UI not implemented yet
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func removeShakeMenuGestureRecognizer() {
|
|
33
|
+
// no-op
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
extension UIApplication {
|
|
38
|
+
class func topViewController(_ base: UIViewController? = UIApplication.shared.connectedScenes
|
|
39
|
+
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
|
|
40
|
+
.first?.rootViewController) -> UIViewController? {
|
|
41
|
+
if let nav = base as? UINavigationController {
|
|
42
|
+
return topViewController(nav.visibleViewController)
|
|
43
|
+
}
|
|
44
|
+
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
|
|
45
|
+
return topViewController(selected)
|
|
46
|
+
}
|
|
47
|
+
if let presented = base?.presentedViewController {
|
|
48
|
+
return topViewController(presented)
|
|
49
|
+
}
|
|
50
|
+
return base
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
public class UpdaterSchemeHandler: NSObject {
|
|
5
|
+
private let activeBundleDirLock = NSLock()
|
|
6
|
+
private var _activeBundleDir: URL?
|
|
7
|
+
|
|
8
|
+
private let activeTasksQueue = DispatchQueue(label: "app.capgo.cordova.updater.schemeHandler")
|
|
9
|
+
private var activeTasks = Set<URLSchemeTaskWrapper>()
|
|
10
|
+
|
|
11
|
+
public var activeBundleDir: URL? {
|
|
12
|
+
get {
|
|
13
|
+
activeBundleDirLock.lock()
|
|
14
|
+
defer { activeBundleDirLock.unlock() }
|
|
15
|
+
return _activeBundleDir
|
|
16
|
+
}
|
|
17
|
+
set {
|
|
18
|
+
activeBundleDirLock.lock()
|
|
19
|
+
_activeBundleDir = newValue
|
|
20
|
+
activeBundleDirLock.unlock()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public func handle(task: WKURLSchemeTask) -> Bool {
|
|
25
|
+
guard let baseDir = activeBundleDir, let requestURL = task.request.url else {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
var relativePath = requestURL.path
|
|
29
|
+
if relativePath.hasPrefix("/") { relativePath = String(relativePath.dropFirst()) }
|
|
30
|
+
if relativePath.isEmpty || relativePath.hasSuffix("/") { relativePath += "index.html" }
|
|
31
|
+
let fileURL = baseDir.appendingPathComponent(relativePath).standardizedFileURL
|
|
32
|
+
let canonicalBase = baseDir.standardizedFileURL.path
|
|
33
|
+
guard fileURL.path == canonicalBase || fileURL.path.hasPrefix(canonicalBase + "/") else { return false }
|
|
34
|
+
var isDirectory: ObjCBool = false
|
|
35
|
+
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory), !isDirectory.boolValue else { return false }
|
|
36
|
+
let wrapper = URLSchemeTaskWrapper(task: task)
|
|
37
|
+
activeTasksQueue.sync { _ = activeTasks.insert(wrapper) }
|
|
38
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
39
|
+
self?.serve(task: task, wrapper: wrapper, fileURL: fileURL, requestURL: requestURL)
|
|
40
|
+
}
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public func stop(task: WKURLSchemeTask) {
|
|
45
|
+
activeTasksQueue.sync { activeTasks = activeTasks.filter { $0.task !== task } }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private func serve(task: WKURLSchemeTask, wrapper: URLSchemeTaskWrapper, fileURL: URL, requestURL: URL) {
|
|
49
|
+
guard let data = try? Data(contentsOf: fileURL) else {
|
|
50
|
+
if activeTasksQueue.sync(execute: { activeTasks.contains(wrapper) }) { task.didFailWithError(NSError(domain: "CapgoCordovaUpdater", code: 1)) }
|
|
51
|
+
activeTasksQueue.sync { _ = activeTasks.remove(wrapper) }
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
let headers = ["Content-Type": mimeType(for: fileURL), "Cache-Control": "no-cache", "Content-Length": String(data.count)]
|
|
55
|
+
guard let response = HTTPURLResponse(url: requestURL, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: headers) else { return }
|
|
56
|
+
if activeTasksQueue.sync(execute: { activeTasks.contains(wrapper) }) {
|
|
57
|
+
task.didReceive(response)
|
|
58
|
+
task.didReceive(data)
|
|
59
|
+
task.didFinish()
|
|
60
|
+
}
|
|
61
|
+
activeTasksQueue.sync { _ = activeTasks.remove(wrapper) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func mimeType(for url: URL) -> String {
|
|
65
|
+
switch url.pathExtension.lowercased() {
|
|
66
|
+
case "js", "mjs": return "application/javascript"
|
|
67
|
+
case "html", "htm": return "text/html"
|
|
68
|
+
case "css": return "text/css"
|
|
69
|
+
case "json": return "application/json"
|
|
70
|
+
case "wasm": return "application/wasm"
|
|
71
|
+
default: return "application/octet-stream"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private final class URLSchemeTaskWrapper: Hashable {
|
|
77
|
+
let task: WKURLSchemeTask
|
|
78
|
+
init(task: WKURLSchemeTask) { self.task = task }
|
|
79
|
+
static func == (lhs: URLSchemeTaskWrapper, rhs: URLSchemeTaskWrapper) -> Bool { lhs.task === rhs.task }
|
|
80
|
+
func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(task as AnyObject)) }
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
|
|
9
|
+
protocol ObjectSavable {
|
|
10
|
+
func setObj<Object>(_ object: Object, forKey: String) throws where Object: Encodable
|
|
11
|
+
func getObj<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
enum ObjectSavableError: String, LocalizedError {
|
|
15
|
+
case unableToEncode = "Unable to encode object into data"
|
|
16
|
+
case noValue = "No data object found for the given key"
|
|
17
|
+
case unableToDecode = "Unable to decode object into given type"
|
|
18
|
+
case checksum = "Checksum failed"
|
|
19
|
+
|
|
20
|
+
var errorDescription: String? {
|
|
21
|
+
rawValue
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
extension UserDefaults: ObjectSavable {
|
|
26
|
+
func setObj<Object>(_ object: Object, forKey: String) throws where Object: Encodable {
|
|
27
|
+
let encoder: JSONEncoder = JSONEncoder()
|
|
28
|
+
do {
|
|
29
|
+
let data: Data = try encoder.encode(object)
|
|
30
|
+
set(data, forKey: forKey)
|
|
31
|
+
} catch {
|
|
32
|
+
throw ObjectSavableError.unableToEncode
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func getObj<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable {
|
|
37
|
+
guard let data: Data = data(forKey: forKey) else { throw ObjectSavableError.noValue }
|
|
38
|
+
let decoder: JSONDecoder = JSONDecoder()
|
|
39
|
+
do {
|
|
40
|
+
let object: Object = try decoder.decode(type, from: data)
|
|
41
|
+
return object
|
|
42
|
+
} catch {
|
|
43
|
+
throw ObjectSavableError.unableToDecode
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|