blob: 85e55d9e71455e103a6252420fa3d3d27ec04184 [file] [log] [blame]
// Copyright 2018 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/apps/intent_helper/apps_navigation_throttle.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/optional.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/browser_app_launcher.h"
#include "chrome/browser/apps/intent_helper/intent_picker_auto_display_service.h"
#include "chrome/browser/apps/intent_helper/page_transition_util.h"
#include "chrome/browser/extensions/menu_manager.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_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/intent_picker_tab_helper.h"
#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
#include "chrome/common/chrome_features.h"
#include "components/page_load_metrics/browser/page_load_metrics_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/constants.h"
#include "third_party/blink/public/mojom/referrer.mojom.h"
#include "url/origin.h"
namespace {
// Returns true if |url| is a known and valid redirector that will redirect a
// navigation elsewhere.
bool IsGoogleRedirectorUrl(const GURL& url) {
// This currently only check for redirectors on the "google" domain.
if (!page_load_metrics::IsGoogleSearchHostname(url))
return false;
return url.path_piece() == "/url" && url.has_query();
}
// Compares the host name of the referrer and target URL to decide whether
// the navigation needs to be overridden.
bool ShouldOverrideUrlLoading(const GURL& previous_url,
const GURL& current_url) {
// When the navigation is initiated in a web page where sending a referrer
// is disabled, |previous_url| can be empty. In this case, we should open
// it in the desktop browser.
if (!previous_url.is_valid() || previous_url.is_empty())
return false;
// Also check |current_url| just in case.
if (!current_url.is_valid() || current_url.is_empty()) {
DVLOG(1) << "Unexpected URL: " << current_url << ", opening it in Chrome.";
return false;
}
// Check the scheme for both |previous_url| and |current_url| since an
// extension could have referred us (e.g. Google Docs).
if (!current_url.SchemeIsHTTPOrHTTPS() ||
previous_url.SchemeIs(extensions::kExtensionScheme)) {
return false;
}
// Skip URL redirectors that are intermediate pages redirecting towards a
// final URL.
if (IsGoogleRedirectorUrl(current_url))
return false;
return true;
}
GURL GetStartingGURL(content::NavigationHandle* navigation_handle) {
// This helps us determine a reference GURL for the current NavigationHandle.
// This is the order or preference: Referrer > LastCommittedURL >
// InitiatorOrigin. InitiatorOrigin *should* only be used on very rare cases,
// e.g. when the navigation goes from https: to http: on a new tab, thus
// losing the other potential referrers.
const GURL referrer_url = navigation_handle->GetReferrer().url;
if (referrer_url.is_valid() && !referrer_url.is_empty())
return referrer_url;
const GURL last_committed_url =
navigation_handle->GetWebContents()->GetLastCommittedURL();
if (last_committed_url.is_valid() && !last_committed_url.is_empty())
return last_committed_url;
const auto& initiator_origin = navigation_handle->GetInitiatorOrigin();
return initiator_origin.has_value() ? initiator_origin->GetURL() : GURL();
}
} // namespace
namespace apps {
// static
const char AppsNavigationThrottle::kUseBrowserForLink[] = "use_browser";
// static
std::unique_ptr<content::NavigationThrottle>
AppsNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
if (!handle->IsInMainFrame())
return nullptr;
content::WebContents* web_contents = handle->GetWebContents();
if (!CanCreate(web_contents))
return nullptr;
return std::make_unique<AppsNavigationThrottle>(handle);
}
// static
void AppsNavigationThrottle::ShowIntentPickerBubble(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url) {
std::vector<IntentPickerAppInfo> apps = FindPwaForUrl(web_contents, url, {});
bool show_persistence_options = ShouldShowPersistenceOptions(apps);
ShowIntentPickerBubbleForApps(
web_contents, std::move(apps),
/*show_stay_in_chrome=*/show_persistence_options,
/*show_remember_selection=*/show_persistence_options,
base::BindOnce(&OnIntentPickerClosed, web_contents,
ui_auto_display_service, url));
}
// static
void AppsNavigationThrottle::OnIntentPickerClosed(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url,
const std::string& launch_name,
PickerEntryType entry_type,
IntentPickerCloseReason close_reason,
bool should_persist) {
const bool should_launch_app =
close_reason == IntentPickerCloseReason::OPEN_APP;
switch (entry_type) {
case PickerEntryType::kWeb:
if (should_launch_app)
web_app::ReparentWebContentsIntoAppBrowser(web_contents, launch_name);
break;
case PickerEntryType::kUnknown:
// We reach here if the picker was closed without an app being chosen,
// e.g. due to the tab being closed. Keep count of this scenario so we can
// stop the UI from showing after 2+ dismissals.
if (close_reason == IntentPickerCloseReason::DIALOG_DEACTIVATED) {
if (ui_auto_display_service)
ui_auto_display_service->IncrementCounter(url);
}
break;
case PickerEntryType::kArc:
case PickerEntryType::kDevice:
case PickerEntryType::kMacNative:
NOTREACHED();
}
}
// static
bool AppsNavigationThrottle::IsGoogleRedirectorUrlForTesting(const GURL& url) {
return IsGoogleRedirectorUrl(url);
}
// static
bool AppsNavigationThrottle::ShouldOverrideUrlLoadingForTesting(
const GURL& previous_url,
const GURL& current_url) {
return ShouldOverrideUrlLoading(previous_url, current_url);
}
// static
void AppsNavigationThrottle::ShowIntentPickerBubbleForApps(
content::WebContents* web_contents,
std::vector<IntentPickerAppInfo> apps,
bool show_stay_in_chrome,
bool show_remember_selection,
IntentPickerResponse callback) {
if (apps.empty())
return;
// It should be safe to bind |web_contents| since closing the current tab will
// close the intent picker and run the callback prior to the WebContents being
// deallocated.
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser)
return;
IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
browser->window()->ShowIntentPickerBubble(
std::move(apps), show_stay_in_chrome, show_remember_selection,
PageActionIconType::kIntentPicker, base::nullopt, std::move(callback));
}
AppsNavigationThrottle::AppsNavigationThrottle(
content::NavigationHandle* navigation_handle)
: content::NavigationThrottle(navigation_handle),
ui_displayed_(false),
ui_auto_display_service_(
IntentPickerAutoDisplayService::Get(Profile::FromBrowserContext(
navigation_handle->GetWebContents()->GetBrowserContext()))),
navigate_from_link_(false) {
// |ui_auto_display_service_| can be null iff the call is coming from
// IntentPickerView. Since the pointer to our service is never modified
// (in case it is successfully created here) this check covers all the
// non-static methods in this class.
DCHECK(ui_auto_display_service_);
}
AppsNavigationThrottle::~AppsNavigationThrottle() = default;
const char* AppsNavigationThrottle::GetNameForLogging() {
return "AppsNavigationThrottle";
}
content::NavigationThrottle::ThrottleCheckResult
AppsNavigationThrottle::WillStartRequest() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
starting_url_ = GetStartingGURL(navigation_handle());
IntentPickerTabHelper::SetShouldShowIcon(
navigation_handle()->GetWebContents(), false);
return HandleRequest();
}
content::NavigationThrottle::ThrottleCheckResult
AppsNavigationThrottle::WillRedirectRequest() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// TODO(dominickn): Consider what to do when there is another URL during the
// same navigation that could be handled by apps. Two ideas are:
// 1) update the bubble with a mix of both app candidates (if different)
// 2) show a bubble based on the last url, thus closing all the previous ones
if (ui_displayed_)
return content::NavigationThrottle::PROCEED;
return HandleRequest();
}
// static
bool AppsNavigationThrottle::CanCreate(content::WebContents* web_contents) {
// Do not create the throttle if no apps can be installed.
// Do not create the throttle in incognito or for a prerender navigation.
if (web_contents->GetBrowserContext()->IsOffTheRecord() ||
prerender::PrerenderContents::FromWebContents(web_contents) != nullptr) {
return false;
}
// Do not create the throttle if there is no browser for the WebContents or we
// are already in an app browser. The former can happen if an initial
// navigation is reparented into a new app browser instance.
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser || browser->deprecated_is_app())
return false;
return true;
}
// static
AppsNavigationThrottle::Platform AppsNavigationThrottle::GetDestinationPlatform(
const std::string& selected_launch_name,
PickerAction picker_action) {
switch (picker_action) {
case PickerAction::ARC_APP_PRESSED:
case PickerAction::ARC_APP_PREFERRED_PRESSED:
return Platform::ARC;
case PickerAction::PWA_APP_PRESSED:
return Platform::PWA;
case PickerAction::MAC_NATIVE_APP_PRESSED:
return Platform::MAC_NATIVE;
case PickerAction::ERROR_BEFORE_PICKER:
case PickerAction::ERROR_AFTER_PICKER:
case PickerAction::DIALOG_DEACTIVATED:
case PickerAction::CHROME_PRESSED:
case PickerAction::CHROME_PREFERRED_PRESSED:
return Platform::CHROME;
case PickerAction::DEVICE_PRESSED:
return Platform::DEVICE;
case PickerAction::PREFERRED_ACTIVITY_FOUND:
case PickerAction::OBSOLETE_ALWAYS_PRESSED:
case PickerAction::OBSOLETE_JUST_ONCE_PRESSED:
case PickerAction::INVALID:
break;
}
NOTREACHED();
return Platform::ARC;
}
// static
AppsNavigationThrottle::PickerAction AppsNavigationThrottle::GetPickerAction(
PickerEntryType entry_type,
IntentPickerCloseReason close_reason,
bool should_persist) {
switch (close_reason) {
case IntentPickerCloseReason::ERROR_BEFORE_PICKER:
return PickerAction::ERROR_BEFORE_PICKER;
case IntentPickerCloseReason::ERROR_AFTER_PICKER:
return PickerAction::ERROR_AFTER_PICKER;
case IntentPickerCloseReason::DIALOG_DEACTIVATED:
return PickerAction::DIALOG_DEACTIVATED;
case IntentPickerCloseReason::PREFERRED_APP_FOUND:
return PickerAction::PREFERRED_ACTIVITY_FOUND;
case IntentPickerCloseReason::STAY_IN_CHROME:
return should_persist ? PickerAction::CHROME_PREFERRED_PRESSED
: PickerAction::CHROME_PRESSED;
case IntentPickerCloseReason::OPEN_APP:
switch (entry_type) {
case PickerEntryType::kUnknown:
NOTREACHED();
return PickerAction::INVALID;
case PickerEntryType::kArc:
return should_persist ? PickerAction::ARC_APP_PREFERRED_PRESSED
: PickerAction::ARC_APP_PRESSED;
case PickerEntryType::kWeb:
return PickerAction::PWA_APP_PRESSED;
case PickerEntryType::kDevice:
return PickerAction::DEVICE_PRESSED;
case PickerEntryType::kMacNative:
return PickerAction::MAC_NATIVE_APP_PRESSED;
}
}
NOTREACHED();
return PickerAction::INVALID;
}
std::vector<IntentPickerAppInfo> AppsNavigationThrottle::FindAppsForUrl(
content::WebContents* web_contents,
const GURL& url,
std::vector<IntentPickerAppInfo> apps) {
return FindPwaForUrl(web_contents, url, std::move(apps));
}
// static
std::vector<IntentPickerAppInfo> AppsNavigationThrottle::FindPwaForUrl(
content::WebContents* web_contents,
const GURL& url,
std::vector<IntentPickerAppInfo> apps) {
// Check if the current URL has an installed desktop PWA, and add that to
// the list of apps if it exists.
Profile* const profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
base::Optional<web_app::AppId> app_id =
web_app::FindInstalledAppWithUrlInScope(profile, url,
/*window_only=*/true);
if (!app_id)
return apps;
// TODO(crbug.com/1052707): Use AppIconManager to read PWA icons.
auto* menu_manager =
extensions::MenuManager::Get(web_contents->GetBrowserContext());
// Prefer the web and place apps of type PWA before apps of type ARC.
// TODO(crbug.com/824598): deterministically sort this list.
apps.emplace(apps.begin(), PickerEntryType::kWeb,
menu_manager->GetIconForExtension(*app_id), *app_id,
web_app::WebAppProviderBase::GetProviderBase(profile)
->registrar()
.GetAppShortName(*app_id));
return apps;
}
// static
void AppsNavigationThrottle::CloseOrGoBack(content::WebContents* web_contents) {
DCHECK(web_contents);
if (web_contents->GetController().CanGoBack())
web_contents->GetController().GoBack();
else
web_contents->ClosePage();
}
// static
bool AppsNavigationThrottle::ContainsOnlyPwasAndMacApps(
const std::vector<apps::IntentPickerAppInfo>& apps) {
return std::all_of(apps.begin(), apps.end(),
[](const apps::IntentPickerAppInfo& app_info) {
return app_info.type == PickerEntryType::kWeb ||
app_info.type == PickerEntryType::kMacNative;
});
}
// static
bool AppsNavigationThrottle::ShouldShowPersistenceOptions(
std::vector<apps::IntentPickerAppInfo>& apps) {
// There is no support persistence for PWA so the selection should be hidden
// if only PWAs are present.
// TODO(crbug.com/826982): Provide the "Remember my choice" option when the
// app registry can support persistence for PWAs.
// This function is also used to hide the "Stay In Chrome" button when the
// "Remember my choice" option is hidden such that the bubble is easy to
// understand.
// TODO(avi): When Chrome gains a UI for managing the persistence of PWAs,
// reuse that UI for managing the persistent behavior of Universal Links.
return !ContainsOnlyPwasAndMacApps(apps);
}
bool AppsNavigationThrottle::ShouldDeferNavigation(
content::NavigationHandle* handle) {
return false;
}
void AppsNavigationThrottle::ShowIntentPickerForApps(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url,
std::vector<IntentPickerAppInfo> apps,
IntentPickerResponse callback) {
if (apps.empty()) {
IntentPickerTabHelper::SetShouldShowIcon(web_contents, false);
ui_displayed_ = false;
return;
}
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser)
return;
const PickerShowState picker_show_state =
GetPickerShowState(apps, web_contents, url);
switch (picker_show_state) {
case PickerShowState::kOmnibox:
ui_displayed_ = false;
IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
break;
case PickerShowState::kPopOut: {
bool show_persistence_options = ShouldShowPersistenceOptions(apps);
ShowIntentPickerBubbleForApps(
web_contents, std::move(apps),
/*show_stay_in_chrome=*/show_persistence_options,
/*show_remember_selection=*/show_persistence_options,
std::move(callback));
break;
}
default:
NOTREACHED();
}
}
AppsNavigationThrottle::PickerShowState
AppsNavigationThrottle::GetPickerShowState(
const std::vector<IntentPickerAppInfo>& apps_for_picker,
content::WebContents* web_contents,
const GURL& url) {
return PickerShowState::kOmnibox;
}
IntentPickerResponse AppsNavigationThrottle::GetOnPickerClosedCallback(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url) {
return base::BindOnce(&OnIntentPickerClosed, web_contents,
ui_auto_display_service, url);
}
bool AppsNavigationThrottle::navigate_from_link() const {
return navigate_from_link_;
}
content::NavigationThrottle::ThrottleCheckResult
AppsNavigationThrottle::HandleRequest() {
content::NavigationHandle* handle = navigation_handle();
// If the navigation happened without changing document or the
// navigation resulted in an error page, don't check intent for the
// navigation.
if (handle->IsSameDocument() || handle->IsErrorPage())
return content::NavigationThrottle::PROCEED;
DCHECK(!ui_displayed_);
navigate_from_link_ = false;
// Always handle http(s) <form> submissions in Chrome for two reasons: 1) we
// don't have a way to send POST data to ARC, and 2) intercepting http(s) form
// submissions is not very important because such submissions are usually
// done within the same domain. ShouldOverrideUrlLoading() below filters out
// such submissions anyway.
constexpr bool kAllowFormSubmit = false;
// Ignore navigations with the CLIENT_REDIRECT qualifier on.
constexpr bool kAllowClientRedirect = true;
ui::PageTransition page_transition = handle->GetPageTransition();
content::WebContents* web_contents = handle->GetWebContents();
const GURL& url = handle->GetURL();
if (!ShouldIgnoreNavigation(page_transition, kAllowFormSubmit,
kAllowClientRedirect) &&
!handle->WasStartedFromContextMenu()) {
navigate_from_link_ = true;
}
MaybeRemoveComingFromArcFlag(web_contents, starting_url_, url);
if (!ShouldOverrideUrlLoading(starting_url_, url))
return content::NavigationThrottle::PROCEED;
if (ShouldDeferNavigation(handle)) {
// Handling is now deferred to ArcIntentPickerAppFetcher, which
// asynchronously queries ARC for apps, and runs
// OnDeferredNavigationProcessed() with an action based on whether an
// acceptable app was found and user consent to open received. We assume the
// UI is shown or a preferred app was found; reset to false if we resume the
// navigation.
ui_displayed_ = true;
return content::NavigationThrottle::DEFER;
}
if (CaptureExperimentalTabStripWebAppScopeNavigations(web_contents, handle))
return content::NavigationThrottle::CANCEL_AND_IGNORE;
// We didn't query ARC, so proceed with the navigation and query if we have an
// installed desktop PWA to handle the URL.
std::vector<IntentPickerAppInfo> apps = FindAppsForUrl(web_contents, url, {});
if (!apps.empty())
ui_displayed_ = true;
ShowIntentPickerForApps(
web_contents, ui_auto_display_service_, url, std::move(apps),
GetOnPickerClosedCallback(web_contents, ui_auto_display_service_, url));
return content::NavigationThrottle::PROCEED;
}
bool AppsNavigationThrottle::CaptureExperimentalTabStripWebAppScopeNavigations(
content::WebContents* web_contents,
content::NavigationHandle* handle) const {
if (!navigate_from_link())
return false;
if (!base::FeatureList::IsEnabled(features::kDesktopPWAsTabStrip) ||
!base::FeatureList::IsEnabled(
features::kDesktopPWAsTabStripLinkCapturing)) {
return false;
}
Profile* const profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
web_app::WebAppProviderBase* provider =
web_app::WebAppProviderBase::GetProviderBase(profile);
if (!provider)
return false;
base::Optional<web_app::AppId> app_id =
provider->registrar().FindInstalledAppWithUrlInScope(
handle->GetURL(), /*window_only=*/true);
if (!app_id)
return false;
if (!provider->registrar().IsInExperimentalTabbedWindowMode(*app_id))
return false;
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (web_app::AppBrowserController::IsForWebAppBrowser(browser, *app_id)) {
// Already in the app window; navigation already captured.
return false;
}
apps::AppLaunchParams launch_params(
*app_id, apps::mojom::LaunchContainer::kLaunchContainerWindow,
WindowOpenDisposition::CURRENT_TAB,
apps::mojom::AppLaunchSource::kSourceUrlHandler);
launch_params.override_url = handle->GetURL();
apps::AppServiceProxyFactory::GetForProfile(profile)
->BrowserAppLauncher()
.LaunchAppWithParams(launch_params);
return true;
}
} // namespace apps