@javascriptcommon/react-native-webview 13.12.21 → 13.17.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/android/gradle.properties +1 -1
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/AndroidManifestNew.xml +13 -0
- package/android/src/main/assets/contentfilter/rules.txt +57751 -0
- package/android/src/main/java/com/reactnativecommunity/webview/ContentFilter.kt +85 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +76 -25
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java +168 -25
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +15 -7
- package/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt +25 -0
- package/android/src/main/jniLibs/arm64-v8a/libcontentfilter.so +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libcontentfilter.so +0 -0
- package/android/src/main/jniLibs/x86_64/libcontentfilter.so +0 -0
- package/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +12 -1
- package/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +7 -0
- package/apple/RNCWebView.h +1 -1
- package/apple/RNCWebView.mm +26 -17
- package/apple/RNCWebViewImpl.h +2 -1
- package/apple/RNCWebViewImpl.m +62 -25
- package/apple/RNCWebViewManager.mm +4 -0
- package/index.d.ts +2 -3
- package/lib/RNCWebViewNativeComponent.d.ts +24 -21
- package/lib/RNCWebViewNativeComponent.js +1 -1
- package/lib/WebView.android.js +1 -1
- package/lib/WebViewShared.d.ts +3 -1
- package/lib/WebViewShared.js +1 -1
- package/lib/WebViewTypes.d.ts +32 -8
- package/package.json +9 -1
- package/src/RNCWebViewNativeComponent.ts +27 -21
- package/src/WebView.android.tsx +4 -0
- package/src/WebViewShared.tsx +10 -0
- package/src/WebViewTypes.ts +36 -8
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.reactnativecommunity.webview
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Kotlin bridge to the native content-filtering engine (`libcontentfilter.so`).
|
|
8
|
+
*
|
|
9
|
+
* The engine is a single, process-wide, immutable rule set. It is initialised
|
|
10
|
+
* once, lazily, on a background thread from a bundled rule-list text asset.
|
|
11
|
+
* Everything fails OPEN: if the library cannot be loaded, the asset is missing,
|
|
12
|
+
* or the engine is not ready yet, [shouldBlock] returns false and requests load
|
|
13
|
+
* normally.
|
|
14
|
+
*
|
|
15
|
+
* Matching itself (`nativeCheck`) is serialized behind a Mutex on the native
|
|
16
|
+
* side and costs microseconds, so it is safe to call from WebView worker
|
|
17
|
+
* threads.
|
|
18
|
+
*/
|
|
19
|
+
object ContentFilter {
|
|
20
|
+
private const val TAG = "ContentFilter"
|
|
21
|
+
private const val LIBRARY = "contentfilter"
|
|
22
|
+
|
|
23
|
+
// Bundled rule list (ABP/EasyList syntax), merged from the library assets.
|
|
24
|
+
private const val RULES_ASSET = "contentfilter/rules.txt"
|
|
25
|
+
|
|
26
|
+
@Volatile private var libraryLoaded = false
|
|
27
|
+
@Volatile private var initStarted = false
|
|
28
|
+
@Volatile private var ready = false
|
|
29
|
+
|
|
30
|
+
init {
|
|
31
|
+
try {
|
|
32
|
+
System.loadLibrary(LIBRARY)
|
|
33
|
+
libraryLoaded = true
|
|
34
|
+
} catch (t: Throwable) {
|
|
35
|
+
Log.e(TAG, "Could not load native library '$LIBRARY'; filtering disabled.", t)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@JvmStatic external fun nativeInit(rules: String): Boolean
|
|
40
|
+
@JvmStatic external fun nativeIsReady(): Boolean
|
|
41
|
+
@JvmStatic external fun nativeCheck(url: String, sourceUrl: String, requestType: String): Boolean
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Kick off a one-time background load of the bundled rule list. Safe to call
|
|
45
|
+
* repeatedly and from any thread; only the first call does work.
|
|
46
|
+
*/
|
|
47
|
+
fun ensureInitialized(context: Context) {
|
|
48
|
+
if (!libraryLoaded || initStarted) return
|
|
49
|
+
synchronized(this) {
|
|
50
|
+
if (initStarted) return
|
|
51
|
+
initStarted = true
|
|
52
|
+
}
|
|
53
|
+
val appContext = context.applicationContext
|
|
54
|
+
Thread({
|
|
55
|
+
try {
|
|
56
|
+
val rules = appContext.assets.open(RULES_ASSET)
|
|
57
|
+
.bufferedReader()
|
|
58
|
+
.use { it.readText() }
|
|
59
|
+
val ok = nativeInit(rules)
|
|
60
|
+
ready = ok && nativeIsReady()
|
|
61
|
+
Log.d(TAG, "content filter initialized (ready=$ready)")
|
|
62
|
+
} catch (t: Throwable) {
|
|
63
|
+
Log.e(TAG, "content filter initialization failed; filtering disabled.", t)
|
|
64
|
+
}
|
|
65
|
+
}, "content-filter-init").start()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun isReady(): Boolean = libraryLoaded && ready
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @return true when the request should be BLOCKED. Fails open.
|
|
72
|
+
* @param url full request URL
|
|
73
|
+
* @param sourceUrl full top-frame (document) URL, or null/empty if unknown
|
|
74
|
+
* @param requestType request-type string ("script", "image", "sub_frame",
|
|
75
|
+
* "xmlhttprequest", "font", "media", ...)
|
|
76
|
+
*/
|
|
77
|
+
fun shouldBlock(url: String, sourceUrl: String?, requestType: String): Boolean {
|
|
78
|
+
if (!isReady()) return false
|
|
79
|
+
return try {
|
|
80
|
+
nativeCheck(url, sourceUrl ?: "", requestType)
|
|
81
|
+
} catch (t: Throwable) {
|
|
82
|
+
false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -2,6 +2,7 @@ package com.reactnativecommunity.webview;
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint;
|
|
4
4
|
import android.graphics.Rect;
|
|
5
|
+
import android.net.Uri;
|
|
5
6
|
import android.text.TextUtils;
|
|
6
7
|
import android.view.ActionMode;
|
|
7
8
|
import android.view.Menu;
|
|
@@ -14,7 +15,12 @@ import android.webkit.WebChromeClient;
|
|
|
14
15
|
import android.webkit.WebView;
|
|
15
16
|
import android.webkit.WebViewClient;
|
|
16
17
|
|
|
18
|
+
import androidx.annotation.NonNull;
|
|
17
19
|
import androidx.annotation.Nullable;
|
|
20
|
+
import androidx.webkit.JavaScriptReplyProxy;
|
|
21
|
+
import androidx.webkit.WebMessageCompat;
|
|
22
|
+
import androidx.webkit.WebViewCompat;
|
|
23
|
+
import androidx.webkit.WebViewFeature;
|
|
18
24
|
|
|
19
25
|
import com.facebook.common.logging.FLog;
|
|
20
26
|
import com.facebook.react.bridge.Arguments;
|
|
@@ -38,6 +44,7 @@ import org.json.JSONObject;
|
|
|
38
44
|
|
|
39
45
|
import java.util.List;
|
|
40
46
|
import java.util.Map;
|
|
47
|
+
import java.util.Set;
|
|
41
48
|
|
|
42
49
|
public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
43
50
|
protected @Nullable
|
|
@@ -46,7 +53,9 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
46
53
|
String injectedJSBeforeContentLoaded;
|
|
47
54
|
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
|
|
48
55
|
protected @Nullable
|
|
49
|
-
RNCWebViewBridge
|
|
56
|
+
RNCWebViewBridge fallbackBridge;
|
|
57
|
+
protected @Nullable
|
|
58
|
+
WebViewCompat.WebMessageListener bridgeListener = null;
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
61
|
* android.webkit.WebChromeClient fundamentally does not support JS injection into frames other
|
|
@@ -68,6 +77,30 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
68
77
|
protected boolean nestedScrollEnabled = false;
|
|
69
78
|
protected ProgressChangedFilter progressChangedFilter;
|
|
70
79
|
|
|
80
|
+
// Content filter: when enabled, RNCWebViewClient.shouldInterceptRequest
|
|
81
|
+
// filters subresource requests through the native content filter.
|
|
82
|
+
protected volatile boolean contentBlockerEnabled = false;
|
|
83
|
+
// Top-frame URL, captured on the UI thread in RNCWebViewClient callbacks and
|
|
84
|
+
// read from shouldInterceptRequest (which runs off the UI thread). `volatile`
|
|
85
|
+
// gives the cross-thread visibility; do NOT call getUrl() off the UI thread.
|
|
86
|
+
protected volatile String lastTopUrl = null;
|
|
87
|
+
|
|
88
|
+
public void setContentBlockerEnabled(boolean enabled) {
|
|
89
|
+
this.contentBlockerEnabled = enabled;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public boolean getContentBlockerEnabled() {
|
|
93
|
+
return this.contentBlockerEnabled;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public void setLastTopUrl(String url) {
|
|
97
|
+
this.lastTopUrl = url;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public String getLastTopUrl() {
|
|
101
|
+
return this.lastTopUrl;
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
/**
|
|
72
105
|
* WebView must be created with an context of the current activity
|
|
73
106
|
* <p>
|
|
@@ -81,10 +114,6 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
81
114
|
progressChangedFilter = new ProgressChangedFilter();
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
public void setIgnoreErrFailedForThisURL(String url) {
|
|
85
|
-
mRNCWebViewClient.setIgnoreErrFailedForThisURL(url);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
117
|
public void setBasicAuthCredential(RNCBasicAuthCredential credential) {
|
|
89
118
|
mRNCWebViewClient.setBasicAuthCredential(credential);
|
|
90
119
|
}
|
|
@@ -246,12 +275,39 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
246
275
|
return this.messagingEnabled;
|
|
247
276
|
}
|
|
248
277
|
|
|
249
|
-
protected
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
protected void createRNCWebViewBridge(RNCWebView webView) {
|
|
279
|
+
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)){
|
|
280
|
+
if (this.bridgeListener == null) {
|
|
281
|
+
this.bridgeListener = new WebViewCompat.WebMessageListener() {
|
|
282
|
+
@Override
|
|
283
|
+
public void onPostMessage(@NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, boolean isMainFrame, @NonNull JavaScriptReplyProxy replyProxy) {
|
|
284
|
+
RNCWebView.this.onMessage(message.getData(), sourceOrigin.toString());
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
WebViewCompat.addWebMessageListener(
|
|
288
|
+
webView,
|
|
289
|
+
JAVASCRIPT_INTERFACE,
|
|
290
|
+
Set.of("*"),
|
|
291
|
+
this.bridgeListener
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
if (fallbackBridge == null) {
|
|
296
|
+
fallbackBridge = new RNCWebViewBridge(webView);
|
|
297
|
+
addJavascriptInterface(fallbackBridge, JAVASCRIPT_INTERFACE);
|
|
298
|
+
}
|
|
253
299
|
}
|
|
254
|
-
|
|
300
|
+
injectJavascriptObject();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private void injectJavascriptObject() {
|
|
304
|
+
if (getSettings().getJavaScriptEnabled()) {
|
|
305
|
+
String js = "(function(){\n" +
|
|
306
|
+
" window." + JAVASCRIPT_INTERFACE + " = window." + JAVASCRIPT_INTERFACE + " || {};\n" +
|
|
307
|
+
" window." + JAVASCRIPT_INTERFACE + ".injectedObjectJson = function () { return " + (injectedJavaScriptObject == null ? null : ("`" + injectedJavaScriptObject + "`")) + "; };\n" +
|
|
308
|
+
"})();";
|
|
309
|
+
evaluateJavascriptWithFallback(js);
|
|
310
|
+
}
|
|
255
311
|
}
|
|
256
312
|
|
|
257
313
|
@SuppressLint("AddJavascriptInterface")
|
|
@@ -276,6 +332,7 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
276
332
|
injectedJS != null &&
|
|
277
333
|
!TextUtils.isEmpty(injectedJS)) {
|
|
278
334
|
evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
|
|
335
|
+
injectJavascriptObject(); // re-inject the Javascript object in case it has been overwritten.
|
|
279
336
|
}
|
|
280
337
|
}
|
|
281
338
|
|
|
@@ -284,17 +341,18 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
284
341
|
injectedJSBeforeContentLoaded != null &&
|
|
285
342
|
!TextUtils.isEmpty(injectedJSBeforeContentLoaded)) {
|
|
286
343
|
evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();");
|
|
344
|
+
injectJavascriptObject(); // re-inject the Javascript object in case it has been overwritten.
|
|
287
345
|
}
|
|
288
346
|
}
|
|
289
347
|
|
|
348
|
+
protected String injectedJavaScriptObject = null;
|
|
349
|
+
|
|
290
350
|
public void setInjectedJavaScriptObject(String obj) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
b.setInjectedObjectJson(obj);
|
|
294
|
-
}
|
|
351
|
+
this.injectedJavaScriptObject = obj;
|
|
352
|
+
injectJavascriptObject();
|
|
295
353
|
}
|
|
296
354
|
|
|
297
|
-
public void onMessage(String message) {
|
|
355
|
+
public void onMessage(String message, String sourceUrl) {
|
|
298
356
|
ThemedReactContext reactContext = getThemedReactContext();
|
|
299
357
|
RNCWebView mWebView = this;
|
|
300
358
|
|
|
@@ -306,7 +364,7 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
306
364
|
if (mRNCWebViewClient == null) {
|
|
307
365
|
return;
|
|
308
366
|
}
|
|
309
|
-
WritableMap data = mRNCWebViewClient.createWebViewEvent(webView,
|
|
367
|
+
WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, sourceUrl);
|
|
310
368
|
data.putString("data", message);
|
|
311
369
|
|
|
312
370
|
if (mMessagingJSModule != null) {
|
|
@@ -408,16 +466,11 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
408
466
|
protected class RNCWebViewBridge {
|
|
409
467
|
private String TAG = "RNCWebViewBridge";
|
|
410
468
|
RNCWebView mWebView;
|
|
411
|
-
String injectedObjectJson;
|
|
412
469
|
|
|
413
470
|
RNCWebViewBridge(RNCWebView c) {
|
|
414
471
|
mWebView = c;
|
|
415
472
|
}
|
|
416
473
|
|
|
417
|
-
public void setInjectedObjectJson(String s) {
|
|
418
|
-
injectedObjectJson = s;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
474
|
/**
|
|
422
475
|
* This method is called whenever JavaScript running within the web view calls:
|
|
423
476
|
* - window[JAVASCRIPT_INTERFACE].postMessage
|
|
@@ -425,14 +478,12 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
|
|
|
425
478
|
@JavascriptInterface
|
|
426
479
|
public void postMessage(String message) {
|
|
427
480
|
if (mWebView.getMessagingEnabled()) {
|
|
428
|
-
mWebView.
|
|
481
|
+
// Post to main thread because `mWebView.getUrl()` requires to be executed on main.
|
|
482
|
+
mWebView.post(() -> mWebView.onMessage(message, mWebView.getUrl()));
|
|
429
483
|
} else {
|
|
430
484
|
FLog.w(TAG, "ReactNativeWebView.postMessage method was called but messaging is disabled. Pass an onMessage handler to the WebView.");
|
|
431
485
|
}
|
|
432
486
|
}
|
|
433
|
-
|
|
434
|
-
@JavascriptInterface
|
|
435
|
-
public String injectedObjectJson() { return injectedObjectJson; }
|
|
436
487
|
}
|
|
437
488
|
|
|
438
489
|
|
|
@@ -2,6 +2,7 @@ package com.reactnativecommunity.webview;
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.TargetApi;
|
|
4
4
|
import android.graphics.Bitmap;
|
|
5
|
+
import android.net.Uri;
|
|
5
6
|
import android.net.http.SslError;
|
|
6
7
|
import android.os.Build;
|
|
7
8
|
import android.os.SystemClock;
|
|
@@ -25,6 +26,7 @@ import com.facebook.react.bridge.ReadableArray;
|
|
|
25
26
|
import com.facebook.react.bridge.WritableMap;
|
|
26
27
|
import com.facebook.react.uimanager.ThemedReactContext;
|
|
27
28
|
import com.facebook.react.uimanager.UIManagerHelper;
|
|
29
|
+
import com.reactnativecommunity.webview.events.SubResourceErrorEvent;
|
|
28
30
|
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
|
|
29
31
|
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
|
|
30
32
|
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
|
|
@@ -34,6 +36,9 @@ import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEven
|
|
|
34
36
|
import android.webkit.CookieManager;
|
|
35
37
|
import android.webkit.CookieSyncManager;
|
|
36
38
|
|
|
39
|
+
import java.io.ByteArrayInputStream;
|
|
40
|
+
import java.util.HashMap;
|
|
41
|
+
import java.util.Map;
|
|
37
42
|
import java.util.concurrent.atomic.AtomicReference;
|
|
38
43
|
|
|
39
44
|
public class RNCWebViewClient extends WebViewClient {
|
|
@@ -42,13 +47,8 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
42
47
|
|
|
43
48
|
protected boolean mLastLoadFailed = false;
|
|
44
49
|
protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;
|
|
45
|
-
protected @Nullable String ignoreErrFailedForThisURL = null;
|
|
46
50
|
protected @Nullable RNCBasicAuthCredential basicAuthCredential = null;
|
|
47
51
|
|
|
48
|
-
public void setIgnoreErrFailedForThisURL(@Nullable String url) {
|
|
49
|
-
ignoreErrFailedForThisURL = url;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
52
|
public void setBasicAuthCredential(@Nullable RNCBasicAuthCredential credential) {
|
|
53
53
|
basicAuthCredential = credential;
|
|
54
54
|
}
|
|
@@ -78,6 +78,10 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
78
78
|
public void doUpdateVisitedHistory (WebView webView, String url, boolean isReload) {
|
|
79
79
|
super.doUpdateVisitedHistory(webView, url, isReload);
|
|
80
80
|
|
|
81
|
+
// Capture the top-frame URL on the UI thread for off-thread use in
|
|
82
|
+
// shouldInterceptRequest (also fires on SPA pushState navigations).
|
|
83
|
+
((RNCWebView) webView).setLastTopUrl(url);
|
|
84
|
+
|
|
81
85
|
((RNCWebView) webView).dispatchEvent(
|
|
82
86
|
webView,
|
|
83
87
|
new TopLoadingStartEvent(
|
|
@@ -91,6 +95,7 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
91
95
|
mLastLoadFailed = false;
|
|
92
96
|
|
|
93
97
|
RNCWebView reactWebView = (RNCWebView) webView;
|
|
98
|
+
reactWebView.setLastTopUrl(url);
|
|
94
99
|
reactWebView.callInjectedJavaScriptBeforeContentLoaded();
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -150,6 +155,138 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
150
155
|
return this.shouldOverrideUrlLoading(view, url);
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
// ------------------------------------------------------------------
|
|
159
|
+
// Content filter: subresource request interception.
|
|
160
|
+
// shouldInterceptRequest runs OFF the UI thread and is invoked for every
|
|
161
|
+
// subresource. Matching goes through the native content filter and fails
|
|
162
|
+
// open (returns super => request proceeds) whenever anything is unclear.
|
|
163
|
+
// ------------------------------------------------------------------
|
|
164
|
+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
165
|
+
@Override
|
|
166
|
+
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
|
167
|
+
if (!(view instanceof RNCWebView) || request == null) {
|
|
168
|
+
return super.shouldInterceptRequest(view, request);
|
|
169
|
+
}
|
|
170
|
+
RNCWebView rncWebView = (RNCWebView) view;
|
|
171
|
+
if (!rncWebView.getContentBlockerEnabled()) {
|
|
172
|
+
return super.shouldInterceptRequest(view, request);
|
|
173
|
+
}
|
|
174
|
+
// Never block top-frame navigations — only subresources.
|
|
175
|
+
if (request.isForMainFrame()) {
|
|
176
|
+
return super.shouldInterceptRequest(view, request);
|
|
177
|
+
}
|
|
178
|
+
Uri uri = request.getUrl();
|
|
179
|
+
if (uri == null) {
|
|
180
|
+
return super.shouldInterceptRequest(view, request);
|
|
181
|
+
}
|
|
182
|
+
String scheme = uri.getScheme();
|
|
183
|
+
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
|
|
184
|
+
return super.shouldInterceptRequest(view, request);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
String url = uri.toString();
|
|
188
|
+
String sourceUrl = rncWebView.getLastTopUrl();
|
|
189
|
+
String requestType = inferRequestType(request);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
if (ContentFilter.INSTANCE.shouldBlock(url, sourceUrl, requestType)) {
|
|
193
|
+
return buildBlockedResponse(requestType);
|
|
194
|
+
}
|
|
195
|
+
} catch (Throwable t) {
|
|
196
|
+
// Fail open on any unexpected error.
|
|
197
|
+
}
|
|
198
|
+
return super.shouldInterceptRequest(view, request);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private static String headerCaseInsensitive(Map<String, String> headers, String name) {
|
|
202
|
+
if (headers == null) return null;
|
|
203
|
+
String v = headers.get(name);
|
|
204
|
+
if (v != null) return v;
|
|
205
|
+
for (Map.Entry<String, String> e : headers.entrySet()) {
|
|
206
|
+
if (name.equalsIgnoreCase(e.getKey())) return e.getValue();
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Android does not expose the resource type on WebResourceRequest, so infer
|
|
212
|
+
// a request-type string from Sec-Fetch-Dest / Accept / extension.
|
|
213
|
+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
214
|
+
private static String inferRequestType(WebResourceRequest request) {
|
|
215
|
+
Map<String, String> headers = request.getRequestHeaders();
|
|
216
|
+
|
|
217
|
+
String dest = headerCaseInsensitive(headers, "Sec-Fetch-Dest");
|
|
218
|
+
if (dest != null) {
|
|
219
|
+
switch (dest) {
|
|
220
|
+
case "script": return "script";
|
|
221
|
+
case "image": return "image";
|
|
222
|
+
case "style": return "stylesheet";
|
|
223
|
+
case "font": return "font";
|
|
224
|
+
case "iframe":
|
|
225
|
+
case "frame": return "sub_frame";
|
|
226
|
+
case "audio":
|
|
227
|
+
case "video":
|
|
228
|
+
case "track": return "media";
|
|
229
|
+
case "document": return "document";
|
|
230
|
+
default: break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
String mode = headerCaseInsensitive(headers, "Sec-Fetch-Mode");
|
|
234
|
+
if ("empty".equals(dest) && ("cors".equals(mode) || "no-cors".equals(mode))) {
|
|
235
|
+
return "xmlhttprequest";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
String accept = headerCaseInsensitive(headers, "Accept");
|
|
239
|
+
if (accept != null) {
|
|
240
|
+
String a = accept.toLowerCase();
|
|
241
|
+
if (a.startsWith("text/css")) return "stylesheet";
|
|
242
|
+
if (a.startsWith("image/")) return "image";
|
|
243
|
+
if (a.contains("javascript") || a.contains("ecmascript")) return "script";
|
|
244
|
+
if (a.startsWith("text/html")) return "sub_frame";
|
|
245
|
+
}
|
|
246
|
+
String requestedWith = headerCaseInsensitive(headers, "X-Requested-With");
|
|
247
|
+
if (requestedWith != null && requestedWith.toLowerCase().contains("xmlhttprequest")) {
|
|
248
|
+
return "xmlhttprequest";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
String path = request.getUrl().getPath();
|
|
252
|
+
if (path != null) {
|
|
253
|
+
String p = path.toLowerCase();
|
|
254
|
+
if (p.endsWith(".js") || p.endsWith(".mjs")) return "script";
|
|
255
|
+
if (p.endsWith(".css")) return "stylesheet";
|
|
256
|
+
if (p.endsWith(".png") || p.endsWith(".jpg") || p.endsWith(".jpeg")
|
|
257
|
+
|| p.endsWith(".webp") || p.endsWith(".gif") || p.endsWith(".svg")
|
|
258
|
+
|| p.endsWith(".ico") || p.endsWith(".bmp")) return "image";
|
|
259
|
+
if (p.endsWith(".woff") || p.endsWith(".woff2") || p.endsWith(".ttf")
|
|
260
|
+
|| p.endsWith(".otf") || p.endsWith(".eot")) return "font";
|
|
261
|
+
if (p.endsWith(".mp4") || p.endsWith(".webm") || p.endsWith(".m3u8")
|
|
262
|
+
|| p.endsWith(".ts") || p.endsWith(".mp3") || p.endsWith(".m4a")
|
|
263
|
+
|| p.endsWith(".aac") || p.endsWith(".ogg")) return "media";
|
|
264
|
+
}
|
|
265
|
+
return "other";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
269
|
+
private WebResourceResponse buildBlockedResponse(String requestType) {
|
|
270
|
+
String mime;
|
|
271
|
+
switch (requestType) {
|
|
272
|
+
case "script": mime = "application/javascript"; break;
|
|
273
|
+
case "stylesheet": mime = "text/css"; break;
|
|
274
|
+
case "image": mime = "image/png"; break;
|
|
275
|
+
case "font": mime = "font/woff2"; break;
|
|
276
|
+
case "media": mime = "video/mp4"; break;
|
|
277
|
+
default: mime = "text/plain"; break;
|
|
278
|
+
}
|
|
279
|
+
WebResourceResponse response =
|
|
280
|
+
new WebResourceResponse(mime, "utf-8", new ByteArrayInputStream(new byte[0]));
|
|
281
|
+
// Set an explicit status so the response headers below are honored
|
|
282
|
+
// (a status-0 response can have its headers ignored on some WebViews).
|
|
283
|
+
response.setStatusCodeAndReasonPhrase(200, "OK");
|
|
284
|
+
Map<String, String> headers = new HashMap<>();
|
|
285
|
+
headers.put("Access-Control-Allow-Origin", "*");
|
|
286
|
+
response.setResponseHeaders(headers);
|
|
287
|
+
return response;
|
|
288
|
+
}
|
|
289
|
+
|
|
153
290
|
@Override
|
|
154
291
|
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
|
|
155
292
|
if (basicAuthCredential != null) {
|
|
@@ -173,12 +310,6 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
173
310
|
// Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL.
|
|
174
311
|
handler.cancel();
|
|
175
312
|
|
|
176
|
-
if (!topWindowUrl.equalsIgnoreCase(failingUrl)) {
|
|
177
|
-
// If error is not due to top-level navigation, then do not call onReceivedError()
|
|
178
|
-
Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
313
|
int code = error.getPrimaryError();
|
|
183
314
|
String description = "";
|
|
184
315
|
String descriptionPrefix = "SSL error: ";
|
|
@@ -210,6 +341,18 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
210
341
|
|
|
211
342
|
description = descriptionPrefix + description;
|
|
212
343
|
|
|
344
|
+
if (!topWindowUrl.equalsIgnoreCase(failingUrl)) {
|
|
345
|
+
// If error is not due to top-level navigation, then do not call onReceivedError()
|
|
346
|
+
Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl);
|
|
347
|
+
this.onReceivedSubResourceSslError(
|
|
348
|
+
webView,
|
|
349
|
+
code,
|
|
350
|
+
description,
|
|
351
|
+
failingUrl
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
213
356
|
this.onReceivedError(
|
|
214
357
|
webView,
|
|
215
358
|
code,
|
|
@@ -218,6 +361,20 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
218
361
|
);
|
|
219
362
|
}
|
|
220
363
|
|
|
364
|
+
public void onReceivedSubResourceSslError(
|
|
365
|
+
WebView webView,
|
|
366
|
+
int errorCode,
|
|
367
|
+
String description,
|
|
368
|
+
String failingUrl) {
|
|
369
|
+
|
|
370
|
+
WritableMap eventData = createWebViewEvent(webView, failingUrl);
|
|
371
|
+
eventData.putDouble("code", errorCode);
|
|
372
|
+
eventData.putString("description", description);
|
|
373
|
+
|
|
374
|
+
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
|
|
375
|
+
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new SubResourceErrorEvent(reactTag, eventData));
|
|
376
|
+
}
|
|
377
|
+
|
|
221
378
|
@Override
|
|
222
379
|
public void onReceivedError(
|
|
223
380
|
WebView webView,
|
|
@@ -225,20 +382,6 @@ public class RNCWebViewClient extends WebViewClient {
|
|
|
225
382
|
String description,
|
|
226
383
|
String failingUrl) {
|
|
227
384
|
|
|
228
|
-
if (ignoreErrFailedForThisURL != null
|
|
229
|
-
&& failingUrl.equals(ignoreErrFailedForThisURL)
|
|
230
|
-
&& errorCode == -1
|
|
231
|
-
&& description.equals("net::ERR_FAILED")) {
|
|
232
|
-
|
|
233
|
-
// This is a workaround for a bug in the WebView.
|
|
234
|
-
// See these chromium issues for more context:
|
|
235
|
-
// https://bugs.chromium.org/p/chromium/issues/detail?id=1023678
|
|
236
|
-
// https://bugs.chromium.org/p/chromium/issues/detail?id=1050635
|
|
237
|
-
// This entire commit should be reverted once this bug is resolved in chromium.
|
|
238
|
-
setIgnoreErrFailedForThisURL(null);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
385
|
super.onReceivedError(webView, errorCode, description, failingUrl);
|
|
243
386
|
mLastLoadFailed = true;
|
|
244
387
|
|
|
@@ -92,7 +92,6 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
|
|
|
92
92
|
WebView.setWebContentsDebuggingEnabled(true)
|
|
93
93
|
}
|
|
94
94
|
webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
|
|
95
|
-
webView.setIgnoreErrFailedForThisURL(url)
|
|
96
95
|
val module = webView.reactApplicationContext.getNativeModule(RNCWebViewModule::class.java) ?: return@DownloadListener
|
|
97
96
|
val request: DownloadManager.Request = try {
|
|
98
97
|
DownloadManager.Request(Uri.parse(url))
|
|
@@ -341,11 +340,9 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
|
|
|
341
340
|
}
|
|
342
341
|
"injectJavaScript" -> webView.evaluateJavascriptWithFallback(args.getString(0))
|
|
343
342
|
"loadUrl" -> {
|
|
344
|
-
|
|
345
|
-
throw RuntimeException("Arguments for loading an url are null!")
|
|
346
|
-
}
|
|
343
|
+
val url = args?.getString(0) ?: throw RuntimeException("Arguments for loading an url are null!")
|
|
347
344
|
webView.progressChangedFilter.setWaitingForCommandLoadUrl(false)
|
|
348
|
-
|
|
345
|
+
webView.loadUrl(url)
|
|
349
346
|
}
|
|
350
347
|
"requestFocus" -> webView.requestFocus()
|
|
351
348
|
"clearFormData" -> webView.clearFormData()
|
|
@@ -541,7 +538,11 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
|
|
|
541
538
|
}
|
|
542
539
|
|
|
543
540
|
fun setContentBlockerEnabled(viewWrapper: RNCWebViewWrapper, value: Boolean) {
|
|
544
|
-
|
|
541
|
+
viewWrapper.webView.setContentBlockerEnabled(value)
|
|
542
|
+
if (value) {
|
|
543
|
+
// Lazily warm up the native engine only when filtering is actually used.
|
|
544
|
+
ContentFilter.ensureInitialized(viewWrapper.webView.context)
|
|
545
|
+
}
|
|
545
546
|
}
|
|
546
547
|
|
|
547
548
|
fun setHasOnScroll(viewWrapper: RNCWebViewWrapper, value: Boolean) {
|
|
@@ -721,4 +722,11 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
|
|
|
721
722
|
fun setWebviewDebuggingEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
|
|
722
723
|
RNCWebView.setWebContentsDebuggingEnabled(enabled)
|
|
723
724
|
}
|
|
724
|
-
|
|
725
|
+
|
|
726
|
+
fun setPaymentRequestEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
|
|
727
|
+
val view = viewWrapper.webView
|
|
728
|
+
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
|
|
729
|
+
WebSettingsCompat.setPaymentRequestEnabled(view.settings, enabled)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
package/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.reactnativecommunity.webview.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.WritableMap
|
|
4
|
+
import com.facebook.react.uimanager.events.Event
|
|
5
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Event emitted when there is an error in loading a subresource
|
|
9
|
+
*/
|
|
10
|
+
class SubResourceErrorEvent(viewId: Int, private val mEventData: WritableMap) :
|
|
11
|
+
Event<SubResourceErrorEvent>(viewId) {
|
|
12
|
+
companion object {
|
|
13
|
+
const val EVENT_NAME = "topLoadingSubResourceError"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override fun getEventName(): String = EVENT_NAME
|
|
17
|
+
|
|
18
|
+
override fun canCoalesce(): Boolean = false
|
|
19
|
+
|
|
20
|
+
override fun getCoalescingKey(): Short = 0
|
|
21
|
+
|
|
22
|
+
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
|
|
23
|
+
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
|
|
24
|
+
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -15,6 +15,7 @@ import com.facebook.react.viewmanagers.RNCWebViewManagerDelegate;
|
|
|
15
15
|
import com.facebook.react.viewmanagers.RNCWebViewManagerInterface;
|
|
16
16
|
import com.facebook.react.views.scroll.ScrollEventType;
|
|
17
17
|
import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent;
|
|
18
|
+
import com.reactnativecommunity.webview.events.SubResourceErrorEvent;
|
|
18
19
|
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
|
|
19
20
|
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
|
|
20
21
|
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
|
|
@@ -232,7 +233,7 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
@Override
|
|
235
|
-
@ReactProp(name = "suppressMenuItems
|
|
236
|
+
@ReactProp(name = "suppressMenuItems")
|
|
236
237
|
public void setSuppressMenuItems(RNCWebViewWrapper view, @Nullable ReadableArray items) {}
|
|
237
238
|
|
|
238
239
|
@Override
|
|
@@ -337,6 +338,12 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
|
|
|
337
338
|
mRNCWebViewManagerImpl.setWebviewDebuggingEnabled(view, value);
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
@Override
|
|
342
|
+
@ReactProp(name = "paymentRequestEnabled")
|
|
343
|
+
public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) {
|
|
344
|
+
mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value);
|
|
345
|
+
}
|
|
346
|
+
|
|
340
347
|
/* iOS PROPS - no implemented here */
|
|
341
348
|
@Override
|
|
342
349
|
public void setAllowingReadAccessToURL(RNCWebViewWrapper view, @Nullable String value) {}
|
|
@@ -401,6 +408,9 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
|
|
|
401
408
|
@Override
|
|
402
409
|
public void setRefreshControlLightMode(RNCWebViewWrapper view, boolean value) {}
|
|
403
410
|
|
|
411
|
+
@Override
|
|
412
|
+
public void setIndicatorStyle(RNCWebViewWrapper view, @Nullable String value) {}
|
|
413
|
+
|
|
404
414
|
@Override
|
|
405
415
|
public void setScrollEnabled(RNCWebViewWrapper view, boolean value) {}
|
|
406
416
|
|
|
@@ -521,6 +531,7 @@ public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
|
|
|
521
531
|
export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart"));
|
|
522
532
|
export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish"));
|
|
523
533
|
export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError"));
|
|
534
|
+
export.put(SubResourceErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingSubResourceError"));
|
|
524
535
|
export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage"));
|
|
525
536
|
// !Default events but adding them here explicitly for clarity
|
|
526
537
|
|