Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.reactnativecommunity.webview;

import android.content.Context;
import android.os.Build;
import androidx.annotation.RequiresApi;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.HashMap;
import java.util.Map;

import static java.nio.charset.StandardCharsets.*;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class InputStreamWithInjectedJS extends InputStream {
private InputStream pageIS;
private InputStream scriptIS;
private Charset charset;
private static final String TAG = "InputStreamWithInjectedJS";
private static Map<Charset, String> script = new HashMap<>();
private boolean hasJS = false;
private boolean headWasFound = false;
private boolean scriptWasInjected = false;
private boolean openingHeadFound = false;
private StringBuffer contentBuffer = new StringBuffer();
private static Charset getCharset(String charsetName) {
Charset cs = UTF_8;
try {
if (charsetName != null) {
cs = Charset.forName(charsetName);
}
} catch (UnsupportedCharsetException e) {
Log.d("CustomWebview", "wrong charset: " + charsetName);
}
return cs;
}
private static InputStream getScript(Charset charset) {
String js = script.get(charset);
if (js == null) {
String defaultJs = script.get(UTF_8);
js = new String(defaultJs.getBytes(UTF_8), charset);
script.put(charset, js);
}
return new ByteArrayInputStream(js.getBytes(charset));
}
InputStreamWithInjectedJS(InputStream is, String js, Charset charset, Context c) {
if (js == null) {
this.pageIS = is;
} else {
this.hasJS = true;
this.charset = charset;
Charset cs = UTF_8;
String jsScript = "<script>" + js + "</script>";
script.put(cs, jsScript);
this.pageIS = is;
}
}
@Override
public int read() throws IOException {
if (scriptWasInjected || !hasJS) {
return pageIS.read();
}

if (!scriptWasInjected && headWasFound) {
int nextByte = scriptIS.read();
if (nextByte == -1) {
scriptIS.close();
scriptWasInjected = true;
return pageIS.read();
} else {
return nextByte;
}
}
if (!headWasFound) {
int nextByte = pageIS.read();
char nextByteStr = (char) nextByte;
contentBuffer.append(nextByteStr);
int bufferLength = contentBuffer.length();
String headString = "<head";
if(openingHeadFound){
if(nextByte == 62){
this.scriptIS = getScript(this.charset);
headWasFound = true;
}
} else {
boolean isLetterD = (nextByte == 68 || nextByte == 100);
if (isLetterD && bufferLength >= 5) {
String stringToMatch = contentBuffer.substring(bufferLength - 5).toLowerCase();
if (stringToMatch.contains(headString)) {
openingHeadFound = true;
}
}
}
return nextByte;
}
return pageIS.read();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@
import android.webkit.GeolocationPermissions;
import android.webkit.JavascriptInterface;
import android.webkit.PermissionRequest;
import android.webkit.ServiceWorkerClient;
import android.webkit.ServiceWorkerController;
import android.webkit.URLUtil;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
Expand Down Expand Up @@ -66,17 +69,28 @@
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.annotation.Nullable;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.OkHttpClient.Builder;
import static okhttp3.internal.Util.UTF_8;

/**
* Manages instances of {@link WebView}
* <p>
Expand Down Expand Up @@ -113,6 +127,9 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public static final int COMMAND_LOAD_URL = 7;
public static final int COMMAND_FOCUS = 8;
protected static final String REACT_CLASS = "RNCWebView";
protected final static String HEADER_CONTENT_TYPE = "content-type";
protected static final String MIME_TEXT_HTML = "text/html";
protected static final String MIME_UNKNOWN = "application/octet-stream";
protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
Expand All @@ -121,13 +138,20 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
// state and release page resources (including any running JavaScript).
protected static final String BLANK_URL = "about:blank";
protected WebViewConfig mWebViewConfig;
private OkHttpClient httpClient;

protected RNCWebChromeClient mWebChromeClient = null;
protected boolean mAllowsFullscreenVideo = false;
protected @Nullable String mUserAgent = null;
protected @Nullable String mUserAgentWithApplicationName = null;

public RNCWebViewManager() {
Builder b = new Builder();
httpClient = b
.followRedirects(false)
.followSslRedirects(false)
.build();

mWebViewConfig = new WebViewConfig() {
public void configWebView(WebView webView) {
}
Expand All @@ -150,6 +174,54 @@ public String getName() {
return REACT_CLASS;
}

public static Boolean urlStringLooksInvalid(String urlString) {
return urlString == null ||
urlString.trim().equals("") ||
!(urlString.startsWith("http") && !urlString.startsWith("www")) ||
urlString.contains("|");
}

public static Boolean responseRequiresJSInjection(Response response) {
if (response.isRedirect()) {
return false;
}
final String contentTypeAndCharset = response.header(HEADER_CONTENT_TYPE, MIME_UNKNOWN);
return contentTypeAndCharset.startsWith(MIME_TEXT_HTML);
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request, Boolean onlyMainFrame, RNCWebView webView) {
Uri url = request.getUrl();
String urlStr = url.toString();
if (onlyMainFrame && !request.isForMainFrame()) {
return null;
}
if (RNCWebViewManager.urlStringLooksInvalid(urlStr)) {
return null;
}
try {


Request req = new Request.Builder()
.header("User-Agent", mUserAgent)
.url(urlStr)
.build();
Response response = httpClient.newCall(req).execute();
if (!RNCWebViewManager.responseRequiresJSInjection(response)) {
return null;
}
InputStream is = response.body().byteStream();
MediaType contentType = response.body().contentType();
Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
if (response.code() < HttpURLConnection.HTTP_MULT_CHOICE || response.code() >= HttpURLConnection.HTTP_BAD_REQUEST) {
is = new InputStreamWithInjectedJS(is, webView.injectedJS, charset, webView.getContext());
}
return new WebResourceResponse("text/html", charset.name(), is);
} catch (IOException e) {
return null;
}
}

protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) {
return new RNCWebView(reactContext);
}
Expand All @@ -162,6 +234,7 @@ protected WebView createViewInstance(ThemedReactContext reactContext) {
reactContext.addLifecycleEventListener(webView);
mWebViewConfig.configWebView(webView);
WebSettings settings = webView.getSettings();
String USER_AGENT = settings.getUserAgentString();
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
settings.setDomStorageEnabled(true);
Expand All @@ -179,10 +252,25 @@ protected WebView createViewInstance(ThemedReactContext reactContext) {
new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));

if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ServiceWorkerController swController = ServiceWorkerController.getInstance();
swController.setServiceWorkerClient(new ServiceWorkerClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, false, webView);
if (response != null) {
return response;
}
return super.shouldInterceptRequest(request);
}
});
}


webView.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
RNCWebViewModule module = getModule(reactContext);
Expand Down Expand Up @@ -653,7 +741,7 @@ public void onHideCustomView() {
}
}

protected static class RNCWebViewClient extends WebViewClient {
protected class RNCWebViewClient extends WebViewClient {

protected boolean mLastLoadFailed = false;
protected @Nullable
Expand Down Expand Up @@ -699,6 +787,14 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
final String url = request.getUrl().toString();
// Disabling the URL schemes that cause problems
String[] blacklistedUrls = { "intent:#Intent;action=com.ledger.android.u2f.bridge.AUTHENTICATE" };
for(int i=0; i< blacklistedUrls.length; i++){
String badUrl = blacklistedUrls[i];
if(url.contains(badUrl)){
return true;
}
}
return this.shouldOverrideUrlLoading(view, url);
}

Expand Down Expand Up @@ -745,6 +841,23 @@ protected WritableMap createWebViewEvent(WebView webView, String url) {
return event;
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return null;
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
WebResourceResponse response = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
response = RNCWebViewManager.this.shouldInterceptRequest(request, true, (RNCWebView) view);
if (response != null) {
return response;
}
}

return super.shouldInterceptRequest(view, request);
}

public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
mUrlPrefixesForDefaultIntent = specialUrls;
}
Expand Down