This repository has been archived by the owner on Jan 12, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 711
/
Copy pathFocusWebViewClient.java
345 lines (295 loc) · 15.7 KB
/
FocusWebViewClient.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.focus.webview;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslCertificate;
import android.net.http.SslError;
import android.os.Bundle;
import android.text.TextUtils;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.mozilla.focus.browser.LocalizedContent;
import org.mozilla.focus.telemetry.TelemetryWrapper;
import org.mozilla.focus.utils.IntentUtils;
import org.mozilla.focus.utils.UrlUtils;
import org.mozilla.focus.web.IWebView;
import java.net.URI;
import java.net.URISyntaxException;
import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO;
import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
/**
* WebViewClient layer that handles browser specific WebViewClient functionality, such as error pages
* and external URL handling.
*/
/* package */ class FocusWebViewClient extends TrackingProtectionWebViewClient {
private static final String ERROR_PROTOCOL = "error:";
private static final String STATE_KEY_URL = "client_last_url";
private static final String STATE_KEY_CERTIFICATE = "client_last_certificate";
private String restoredUrl;
private SslCertificate restoredCertificate;
private boolean errorReceived;
private boolean shouldReadURL = true; // Flag to ensure URL is only read once per load
/* package */ FocusWebViewClient(Context context) {
super(context);
}
/**
* Always ensure the following is wrapped in an anonymous function before execution.
* (We don't wrap here, since this code might be run as part of a larger function, see
* e.g. onLoadResource().)
*/
private static final String CLEAR_VISITED_CSS =
"var nSheets = document.styleSheets.length;" +
"for (s=0; s < nSheets; s++) {" +
" var stylesheet = document.styleSheets[s];" +
" var nRules = stylesheet.cssRules ? stylesheet.cssRules.length : 0;" +
// rules need to be removed by index. That modifies the whole list - it's easiest
// to therefore process the list from the back, so that we don't need to care about
// indexes changing after deletion (all indexes before the removed item are unchanged,
// so by moving towards the start we'll always process all previously unprocessed items -
// moving in the other direction we'd need to remember to process a given index
// again which is more complicated).
" for (i = nRules - 1; i >= 0; i--) {" +
" var cssRule = stylesheet.cssRules[i];" +
// Depending on style type, there might be no selector
" if (cssRule.selectorText && cssRule.selectorText.includes(':visited')) {" +
" var tokens = cssRule.selectorText.split(',');" +
" var j = tokens.length;" +
" while (j--) {" +
" if (tokens[j].includes(':visited')) {" +
" tokens.splice(j, 1);" +
" }" +
" }" +
" if (tokens.length == 0) {" +
" stylesheet.deleteRule(i);" +
" } else {" +
" cssRule.selectorText = tokens.join(',');" +
" }" +
" }" +
" }" +
"}";
@Override
public void onLoadResource(WebView view, String url) {
// We can't access the webview during shouldInterceptRequest(), however onLoadResource()
// is called on the UI thread so we're allowed to do this now:
view.evaluateJavascript(
"(function() {" +
"function cleanupVisited() {" +
CLEAR_VISITED_CSS +
"}" +
// Add an onLoad() listener so that we run the cleanup script every time
// a <link>'d css stylesheet is loaded:
"var links = document.getElementsByTagName('link');" +
"for (i = 0; i < links.length; i++) {" +
" link = links[i];" +
" if (link.rel == 'stylesheet') {" +
" link.addEventListener('load', cleanupVisited, false);" +
" }" +
"}" +
"})();",
null);
super.onLoadResource(view, url);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {
// Only update the user visible URL if:
// 1. The purported site URL has actually been requested
// 2. And it's being loaded for the main frame (and not a fake/hidden/iframe request)
// Note also: shouldInterceptRequest() runs on a background thread, so we can't actually
// query WebView.getURL().
// We update the URL when loading has finished too (redirects can happen after a request has been
// made in which case we don't get shouldInterceptRequest with the final URL), but this
// allows us to update the URL during loading.
if (request.isForMainFrame()) {
// WebView will always add a trailing / to the request URL, but currentPageURL may or may
// not have a trailing URL (usually no trailing / when a link is entered via UrlInputFragment),
// hence we do a somewhat convoluted test:
final String requestURL = request.getUrl().toString();
final String currentURL = currentPageURL;
if (UrlUtils.urlsMatchExceptForTrailingSlash(currentURL, requestURL)) {
view.post(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onURLChanged(currentURL);
}
}
});
}
if (callback != null) {
callback.onRequest(request.hasGesture());
}
}
return super.shouldInterceptRequest(view, request);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
/* Temporarily disable TalkBack on WebView so it doesn't grab the focus before we can
read out the URL. */
view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
if (errorReceived) {
// When dealing with error pages, WebView sometimes sends onPageStarted()
// without a matching onPageFinished(). We hack around that by using
// a flag to ignore the first onPageStarted() after onReceivedError() has
// been called. (The usual chain is: onPageStarted(url), onReceivedError(url),
// onPageFinished(url), onPageStarted(url), finally and only sometimes: onPageFinished().
// Since the final onPageFinished isn't guaranteed (and we know we're showing an error
// page already), we don't need to send the onPageStarted() callback a second time anyway.
errorReceived = false;
} else if (callback != null) {
callback.onPageStarted(url);
if (shouldReadURL) {
view.announceForAccessibility("Loading " + url);
shouldReadURL = false;
}
}
// Enable TalkBack again for WebView now that we've announced the URL
view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
super.onPageStarted(view, url, favicon);
}
/* package */ void saveState(WebView view, Bundle bundle) {
final SslCertificate certificate = view.getCertificate();
if (certificate != null) {
bundle.putString(STATE_KEY_URL, view.getUrl());
bundle.putBundle(STATE_KEY_CERTIFICATE, SslCertificate.saveState(certificate));
}
}
/* package */ void restoreState(Bundle bundle) {
if (bundle != null && bundle.containsKey(STATE_KEY_CERTIFICATE)) {
restoredUrl = bundle.getString(STATE_KEY_URL);
restoredCertificate = SslCertificate.restoreState(bundle.getBundle("client_last_certificate"));
}
}
@Override
public void onPageFinished(WebView view, final String url) {
SslCertificate certificate = view.getCertificate();
shouldReadURL = true;
if (!TextUtils.isEmpty(restoredUrl)) {
if (restoredUrl.equals(url) && certificate == null) {
// We just restored the previous state. Let's re-use the certificate we restored.
// The reason for that is that WebView doesn't restore the certificate itself.
// Without restoring the certificate manually we'd lose the certificate when
// switching tabs or restoring a previous session for other reasons.
certificate = restoredCertificate;
} else {
// The URL has changed since we restored the last state. Let's just clear all
// restored data because we do not need it anymore.
restoredUrl = null;
restoredCertificate = null;
}
}
if (callback != null) {
// The page is secure when the url is a localized content or when the certificate isn't null
final boolean isSecure = certificate != null || UrlUtils.isLocalizedContent(view.getUrl());
callback.onPageFinished(isSecure);
String host = null;
try {
host = new URI(url).getHost();
} catch (URISyntaxException e) {
e.printStackTrace();
}
callback.onSecurityChanged(isSecure, host, (certificate != null) ? certificate.getIssuedBy().getOName() : null);
// The URL which is supplied in onPageFinished() could be fake (see #301), but webview's
// URL is always correct _except_ for error pages
final String viewURL = view.getUrl();
if (!UrlUtils.isInternalErrorURL(viewURL) && viewURL != null) {
callback.onURLChanged(viewURL);
}
}
super.onPageFinished(view, url);
view.evaluateJavascript(
"(function() {" +
CLEAR_VISITED_CSS +
"})();",
null);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// If this is an internal URL like focus:about then we load the content ourselves here.
if (LocalizedContent.handleInternalContent(url, (IWebView) view, view.getContext())) {
return true;
}
// Allow pages to blank themselves by loading about:blank. While it's a little incorrect to let pages
// access our internal URLs, Chrome allows loads to about:blank and, to ensure our behavior conforms
// to the behavior that most of the web is developed against, we do too.
if (url.equals("about:blank")) {
return false;
}
// shouldOverrideUrlLoading() is called for both the main frame, and iframes.
// That can get problematic if an iframe tries to load an unsupported URL.
// We then try to either handle that URL (ask to open relevant app), or extract
// a fallback URL from the intent (or worst case fall back to an error page). In the
// latter 2 cases, we explicitly open the fallback/error page in the main view.
// Websites probably shouldn't use unsupported URLs in iframes, but we do need to
// be careful to handle all valid schemes here to avoid redirecting due to such an iframe
// (e.g. we don't want to redirect to a data: URI just because an iframe shows such
// a URI).
// (The API 24+ version of shouldOverrideUrlLoading() lets us determine whether
// the request is for the main frame, and if it's not we could then completely
// skip the external URL handling.)
final Uri uri = Uri.parse(url);
return !UrlUtils.isSupportedProtocol(uri.getScheme()) && callback != null && IntentUtils.INSTANCE.handleExternalUri(view.getContext(), (IWebView) view, url) || super.shouldOverrideUrlLoading(view, url);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.cancel();
if (callback != null) {
callback.onSecurityChanged(error.getCertificate() != null, null, (error.getCertificate() != null) ? error.getCertificate().getIssuedBy().getOName() : null);
}
// WebView can try to load the favicon for a bad page when you set a new URL. If we then
// loadErrorPage() again, WebView tries to load the favicon again. We end up in onReceivedSSlError()
// again, and we get an infinite loop of reloads (we also erroneously show the favicon URL
// in the toolbar, but that's less noticeable). Hence we check whether this error is from
// the desired page, or a page resource:
if (error.getUrl().equals(currentPageURL)) {
TelemetryWrapper.sslErrorEvent(true, error);
ErrorPage.loadErrorPage(view, error.getUrl(), WebViewClient.ERROR_FAILED_SSL_HANDSHAKE);
} else {
TelemetryWrapper.sslErrorEvent(false, error);
}
}
@Override
public void onReceivedError(final WebView webView, int errorCode, final String description, String failingUrl) {
errorReceived = true;
// This is a hack: onReceivedError(WebView, WebResourceRequest, WebResourceError) is API 23+ only,
// - the WebResourceRequest would let us know if the error affects the main frame or not. As a workaround
// we just check whether the failing URL is the current URL, which is enough to detect an error
// in the main frame.
// WebView swallows odd pages and only sends an error (i.e. it doesn't go through the usual
// shouldOverrideUrlLoading), so we need to handle special pages here:
// about: urls are even more odd: webview doesn't tell us _anything_, hence the use of
// a different prefix:
if (failingUrl.startsWith(ERROR_PROTOCOL)) {
// format: error:<error_code>
final int errorCodePosition = ERROR_PROTOCOL.length();
final String errorCodeString = failingUrl.substring(errorCodePosition);
int desiredErrorCode;
try {
desiredErrorCode = Integer.parseInt(errorCodeString);
if (!ErrorPage.supportsErrorCode(desiredErrorCode)) {
// I don't think there's any good way of showing an error if there's an error
// in requesting an error page?
desiredErrorCode = WebViewClient.ERROR_BAD_URL;
}
} catch (final NumberFormatException e) {
desiredErrorCode = WebViewClient.ERROR_BAD_URL;
}
ErrorPage.loadErrorPage(webView, failingUrl, desiredErrorCode);
return;
}
// The API 23+ version also return a *slightly* more usable description, via WebResourceError.getError();
// e.g.. "There was a network error.", whereas this version provides things like "net::ERR_NAME_NOT_RESOLVED"
if (failingUrl.equals(currentPageURL) &&
ErrorPage.supportsErrorCode(errorCode)) {
ErrorPage.loadErrorPage(webView, currentPageURL, errorCode);
return;
}
super.onReceivedError(webView, errorCode, description, failingUrl);
}
}