blob: bb4c8a42ee8d7e7dca36d58505f00936a5384c1c [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h"
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/task/single_thread_task_runner.h"
#include "base/types/cxx23_to_underlying.h"
#include "chrome/browser/apps/link_capturing/link_capturing_tab_data.h"
#include "chrome/browser/preloading/prefetch/no_state_prefetch/chrome_no_state_prefetch_contents_delegate.h" // nogncheck https://crbug.com/1474116
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h" // nogncheck https://crbug.com/1474116
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h" // nogncheck https://crbug.com/1474116
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_finder.h" // nogncheck https://crbug.com/1474984
#include "chrome/browser/ui/web_applications/navigation_capturing_process.h" // nogncheck https://crbug.com/377760841
#include "chrome/browser/web_applications/link_capturing_features.h"
#include "chrome/browser/web_applications/web_app_ui_manager.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/page_load_metrics/google/browser/google_url_util.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/constants.h"
#include "url/origin.h"
namespace apps {
namespace {
using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
// Retrieves the 'starting' url for the given navigation handle. This considers
// the referrer url, last committed url, and the initiator origin.
GURL GetStartingUrl(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();
}
// Returns if the navigation appears to be a link navigation, but not from an
// HTML post form.
bool IsNavigateFromNonFormNonContextMenuLink(
content::NavigationHandle* navigation_handle) {
// 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;
ui::PageTransition page_transition = navigation_handle->GetPageTransition();
return LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
page_transition, kAllowFormSubmit,
navigation_handle->IsInFencedFrameTree(),
navigation_handle->HasUserGesture()) &&
!navigation_handle->WasStartedFromContextMenu();
}
bool IsNavigationUserInitiated(content::NavigationHandle* handle) {
switch (handle->GetNavigationInitiatorActivationAndAdStatus()) {
case blink::mojom::NavigationInitiatorActivationAndAdStatus::
kDidNotStartWithTransientActivation:
return false;
case blink::mojom::NavigationInitiatorActivationAndAdStatus::
kStartedWithTransientActivationFromNonAd:
case blink::mojom::NavigationInitiatorActivationAndAdStatus::
kStartedWithTransientActivationFromAd:
return true;
}
}
} // namespace
bool LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
ui::PageTransition page_transition,
bool allow_form_submit,
bool is_in_fenced_frame_tree,
bool has_user_gesture) {
// Navigations inside fenced frame trees are marked with
// PAGE_TRANSITION_AUTO_SUBFRAME in order not to add session history items
// (see https://crrev.com/c/3265344). So we only check |has_user_gesture|.
if (is_in_fenced_frame_tree) {
DCHECK(ui::PageTransitionCoreTypeIs(page_transition,
ui::PAGE_TRANSITION_AUTO_SUBFRAME));
return has_user_gesture;
}
// Mask out any redirect qualifiers
page_transition = MaskOutPageTransition(page_transition,
ui::PAGE_TRANSITION_IS_REDIRECT_MASK);
if (!ui::PageTransitionCoreTypeIs(page_transition,
ui::PAGE_TRANSITION_LINK) &&
!(allow_form_submit &&
ui::PageTransitionCoreTypeIs(page_transition,
ui::PAGE_TRANSITION_FORM_SUBMIT))) {
// Do not handle the |url| if this event wasn't spawned by the user clicking
// on a link.
return false;
}
if (base::to_underlying(ui::PageTransitionGetQualifier(page_transition)) !=
0) {
// Qualifiers indicate that this navigation was the result of a click on a
// forward/back button, or typing in the URL bar. Don't handle any of those
// types of navigations.
return false;
}
return true;
}
ui::PageTransition LinkCapturingNavigationThrottle::MaskOutPageTransition(
ui::PageTransition page_transition,
ui::PageTransition mask) {
return ui::PageTransitionFromInt(page_transition & ~mask);
}
// static
bool LinkCapturingNavigationThrottle::
IsEmptyDanglingWebContentsAfterLinkCapture(
content::NavigationHandle* handle) {
const GURL& last_committed_url =
handle->GetWebContents()->GetLastCommittedURL();
return !last_committed_url.is_valid() || last_committed_url.IsAboutBlank() ||
// Some navigations are via JavaScript `location.href = url;`.
// This can be used for user clicked buttons as well as redirects.
// Check whether the action was in the context of a user activation to
// distinguish redirects from click event handlers.
!IsNavigationUserInitiated(handle);
}
LinkCapturingNavigationThrottle::Delegate::~Delegate() = default;
// static
bool LinkCapturingNavigationThrottle::MaybeCreateAndAdd(
content::NavigationThrottleRegistry& registry,
std::unique_ptr<Delegate> delegate) {
// If the reimplementation params of the link capturing feature flag is
// enabled for all navigations, turn off the "old" link capturing behavior.
if (features::IsNavigationCapturingReimplEnabled()) {
return false;
}
// Don't handle navigations in subframes or main frames that are in a nested
// frame tree (e.g. fenced-frame). We specifically allow
// prerendering navigations so that we can destroy the prerender. Opening an
// app must only happen when the user intentionally navigates; however, for a
// prerender, the prerender-activating navigation doesn't run throttles so we
// must cancel it during initial loading to get a standard (non-prerendering)
// navigation at link-click-time.
content::NavigationHandle& handle = registry.GetNavigationHandle();
if (!handle.IsInOutermostMainFrame()) {
return false;
}
content::WebContents* web_contents = handle.GetWebContents();
if (prerender::ChromeNoStatePrefetchContentsDelegate::FromWebContents(
web_contents) != nullptr) {
return false;
}
if (delegate->ShouldCancelThrottleCreation(registry)) {
return false;
}
// If there is no browser attached to this web-contents yet, this was a
// middle-mouse-click action, which should not be captured.
// TODO(crbug.com/40279479): Find a better way to detect middle-clicks.
if (chrome::FindBrowserWithTab(web_contents) == nullptr) {
return false;
}
// Never link capture links that open in a popup window. Popups are closely
// associated with the tab that opened them, so the popup should open in the
// same (app/non-app) context as its opener.
WindowOpenDisposition disposition =
GetLinkCapturingSourceDisposition(web_contents);
if (disposition == WindowOpenDisposition::NEW_POPUP &&
!web_contents->GetLastCommittedURL().is_valid()) {
return false;
}
registry.AddThrottle(base::WrapUnique(
new LinkCapturingNavigationThrottle(registry, std::move(delegate))));
return true;
}
LinkCapturingNavigationThrottle::LaunchCallbackForTesting&
LinkCapturingNavigationThrottle::GetLinkCaptureLaunchCallbackForTesting() {
static base::NoDestructor<LaunchCallbackForTesting> callback;
return *callback;
}
LinkCapturingNavigationThrottle::~LinkCapturingNavigationThrottle() = default;
const char* LinkCapturingNavigationThrottle::GetNameForLogging() {
return "LinkCapturingNavigationThrottle";
}
ThrottleCheckResult LinkCapturingNavigationThrottle::WillStartRequest() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
starting_url_ = GetStartingUrl(navigation_handle());
return HandleRequest();
}
ThrottleCheckResult LinkCapturingNavigationThrottle::WillRedirectRequest() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return HandleRequest();
}
// Returns true if |url| is a known and valid redirector that will redirect a
// navigation elsewhere.
// static
bool LinkCapturingNavigationThrottle::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();
}
// If the previous url and current url are not the same (AKA a redirection),
// determines if the redirection should be considered for an app launch. Returns
// false for redirections where:
// * `previous_url` is an extension.
// * `previous_url` is a google redirector.
// static
bool LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
const GURL& previous_url,
const GURL& current_url) {
// Check the scheme for both |previous_url| and |current_url| since an
// extension could have referred us (e.g. Google Docs).
if (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;
}
ThrottleCheckResult LinkCapturingNavigationThrottle::HandleRequest() {
content::NavigationHandle* handle = navigation_handle();
// Exit early if the reimplementation data is attached, to avoid running two
// different throttles simultaneously. Note: this cannot be checked in
// `MaybeCreate()` since the data might get attached after it's executed.
if (web_app::NavigationCapturingProcess::GetForNavigationHandle(*handle)) {
return content::NavigationThrottle::PROCEED;
}
// If the navigation will update the same document, don't consider as a
// capturable link.
if (handle->IsSameDocument()) {
return content::NavigationThrottle::PROCEED;
}
const GURL& url = handle->GetURL();
if (!url.is_valid()) {
DVLOG(1) << "Unexpected URL: " << url << ", opening in Chrome.";
return content::NavigationThrottle::PROCEED;
}
// Only http-style schemes are allowed.
if (!url.SchemeIsHTTPOrHTTPS()) {
return content::NavigationThrottle::PROCEED;
}
if (!ShouldOverrideUrlIfRedirected(starting_url_, url)) {
return content::NavigationThrottle::PROCEED;
}
bool is_navigation_from_link =
IsNavigateFromNonFormNonContextMenuLink(handle);
content::WebContents* web_contents = handle->GetWebContents();
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
std::optional<LaunchCallback> launch_link_capture =
delegate_->CreateLinkCaptureLaunchClosure(
profile, web_contents, url, is_navigation_from_link,
handle->GetRedirectChain().size());
if (!launch_link_capture.has_value()) {
return content::NavigationThrottle::PROCEED;
}
// If this is a prerender navigation that would otherwise launch an app, we
// must cancel it. We only want to launch an app once the URL is intentionally
// navigated to by the user. We cancel the navigation here so that when the
// link is clicked, we'll run NavigationThrottles again. If we leave the
// prerendering alive, the activating navigation won't run throttles.
if (handle->IsInPrerenderedMainFrame()) {
return content::NavigationThrottle::CANCEL_AND_IGNORE;
}
// Browser & profile keep-alives must be used to keep the browser & profile
// alive because the old window is required to be closed before the new app is
// launched, which will destroy the profile & browser if it is the last
// window.
// Why close the tab first? The way web contents currently work, closing a tab
// in a window will re-activate that window if there are more tabs there. So
// if we wait until after the launch completes to close the tab, then it will
// cause the old window to come to the front hiding the newly launched app
// window.
bool closed_web_contents = false;
std::unique_ptr<ScopedKeepAlive> browser_keep_alive;
std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive;
if (IsEmptyDanglingWebContentsAfterLinkCapture(handle)) {
browser_keep_alive = std::make_unique<ScopedKeepAlive>(
KeepAliveOrigin::APP_LAUNCH, KeepAliveRestartOption::ENABLED);
if (!profile->IsOffTheRecord()) {
profile_keep_alive = std::make_unique<ScopedProfileKeepAlive>(
profile, ProfileKeepAliveOrigin::kAppWindow);
}
web_contents->ClosePage();
closed_web_contents = true;
}
// Note: This callback currently serves to own the "keep alive" objects
// until the launch is complete.
base::OnceClosure launch_callback = base::BindOnce(
[](std::unique_ptr<ScopedKeepAlive> browser_keep_alive,
std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive,
bool closed_web_contents) {
if (GetLinkCaptureLaunchCallbackForTesting()) { // IN-TEST
std::move(GetLinkCaptureLaunchCallbackForTesting()) // IN-TEST
.Run(closed_web_contents); // IN-TEST
}
},
std::move(browser_keep_alive), std::move(profile_keep_alive),
closed_web_contents);
// The tab may have been closed, which runs async and causes the browser
// window to be refocused. Post a task to launch the app to ensure launching
// happens after the tab closed, otherwise the opened app window might be
// inactivated.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(launch_link_capture.value()),
std::move(launch_callback)));
return content::NavigationThrottle::CANCEL_AND_IGNORE;
}
LinkCapturingNavigationThrottle::LinkCapturingNavigationThrottle(
content::NavigationThrottleRegistry& registry,
std::unique_ptr<Delegate> delegate)
: content::NavigationThrottle(registry), delegate_(std::move(delegate)) {}
} // namespace apps