blob: 6621e92bbee5f4a2613251787a684ac3ae1799f0 [file] [log] [blame]
// Copyright 2017 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.
#include "chrome/browser/extensions/bookmark_app_navigation_throttle.h"
#include <memory>
#include "base/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/extensions/launch_util.h"
#include "chrome/browser/prerender/prerender_contents.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/extensions/app_launch_params.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/common/extensions/api/url_handlers/url_handlers_parser.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"
using content::BrowserThread;
namespace extensions {
using ProcessNavigationResult =
BookmarkAppNavigationThrottle::ProcessNavigationResult;
namespace {
// Non-app site navigations: The majority of navigations will be in-browser to
// sites for which there is no app installed. These navigations offer no insight
// so we avoid recording their outcome.
void RecordProcessNavigationResult(ProcessNavigationResult result) {
UMA_HISTOGRAM_ENUMERATION("Extensions.BookmarkApp.NavigationResult", result,
ProcessNavigationResult::kCount);
}
void RecordProceedWithTransitionType(ui::PageTransition transition_type) {
if (PageTransitionCoreTypeIs(transition_type, ui::PAGE_TRANSITION_LINK)) {
// Link navigations are a special case and shouldn't use this code path.
NOTREACHED();
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_TYPED)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionTyped);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_AUTO_BOOKMARK)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionAutoBookmark);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_AUTO_SUBFRAME)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionAutoSubframe);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_MANUAL_SUBFRAME)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionManualSubframe);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_GENERATED)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionGenerated);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionAutoToplevel);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_FORM_SUBMIT)) {
// Form navigations are a special case and shouldn't use this code path.
// TODO(crbug.com/772803): Add NOTREACHED() once form navigations are
// handled.
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_RELOAD)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionReload);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_KEYWORD)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionKeyword);
} else if (PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_KEYWORD_GENERATED)) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionKeywordGenerated);
} else {
NOTREACHED();
}
}
bool IsWindowedBookmarkApp(const Extension* app,
content::BrowserContext* context) {
if (!app || !app->from_bookmark())
return false;
if (GetLaunchContainer(extensions::ExtensionPrefs::Get(context), app) !=
LAUNCH_CONTAINER_WINDOW) {
return false;
}
return true;
}
scoped_refptr<const Extension> GetAppForURL(
const GURL& url,
const content::WebContents* web_contents) {
content::BrowserContext* context = web_contents->GetBrowserContext();
for (scoped_refptr<const extensions::Extension> app :
ExtensionRegistry::Get(context)->enabled_extensions()) {
if (!IsWindowedBookmarkApp(app.get(), context))
continue;
const UrlHandlerInfo* url_handler =
UrlHandlers::FindMatchingUrlHandler(app.get(), url);
if (!url_handler)
continue;
return app;
}
return nullptr;
}
} // namespace
// static
std::unique_ptr<content::NavigationThrottle>
BookmarkAppNavigationThrottle::MaybeCreateThrottleFor(
content::NavigationHandle* navigation_handle) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DVLOG(1) << "Considering URL for interception: "
<< navigation_handle->GetURL().spec();
if (!navigation_handle->IsInMainFrame()) {
DVLOG(1) << "Don't intercept: Navigation is not in main frame.";
return nullptr;
}
content::BrowserContext* browser_context =
navigation_handle->GetWebContents()->GetBrowserContext();
Profile* profile = Profile::FromBrowserContext(browser_context);
if (profile->GetProfileType() == Profile::INCOGNITO_PROFILE) {
DVLOG(1) << "Don't intercept: Navigation is in incognito.";
return nullptr;
}
DVLOG(1) << "Attaching Bookmark App Navigation Throttle.";
return std::make_unique<extensions::BookmarkAppNavigationThrottle>(
navigation_handle);
}
BookmarkAppNavigationThrottle::BookmarkAppNavigationThrottle(
content::NavigationHandle* navigation_handle)
: content::NavigationThrottle(navigation_handle), weak_ptr_factory_(this) {}
BookmarkAppNavigationThrottle::~BookmarkAppNavigationThrottle() {}
const char* BookmarkAppNavigationThrottle::GetNameForLogging() {
return "BookmarkAppNavigationThrottle";
}
content::NavigationThrottle::ThrottleCheckResult
BookmarkAppNavigationThrottle::WillStartRequest() {
return ProcessNavigation(false /* is_redirect */);
}
content::NavigationThrottle::ThrottleCheckResult
BookmarkAppNavigationThrottle::WillRedirectRequest() {
return ProcessNavigation(true /* is_redirect */);
}
content::NavigationThrottle::ThrottleCheckResult
BookmarkAppNavigationThrottle::ProcessNavigation(bool is_redirect) {
scoped_refptr<const Extension> target_app = GetTargetApp();
if (navigation_handle()->WasStartedFromContextMenu()) {
DVLOG(1) << "Don't intercept: Navigation started from the context menu.";
// See "Non-app site navigations" note above.
if (target_app) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedStartedFromContextMenu);
}
return content::NavigationThrottle::PROCEED;
}
ui::PageTransition transition_type = navigation_handle()->GetPageTransition();
// When launching an app, if the page redirects to an out-of-scope URL, then
// continue the navigation in a regular browser window. (Launching an app
// results in an AUTO_BOOKMARK transition).
//
// Note that for non-redirecting app launches, GetAppForWindow() might return
// null, because the navigation's WebContents might not be attached to a
// window yet.
//
// TODO(crbug.com/789051): Possibly fall through to the logic below to
// open target in app window, if it belongs to an app.
if (is_redirect && PageTransitionCoreTypeIs(
transition_type, ui::PAGE_TRANSITION_AUTO_BOOKMARK)) {
auto app_for_window = GetAppForWindow();
// If GetAppForWindow returned nullptr, we are already in the browser, so
// don't open a new tab.
if (app_for_window && app_for_window != GetTargetApp()) {
DVLOG(1) << "Out-of-scope navigation during launch. Opening in Chrome.";
RecordProcessNavigationResult(
ProcessNavigationResult::kOpenInChromeProceedOutOfScopeLaunch);
Browser* browser = chrome::FindBrowserWithWebContents(
navigation_handle()->GetWebContents());
DCHECK(browser);
chrome::OpenInChrome(browser);
return content::NavigationThrottle::PROCEED;
}
}
if (!PageTransitionCoreTypeIs(transition_type, ui::PAGE_TRANSITION_LINK) &&
!PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_FORM_SUBMIT)) {
DVLOG(1) << "Don't intercept: Transition type is "
<< PageTransitionGetCoreTransitionString(transition_type);
// We are in one of three possible states:
// 1. In-browser no-target-app navigations,
// 2. In-browser same-scope navigations, or
// 3. In-app same-scope navigations
// Ignore (1) since that's the majority of navigations and offer no insight.
if (target_app)
RecordProceedWithTransitionType(transition_type);
return content::NavigationThrottle::PROCEED;
}
int32_t transition_qualifier = PageTransitionGetQualifier(transition_type);
if (transition_qualifier & ui::PAGE_TRANSITION_FORWARD_BACK) {
DVLOG(1) << "Don't intercept: Forward or back navigation.";
// See "Non-app site navigations" note above.
if (target_app) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionForwardBack);
}
return content::NavigationThrottle::PROCEED;
}
if (transition_qualifier & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) {
DVLOG(1) << "Don't intercept: Address bar navigation.";
// See "Non-app site navigations" note above.
if (target_app) {
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedTransitionFromAddressBar);
}
return content::NavigationThrottle::PROCEED;
}
scoped_refptr<const Extension> app_for_window = GetAppForWindow();
if (app_for_window == target_app) {
if (app_for_window) {
DVLOG(1) << "Don't intercept: The target URL is in the same scope as the "
<< "current app.";
// We know we are navigating within the same app window (both
// |app_for_window| and |target_app| are the same and non-null). This is
// relevant, so record the result.
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedInAppSameScope);
} else {
DVLOG(1) << "No matching Bookmark App for URL: "
<< navigation_handle()->GetURL();
// See "Non-app site navigations" note above.
}
DVLOG(1) << "Don't intercept: The target URL is in the same scope as the "
<< "current app.";
return content::NavigationThrottle::PROCEED;
}
// If this is a browser tab, and the user is submitting a form, then keep the
// navigation in the browser tab.
if (!app_for_window &&
PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_FORM_SUBMIT)) {
DVLOG(1) << "Keep form submissions in the browser.";
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedInBrowserFormSubmission);
return content::NavigationThrottle::PROCEED;
}
// If this is a browser tab, and the current and target URL are within-scope
// of the same app, don't intercept the navigation.
// This ensures that navigating from
// https://www.youtube.com/ to https://www.youtube.com/some_video doesn't
// open a new app window if the Youtube app is installed, but navigating from
// https://www.google.com/ to https://www.google.com/maps does open a new
// app window if only the Maps app is installed.
if (!app_for_window && target_app == GetAppForCurrentURL()) {
DVLOG(1) << "Don't intercept: Keep same-app navigations in the browser.";
RecordProcessNavigationResult(
ProcessNavigationResult::kProceedInBrowserSameScope);
return content::NavigationThrottle::PROCEED;
}
if (target_app) {
auto* prerender_contents = prerender::PrerenderContents::FromWebContents(
navigation_handle()->GetWebContents());
if (prerender_contents) {
// If prerendering, don't launch the app but abort the navigation.
prerender_contents->Destroy(
prerender::FINAL_STATUS_NAVIGATION_INTERCEPTED);
RecordProcessNavigationResult(
ProcessNavigationResult::kCancelPrerenderContents);
return content::NavigationThrottle::CANCEL_AND_IGNORE;
}
content::NavigationEntry* last_entry = navigation_handle()
->GetWebContents()
->GetController()
.GetLastCommittedEntry();
// We are about to open a new app window context. Record the time since the
// last navigation in this context. (If it is very small, this context
// probably redirected immediately, which is a bad user experience.)
if (last_entry && !last_entry->GetTimestamp().is_null()) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Extensions.BookmarkApp.OpenAppDeltaSinceLastNavigation",
base::Time::Now() - last_entry->GetTimestamp());
}
content::NavigationThrottle::ThrottleCheckResult result =
OpenInAppWindowAndCloseTabIfNecessary(target_app);
ProcessNavigationResult open_in_app_result;
switch (result.action()) {
case content::NavigationThrottle::DEFER:
open_in_app_result =
ProcessNavigationResult::kDeferOpenAppCloseEmptyWebContents;
break;
case content::NavigationThrottle::CANCEL_AND_IGNORE:
open_in_app_result = ProcessNavigationResult::kCancelOpenedApp;
break;
default:
NOTREACHED();
open_in_app_result =
ProcessNavigationResult::kDeferOpenAppCloseEmptyWebContents;
}
RecordProcessNavigationResult(open_in_app_result);
return result;
}
if (app_for_window) {
// The experience when navigating to an out-of-scope website inside an app
// window is not great, so we bounce these navigations back to the browser.
// TODO(crbug.com/774895): Stop bouncing back to the browser once the
// experience for out-of-scope navigations improves.
DVLOG(1) << "Open in new tab.";
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&BookmarkAppNavigationThrottle::OpenInNewTab,
weak_ptr_factory_.GetWeakPtr()));
RecordProcessNavigationResult(
ProcessNavigationResult::kDeferOpenNewTabInAppOutOfScope);
return content::NavigationThrottle::DEFER;
}
DVLOG(1) << "No matching Bookmark App for URL: "
<< navigation_handle()->GetURL();
return content::NavigationThrottle::PROCEED;
}
content::NavigationThrottle::ThrottleCheckResult
BookmarkAppNavigationThrottle::OpenInAppWindowAndCloseTabIfNecessary(
scoped_refptr<const Extension> target_app) {
content::WebContents* source = navigation_handle()->GetWebContents();
if (source->GetController().IsInitialNavigation()) {
// When a new WebContents has no opener, the first navigation will happen
// synchronously. This could result in us opening the app and then focusing
// the original WebContents. To avoid this we open the app asynchronously.
if (!source->HasOpener()) {
DVLOG(1) << "Deferring opening app.";
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&BookmarkAppNavigationThrottle::OpenBookmarkApp,
weak_ptr_factory_.GetWeakPtr(), target_app));
} else {
OpenBookmarkApp(target_app);
}
// According to NavigationThrottle::WillStartRequest's documentation closing
// a WebContents should be done asynchronously to avoid UAFs. Closing the
// WebContents will cancel the navigation.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&BookmarkAppNavigationThrottle::CloseWebContents,
weak_ptr_factory_.GetWeakPtr()));
return content::NavigationThrottle::DEFER;
}
OpenBookmarkApp(target_app);
return content::NavigationThrottle::CANCEL_AND_IGNORE;
}
void BookmarkAppNavigationThrottle::OpenBookmarkApp(
scoped_refptr<const Extension> bookmark_app) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
content::WebContents* source = navigation_handle()->GetWebContents();
content::BrowserContext* browser_context = source->GetBrowserContext();
Profile* profile = Profile::FromBrowserContext(browser_context);
AppLaunchParams launch_params(
profile, bookmark_app.get(), extensions::LAUNCH_CONTAINER_WINDOW,
WindowOpenDisposition::CURRENT_TAB, extensions::SOURCE_URL_HANDLER);
launch_params.override_url = navigation_handle()->GetURL();
DVLOG(1) << "Opening app.";
OpenApplication(launch_params);
}
void BookmarkAppNavigationThrottle::CloseWebContents() {
DVLOG(1) << "Closing empty tab.";
navigation_handle()->GetWebContents()->Close();
}
void BookmarkAppNavigationThrottle::OpenInNewTab() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
content::WebContents* source = navigation_handle()->GetWebContents();
content::OpenURLParams url_params(navigation_handle()->GetURL(),
navigation_handle()->GetReferrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
navigation_handle()->GetPageTransition(),
navigation_handle()->IsRendererInitiated());
url_params.uses_post = navigation_handle()->IsPost();
url_params.post_data = navigation_handle()->GetResourceRequestBody();
url_params.redirect_chain = navigation_handle()->GetRedirectChain();
url_params.frame_tree_node_id = navigation_handle()->GetFrameTreeNodeId();
url_params.user_gesture = navigation_handle()->HasUserGesture();
url_params.started_from_context_menu =
navigation_handle()->WasStartedFromContextMenu();
source->OpenURL(url_params);
CancelDeferredNavigation(content::NavigationThrottle::CANCEL_AND_IGNORE);
}
scoped_refptr<const Extension>
BookmarkAppNavigationThrottle::GetAppForWindow() {
SCOPED_UMA_HISTOGRAM_TIMER("Extensions.BookmarkApp.GetAppForWindowDuration");
content::WebContents* source = navigation_handle()->GetWebContents();
content::BrowserContext* context = source->GetBrowserContext();
Browser* browser = chrome::FindBrowserWithWebContents(source);
if (!browser || !browser->is_app())
return nullptr;
const Extension* app = ExtensionRegistry::Get(context)->GetExtensionById(
web_app::GetExtensionIdFromApplicationName(browser->app_name()),
extensions::ExtensionRegistry::ENABLED);
if (!IsWindowedBookmarkApp(app, context))
return nullptr;
// Bookmark Apps for installable websites have scope.
// TODO(crbug.com/774918): Replace once there is a more explicit indicator
// of a Bookmark App for an installable website.
if (UrlHandlers::GetUrlHandlers(app) == nullptr)
return nullptr;
return app;
}
scoped_refptr<const Extension> BookmarkAppNavigationThrottle::GetTargetApp() {
SCOPED_UMA_HISTOGRAM_TIMER("Extensions.BookmarkApp.GetTargetAppDuration");
return GetAppForURL(navigation_handle()->GetURL(),
navigation_handle()->GetWebContents());
}
scoped_refptr<const Extension>
BookmarkAppNavigationThrottle::GetAppForCurrentURL() {
SCOPED_UMA_HISTOGRAM_TIMER(
"Extensions.BookmarkApp.GetAppForCurrentURLDuration");
return GetAppForURL(navigation_handle()
->GetWebContents()
->GetMainFrame()
->GetLastCommittedURL(),
navigation_handle()->GetWebContents());
}
} // namespace extensions