@capgo/capacitor-inappbrowser 8.21.2 → 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.
@@ -43,9 +43,33 @@ private enum DownloadReservationStore {
43
43
  static let lock = NSLock()
44
44
  }
45
45
 
46
- private enum BlobDownloadSupport {
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
- @discardableResult
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
- let fileHandle = try FileHandle(forWritingTo: destinationURL)
958
- let expectedSize = (jsonPayload["size"] as? NSNumber)?.int64Value
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
- emitDownloadFailed(sourceURL: nil, error: "Failed to start blob download: \(error.localizedDescription)")
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
- emitDownloadFailed(sourceURL: session.sourceURL, error: "Failed to save blob download: \(error.localizedDescription)")
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
- let session = blobDownloadSessions.removeValue(forKey: sessionId) else {
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: String((error && error.message) || error || 'Blob download failed')
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-inappbrowser",
3
- "version": "8.21.2",
3
+ "version": "8.22.0",
4
4
  "description": "Capacitor plugin in app browser",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",