blob: e6b4dd74c78096854fd4101e0839a0c1100db57f [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.tab;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl;
import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler;
import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler.OverrideUrlLoadingResult;
import org.chromium.chrome.browser.externalnav.ExternalNavigationParams;
import org.chromium.chrome.browser.tabmodel.TabLaunchType;
import org.chromium.components.navigation_interception.InterceptNavigationDelegate;
import org.chromium.components.navigation_interception.NavigationParams;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ConsoleMessageLevel;
/**
* Class that controls navigations and allows to intercept them. It is used on Android to 'convert'
* certain navigations to Intents to 3rd party applications.
* Note the Intent is often created together with a new empty tab which then should be closed
* immediately. Closing the tab will cancel the navigation that this delegate is running for,
* hence can cause UAF error. It should be done in an asynchronous fashion to avoid it.
* See https://crbug.com/732260.
*/
public class InterceptNavigationDelegateImpl implements InterceptNavigationDelegate {
private final Tab mTab;
private final ExternalNavigationHandler mExternalNavHandler;
private final AuthenticatorNavigationInterceptor mAuthenticatorHelper;
private @OverrideUrlLoadingResult int mLastOverrideUrlLoadingResult =
OverrideUrlLoadingResult.NO_OVERRIDE;
/**
* Whether forward history should be cleared after navigation is committed.
*/
private boolean mClearAllForwardHistoryRequired;
private boolean mShouldClearRedirectHistoryForTabClobbering;
/**
* Default constructor of {@link InterceptNavigationDelegateImpl}.
*/
public InterceptNavigationDelegateImpl(Tab tab) {
this(new ExternalNavigationHandler(tab), tab);
}
/**
* Constructs a new instance of {@link InterceptNavigationDelegateImpl} with the given
* {@link ExternalNavigationDelegate}.
*/
public InterceptNavigationDelegateImpl(ExternalNavigationDelegateImpl delegate, Tab tab) {
this(new ExternalNavigationHandler(delegate), tab);
}
/**
* Constructs a new instance of {@link InterceptNavigationDelegateImpl} with the given
* {@link ExternalNavigationHandler}.
*/
public InterceptNavigationDelegateImpl(ExternalNavigationHandler externalNavHandler, Tab tab) {
mTab = tab;
mExternalNavHandler = externalNavHandler;
mAuthenticatorHelper = AppHooks.get().createAuthenticatorNavigationInterceptor(mTab);
}
public boolean shouldIgnoreNewTab(String url, boolean incognito) {
if (mAuthenticatorHelper != null && mAuthenticatorHelper.handleAuthenticatorUrl(url)) {
return true;
}
ExternalNavigationParams params = new ExternalNavigationParams.Builder(url, incognito)
.setTab(mTab)
.setOpenInNewTab(true)
.build();
return mExternalNavHandler.shouldOverrideUrlLoading(params)
!= ExternalNavigationHandler.OverrideUrlLoadingResult.NO_OVERRIDE;
}
@VisibleForTesting
public @OverrideUrlLoadingResult int getLastOverrideUrlLoadingResultForTests() {
return mLastOverrideUrlLoadingResult;
}
@Override
public boolean shouldIgnoreNavigation(NavigationParams navigationParams) {
String url = navigationParams.url;
ChromeActivity associatedActivity = mTab.getActivity();
long lastUserInteractionTime =
(associatedActivity == null) ? -1 : associatedActivity.getLastUserInteractionTime();
if (mAuthenticatorHelper != null && mAuthenticatorHelper.handleAuthenticatorUrl(url)) {
return true;
}
TabRedirectHandler tabRedirectHandler = null;
if (navigationParams.isMainFrame) {
tabRedirectHandler = TabRedirectHandler.from(mTab);
} else if (navigationParams.isExternalProtocol) {
// Only external protocol navigations are intercepted for iframe navigations. Since
// we do not see all previous navigations for the iframe, we can not build a complete
// redirect handler for each iframe. Nor can we use the top level redirect handler as
// that has the potential to incorrectly give access to the navigation due to previous
// main frame gestures.
//
// By creating a new redirect handler for each external navigation, we are specifically
// not covering the case where a gesture is carried over via a redirect. This is
// currently not feasible because we do not see all navigations for iframes and it is
// better to error on the side of caution and require direct user gestures for iframes.
tabRedirectHandler = TabRedirectHandler.create(associatedActivity);
} else {
assert false;
return false;
}
tabRedirectHandler.updateNewUrlLoading(navigationParams.pageTransitionType,
navigationParams.isRedirect,
navigationParams.hasUserGesture || navigationParams.hasUserGestureCarryover,
lastUserInteractionTime, getLastCommittedEntryIndex());
boolean shouldCloseTab = shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent();
ExternalNavigationParams params = buildExternalNavigationParams(navigationParams,
tabRedirectHandler,
shouldCloseTab).build();
@OverrideUrlLoadingResult
int result = mExternalNavHandler.shouldOverrideUrlLoading(params);
mLastOverrideUrlLoadingResult = result;
RecordHistogram.recordEnumeratedHistogram("Android.TabNavigationInterceptResult", result,
OverrideUrlLoadingResult.NUM_ENTRIES);
switch (result) {
case OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT:
assert mExternalNavHandler.canExternalAppHandleUrl(url);
if (navigationParams.isMainFrame) onOverrideUrlLoadingAndLaunchIntent();
return true;
case OverrideUrlLoadingResult.OVERRIDE_WITH_CLOBBERING_TAB:
mShouldClearRedirectHistoryForTabClobbering = true;
return true;
case OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION:
if (!shouldCloseTab && navigationParams.isMainFrame) {
onOverrideUrlLoadingAndLaunchIntent();
}
return true;
case OverrideUrlLoadingResult.NO_OVERRIDE:
default:
if (navigationParams.isExternalProtocol) {
logBlockedNavigationToDevToolsConsole(url);
return true;
}
return false;
}
}
/**
* Returns ExternalNavigationParams.Builder to generate ExternalNavigationParams for
* ExternalNavigationHandler#shouldOverrideUrlLoading().
*/
public ExternalNavigationParams.Builder buildExternalNavigationParams(
NavigationParams navigationParams, TabRedirectHandler tabRedirectHandler,
boolean shouldCloseTab) {
boolean isInitialTabLaunchInBackground =
mTab.getLaunchType() == TabLaunchType.FROM_LONGPRESS_BACKGROUND && shouldCloseTab;
// http://crbug.com/448977: If a new tab is closed by this overriding, we should open an
// Intent in a new tab when Chrome receives it again.
return new ExternalNavigationParams
.Builder(navigationParams.url, mTab.isIncognito(), navigationParams.referrer,
navigationParams.pageTransitionType, navigationParams.isRedirect)
.setTab(mTab)
.setApplicationMustBeInForeground(true)
.setRedirectHandler(tabRedirectHandler)
.setOpenInNewTab(shouldCloseTab)
.setIsBackgroundTabNavigation(mTab.isHidden() && !isInitialTabLaunchInBackground)
.setIsMainFrame(navigationParams.isMainFrame)
.setHasUserGesture(navigationParams.hasUserGesture)
.setShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent(
shouldCloseTab && navigationParams.isMainFrame);
}
/**
* Updates navigation history if navigation is canceled due to intent handler. We go back to the
* last committed entry index which was saved before the navigation, and remove the empty
* entries from the navigation history. See crbug.com/426679
*/
public void maybeUpdateNavigationHistory() {
WebContents webContents = mTab.getWebContents();
if (mClearAllForwardHistoryRequired && webContents != null) {
NavigationController navigationController =
webContents.getNavigationController();
int lastCommittedEntryIndex = getLastCommittedEntryIndex();
while (navigationController.canGoForward()) {
boolean ret = navigationController.removeEntryAtIndex(
lastCommittedEntryIndex + 1);
assert ret;
}
} else if (mShouldClearRedirectHistoryForTabClobbering
&& webContents != null) {
// http://crbug/479056: Even if we clobber the current tab, we want to remove
// redirect history to be consistent.
NavigationController navigationController =
webContents.getNavigationController();
int indexBeforeRedirection =
TabRedirectHandler.from(mTab)
.getLastCommittedEntryIndexBeforeStartingNavigation();
int lastCommittedEntryIndex = getLastCommittedEntryIndex();
for (int i = lastCommittedEntryIndex - 1; i > indexBeforeRedirection; --i) {
boolean ret = navigationController.removeEntryAtIndex(i);
assert ret;
}
}
mClearAllForwardHistoryRequired = false;
mShouldClearRedirectHistoryForTabClobbering = false;
}
AuthenticatorNavigationInterceptor getAuthenticatorNavigationInterceptor() {
return mAuthenticatorHelper;
}
private int getLastCommittedEntryIndex() {
if (mTab.getWebContents() == null) return -1;
return mTab.getWebContents().getNavigationController().getLastCommittedEntryIndex();
}
private boolean shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent() {
if (mTab.getWebContents() == null) return false;
if (!mTab.getWebContents().getNavigationController().canGoToOffset(0)) return true;
// http://crbug/415948 : if the last committed entry index which was saved before this
// navigation is invalid, it means that this navigation is the first one since this tab was
// created.
// In such case, we would like to close this tab.
if (TabRedirectHandler.from(mTab).isOnNavigation()) {
return TabRedirectHandler.from(mTab)
.getLastCommittedEntryIndexBeforeStartingNavigation()
== TabRedirectHandler.INVALID_ENTRY_INDEX;
}
return false;
}
/**
* Called when Chrome decides to override URL loading and launch an intent or an asynchronous
* action.
*/
private void onOverrideUrlLoadingAndLaunchIntent() {
if (mTab.getWebContents() == null) return;
// Before leaving Chrome, close the empty child tab.
// If a new tab is created through JavaScript open to load this
// url, we would like to close it as we will load this url in a
// different Activity.
if (shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent()) {
if (mTab.getLaunchType() == TabLaunchType.FROM_EXTERNAL_APP) {
// Moving task back before closing the tab allows back button to function better
// when Chrome was an intermediate link redirector between two apps.
// crbug.com/487938.
mTab.getActivity().moveTaskToBack(false);
}
// Defer closing a tab (and the associated WebContents) till the navigation
// request and the throttle finishes the job with it.
PostTask.postTask(UiThreadTaskTraits.DEFAULT, new Runnable() {
@Override
public void run() {
mTab.getTabModelSelector().closeTab(mTab);
}
});
} else if (TabRedirectHandler.from(mTab).isOnNavigation()) {
int lastCommittedEntryIndexBeforeNavigation =
TabRedirectHandler.from(mTab)
.getLastCommittedEntryIndexBeforeStartingNavigation();
if (getLastCommittedEntryIndex() > lastCommittedEntryIndexBeforeNavigation) {
// http://crbug/426679 : we want to go back to the last committed entry index which
// was saved before this navigation, and remove the empty entries from the
// navigation history.
mClearAllForwardHistoryRequired = true;
mTab.getWebContents().getNavigationController().goToNavigationIndex(
lastCommittedEntryIndexBeforeNavigation);
}
}
}
private void logBlockedNavigationToDevToolsConsole(String url) {
int resId = mExternalNavHandler.canExternalAppHandleUrl(url)
? R.string.blocked_navigation_warning
: R.string.unreachable_navigation_warning;
mTab.getWebContents().addMessageToDevToolsConsole(
ConsoleMessageLevel.WARNING, mTab.getApplicationContext().getString(resId, url));
}
}