blob: bded6ffc2f4bc5b3a91eb06198bad7c6755f837d [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include <algorithm>
#include <memory>
#include <tuple>
#include <vector>
#include "base/containers/cxx20_erase.h"
#include "content/browser/browser_context_impl.h"
#include "content/browser/preloading/prefetch/prefetch_container.h"
#include "content/browser/preloading/prefetch/prefetch_params.h"
#include "content/browser/preloading/prefetch/prefetch_service.h"
#include "content/browser/preloading/prefetch/prefetch_serving_page_metrics_container.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/prefetch_metrics.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "net/http/http_no_vary_search_data.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/mojom/no_vary_search.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/origin.h"
namespace content {
namespace {
static PrefetchService* g_prefetch_service_for_testing = nullptr;
} // namespace
PrefetchDocumentManager::PrefetchDocumentManager(RenderFrameHost* rfh)
: DocumentUserData(rfh),
WebContentsObserver(WebContents::FromRenderFrameHost(rfh)),
no_vary_search_helper_(base::MakeRefCounted<NoVarySearchHelper>()) {}
PrefetchDocumentManager::~PrefetchDocumentManager() {
// On destruction, removes any owned prefetches from |PrefetchService|. Other
// prefetches associated by |this| are owned by |PrefetchService| and can
// still be used after the destruction of |this|.
PrefetchService* prefetch_service = GetPrefetchService();
if (!prefetch_service)
return;
for (const auto& prefetch_iter : owned_prefetches_) {
DCHECK(prefetch_iter.second);
prefetch_service->RemovePrefetch(
prefetch_iter.second->GetPrefetchContainerKey());
}
}
void PrefetchDocumentManager::DidStartNavigation(
NavigationHandle* navigation_handle) {
// Ignore navigations for a different RenderFrameHost.
if (render_frame_host().GetGlobalId() !=
navigation_handle->GetPreviousRenderFrameHostId()) {
DVLOG(1) << "PrefetchDocumentManager::DidStartNavigation() for "
<< navigation_handle->GetURL()
<< ": skipped (different RenderFrameHost)";
return;
}
// Ignores any same document navigations since we can't use prefetches to
// speed them up.
if (navigation_handle->IsSameDocument()) {
DVLOG(1) << "PrefetchDocumentManager::DidStartNavigation() for "
<< navigation_handle->GetURL() << ": skipped (same document)";
return;
}
// Create |PrefetchServingPageMetricsContainer| for potential navigation that
// might use a prefetch, and update it with metrics from the page load
// associated with |this|.
PrefetchServingPageMetricsContainer* serving_page_metrics_container =
PrefetchServingPageMetricsContainer::GetOrCreateForNavigationHandle(
*navigation_handle);
// Currently, prefetches can only be used with a navigation from the referring
// page and in the same tab. Eventually we will support other types of
// navigations where the prefetch is used in a different tab.
serving_page_metrics_container->SetSameTabAsPrefetchingTab(true);
// Get the prefetch for the URL being navigated to. If there is no prefetch
// for that URL, then check if there is an equivalent prefetch using
// No-Vary-Search equivalence. If there is not then stop.
auto prefetch_iter = all_prefetches_.find(navigation_handle->GetURL());
if (prefetch_iter == all_prefetches_.end() || !prefetch_iter->second) {
if (no_vary_search_support_enabled_ &&
base::FeatureList::IsEnabled(
network::features::kPrefetchNoVarySearch)) {
const auto no_vary_search_match_url =
GetNoVarySearchHelper().MatchUrl(navigation_handle->GetURL());
if (no_vary_search_match_url.has_value()) {
// Find the prefetched url matching |navigation_handle->GetURL()| based
// on No-Vary-Search in |all_prefetches_|.
prefetch_iter = all_prefetches_.find(no_vary_search_match_url.value());
}
}
}
if (prefetch_iter == all_prefetches_.end() || !prefetch_iter->second) {
DVLOG(1) << "PrefetchDocumentManager::DidStartNavigation() for "
<< navigation_handle->GetURL()
<< ": skipped (PrefetchContainer not found)";
return;
}
// If this prefetch has already been used with another navigation then stop.
if (prefetch_iter->second->HasPrefetchBeenConsideredToServe()) {
DVLOG(1) << "PrefetchDocumentManager::DidStartNavigation() for "
<< *prefetch_iter->second
<< ": skipped (already used for another navigation)";
return;
}
prefetch_iter->second->SetServingPageMetrics(
serving_page_metrics_container->GetWeakPtr());
prefetch_iter->second->UpdateServingPageMetrics();
// Inform |PrefetchService| of the navigation to the prefetch.
// |navigation_handle->GetURL()| and |prefetched_iter->second->GetURL()|
// might be different but be equivalent under No-Vary-Search.
PrefetchService* prefetch_service = GetPrefetchService();
if (prefetch_service) {
prefetch_service->PrepareToServe(navigation_handle->GetURL(),
prefetch_iter->second);
}
}
void PrefetchDocumentManager::ProcessCandidates(
std::vector<blink::mojom::SpeculationCandidatePtr>& candidates,
base::WeakPtr<SpeculationHostDevToolsObserver> devtools_observer) {
// Filter out candidates that can be handled by |PrefetchService| and
// determine the type of prefetch required.
// TODO(https://crbug.com/1299059): Once this code becomes enabled by default
// to handle all prefetches and the prefetch proxy code in chrome/browser/ is
// removed, then we can move the logic of which speculation candidates this
// code can handle up a layer to |SpeculationHostImpl|.
net::SchemefulSite referring_site(
render_frame_host().GetLastCommittedOrigin());
std::vector<std::tuple<GURL, PrefetchType, blink::mojom::Referrer,
network::mojom::NoVarySearchPtr,
blink::mojom::SpeculationInjectionWorld>>
prefetches;
auto should_process_entry =
[&](const blink::mojom::SpeculationCandidatePtr& candidate) {
// This code doesn't not support speculation candidates with the action
// of |blink::mojom::SpeculationAction::kPrefetchWithSubresources|. See
// https://crbug.com/1296309.
if (candidate->action != blink::mojom::SpeculationAction::kPrefetch) {
return false;
}
net::SchemefulSite prefetch_site(candidate->url);
prefetches.emplace_back(
candidate->url,
PrefetchType(
/*use_isolated_network_context=*/referring_site !=
prefetch_site,
/*use_prefetch_proxy=*/
candidate->requires_anonymous_client_ip_when_cross_origin,
candidate->eagerness),
*candidate->referrer, candidate->no_vary_search_expected.Clone(),
candidate->injection_world);
return true;
};
base::EraseIf(candidates, should_process_entry);
if (const auto& host_to_bypass = PrefetchBypassProxyForHost()) {
for (auto& [prefetch_url, prefetch_type, referrer, no_vary_search_expected,
world] : prefetches) {
if (prefetch_type.IsProxyRequiredWhenCrossOrigin() &&
prefetch_url.host() == *host_to_bypass) {
prefetch_type.SetProxyBypassedForTest();
}
}
}
for (auto& [prefetch_url, prefetch_type, referrer, no_vary_search_expected,
world] : prefetches) {
PrefetchUrl(prefetch_url, prefetch_type, referrer, no_vary_search_expected,
world, devtools_observer);
}
}
void PrefetchDocumentManager::PrefetchUrl(
const GURL& url,
const PrefetchType& prefetch_type,
const blink::mojom::Referrer& referrer,
const network::mojom::NoVarySearchPtr& mojo_no_vary_search_expected,
blink::mojom::SpeculationInjectionWorld world,
base::WeakPtr<SpeculationHostDevToolsObserver> devtools_observer) {
// Skip any prefetches that have already been requested.
auto prefetch_container_iter = all_prefetches_.find(url);
if (prefetch_container_iter != all_prefetches_.end() &&
prefetch_container_iter->second != nullptr) {
if (prefetch_container_iter->second->GetPrefetchType() != prefetch_type) {
// TODO(https://crbug.com/1299059): Handle changing the PrefetchType of an
// existing prefetch.
}
return;
}
absl::optional<net::HttpNoVarySearchData> no_vary_search_expected;
if (mojo_no_vary_search_expected) {
no_vary_search_expected =
NoVarySearchHelper::ParseHttpNoVarySearchDataFromMojom(
mojo_no_vary_search_expected);
}
// Create a new |PrefetchContainer| and take ownership of it
auto container = std::make_unique<PrefetchContainer>(
render_frame_host().GetGlobalId(), url, prefetch_type, referrer,
std::move(no_vary_search_expected), world,
weak_method_factory_.GetWeakPtr());
container->SetDevToolsObserver(std::move(devtools_observer));
if (base::FeatureList::IsEnabled(network::features::kPrefetchNoVarySearch)) {
container->SetNoVarySearchHelper(no_vary_search_helper_);
}
DVLOG(1) << *container << ": created";
base::WeakPtr<PrefetchContainer> weak_container = container->GetWeakPtr();
owned_prefetches_[url] = std::move(container);
all_prefetches_[url] = weak_container;
referring_page_metrics_.prefetch_attempted_count++;
// Send a reference of the new |PrefetchContainer| to |PrefetchService| to
// start the prefetch process.
PrefetchService* prefetch_service = GetPrefetchService();
if (prefetch_service) {
prefetch_service->PrefetchUrl(weak_container);
}
}
std::unique_ptr<PrefetchContainer>
PrefetchDocumentManager::ReleasePrefetchContainer(const GURL& url) {
DCHECK(owned_prefetches_.find(url) != owned_prefetches_.end());
std::unique_ptr<PrefetchContainer> prefetch_container =
std::move(owned_prefetches_[url]);
owned_prefetches_.erase(url);
return prefetch_container;
}
bool PrefetchDocumentManager::IsPrefetchAttemptFailedOrDiscarded(
const GURL& url) {
auto it = all_prefetches_.find(url);
if (it == all_prefetches_.end() || !it->second)
return true;
const auto& container = it->second;
if (!container->HasPrefetchStatus())
return false; // the container is not processed yet
switch (container->GetPrefetchStatus()) {
case PrefetchStatus::kPrefetchSuccessful:
case PrefetchStatus::kPrefetchResponseUsed:
return false;
case PrefetchStatus::kPrefetchNotEligibleUserHasCookies:
case PrefetchStatus::kPrefetchNotEligibleUserHasServiceWorker:
case PrefetchStatus::kPrefetchNotEligibleGoogleDomain:
case PrefetchStatus::kPrefetchNotEligibleSchemeIsNotHttps:
case PrefetchStatus::kPrefetchNotEligibleNonDefaultStoragePartition:
case PrefetchStatus::kPrefetchPositionIneligible:
case PrefetchStatus::kPrefetchIneligibleRetryAfter:
case PrefetchStatus::kPrefetchProxyNotAvailable:
case PrefetchStatus::kPrefetchNotEligibleHostIsNonUnique:
case PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled:
case PrefetchStatus::kPrefetchNotEligibleExistingProxy:
case PrefetchStatus::kPrefetchUsedNoProbe:
case PrefetchStatus::kPrefetchNotUsedProbeFailed:
case PrefetchStatus::kPrefetchNotStarted:
case PrefetchStatus::kPrefetchNotFinishedInTime:
case PrefetchStatus::kPrefetchFailedNetError:
case PrefetchStatus::kPrefetchFailedNon2XX:
case PrefetchStatus::kPrefetchFailedMIMENotSupported:
case PrefetchStatus::kNavigatedToLinkNotOnSRP:
case PrefetchStatus::kSubresourceThrottled:
case PrefetchStatus::kPrefetchUsedNoProbeWithNSP:
case PrefetchStatus::kPrefetchUsedProbeSuccessWithNSP:
case PrefetchStatus::kPrefetchNotUsedProbeFailedWithNSP:
case PrefetchStatus::kPrefetchUsedNoProbeNSPAttemptDenied:
case PrefetchStatus::kPrefetchUsedProbeSuccessNSPAttemptDenied:
case PrefetchStatus::kPrefetchNotUsedProbeFailedNSPAttemptDenied:
case PrefetchStatus::kPrefetchUsedNoProbeNSPNotStarted:
case PrefetchStatus::kPrefetchUsedProbeSuccessNSPNotStarted:
case PrefetchStatus::kPrefetchNotUsedProbeFailedNSPNotStarted:
case PrefetchStatus::kPrefetchIsPrivacyDecoy:
case PrefetchStatus::kPrefetchIsStale:
case PrefetchStatus::kPrefetchIsStaleWithNSP:
case PrefetchStatus::kPrefetchIsStaleNSPAttemptDenied:
case PrefetchStatus::kPrefetchIsStaleNSPNotStarted:
case PrefetchStatus::kPrefetchNotUsedCookiesChanged:
case PrefetchStatus::kPrefetchFailedRedirectsDisabled_DEPRECATED:
case PrefetchStatus::kPrefetchNotEligibleBrowserContextOffTheRecord:
case PrefetchStatus::kPrefetchHeldback:
case PrefetchStatus::kPrefetchAllowed:
case PrefetchStatus::kPrefetchFailedInvalidRedirect:
case PrefetchStatus::kPrefetchFailedIneligibleRedirect:
case PrefetchStatus::kPrefetchFailedPerPageLimitExceeded:
case PrefetchStatus::
kPrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy:
return true;
}
}
// static
void PrefetchDocumentManager::SetPrefetchServiceForTesting(
PrefetchService* prefetch_service) {
g_prefetch_service_for_testing = prefetch_service;
}
PrefetchService* PrefetchDocumentManager::GetPrefetchService() const {
if (g_prefetch_service_for_testing) {
return g_prefetch_service_for_testing;
}
DCHECK(BrowserContextImpl::From(render_frame_host().GetBrowserContext())
->GetPrefetchService());
return BrowserContextImpl::From(render_frame_host().GetBrowserContext())
->GetPrefetchService();
}
const NoVarySearchHelper& PrefetchDocumentManager::GetNoVarySearchHelper()
const {
return *no_vary_search_helper_.get();
}
void PrefetchDocumentManager::OnEligibilityCheckComplete(bool is_eligible) {
if (is_eligible)
referring_page_metrics_.prefetch_eligible_count++;
}
void PrefetchDocumentManager::OnPrefetchedHeadReceived(const GURL& url) {
if (!no_vary_search_support_enabled_ ||
!base::FeatureList::IsEnabled(network::features::kPrefetchNoVarySearch)) {
return;
}
// Find the PrefetchContainer associated with |url|.
const auto it = all_prefetches_.find(url);
if (it == all_prefetches_.end() || !it->second) {
return;
}
const auto* head = it->second->GetHead();
DCHECK(head);
no_vary_search_helper_->MaybeSendErrorsToConsole(url, *head,
render_frame_host());
no_vary_search_helper_->AddUrl(url, *head);
}
void PrefetchDocumentManager::OnPrefetchSuccessful() {
referring_page_metrics_.prefetch_successful_count++;
}
void PrefetchDocumentManager::EnableNoVarySearchSupport() {
no_vary_search_support_enabled_ = true;
}
DOCUMENT_USER_DATA_KEY_IMPL(PrefetchDocumentManager);
} // namespace content