@capgo/capacitor-inappbrowser 8.21.2 → 8.22.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.
@@ -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);
@@ -1228,30 +1359,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
1228
1359
  navigationBar.backgroundColor = color
1229
1360
  navigationBar.barTintColor = color
1230
1361
  navigationBar.isTranslucent = false
1231
- navigationBar.setBackgroundImage(UIImage(), for: .default)
1232
- navigationBar.shadowImage = UIImage()
1233
- navigationBar.setValue(true, forKey: "hidesShadow")
1234
-
1235
- if #available(iOS 13.0, *) {
1236
- let appearance = UINavigationBarAppearance()
1237
- appearance.configureWithOpaqueBackground()
1238
- appearance.backgroundColor = color
1239
- appearance.backgroundEffect = nil
1240
- appearance.shadowColor = .clear
1241
- appearance.shadowImage = UIImage()
1242
-
1243
- if let titleTextAttributes = navigationBar.titleTextAttributes {
1244
- appearance.titleTextAttributes = titleTextAttributes
1245
- }
1246
- if let largeTitleTextAttributes = navigationBar.largeTitleTextAttributes {
1247
- appearance.largeTitleTextAttributes = largeTitleTextAttributes
1248
- }
1249
-
1250
- navigationBar.standardAppearance = appearance
1251
- navigationBar.compactAppearance = appearance
1252
- navigationBar.scrollEdgeAppearance = appearance
1253
- navigationBar.compactScrollEdgeAppearance = appearance
1254
- }
1362
+ NavigationBarShadowSupport.hideShadow(on: navigationBar, backgroundColor: color)
1255
1363
  }
1256
1364
 
1257
1365
  // Make status bar background with colored view underneath
@@ -1440,6 +1548,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
1440
1548
  }
1441
1549
 
1442
1550
  deinit {
1551
+ abortAllBlobDownloadSessions()
1443
1552
  NotificationCenter.default.removeObserver(self)
1444
1553
  webView?.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
1445
1554
  if websiteTitleInNavigationBar {
@@ -1859,6 +1968,8 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
1859
1968
  finishBlobDownloadPayload(message.body)
1860
1969
  } else if message.name == "blobDownloadAbort" {
1861
1970
  abortBlobDownloadPayload(message.body)
1971
+ } else if message.name == "blobDownloadRejectAck" {
1972
+ acknowledgeBlobDownloadRejectionPayload(message.body)
1862
1973
  } else if message.name == "consoleMessageHandler" {
1863
1974
  if let messageBody = message.body as? [String: Any] {
1864
1975
  emit("consoleMessage", data: ConsoleMessageSupport.normalizePayload(from: messageBody))
@@ -2019,6 +2130,9 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
2019
2130
  },
2020
2131
  abortBlobDownload: function(payload) {
2021
2132
  window.webkit.messageHandlers.blobDownloadAbort.postMessage(payload);
2133
+ },
2134
+ acknowledgeBlobDownloadRejection: function(payload) {
2135
+ window.webkit.messageHandlers.blobDownloadRejectAck.postMessage(payload);
2022
2136
  }\(extraControls)\(screenshotControls)
2023
2137
  });
2024
2138
  if (!window.__capgoInAppBrowserWindowCloseInstalled) {
@@ -2155,6 +2269,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
2155
2269
  userContentController.add(weakHandler, name: "blobDownloadChunk")
2156
2270
  userContentController.add(weakHandler, name: "blobDownloadFinish")
2157
2271
  userContentController.add(weakHandler, name: "blobDownloadAbort")
2272
+ userContentController.add(weakHandler, name: "blobDownloadRejectAck")
2158
2273
  if allowScreenshotsFromWebPage {
2159
2274
  userContentController.add(weakHandler, name: "takeScreenshot")
2160
2275
  }
@@ -2255,11 +2370,6 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
2255
2370
 
2256
2371
  let webView = WKWebView(frame: .zero, configuration: webConfiguration)
2257
2372
 
2258
- // if webView.responds(to: Selector(("setInspectable:"))) {
2259
- // // Fix: https://stackoverflow.com/questions/76216183/how-to-debug-wkwebview-in-ios-16-4-1-using-xcode-14-2/76603043#76603043
2260
- // webView.perform(Selector(("setInspectable:")), with: isInspectable)
2261
- // }
2262
-
2263
2373
  if #available(iOS 16.4, *) {
2264
2374
  webView.isInspectable = isInspectable
2265
2375
  }
@@ -2667,6 +2777,7 @@ public extension WKWebViewController {
2667
2777
 
2668
2778
  func cleanupWebView() {
2669
2779
  setBrowserFullscreen(false)
2780
+ abortAllBlobDownloadSessions()
2670
2781
  guard let webView = self.webView else { return }
2671
2782
  webView.stopLoading()
2672
2783
  previewItemURL = nil
@@ -3227,8 +3338,9 @@ fileprivate extension WKWebViewController {
3227
3338
 
3228
3339
  // Separated the actual sharing functionality
3229
3340
  private func showShareSheet(items: [Any], sender: AnyObject) {
3230
- let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
3231
- activityViewController.setValue(self.shareSubject ?? self.title, forKey: "subject")
3341
+ let subject = self.shareSubject ?? self.title
3342
+ let activityItems = items.map { ShareActivityItemSource(item: $0, subject: subject) }
3343
+ let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
3232
3344
  if let barButtonItem = sender as? UIBarButtonItem {
3233
3345
  activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
3234
3346
  }
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.1",
4
4
  "description": "Capacitor plugin in app browser",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",