@capgo/capacitor-inappbrowser 8.14.2 → 8.15.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 +23 -5
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/BundledAssetSupport.java +397 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/CapgoInAppBrowserPlugin.java +42 -4
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +39 -1
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/SafeAreaInsetsSupport.java +34 -48
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +462 -335
- package/dist/docs.json +17 -5
- package/dist/esm/bundled-asset-support.d.ts +40 -0
- package/dist/esm/bundled-asset-support.js +157 -0
- package/dist/esm/bundled-asset-support.js.map +1 -0
- package/dist/esm/definitions.d.ts +26 -4
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +43 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/plugin.cjs.js +199 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +199 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/InAppBrowserPlugin/BundledAssetSchemeHandler.swift +188 -0
- package/ios/Sources/InAppBrowserPlugin/BundledAssetSupport.swift +221 -0
- package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +26 -4
- package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +21 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -591,6 +591,24 @@ And in `AndroidManifest.xml`:
|
|
|
591
591
|
</activity>
|
|
592
592
|
```
|
|
593
593
|
|
|
594
|
+
### Bundled local assets
|
|
595
|
+
|
|
596
|
+
`openWebView()` can load HTML, JavaScript, CSS, and other files from the app `public/` folder without starting a local HTTP server. Pass a relative path and the plugin resolves it to the Capacitor local URL for the current platform.
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
import { InAppBrowser } from '@capgo/capacitor-inappbrowser';
|
|
600
|
+
|
|
601
|
+
await InAppBrowser.openWebView({
|
|
602
|
+
url: '/index.html',
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
- iOS resolves to `capacitor://localhost/...` and serves files from the packaged `public/` directory.
|
|
607
|
+
- Android resolves to `https://localhost/...` and intercepts requests with `WebViewAssetLoader`, matching Capacitor's main WebView behavior.
|
|
608
|
+
- Remote `http://` and `https://` URLs are unchanged.
|
|
609
|
+
|
|
610
|
+
`open()` uses the system browser (Safari / Custom Tabs) and still requires a real remote URL.
|
|
611
|
+
|
|
594
612
|
## API
|
|
595
613
|
|
|
596
614
|
<docgen-index>
|
|
@@ -1469,7 +1487,7 @@ See the README section "openSecureWindow (OAuth)" for full setup examples.
|
|
|
1469
1487
|
|
|
1470
1488
|
| Prop | Type | Description | Default | Since |
|
|
1471
1489
|
| ---------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
|
|
1472
|
-
| **`url`** | <code>string</code> | Target URL to load.
|
|
1490
|
+
| **`url`** | <code>string</code> | Target URL to load. Remote `http://` and `https://` URLs are loaded as-is. Relative bundled paths such as `/index.html` are not supported; use `openWebView()` instead. | | 0.1.0 |
|
|
1473
1491
|
| **`isPresentAfterPageLoad`** | <code>boolean</code> | if true, the browser will be presented after the page is loaded, if false, the browser will be presented immediately. | | 0.1.0 |
|
|
1474
1492
|
| **`preventDeeplink`** | <code>boolean</code> | if true the deeplink will not be opened, if false the deeplink will be opened when clicked on the link | | 0.1.0 |
|
|
1475
1493
|
| **`toolbarColor`** | <code>string</code> | Toolbar background color in hex format (e.g., "#1A1A2E"). Applied to both light and dark color schemes. Also sets the navigation bar color to match. **Android only** — ignored on iOS. | | 8.2.0 |
|
|
@@ -1556,7 +1574,7 @@ See the README section "openSecureWindow (OAuth)" for full setup examples.
|
|
|
1556
1574
|
|
|
1557
1575
|
| Prop | Type | Description | Default | Since |
|
|
1558
1576
|
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------ |
|
|
1559
|
-
| **`url`** | <code>string</code> | Target URL to load.
|
|
1577
|
+
| **`url`** | <code>string</code> | Target URL to load. Remote `http://` and `https://` URLs are loaded as-is. To open bundled web assets from the app bundle without running a local HTTP server, pass a relative path such as `/index.html` or `assets/page.html`. The plugin resolves it to the Capacitor local URL for the current platform (defaults: `capacitor://localhost/...` on iOS, `https://localhost/...` on Android; actual scheme and host follow the app's configured Capacitor local URL) and serves files from the packaged `public/` directory, or `www/` on iOS when `public/` is absent. | | 0.1.0 |
|
|
1560
1578
|
| **`headers`** | <code><a href="#headers">Headers</a></code> | <a href="#headers">Headers</a> to send with the request. | | 0.1.0 |
|
|
1561
1579
|
| **`customUserAgent`** | <code>string</code> | Custom User-Agent string for the webview. When set, replaces the system default webview User-Agent on iOS and Android. Takes precedence over a `User-Agent` entry in `headers`. | | 8.13.0 |
|
|
1562
1580
|
| **`credentials`** | <code><a href="#credentials">Credentials</a></code> | <a href="#credentials">Credentials</a> to send with the request and all subsequent requests for the same host. | | 6.1.0 |
|
|
@@ -1610,9 +1628,9 @@ See the README section "openSecureWindow (OAuth)" for full setup examples.
|
|
|
1610
1628
|
| **`preventDeeplink`** | <code>boolean</code> | If true, deeplinks and external app hand-off are blocked and stay in the webview. If false (default), custom schemes such as `tel:`, `mailto:`, and `sms:` open natively. On iOS, listing a custom scheme under `LSApplicationQueriesSchemes` is only required when you rely on `canOpenURL` for that scheme (for example `instagram://`). It is not required for HTTPS `authorizedAppLinks`, and `mailto`/`tel`/`sms` open without that Info.plist entry. | <code>false</code> | 0.1.0 |
|
|
1611
1629
|
| **`openBlankTargetInWebView`** | <code>boolean</code> | When true, HTTP and HTTPS links opened from `target="_blank"` anchors stay in the current webview instead of spawning a popup or opening in the system browser. By default, blank-target HTTP(S) links stay inside the plugin as managed popups that share cookies with the opener; enabling this option keeps everything in the same tab. Custom schemes such as `tel:` and `mailto:` and authorized app links still prefer their native handlers unless `preventDeeplink` is enabled. | <code>false</code> | 8.5.6 |
|
|
1612
1630
|
| **`authorizedAppLinks`** | <code>string[]</code> | List of base URLs whose hosts are treated as authorized App Links (Android) and Universal Links (iOS). - On both platforms, only HTTP(S) links whose host matches any entry in this list will attempt to leave the in-app browser for the native / system handler. - iOS tries a Universal Link first (`universalLinksOnly`), then falls back to a normal system open (App Store, Safari, etc.). Only if both fail does the URL stay in-webview. - Android uses an `ACTION_VIEW` intent for matching hosts. - Matching is host-based (case-insensitive), ignoring the "www." prefix. - HTTPS hosts do not need `LSApplicationQueriesSchemes`; that Info.plist key is for custom schemes like `instagram://`, not `https://instagram.com`. - When `preventDeeplink` is enabled, all external handling is blocked regardless of this list. | <code>[]</code> | 7.12.0 |
|
|
1613
|
-
| **`enabledSafeBottomMargin`** | <code>boolean</code> | If true, the webView
|
|
1614
|
-
| **`enabledSafeTopMargin`** | <code>boolean</code> | If false, the webView will extend behind the status bar for true full-screen immersive content. When true (default), respects the safe area at the top of the screen. Works independently of toolbarType - use for full-screen video players, games, or immersive web apps.
|
|
1615
|
-
| **`useTopInset`** | <code>boolean</code> | When true, applies the system status bar inset
|
|
1631
|
+
| **`enabledSafeBottomMargin`** | <code>boolean</code> | If true, the webView is inset by the bottom system bar (navigation bar) so bottom-anchored content stays reachable. On Android 15+ the browser window is always edge-to-edge, so the bottom inset is applied there regardless of this option. | <code>false</code> | 7.13.0 |
|
|
1632
|
+
| **`enabledSafeTopMargin`** | <code>boolean</code> | If false, the webView will extend behind the status bar for true full-screen immersive content. When true (default), respects the safe area at the top of the screen. Works independently of toolbarType - use for full-screen video players, games, or immersive web apps. On Android, when a toolbar is visible the toolbar itself provides that safe area; with `toolbarType: 'blank'` on Android 15+ the status bar inset is applied to the webView instead. | <code>true</code> | 8.2.0 |
|
|
1633
|
+
| **`useTopInset`** | <code>boolean</code> | When true, applies the system status bar inset to the top of the WebView on Android even when the window is not edge-to-edge (before Android 15). Keeps the legacy 0px inset by default for apps that handle padding themselves. On Android 15+ this is not needed: without a visible toolbar the status bar inset already follows `enabledSafeTopMargin`. | <code>false</code> | |
|
|
1616
1634
|
| **`enableGooglePaySupport`** | <code>boolean</code> | enableGooglePaySupport: if true, enables support for Google Pay popups and Payment Request API. This fixes OR_BIBED_15 errors by allowing popup windows and configuring Cross-Origin-Opener-Policy. Only enable this if you need Google Pay functionality as it allows popup windows. When enabled: - Allows popup windows for Google Pay authentication - Sets proper CORS headers for Payment Request API - Enables multiple window support in WebView - Configures secure context for payment processing | <code>false</code> | 7.13.0 |
|
|
1617
1635
|
| **`hiddenPopupWindow`** | <code>boolean</code> | Opens popup windows created by the page in hidden mode. Hidden popup windows can still be controlled with `executeScript`, `postMessage`, `show`, and `close`. Listen to `popupWindowOpened` to capture the popup id, then call `show({ id })` only if you want to reveal it. | <code>false</code> | 8.6.0 |
|
|
1618
1636
|
| **`blockedHosts`** | <code>string[]</code> | blockedHosts: List of host patterns that should be blocked from loading in the InAppBrowser's internal navigations. Any request inside WebView to a URL with a host matching any of these patterns will be blocked. Supports wildcard patterns like: - "*.example.com" to block all subdomains - "www.example.*" to block wildcard domain extensions | <code>[]</code> | 7.17.0 |
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
package ee.forgr.capacitor_inappbrowser;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.res.AssetManager;
|
|
5
|
+
import android.webkit.MimeTypeMap;
|
|
6
|
+
import android.webkit.WebResourceResponse;
|
|
7
|
+
import androidx.webkit.WebViewAssetLoader;
|
|
8
|
+
import com.getcapacitor.Bridge;
|
|
9
|
+
import java.io.IOException;
|
|
10
|
+
import java.io.InputStream;
|
|
11
|
+
import java.net.URI;
|
|
12
|
+
import java.util.Arrays;
|
|
13
|
+
import java.util.Collections;
|
|
14
|
+
import java.util.HashSet;
|
|
15
|
+
import java.util.Locale;
|
|
16
|
+
import java.util.Set;
|
|
17
|
+
|
|
18
|
+
final class BundledAssetSupport {
|
|
19
|
+
|
|
20
|
+
static final class LocalConfig {
|
|
21
|
+
|
|
22
|
+
final String scheme;
|
|
23
|
+
final String host;
|
|
24
|
+
|
|
25
|
+
LocalConfig(String scheme, String host) {
|
|
26
|
+
this.scheme = scheme;
|
|
27
|
+
this.host = host;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static final class Resolution {
|
|
32
|
+
|
|
33
|
+
final String url;
|
|
34
|
+
final boolean needsAssetLoader;
|
|
35
|
+
|
|
36
|
+
Resolution(String url) {
|
|
37
|
+
this(url, false);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Resolution(String url, boolean needsAssetLoader) {
|
|
41
|
+
this.url = url;
|
|
42
|
+
this.needsAssetLoader = needsAssetLoader;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private static final String[] RESERVED_WEB_SCHEMES = { "http", "https" };
|
|
47
|
+
private static final String PROXY_BRIDGE_MARKER_PATH = "/_capgo_proxy_";
|
|
48
|
+
|
|
49
|
+
private BundledAssetSupport() {}
|
|
50
|
+
|
|
51
|
+
static LocalConfig parseLocalConfig(String localUrl) {
|
|
52
|
+
if (isBlank(localUrl)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
URI uri = URI.create(localUrl.endsWith("/") ? localUrl : localUrl + "/");
|
|
58
|
+
String scheme = uri.getScheme();
|
|
59
|
+
String host = uri.getHost();
|
|
60
|
+
if (scheme == null || host == null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return new LocalConfig(scheme.toLowerCase(Locale.ROOT), host.toLowerCase(Locale.ROOT));
|
|
64
|
+
} catch (IllegalArgumentException error) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static String assetLoaderScheme(LocalConfig localConfig) {
|
|
70
|
+
if ("http".equals(localConfig.scheme) || "https".equals(localConfig.scheme)) {
|
|
71
|
+
return localConfig.scheme;
|
|
72
|
+
}
|
|
73
|
+
return "https";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static Resolution resolve(String url, Bridge bridge) {
|
|
77
|
+
LocalConfig localConfig = parseLocalConfig(bridge != null ? bridge.getLocalUrl() : null);
|
|
78
|
+
if (localConfig == null) {
|
|
79
|
+
localConfig = new LocalConfig("https", "localhost");
|
|
80
|
+
}
|
|
81
|
+
return resolve(url, localConfig);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static Resolution resolve(String url, LocalConfig localConfig) {
|
|
85
|
+
String trimmed = url == null ? "" : url.trim();
|
|
86
|
+
String navigationScheme = assetLoaderScheme(localConfig);
|
|
87
|
+
|
|
88
|
+
if (isRelativeBundledPath(trimmed)) {
|
|
89
|
+
String path = normalizeBundledPath(trimmed);
|
|
90
|
+
if (path == null) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return new Resolution(navigationScheme + "://" + localConfig.host + path, true);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isBundledLocalUrl(trimmed, localConfig)) {
|
|
97
|
+
String rewritten = rewriteBundledLocalUrl(trimmed, localConfig, navigationScheme);
|
|
98
|
+
return new Resolution(rewritten != null ? rewritten : trimmed, true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return new Resolution(trimmed, false);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static boolean isBundledLocalUrl(String url, LocalConfig localConfig) {
|
|
105
|
+
if (!isAbsoluteUrl(url)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
URI uri = URI.create(url);
|
|
111
|
+
String scheme = uri.getScheme();
|
|
112
|
+
String host = uri.getHost();
|
|
113
|
+
if (scheme == null || host == null) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!host.equalsIgnoreCase(localConfig.host)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (uri.getPort() != -1) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
String normalizedScheme = scheme.toLowerCase(Locale.ROOT);
|
|
126
|
+
if (normalizedScheme.equalsIgnoreCase(localConfig.scheme) || normalizedScheme.equals(assetLoaderScheme(localConfig))) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return isReservedWebScheme(normalizedScheme) && isReservedWebScheme(localConfig.scheme);
|
|
131
|
+
} catch (IllegalArgumentException error) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static boolean isRelativeBundledPath(String url) {
|
|
137
|
+
if (isBlank(url)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return !isAbsoluteUrl(url);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private static final Set<String> BUNDLED_ASSET_EXTENSIONS = Collections.unmodifiableSet(
|
|
144
|
+
new HashSet<>(
|
|
145
|
+
Arrays.asList(
|
|
146
|
+
"html",
|
|
147
|
+
"htm",
|
|
148
|
+
"js",
|
|
149
|
+
"css",
|
|
150
|
+
"json",
|
|
151
|
+
"xml",
|
|
152
|
+
"svg",
|
|
153
|
+
"png",
|
|
154
|
+
"jpg",
|
|
155
|
+
"jpeg",
|
|
156
|
+
"gif",
|
|
157
|
+
"webp",
|
|
158
|
+
"woff",
|
|
159
|
+
"woff2",
|
|
160
|
+
"ttf",
|
|
161
|
+
"map"
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
static boolean isLikelyBundledRelativePath(String url) {
|
|
167
|
+
if (!isRelativeBundledPath(url)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
String trimmed = stripPathQueryAndFragment(url.trim());
|
|
172
|
+
if (finalPathComponentStartsWithDot(trimmed)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (trimmed.startsWith("/") || trimmed.contains("/")) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
int dot = trimmed.lastIndexOf('.');
|
|
181
|
+
if (dot < 0) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (dot == 0) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
String extension = trimmed.substring(dot + 1).toLowerCase(Locale.ROOT);
|
|
189
|
+
return BUNDLED_ASSET_EXTENSIONS.contains(extension);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private static boolean isBlank(String value) {
|
|
193
|
+
if (value == null || value.isEmpty()) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (int index = 0; index < value.length(); index++) {
|
|
198
|
+
if (!Character.isWhitespace(value.charAt(index))) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private static boolean finalPathComponentStartsWithDot(String path) {
|
|
207
|
+
int lastSlash = path.lastIndexOf('/');
|
|
208
|
+
String finalComponent = lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
|
209
|
+
return !finalComponent.isEmpty() && finalComponent.charAt(0) == '.';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private static String stripPathQueryAndFragment(String path) {
|
|
213
|
+
int query = path.indexOf('?');
|
|
214
|
+
if (query >= 0) {
|
|
215
|
+
path = path.substring(0, query);
|
|
216
|
+
}
|
|
217
|
+
int fragment = path.indexOf('#');
|
|
218
|
+
if (fragment >= 0) {
|
|
219
|
+
path = path.substring(0, fragment);
|
|
220
|
+
}
|
|
221
|
+
return path;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static String normalizeBundledPath(String path) {
|
|
225
|
+
String trimmed = path == null ? "" : path.trim();
|
|
226
|
+
if (trimmed.isEmpty() || "/".equals(trimmed)) {
|
|
227
|
+
return "/";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (containsPathTraversal(trimmed)) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return trimmed.startsWith("/") ? trimmed : "/" + trimmed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static boolean isProxyBridgeMarkerPath(String path) {
|
|
238
|
+
return PROXY_BRIDGE_MARKER_PATH.equals(path);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static WebViewAssetLoader createAssetLoader(Context context, String hostname, String scheme) {
|
|
242
|
+
WebViewAssetLoader.Builder builder = new WebViewAssetLoader.Builder()
|
|
243
|
+
.setDomain(hostname)
|
|
244
|
+
.addPathHandler("/", new PublicAssetsPathHandler(context.getAssets()));
|
|
245
|
+
if ("http".equalsIgnoreCase(scheme)) {
|
|
246
|
+
builder.setHttpAllowed(true);
|
|
247
|
+
}
|
|
248
|
+
return builder.build();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static String mimeTypeForPath(String path) {
|
|
252
|
+
String extension = MimeTypeMap.getFileExtensionFromUrl(path);
|
|
253
|
+
if (extension != null && !extension.isEmpty()) {
|
|
254
|
+
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase(Locale.ROOT));
|
|
255
|
+
if (mimeType != null) {
|
|
256
|
+
return mimeType;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return extension == null || extension.isEmpty() ? "text/html" : "application/octet-stream";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
static String encodingForMimeType(String mimeType) {
|
|
263
|
+
if (mimeType == null || mimeType.isEmpty()) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
String normalized = mimeType.toLowerCase(Locale.ROOT);
|
|
268
|
+
if (normalized.startsWith("text/")) {
|
|
269
|
+
return "utf-8";
|
|
270
|
+
}
|
|
271
|
+
if (
|
|
272
|
+
normalized.contains("javascript") ||
|
|
273
|
+
normalized.contains("json") ||
|
|
274
|
+
normalized.contains("xml") ||
|
|
275
|
+
normalized.endsWith("+json") ||
|
|
276
|
+
normalized.endsWith("+xml")
|
|
277
|
+
) {
|
|
278
|
+
return "utf-8";
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private static WebResourceResponse notFoundResponse(String path) {
|
|
284
|
+
return new WebResourceResponse("text/plain", "utf-8", 404, "Not Found", null, new java.io.ByteArrayInputStream(new byte[0]));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private static boolean isReservedWebScheme(String scheme) {
|
|
288
|
+
for (String reservedScheme : RESERVED_WEB_SCHEMES) {
|
|
289
|
+
if (reservedScheme.equals(scheme)) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private static boolean isAbsoluteUrl(String url) {
|
|
297
|
+
if (url.startsWith("//")) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
int separatorIndex = url.indexOf(':');
|
|
302
|
+
if (separatorIndex <= 0) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (int index = 0; index < separatorIndex; index++) {
|
|
307
|
+
char character = url.charAt(index);
|
|
308
|
+
if (!Character.isLetterOrDigit(character) && character != '+' && character != '-' && character != '.') {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Character.isLetter(url.charAt(0));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private static boolean containsPathTraversal(String path) {
|
|
317
|
+
for (String segment : path.split("/")) {
|
|
318
|
+
if ("..".equals(segment)) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private static String rewriteBundledLocalUrl(String url, LocalConfig localConfig, String navigationScheme) {
|
|
326
|
+
try {
|
|
327
|
+
URI uri = URI.create(url);
|
|
328
|
+
String scheme = uri.getScheme();
|
|
329
|
+
String host = uri.getHost();
|
|
330
|
+
if (scheme == null || host == null || !host.equalsIgnoreCase(localConfig.host)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
if (scheme.equalsIgnoreCase(navigationScheme)) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return new URI(
|
|
337
|
+
navigationScheme,
|
|
338
|
+
uri.getUserInfo(),
|
|
339
|
+
host,
|
|
340
|
+
uri.getPort(),
|
|
341
|
+
uri.getPath(),
|
|
342
|
+
uri.getQuery(),
|
|
343
|
+
uri.getFragment()
|
|
344
|
+
).toString();
|
|
345
|
+
} catch (Exception error) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private static String mapToAssetPath(String path) {
|
|
351
|
+
if (containsPathTraversal(path)) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
String normalizedPath = path == null || path.isEmpty() ? "/" : path;
|
|
356
|
+
if (!normalizedPath.startsWith("/")) {
|
|
357
|
+
normalizedPath = "/" + normalizedPath;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
int lastSlash = normalizedPath.lastIndexOf('/');
|
|
361
|
+
String fileName = lastSlash >= 0 ? normalizedPath.substring(lastSlash + 1) : normalizedPath;
|
|
362
|
+
if (!fileName.contains(".")) {
|
|
363
|
+
return "public/index.html";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return "public" + normalizedPath;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private static final class PublicAssetsPathHandler implements WebViewAssetLoader.PathHandler {
|
|
370
|
+
|
|
371
|
+
private final AssetManager assetManager;
|
|
372
|
+
|
|
373
|
+
PublicAssetsPathHandler(AssetManager assetManager) {
|
|
374
|
+
this.assetManager = assetManager;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@Override
|
|
378
|
+
public WebResourceResponse handle(String path) {
|
|
379
|
+
if (isProxyBridgeMarkerPath(path)) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
String assetPath = mapToAssetPath(path);
|
|
384
|
+
if (assetPath == null) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
InputStream stream = assetManager.open(assetPath);
|
|
390
|
+
String mimeType = mimeTypeForPath(path);
|
|
391
|
+
return new WebResourceResponse(mimeType, encodingForMimeType(mimeType), stream);
|
|
392
|
+
} catch (IOException error) {
|
|
393
|
+
return notFoundResponse(path);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -32,6 +32,7 @@ import androidx.browser.customtabs.CustomTabsIntent;
|
|
|
32
32
|
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
|
33
33
|
import androidx.browser.customtabs.CustomTabsSession;
|
|
34
34
|
import androidx.core.content.ContextCompat;
|
|
35
|
+
import com.getcapacitor.Bridge;
|
|
35
36
|
import com.getcapacitor.JSArray;
|
|
36
37
|
import com.getcapacitor.JSObject;
|
|
37
38
|
import com.getcapacitor.Plugin;
|
|
@@ -358,6 +359,26 @@ public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.Per
|
|
|
358
359
|
return dialog;
|
|
359
360
|
}
|
|
360
361
|
|
|
362
|
+
private boolean applyBundledAssetOptions(Options options, String url) {
|
|
363
|
+
Bridge bridge = getBridge();
|
|
364
|
+
BundledAssetSupport.Resolution resolution = BundledAssetSupport.resolve(url, bridge);
|
|
365
|
+
if (resolution == null) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
options.setUrl(resolution.url);
|
|
369
|
+
if (resolution.needsAssetLoader) {
|
|
370
|
+
BundledAssetSupport.LocalConfig localConfig = BundledAssetSupport.parseLocalConfig(
|
|
371
|
+
bridge != null ? bridge.getLocalUrl() : null
|
|
372
|
+
);
|
|
373
|
+
options.setBundledAssetHost(localConfig != null ? localConfig.host : "localhost");
|
|
374
|
+
options.setBundledAssetScheme(localConfig != null ? BundledAssetSupport.assetLoaderScheme(localConfig) : "https");
|
|
375
|
+
options.setServeBundledAssets(true);
|
|
376
|
+
} else {
|
|
377
|
+
options.setServeBundledAssets(false);
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
361
382
|
private void notifyPopupWindowOpened(String popupId, String parentId, String popupUrl, boolean visible) {
|
|
362
383
|
JSObject event = new JSObject();
|
|
363
384
|
event.put("id", popupId);
|
|
@@ -752,13 +773,21 @@ public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.Per
|
|
|
752
773
|
return;
|
|
753
774
|
}
|
|
754
775
|
|
|
755
|
-
|
|
776
|
+
BundledAssetSupport.Resolution resolution = BundledAssetSupport.resolve(url, getBridge());
|
|
777
|
+
if (resolution == null) {
|
|
778
|
+
call.reject("Invalid bundled asset path");
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
final String resolvedUrl = resolution.url;
|
|
782
|
+
currentUrl = resolvedUrl;
|
|
783
|
+
final String localUrl = getBridge() != null ? getBridge().getLocalUrl() : null;
|
|
756
784
|
this.getActivity().runOnUiThread(
|
|
757
785
|
new Runnable() {
|
|
758
786
|
@Override
|
|
759
787
|
public void run() {
|
|
760
788
|
try {
|
|
761
|
-
webViewDialog.
|
|
789
|
+
webViewDialog.applyBundledAssetResolution(resolution, localUrl);
|
|
790
|
+
webViewDialog.setUrl(resolvedUrl);
|
|
762
791
|
call.resolve();
|
|
763
792
|
} catch (Exception e) {
|
|
764
793
|
Log.e("InAppBrowser", "Error setting URL: " + e.getMessage());
|
|
@@ -785,6 +814,11 @@ public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.Per
|
|
|
785
814
|
call.reject("Invalid URL");
|
|
786
815
|
return;
|
|
787
816
|
}
|
|
817
|
+
|
|
818
|
+
if (BundledAssetSupport.isLikelyBundledRelativePath(url)) {
|
|
819
|
+
call.reject("Bundled assets require openWebView()");
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
788
822
|
currentUrl = url;
|
|
789
823
|
awaitCustomTabsClientIfNeeded();
|
|
790
824
|
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(getCustomTabsSession());
|
|
@@ -1054,11 +1088,15 @@ public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.Per
|
|
|
1054
1088
|
String url = call.getString("url");
|
|
1055
1089
|
if (url == null || TextUtils.isEmpty(url)) {
|
|
1056
1090
|
call.reject("Invalid URL");
|
|
1091
|
+
return;
|
|
1057
1092
|
}
|
|
1058
|
-
currentUrl = url;
|
|
1059
1093
|
final String webViewId = UUID.randomUUID().toString();
|
|
1060
1094
|
final Options options = new Options();
|
|
1061
|
-
options
|
|
1095
|
+
if (!applyBundledAssetOptions(options, url)) {
|
|
1096
|
+
call.reject("Invalid bundled asset path");
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
currentUrl = options.getUrl();
|
|
1062
1100
|
options.setHeaders(call.getObject("headers"));
|
|
1063
1101
|
options.setCustomUserAgent(call.getString("customUserAgent"));
|
|
1064
1102
|
options.setCredentials(call.getObject("credentials"));
|
|
@@ -8,7 +8,7 @@ import java.io.IOException;
|
|
|
8
8
|
import java.io.InputStream;
|
|
9
9
|
import java.util.ArrayList;
|
|
10
10
|
import java.util.List;
|
|
11
|
-
import java.util.
|
|
11
|
+
import java.util.Locale;
|
|
12
12
|
import java.util.Objects;
|
|
13
13
|
import java.util.regex.Pattern;
|
|
14
14
|
|
|
@@ -151,6 +151,9 @@ public class Options {
|
|
|
151
151
|
private String CloseModalOk;
|
|
152
152
|
private Pattern closeModalURLPattern;
|
|
153
153
|
private String url;
|
|
154
|
+
private volatile String bundledAssetHost = "localhost";
|
|
155
|
+
private volatile String bundledAssetScheme = "https";
|
|
156
|
+
private volatile boolean serveBundledAssets = false;
|
|
154
157
|
private JSObject headers;
|
|
155
158
|
private String customUserAgent;
|
|
156
159
|
private JSObject credentials;
|
|
@@ -493,6 +496,38 @@ public class Options {
|
|
|
493
496
|
this.url = url;
|
|
494
497
|
}
|
|
495
498
|
|
|
499
|
+
public String getBundledAssetHost() {
|
|
500
|
+
return bundledAssetHost;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
public void setBundledAssetHost(String bundledAssetHost) {
|
|
504
|
+
if (bundledAssetHost == null || bundledAssetHost.isBlank()) {
|
|
505
|
+
this.bundledAssetHost = "localhost";
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.bundledAssetHost = bundledAssetHost;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
public String getBundledAssetScheme() {
|
|
512
|
+
return bundledAssetScheme;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
public void setBundledAssetScheme(String bundledAssetScheme) {
|
|
516
|
+
if (bundledAssetScheme == null || bundledAssetScheme.isBlank()) {
|
|
517
|
+
this.bundledAssetScheme = "https";
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
this.bundledAssetScheme = bundledAssetScheme.toLowerCase(Locale.ROOT);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
public boolean getServeBundledAssets() {
|
|
524
|
+
return serveBundledAssets;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
public void setServeBundledAssets(boolean serveBundledAssets) {
|
|
528
|
+
this.serveBundledAssets = serveBundledAssets;
|
|
529
|
+
}
|
|
530
|
+
|
|
496
531
|
public JSObject getHeaders() {
|
|
497
532
|
return headers;
|
|
498
533
|
}
|
|
@@ -784,6 +819,9 @@ public class Options {
|
|
|
784
819
|
copy.setTitleFontFamily(titleFontFamily);
|
|
785
820
|
copy.setTitleIcon(titleIcon);
|
|
786
821
|
copy.setUrl("about:blank");
|
|
822
|
+
copy.setBundledAssetHost(bundledAssetHost);
|
|
823
|
+
copy.setBundledAssetScheme(bundledAssetScheme);
|
|
824
|
+
copy.setServeBundledAssets(serveBundledAssets);
|
|
787
825
|
copy.setHeaders(headers);
|
|
788
826
|
copy.setCustomUserAgent(customUserAgent);
|
|
789
827
|
copy.setCredentials(credentials);
|