@capgo/capacitor-inappbrowser 8.21.1 → 8.22.0
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 +22 -12
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/CapgoInAppBrowserPlugin.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/FileInputCaptureChooserSupport.java +150 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +10 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCustomFullscreenSupport.java +4 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +87 -132
- package/dist/docs.json +20 -0
- package/dist/esm/definitions.d.ts +12 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +1 -1
- package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +148 -9
- package/package.json +1 -1
|
@@ -43,9 +43,33 @@ private enum DownloadReservationStore {
|
|
|
43
43
|
static let lock = NSLock()
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
enum BlobDownloadSupport {
|
|
47
47
|
static let maxLegacyBytes = 512 * 1024
|
|
48
48
|
static let chunkBytes = 64 * 1024
|
|
49
|
+
/// Each active chunked session keeps a file handle open until finish, abort or teardown.
|
|
50
|
+
static let maxActiveSessions = 4
|
|
51
|
+
|
|
52
|
+
static func canStartSession(activeSessionCount: Int) -> Bool {
|
|
53
|
+
activeSessionCount < maxActiveSessions
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// The declared blob size caps how many bytes a session may write, so a start without one is rejected.
|
|
57
|
+
static func expectedSize(from value: Any?) -> Int64? {
|
|
58
|
+
guard let size = (value as? NSNumber)?.int64Value, size >= 0 else {
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
return size
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Creates the empty destination file, then opens it for writing.
|
|
65
|
+
/// `FileHandle(forWritingTo:)` throws when the file does not exist yet, and
|
|
66
|
+
/// `uniqueDownloadDestination(for:)` only reserves the path without creating it.
|
|
67
|
+
static func openWriteHandle(at url: URL) throws -> FileHandle {
|
|
68
|
+
guard FileManager.default.createFile(atPath: url.path, contents: nil) else {
|
|
69
|
+
throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create blob download file"])
|
|
70
|
+
}
|
|
71
|
+
return try FileHandle(forWritingTo: url)
|
|
72
|
+
}
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
/// Script message handlers registered on every in-app browser WKWebView.
|
|
@@ -64,6 +88,7 @@ enum ScriptMessageHandlerSupport {
|
|
|
64
88
|
"blobDownloadChunk",
|
|
65
89
|
"blobDownloadFinish",
|
|
66
90
|
"blobDownloadAbort",
|
|
91
|
+
"blobDownloadRejectAck",
|
|
67
92
|
"takeScreenshot",
|
|
68
93
|
"consoleMessageHandler",
|
|
69
94
|
"magicPrint",
|
|
@@ -532,6 +557,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
532
557
|
private var lastInjectedSafeAreaInsets: InjectedSafeAreaInsets?
|
|
533
558
|
private var downloadStates: [ObjectIdentifier: WKDownloadState] = [:]
|
|
534
559
|
private var blobDownloadSessions: [String: BlobDownloadSession] = [:]
|
|
560
|
+
private var rejectedBlobDownloadSessionIds: Set<String> = []
|
|
535
561
|
private var previewItemURL: URL?
|
|
536
562
|
|
|
537
563
|
func setHeaders(headers: [String: String]) {
|
|
@@ -874,7 +900,46 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
874
900
|
}
|
|
875
901
|
}
|
|
876
902
|
|
|
877
|
-
|
|
903
|
+
private func abortAllBlobDownloadSessions() {
|
|
904
|
+
for session in blobDownloadSessions.values {
|
|
905
|
+
cleanupBlobDownloadSession(session, deleteFile: true)
|
|
906
|
+
}
|
|
907
|
+
blobDownloadSessions.removeAll()
|
|
908
|
+
rejectedBlobDownloadSessionIds.removeAll()
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private func signalBlobDownloadRejected(sessionId: String, reason: String) {
|
|
912
|
+
guard !sessionId.isEmpty else {
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
rejectedBlobDownloadSessionIds.insert(sessionId)
|
|
917
|
+
|
|
918
|
+
guard let webView else {
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
let escapedSessionId = sessionId
|
|
923
|
+
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
924
|
+
.replacingOccurrences(of: "'", with: "\\'")
|
|
925
|
+
let escapedReason = reason
|
|
926
|
+
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
927
|
+
.replacingOccurrences(of: "'", with: "\\'")
|
|
928
|
+
.replacingOccurrences(of: "\n", with: "\\n")
|
|
929
|
+
let script = """
|
|
930
|
+
(function() {
|
|
931
|
+
if (!window.__capgoBlobDownloadRejections) {
|
|
932
|
+
window.__capgoBlobDownloadRejections = {};
|
|
933
|
+
}
|
|
934
|
+
window.__capgoBlobDownloadRejections['\(escapedSessionId)'] = '\(escapedReason)';
|
|
935
|
+
})();
|
|
936
|
+
"""
|
|
937
|
+
|
|
938
|
+
DispatchQueue.main.async {
|
|
939
|
+
webView.evaluateJavaScript(script, completionHandler: nil)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
878
943
|
private func abortBlobDownloadSession(sessionId: String, deleteFile: Bool) -> BlobDownloadSession? {
|
|
879
944
|
guard let session = blobDownloadSessions.removeValue(forKey: sessionId) else {
|
|
880
945
|
return nil
|
|
@@ -938,6 +1003,8 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
938
1003
|
return
|
|
939
1004
|
}
|
|
940
1005
|
|
|
1006
|
+
var reservedDestinationURL: URL?
|
|
1007
|
+
var shouldSignalRejection = true
|
|
941
1008
|
do {
|
|
942
1009
|
guard let jsonPayload = parseBlobBridgePayload(payload) else {
|
|
943
1010
|
throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download start payload is missing"])
|
|
@@ -949,13 +1016,22 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
949
1016
|
}
|
|
950
1017
|
|
|
951
1018
|
if blobDownloadSessions[sessionId] != nil {
|
|
1019
|
+
shouldSignalRejection = false
|
|
952
1020
|
throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download session already exists"])
|
|
953
1021
|
}
|
|
954
1022
|
|
|
1023
|
+
guard BlobDownloadSupport.canStartSession(activeSessionCount: blobDownloadSessions.count) else {
|
|
1024
|
+
throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Too many active blob downloads"])
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
guard let expectedSize = BlobDownloadSupport.expectedSize(from: jsonPayload["size"]) else {
|
|
1028
|
+
throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download size is missing"])
|
|
1029
|
+
}
|
|
1030
|
+
|
|
955
1031
|
let fileName = blobDownloadFileName(from: jsonPayload)
|
|
956
1032
|
let destinationURL = try uniqueDownloadDestination(for: fileName)
|
|
957
|
-
|
|
958
|
-
let
|
|
1033
|
+
reservedDestinationURL = destinationURL
|
|
1034
|
+
let fileHandle = try BlobDownloadSupport.openWriteHandle(at: destinationURL)
|
|
959
1035
|
blobDownloadSessions[sessionId] = BlobDownloadSession(
|
|
960
1036
|
destinationURL: destinationURL,
|
|
961
1037
|
sourceURL: jsonPayload["sourceUrl"] as? String,
|
|
@@ -964,7 +1040,16 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
964
1040
|
expectedSize: expectedSize
|
|
965
1041
|
)
|
|
966
1042
|
} catch {
|
|
967
|
-
|
|
1043
|
+
let sessionId = (parseBlobBridgePayload(payload)?["sessionId"] as? String) ?? ""
|
|
1044
|
+
if let reservedDestinationURL {
|
|
1045
|
+
try? FileManager.default.removeItem(at: reservedDestinationURL)
|
|
1046
|
+
releaseDownloadDestination(reservedDestinationURL)
|
|
1047
|
+
}
|
|
1048
|
+
let reason = error.localizedDescription
|
|
1049
|
+
if !sessionId.isEmpty && shouldSignalRejection {
|
|
1050
|
+
signalBlobDownloadRejected(sessionId: sessionId, reason: reason)
|
|
1051
|
+
}
|
|
1052
|
+
emitDownloadFailed(sourceURL: nil, error: "Failed to start blob download: \(reason)")
|
|
968
1053
|
}
|
|
969
1054
|
}
|
|
970
1055
|
|
|
@@ -983,6 +1068,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
983
1068
|
let base64 = jsonPayload["base64"] as? String ?? ""
|
|
984
1069
|
guard !base64.isEmpty, let data = Data(base64Encoded: base64) else {
|
|
985
1070
|
abortBlobDownloadSession(sessionId: sessionId, deleteFile: true)
|
|
1071
|
+
signalBlobDownloadRejected(sessionId: sessionId, reason: "Failed to save blob download")
|
|
986
1072
|
emitDownloadFailed(sourceURL: session.sourceURL, error: "Failed to save blob download")
|
|
987
1073
|
return
|
|
988
1074
|
}
|
|
@@ -997,7 +1083,9 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
997
1083
|
blobDownloadSessions[sessionId] = session
|
|
998
1084
|
} catch {
|
|
999
1085
|
abortBlobDownloadSession(sessionId: sessionId, deleteFile: true)
|
|
1000
|
-
|
|
1086
|
+
let reason = error.localizedDescription
|
|
1087
|
+
signalBlobDownloadRejected(sessionId: sessionId, reason: reason)
|
|
1088
|
+
emitDownloadFailed(sourceURL: session.sourceURL, error: "Failed to save blob download: \(reason)")
|
|
1001
1089
|
}
|
|
1002
1090
|
}
|
|
1003
1091
|
|
|
@@ -1008,8 +1096,15 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1008
1096
|
|
|
1009
1097
|
guard let jsonPayload = parseBlobBridgePayload(payload),
|
|
1010
1098
|
let sessionId = jsonPayload["sessionId"] as? String,
|
|
1011
|
-
!sessionId.isEmpty
|
|
1012
|
-
|
|
1099
|
+
!sessionId.isEmpty else {
|
|
1100
|
+
emitDownloadFailed(sourceURL: nil, error: "Blob download session was not initialized")
|
|
1101
|
+
return
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
guard let session = blobDownloadSessions.removeValue(forKey: sessionId) else {
|
|
1105
|
+
if rejectedBlobDownloadSessionIds.remove(sessionId) != nil {
|
|
1106
|
+
return
|
|
1107
|
+
}
|
|
1013
1108
|
emitDownloadFailed(sourceURL: nil, error: "Blob download session was not initialized")
|
|
1014
1109
|
return
|
|
1015
1110
|
}
|
|
@@ -1029,6 +1124,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1029
1124
|
return
|
|
1030
1125
|
}
|
|
1031
1126
|
|
|
1127
|
+
releaseDownloadDestination(session.destinationURL)
|
|
1032
1128
|
previewDownloadedFile(session.destinationURL, mimeType: session.mimeType, sourceURL: session.sourceURL)
|
|
1033
1129
|
}
|
|
1034
1130
|
|
|
@@ -1049,6 +1145,15 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1049
1145
|
)
|
|
1050
1146
|
}
|
|
1051
1147
|
|
|
1148
|
+
private func acknowledgeBlobDownloadRejectionPayload(_ payload: Any) {
|
|
1149
|
+
guard let jsonPayload = parseBlobBridgePayload(payload),
|
|
1150
|
+
let sessionId = jsonPayload["sessionId"] as? String,
|
|
1151
|
+
!sessionId.isEmpty else {
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
rejectedBlobDownloadSessionIds.remove(sessionId)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1052
1157
|
private func handleBlobDownloadFromPage(blobUrl: String, mimeType: String?, contentDisposition: String?) {
|
|
1053
1158
|
guard handleDownloads, let webView else {
|
|
1054
1159
|
emitDownloadFailed(sourceURL: blobUrl, error: "Blob download requires an active WebView")
|
|
@@ -1092,6 +1197,18 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1092
1197
|
: null;
|
|
1093
1198
|
const sessionId = 'blob-download-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
1094
1199
|
let fileName = fallbackFileName;
|
|
1200
|
+
if (!window.__capgoBlobDownloadRejections) {
|
|
1201
|
+
window.__capgoBlobDownloadRejections = {};
|
|
1202
|
+
}
|
|
1203
|
+
const blobDownloadRejectionReason = function(id) {
|
|
1204
|
+
return window.__capgoBlobDownloadRejections && window.__capgoBlobDownloadRejections[id];
|
|
1205
|
+
};
|
|
1206
|
+
const ensureBlobDownloadNotRejected = function(id) {
|
|
1207
|
+
const reason = blobDownloadRejectionReason(id);
|
|
1208
|
+
if (reason) {
|
|
1209
|
+
throw new Error(String(reason));
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1095
1212
|
const readChunkAsBase64 = function(chunk) {
|
|
1096
1213
|
return new Promise(function(resolve, reject) {
|
|
1097
1214
|
const reader = new FileReader();
|
|
@@ -1123,10 +1240,15 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1123
1240
|
mimeType: blob.type || fallbackMimeType,
|
|
1124
1241
|
size: blob.size
|
|
1125
1242
|
}));
|
|
1243
|
+
await new Promise(function(resolve) { setTimeout(resolve, 0); });
|
|
1244
|
+
ensureBlobDownloadNotRejected(sessionId);
|
|
1126
1245
|
for (let offset = 0; offset < blob.size; offset += chunkSize) {
|
|
1246
|
+
ensureBlobDownloadNotRejected(sessionId);
|
|
1127
1247
|
const base64 = await readChunkAsBase64(blob.slice(offset, offset + chunkSize));
|
|
1248
|
+
ensureBlobDownloadNotRejected(sessionId);
|
|
1128
1249
|
bridge.appendBlobDownloadChunk(JSON.stringify({ sessionId: sessionId, base64: base64 }));
|
|
1129
1250
|
}
|
|
1251
|
+
ensureBlobDownloadNotRejected(sessionId);
|
|
1130
1252
|
bridge.finishBlobDownload(JSON.stringify({ sessionId: sessionId }));
|
|
1131
1253
|
return;
|
|
1132
1254
|
}
|
|
@@ -1144,13 +1266,22 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1144
1266
|
base64: base64
|
|
1145
1267
|
}));
|
|
1146
1268
|
})().catch(function(error) {
|
|
1269
|
+
const reason = String((error && error.message) || error || 'Blob download failed');
|
|
1270
|
+
if (blobDownloadRejectionReason(sessionId)) {
|
|
1271
|
+
delete window.__capgoBlobDownloadRejections[sessionId];
|
|
1272
|
+
if (bridge && bridge.acknowledgeBlobDownloadRejection) {
|
|
1273
|
+
bridge.acknowledgeBlobDownloadRejection(JSON.stringify({ sessionId: sessionId }));
|
|
1274
|
+
}
|
|
1275
|
+
console.error('Failed to capture blob download', error);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1147
1278
|
if (bridge && bridge.abortBlobDownload) {
|
|
1148
1279
|
bridge.abortBlobDownload(JSON.stringify({
|
|
1149
1280
|
sessionId: sessionId,
|
|
1150
1281
|
fileName: fileName,
|
|
1151
1282
|
sourceUrl: blobUrl,
|
|
1152
1283
|
mimeType: fallbackMimeType,
|
|
1153
|
-
reason:
|
|
1284
|
+
reason: reason
|
|
1154
1285
|
}));
|
|
1155
1286
|
}
|
|
1156
1287
|
console.error('Failed to capture blob download', error);
|
|
@@ -1440,6 +1571,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1440
1571
|
}
|
|
1441
1572
|
|
|
1442
1573
|
deinit {
|
|
1574
|
+
abortAllBlobDownloadSessions()
|
|
1443
1575
|
NotificationCenter.default.removeObserver(self)
|
|
1444
1576
|
webView?.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
|
|
1445
1577
|
if websiteTitleInNavigationBar {
|
|
@@ -1859,6 +1991,8 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
1859
1991
|
finishBlobDownloadPayload(message.body)
|
|
1860
1992
|
} else if message.name == "blobDownloadAbort" {
|
|
1861
1993
|
abortBlobDownloadPayload(message.body)
|
|
1994
|
+
} else if message.name == "blobDownloadRejectAck" {
|
|
1995
|
+
acknowledgeBlobDownloadRejectionPayload(message.body)
|
|
1862
1996
|
} else if message.name == "consoleMessageHandler" {
|
|
1863
1997
|
if let messageBody = message.body as? [String: Any] {
|
|
1864
1998
|
emit("consoleMessage", data: ConsoleMessageSupport.normalizePayload(from: messageBody))
|
|
@@ -2019,6 +2153,9 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
2019
2153
|
},
|
|
2020
2154
|
abortBlobDownload: function(payload) {
|
|
2021
2155
|
window.webkit.messageHandlers.blobDownloadAbort.postMessage(payload);
|
|
2156
|
+
},
|
|
2157
|
+
acknowledgeBlobDownloadRejection: function(payload) {
|
|
2158
|
+
window.webkit.messageHandlers.blobDownloadRejectAck.postMessage(payload);
|
|
2022
2159
|
}\(extraControls)\(screenshotControls)
|
|
2023
2160
|
});
|
|
2024
2161
|
if (!window.__capgoInAppBrowserWindowCloseInstalled) {
|
|
@@ -2155,6 +2292,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
2155
2292
|
userContentController.add(weakHandler, name: "blobDownloadChunk")
|
|
2156
2293
|
userContentController.add(weakHandler, name: "blobDownloadFinish")
|
|
2157
2294
|
userContentController.add(weakHandler, name: "blobDownloadAbort")
|
|
2295
|
+
userContentController.add(weakHandler, name: "blobDownloadRejectAck")
|
|
2158
2296
|
if allowScreenshotsFromWebPage {
|
|
2159
2297
|
userContentController.add(weakHandler, name: "takeScreenshot")
|
|
2160
2298
|
}
|
|
@@ -2667,6 +2805,7 @@ public extension WKWebViewController {
|
|
|
2667
2805
|
|
|
2668
2806
|
func cleanupWebView() {
|
|
2669
2807
|
setBrowserFullscreen(false)
|
|
2808
|
+
abortAllBlobDownloadSessions()
|
|
2670
2809
|
guard let webView = self.webView else { return }
|
|
2671
2810
|
webView.stopLoading()
|
|
2672
2811
|
previewItemURL = nil
|