Breaking Change: Restrict WebView to only manage navigation of whitelisted URLs: http(s) by default
Reviewed By: achen1 Differential Revision: D7834050 fbshipit-source-id: 80f7fd3cd20979590b75804819e154afc14a3c64
This commit is contained in:
parent
1e68ca7dc8
commit
23f8f7aecb
|
@ -16,6 +16,7 @@ const StyleSheet = require('StyleSheet');
|
|||
const UIManager = require('UIManager');
|
||||
const View = require('View');
|
||||
const ViewPropTypes = require('ViewPropTypes');
|
||||
const WebViewShared = require('WebViewShared');
|
||||
|
||||
const deprecatedPropType = require('deprecatedPropType');
|
||||
const keyMirror = require('fbjs/lib/keyMirror');
|
||||
|
@ -179,6 +180,15 @@ class WebView extends React.Component {
|
|||
*/
|
||||
allowUniversalAccessFromFileURLs: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* List of origin strings to allow being navigated to. The strings allow
|
||||
* wildcards and get matched against *just* the origin (not the full URL).
|
||||
* If the user taps to navigate to a new page but the new page is not in
|
||||
* this whitelist, the URL will be oppened by the Android OS.
|
||||
* The default whitelisted origins are "http://*" and "https://*".
|
||||
*/
|
||||
originWhitelist: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
/**
|
||||
* Function that accepts a string that will be passed to the WebView and
|
||||
* executed immediately as JavaScript.
|
||||
|
@ -241,7 +251,8 @@ class WebView extends React.Component {
|
|||
javaScriptEnabled : true,
|
||||
thirdPartyCookiesEnabled: true,
|
||||
scalesPageToFit: true,
|
||||
saveFormDataDisabled: false
|
||||
saveFormDataDisabled: false,
|
||||
originWhitelist: WebViewShared.defaultOriginWhitelist,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -293,6 +304,8 @@ class WebView extends React.Component {
|
|||
|
||||
const nativeConfig = this.props.nativeConfig || {};
|
||||
|
||||
const originWhitelist = (this.props.originWhitelist || []).map(WebViewShared.originWhitelistToRegex);
|
||||
|
||||
let NativeWebView = nativeConfig.component || RCTWebView;
|
||||
|
||||
const webView =
|
||||
|
@ -319,6 +332,7 @@ class WebView extends React.Component {
|
|||
geolocationEnabled={this.props.geolocationEnabled}
|
||||
mediaPlaybackRequiresUserAction={this.props.mediaPlaybackRequiresUserAction}
|
||||
allowUniversalAccessFromFileURLs={this.props.allowUniversalAccessFromFileURLs}
|
||||
originWhitelist={originWhitelist}
|
||||
mixedContentMode={this.props.mixedContentMode}
|
||||
saveFormDataDisabled={this.props.saveFormDataDisabled}
|
||||
urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
|
||||
package com.facebook.react.views.webview;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
@ -110,6 +115,7 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
|
|||
|
||||
protected boolean mLastLoadFailed = false;
|
||||
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
|
||||
protected @Nullable List<Pattern> mOriginWhitelist;
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView webView, String url) {
|
||||
|
@ -137,32 +143,50 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
|
|||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
boolean useDefaultIntent = false;
|
||||
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
|
||||
ArrayList<Object> urlPrefixesForDefaultIntent =
|
||||
mUrlPrefixesForDefaultIntent.toArrayList();
|
||||
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
|
||||
if (url.startsWith((String) urlPrefix)) {
|
||||
useDefaultIntent = true;
|
||||
break;
|
||||
}
|
||||
if (url.equals(BLANK_URL)) return false;
|
||||
|
||||
// url blacklisting
|
||||
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
|
||||
ArrayList<Object> urlPrefixesForDefaultIntent =
|
||||
mUrlPrefixesForDefaultIntent.toArrayList();
|
||||
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
|
||||
if (url.startsWith((String) urlPrefix)) {
|
||||
launchIntent(view.getContext(), url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!useDefaultIntent &&
|
||||
(url.startsWith("http://") || url.startsWith("https://") ||
|
||||
url.startsWith("file://") || url.equals("about:blank"))) {
|
||||
return false;
|
||||
} else {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
view.getContext().startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
|
||||
}
|
||||
if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
launchIntent(view.getContext(), url);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void launchIntent(Context context, String url) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
|
||||
Uri uri = Uri.parse(url);
|
||||
String scheme = uri.getScheme() != null ? uri.getScheme() : "";
|
||||
String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
|
||||
String urlToCheck = scheme + "://" + authority;
|
||||
for (Pattern pattern : originWhitelist) {
|
||||
if (pattern.matcher(urlToCheck).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -211,6 +235,10 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
|
|||
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
|
||||
mUrlPrefixesForDefaultIntent = specialUrls;
|
||||
}
|
||||
|
||||
public void setOriginWhitelist(List<Pattern> originWhitelist) {
|
||||
mOriginWhitelist = originWhitelist;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -356,6 +384,7 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
|
|||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
protected WebView createViewInstance(ThemedReactContext reactContext) {
|
||||
ReactWebView webView = createReactWebViewInstance(reactContext);
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
|
@ -375,9 +404,18 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
|
|||
});
|
||||
reactContext.addLifecycleEventListener(webView);
|
||||
mWebViewConfig.configWebView(webView);
|
||||
webView.getSettings().setBuiltInZoomControls(true);
|
||||
webView.getSettings().setDisplayZoomControls(false);
|
||||
webView.getSettings().setDomStorageEnabled(true);
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setDisplayZoomControls(false);
|
||||
settings.setDomStorageEnabled(true);
|
||||
|
||||
settings.setAllowFileAccess(false);
|
||||
settings.setAllowContentAccess(false);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
settings.setAllowFileAccessFromFileURLs(false);
|
||||
setAllowUniversalAccessFromFileURLs(webView, false);
|
||||
}
|
||||
setMixedContentMode(webView, "never");
|
||||
|
||||
// Fixes broken full-screen modals/galleries due to body height being 0.
|
||||
webView.setLayoutParams(
|
||||
|
@ -546,6 +584,20 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
|
|||
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
|
||||
}
|
||||
|
||||
@ReactProp(name = "originWhitelist")
|
||||
public void setOriginWhitelist(
|
||||
WebView view,
|
||||
@Nullable ReadableArray originWhitelist) {
|
||||
ReactWebViewClient client = ((ReactWebView) view).getReactWebViewClient();
|
||||
if (client != null && originWhitelist != null) {
|
||||
List<Pattern> whiteList = new LinkedList<>();
|
||||
for (int i = 0 ; i < originWhitelist.size() ; i++) {
|
||||
whiteList.add(Pattern.compile(originWhitelist.getString(i)));
|
||||
}
|
||||
client.setOriginWhitelist(whiteList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
|
||||
// Do not register default touch emitter and let WebView implementation handle touches
|
||||
|
|
Loading…
Reference in New Issue