blob: 23b9b769c7b29da94d372329810c461d1f4f995a [file] [log] [blame]
// Copyright 2020 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 "content/browser/preloading/prerender/prerender_host.h"
#include "base/feature_list.h"
#include "base/observer_list.h"
#include "base/run_loop.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/trace_event/trace_conversion_helper.h"
#include "base/trace_event/typed_macros.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/browser/preloading/prerender/prerender_metrics.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/navigation_entry_restore_context_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/page_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_frame_proxy_host.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/site_info.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/referrer.h"
#include "net/base/load_flags.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/loader/referrer.mojom.h"
namespace content {
namespace {
bool AreHttpRequestHeadersCompatible(
const std::string& potential_activation_headers_str,
const std::string& prerender_headers_str) {
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(prerender_headers_str);
net::HttpRequestHeaders potential_activation_headers;
potential_activation_headers.AddHeadersFromString(
potential_activation_headers_str);
// `prerender_headers` contains the "Purpose: prefetch" and "Sec-Purpose:
// prefetch;prerender" to notify servers of prerender requests, while
// `potential_activation_headers` doesn't contain it. Remove "Purpose" and
// "Sec-Purpose" matching from consideration so that activation works with the
// header.
prerender_headers.RemoveHeader("Purpose");
potential_activation_headers.RemoveHeader("Purpose");
prerender_headers.RemoveHeader("Sec-Purpose");
potential_activation_headers.RemoveHeader("Sec-Purpose");
return prerender_headers.ToString() ==
potential_activation_headers.ToString();
}
PreloadingFailureReason ToPreloadingFailureReason(
PrerenderHost::FinalStatus status) {
return static_cast<PreloadingFailureReason>(
static_cast<int>(status) +
static_cast<int>(
PreloadingFailureReason::kPreloadingFailureReasonCommonEnd));
}
} // namespace
class PrerenderHost::PageHolder : public FrameTree::Delegate,
public NavigationControllerDelegate {
public:
explicit PageHolder(WebContentsImpl& web_contents)
: web_contents_(web_contents),
frame_tree_(
std::make_unique<FrameTree>(web_contents.GetBrowserContext(),
this,
this,
&web_contents,
&web_contents,
&web_contents,
&web_contents,
&web_contents,
&web_contents,
FrameTree::Type::kPrerender)) {
scoped_refptr<SiteInstance> site_instance =
SiteInstance::Create(web_contents.GetBrowserContext());
frame_tree_->Init(site_instance.get(),
/*renderer_initiated_creation=*/false,
/*main_frame_name=*/"", /*opener=*/nullptr,
/*frame_policy=*/blink::FramePolicy());
// Use the same SessionStorageNamespace as the primary page for the
// prerendering page.
frame_tree_->controller().SetSessionStorageNamespace(
site_instance->GetStoragePartitionConfig(),
web_contents_.GetPrimaryFrameTree()
.controller()
.GetSessionStorageNamespace(
site_instance->GetStoragePartitionConfig()));
// TODO(https://crbug.com/1199679): This should be moved to FrameTree::Init
web_contents_.NotifySwappedFromRenderManager(
/*old_frame=*/nullptr,
frame_tree_->root()->render_manager()->current_frame_host());
}
~PageHolder() override {
// If we are still waiting on test loop, we can assume the page loading step
// has been cancelled and the PageHolder is being discarded without
// completing loading the page.
if (on_wait_loading_finished_)
std::move(on_wait_loading_finished_)
.Run(PrerenderHost::LoadingOutcome::kPrerenderingCancelled);
if (frame_tree_)
frame_tree_->Shutdown();
}
// FrameTree::Delegate
// TODO(https://crbug.com/1199682): Correctly handle load events. Ignored for
// now as it confuses WebContentsObserver instances because they can not
// distinguish between the different FrameTrees.
void DidStartLoading(FrameTreeNode* frame_tree_node,
bool should_show_loading_ui) override {}
void DidStopLoading() override {
if (on_wait_loading_finished_) {
std::move(on_wait_loading_finished_)
.Run(PrerenderHost::LoadingOutcome::kLoadingCompleted);
}
}
void DidChangeLoadProgress() override {}
bool IsHidden() override { return true; }
FrameTree* LoadingTree() override {
// For prerendering loading tree is the same as its frame tree as loading is
// done at a frame tree level in the background, unlike the loading visible
// to the user where we account for nested frame tree loading state.
return frame_tree_.get();
}
void NotifyPageChanged(PageImpl& page) override {}
int GetOuterDelegateFrameTreeNodeId() override {
// A prerendered FrameTree is not "inner to" or "nested inside" another
// FrameTree; it exists in parallel to the primary FrameTree of the current
// WebContents. Therefore, it must not attempt to access the primary
// FrameTree in the sense of an "outer delegate" relationship, so we return
// the invalid ID here.
return FrameTreeNode::kFrameTreeNodeInvalidId;
}
bool IsPortal() override { return false; }
// NavigationControllerDelegate
void NotifyNavigationStateChanged(InvalidateTypes changed_flags) override {}
void NotifyBeforeFormRepostWarningShow() override {}
void NotifyNavigationEntryCommitted(
const LoadCommittedDetails& load_details) override {}
void NotifyNavigationEntryChanged(
const EntryChangedDetails& change_details) override {}
void NotifyNavigationListPruned(
const PrunedDetails& pruned_details) override {}
void NotifyNavigationEntriesDeleted() override {}
void ActivateAndShowRepostFormWarningDialog() override {
// Not supported, cancel pending reload.
GetNavigationController().CancelPendingReload();
}
bool ShouldPreserveAbortedURLs() override { return false; }
WebContents* DeprecatedGetWebContents() override { return GetWebContents(); }
void UpdateOverridingUserAgent() override {}
NavigationControllerImpl& GetNavigationController() {
return frame_tree_->controller();
}
WebContents* GetWebContents() { return &web_contents_; }
FrameTree& GetPrimaryFrameTree() {
return web_contents_.GetPrimaryFrameTree();
}
std::unique_ptr<StoredPage> Activate(NavigationRequest& navigation_request) {
// There should be no ongoing main-frame navigation during activation.
// TODO(https://crbug.com/1190644): Make sure sub-frame navigations are
// fine.
DCHECK(!frame_tree_->root()->HasNavigation());
// Before the root's current_frame_host is cleared, collect the subframes of
// `frame_tree_` whose FrameTree will need to be updated.
FrameTree::NodeRange node_range = frame_tree_->Nodes();
std::vector<FrameTreeNode*> subframe_nodes(std::next(node_range.begin()),
node_range.end());
// Before the root's current_frame_host is cleared, collect the replication
// state so that it can be used for post-activation validation.
blink::mojom::FrameReplicationState prior_replication_state =
frame_tree_->root()->current_replication_state();
// Update FrameReplicationState::has_received_user_gesture_before_nav of the
// prerendered page.
//
// On regular navigation, it is updated via a renderer => browser IPC
// (RenderFrameHostImpl::HadStickyUserActivationBeforeNavigationChanged),
// which is sent from blink::DocumentLoader::CommitNavigation. However,
// this doesn't happen on prerender page activation, so the value is not
// correctly updated without this treatment.
//
// The updated value will be sent to the renderer on
// blink::mojom::Page::ActivatePrerenderedPage.
prior_replication_state.has_received_user_gesture_before_nav =
navigation_request.frame_tree_node()
->has_received_user_gesture_before_nav();
// frame_tree_->root(). Do not add any code between here and
// frame_tree_.reset() that calls into observer functions to minimize the
// duration of current_frame_host being null.
//
// TODO(https://crbug.com/1176148): Investigate how to combine taking the
// prerendered page and frame_tree_ destruction.
std::unique_ptr<StoredPage> page =
frame_tree_->root()->render_manager()->TakePrerenderedPage();
std::unique_ptr<NavigationEntryRestoreContextImpl> context =
std::make_unique<NavigationEntryRestoreContextImpl>();
std::unique_ptr<NavigationEntryImpl> nav_entry =
GetNavigationController()
.GetEntryWithUniqueID(page->render_frame_host->nav_entry_id())
->CloneWithoutSharing(context.get());
navigation_request.SetPrerenderActivationNavigationState(
std::move(nav_entry), prior_replication_state);
FrameTree& target_frame_tree = GetPrimaryFrameTree();
DCHECK_EQ(&target_frame_tree,
navigation_request.frame_tree_node()->frame_tree());
// We support activating the prerenderd page only to the topmost
// RenderFrameHost.
CHECK(!page->render_frame_host->GetParentOrOuterDocumentOrEmbedder());
page->render_frame_host->SetFrameTreeNode(*(target_frame_tree.root()));
// Copy frame name into the replication state of the primary main frame to
// ensure that the replication state of the primary main frame after
// activation matches the replication state stored in the renderer.
// TODO(https://crbug.com/1237091): Copying frame name here is suboptimal
// and ideally we'd do this at the same time when transferring the proxies
// from the StoredPage into RenderFrameHostManager. However, this is a
// temporary solution until we move this into BrowsingContextState,
// along with RenderFrameProxyHost.
page->render_frame_host->frame_tree_node()->set_frame_name_for_activation(
prior_replication_state.unique_name, prior_replication_state.name);
for (auto& it : page->proxy_hosts) {
it.second->set_frame_tree_node(*(target_frame_tree.root()));
}
// Iterate over the root RenderFrameHost's subframes and update the
// associated frame tree. Note that subframe proxies don't need their
// FrameTrees independently updated, since their FrameTreeNodes don't
// change, and FrameTree references in those FrameTreeNodes will be updated
// through RenderFrameHosts.
//
// TODO(https://crbug.com/1199693): Need to investigate if and how
// pending delete RenderFrameHost objects should be handled if prerendering
// runs all of the unload handlers; they are not currently handled here.
// This is because pending delete RenderFrameHosts can still receive and
// process some messages while the RenderFrameHost FrameTree and
// FrameTreeNode are stale.
for (FrameTreeNode* subframe_node : subframe_nodes) {
subframe_node->SetFrameTree(target_frame_tree);
}
page->render_frame_host->ForEachRenderFrameHostIncludingSpeculative(
base::BindRepeating(
[](const WebContentsImpl& web_contents, RenderFrameHostImpl* rfh) {
// The visibility state of the prerendering page has not been
// updated by
// WebContentsImpl::UpdateVisibilityAndNotifyPageAndView(). So
// updates the visibility state using the PageVisibilityState of
// |web_contents|.
rfh->render_view_host()->SetFrameTreeVisibility(
web_contents.GetPageVisibilityState());
},
std::cref(web_contents_)));
frame_tree_->Shutdown();
frame_tree_.reset();
return page;
}
PrerenderHost::LoadingOutcome WaitForLoadCompletionForTesting() {
PrerenderHost::LoadingOutcome status =
PrerenderHost::LoadingOutcome::kLoadingCompleted;
if (!frame_tree_->IsLoadingIncludingInnerFrameTrees())
return status;
base::RunLoop loop;
on_wait_loading_finished_ =
base::BindOnce(&PrerenderHost::PageHolder::FinishWaitingForTesting,
loop.QuitClosure(), &status);
loop.Run();
return status;
}
FrameTree* frame_tree() { return frame_tree_.get(); }
private:
static void FinishWaitingForTesting(base::OnceClosure on_close, // IN-TEST
PrerenderHost::LoadingOutcome* result,
PrerenderHost::LoadingOutcome status) {
*result = status;
std::move(on_close).Run();
}
// WebContents where this prerenderer is embedded.
WebContentsImpl& web_contents_;
// Used for testing, this closure is only set when waiting a page to be
// either loaded for pre-rendering. |frame_tree_| provides us with a trigger
// for when the page is loaded.
base::OnceCallback<void(PrerenderHost::LoadingOutcome)>
on_wait_loading_finished_;
// Frame tree created for the prerenderer to load the page and prepare it for
// a future activation. During activation, the prerendered page will be taken
// out from |frame_tree_| and moved over to |web_contents_|'s primary frame
// tree, while |frame_tree_| will be deleted.
std::unique_ptr<FrameTree> frame_tree_;
};
PrerenderHost::PrerenderHost(const PrerenderAttributes& attributes,
WebContents& web_contents,
PreloadingAttemptImpl* attempt)
: attributes_(attributes), attempt_(attempt) {
DCHECK(blink::features::IsPrerender2Enabled());
// If the prerendering is browser-initiated, it is expected to have no
// initiator. All initiator related information should be null or invalid. On
// the other hand, renderer-initiated prerendering should have valid initiator
// information.
if (attributes.IsBrowserInitiated()) {
DCHECK(!attributes.initiator_origin.has_value());
DCHECK(!attributes.initiator_frame_token.has_value());
DCHECK_EQ(attributes.initiator_process_id,
ChildProcessHost::kInvalidUniqueID);
DCHECK_EQ(attributes.initiator_ukm_id, ukm::kInvalidSourceId);
DCHECK_EQ(attributes.initiator_frame_tree_node_id,
RenderFrameHost::kNoFrameTreeNodeId);
} else {
DCHECK(attributes.initiator_origin.has_value());
DCHECK(attributes.initiator_frame_token.has_value());
// TODO(https://crbug.com/1325211): Add back the following DCHECKs after
// fixing prerendering activation for embedder-triggered prerendering in
// unittests.
// DCHECK_NE(attributes.initiator_process_id,
// ChildProcessHost::kInvalidUniqueID);
// DCHECK_NE(attributes.initiator_ukm_id, ukm::kInvalidSourceId);
// DCHECK_NE(attributes.initiator_frame_tree_node_id,
// RenderFrameHost::kNoFrameTreeNodeId);
}
CreatePageHolder(*static_cast<WebContentsImpl*>(&web_contents));
}
PrerenderHost::~PrerenderHost() {
// Stop observing here. Otherwise, destructing members may lead
// DidFinishNavigation call after almost everything being destructed.
Observe(nullptr);
for (auto& observer : observers_)
observer.OnHostDestroyed();
if (!final_status_)
RecordFinalStatus(FinalStatus::kDestroyed, attributes_.initiator_ukm_id,
ukm::kInvalidSourceId);
}
// TODO(https://crbug.com/1132746): Inspect diffs from the current
// no-state-prefetch implementation. See PrerenderContents::StartPrerendering()
// for example.
bool PrerenderHost::StartPrerendering() {
TRACE_EVENT0("navigation", "PrerenderHost::StartPrerendering");
// Observe events about the prerendering contents.
Observe(page_holder_->GetWebContents());
// Since prerender started we mark it as eligible and set it to running.
SetTriggeringOutcome(PreloadingTriggeringOutcome::kRunning);
// Start prerendering navigation.
NavigationController::LoadURLParams load_url_params(
attributes_.prerendering_url);
load_url_params.initiator_origin = attributes_.initiator_origin;
load_url_params.initiator_process_id = attributes_.initiator_process_id;
load_url_params.initiator_frame_token = attributes_.initiator_frame_token;
load_url_params.is_renderer_initiated = !attributes_.IsBrowserInitiated();
load_url_params.transition_type =
ui::PageTransitionFromInt(attributes_.transition_type);
// Just use the referrer from attributes, as NoStatePrefetch does.
// TODO(crbug.com/1176054): For cross-origin prerender, follow the spec steps
// for "sufficiently-strict speculative navigation referrer policies".
load_url_params.referrer = attributes_.referrer;
// TODO(https://crbug.com/1189034): Should we set `override_user_agent` here?
// Things seem to work without it.
// TODO(https://crbug.com/1132746): Set up other fields of `load_url_params`
// as well, and add tests for them.
base::WeakPtr<NavigationHandle> created_navigation_handle =
page_holder_->GetNavigationController().LoadURLWithParams(
load_url_params);
if (!created_navigation_handle)
return false;
if (initial_navigation_id_.has_value()) {
// In usual code path, `initial_navigation_id_` should be set by
// PrerenderNavigationThrottle during `LoadURLWithParams` above.
DCHECK_EQ(*initial_navigation_id_,
created_navigation_handle->GetNavigationId());
DCHECK(begin_params_);
DCHECK(common_params_);
} else {
// In some exceptional code path, such as the navigation failed due to CSP
// violations, PrerenderNavigationThrottle didn't run at this point. So,
// set the ID here.
initial_navigation_id_ = created_navigation_handle->GetNavigationId();
// |begin_params_| and |common_params_| is null here, but it doesn't matter
// as this branch is reached only when the initial navigation fails,
// so this PrerenderHost can't be activated.
}
NavigationRequest* navigation_request =
NavigationRequest::From(created_navigation_handle.get());
// The initial navigation in the prerender frame tree should not wait for
// `beforeunload` in the old page, so BeginNavigation stage should be reached
// synchronously.
DCHECK_GE(navigation_request->state(),
NavigationRequest::WAITING_FOR_RENDERER_RESPONSE);
return true;
}
void PrerenderHost::DidFinishNavigation(NavigationHandle* navigation_handle) {
auto* navigation_request = NavigationRequest::From(navigation_handle);
if (navigation_request->IsSameDocument())
return;
const bool is_inside_prerender_frame_tree =
navigation_request->frame_tree_node()->frame_tree() ==
page_holder_->frame_tree();
// Observe navigation only in the prerendering frame tree.
if (!is_inside_prerender_frame_tree)
return;
// Cancel prerendering on navigation request failure.
//
// Check net::Error here rather than PrerenderNavigationThrottle as CSP
// blocking occurs before NavigationThrottles so cannot be observed in
// NavigationThrottle::WillFailRequest().
net::Error net_error = navigation_request->GetNetErrorCode();
const bool is_prerender_main_frame =
navigation_request->GetFrameTreeNodeId() == frame_tree_node_id_;
absl::optional<FinalStatus> status;
if (net_error == net::Error::ERR_BLOCKED_BY_CSP) {
status = FinalStatus::kNavigationRequestBlockedByCsp;
} else if (net_error == net::Error::ERR_BLOCKED_BY_CLIENT) {
status = FinalStatus::kBlockedByClient;
} else if (is_prerender_main_frame && net_error != net::Error::OK) {
status = FinalStatus::kNavigationRequestNetworkError;
} else if (is_prerender_main_frame && !navigation_request->HasCommitted()) {
status = FinalStatus::kNavigationNotCommitted;
}
if (status.has_value()) {
Cancel(*status);
return;
}
// The prerendered contents are considered ready for activation when the
// main frame navigation reaches DidFinishNavigation.
if (is_prerender_main_frame) {
DCHECK(!is_ready_for_activation_);
is_ready_for_activation_ = true;
// Prerender is ready to activate. Set the status to kReady.
SetTriggeringOutcome(PreloadingTriggeringOutcome::kReady);
}
}
void PrerenderHost::OnVisibilityChanged(Visibility visibility) {
TRACE_EVENT("navigation", "PrerenderHost::OnVisibilityChanged");
if (visibility == Visibility::HIDDEN) {
Cancel(FinalStatus::kTriggerBackgrounded);
}
}
void PrerenderHost::ResourceLoadComplete(
RenderFrameHost* render_frame_host,
const GlobalRequestID& request_id,
const blink::mojom::ResourceLoadInfo& resource_load_info) {
// Observe resource loads only in the prerendering frame tree.
if (&render_frame_host->GetPage() !=
&GetPrerenderedMainFrameHost()->GetPage()) {
return;
}
if (resource_load_info.net_error == net::Error::ERR_BLOCKED_BY_CLIENT) {
Cancel(FinalStatus::kBlockedByClient);
}
}
std::unique_ptr<StoredPage> PrerenderHost::Activate(
NavigationRequest& navigation_request) {
TRACE_EVENT1("navigation", "PrerenderHost::Activate", "navigation_request",
&navigation_request);
DCHECK(is_ready_for_activation_);
is_ready_for_activation_ = false;
std::unique_ptr<StoredPage> page = page_holder_->Activate(navigation_request);
for (auto& observer : observers_)
observer.OnActivated();
// TODO(crbug.com/1299330): Replace
// `navigation_request.GetNextPageUkmSourceId()` with prerendered page's UKM
// source ID.
RecordFinalStatus(FinalStatus::kActivated, attributes_.initiator_ukm_id,
navigation_request.GetNextPageUkmSourceId());
// Prerender is activated. Set the status to kSuccess.
SetTriggeringOutcome(PreloadingTriggeringOutcome::kSuccess);
devtools_instrumentation::DidActivatePrerender(navigation_request);
return page;
}
// Ensure that the frame policies are compatible between primary main frame and
// prerendering main frame:
// a) primary main frame's pending_frame_policy would normally apply to the new
// document during its creation. However, for prerendering we can't apply it as
// the document is already created.
// b) prerender main frame's pending_frame_policy can't be transferred to the
// primary main frame, we should not activate if it's non-zero.
// c) Existing document can't change the frame_policy it is affected by, so we
// can't transfer RenderFrameHosts between FrameTreeNodes with different frame
// policies.
//
// Usually frame policy for the main frame is empty as in the most common case a
// parent document sets a policy on the child iframe.
bool PrerenderHost::IsFramePolicyCompatibleWithPrimaryFrameTree() {
FrameTreeNode* prerender_root_ftn = page_holder_->frame_tree()->root();
FrameTreeNode* primary_root_ftn = page_holder_->GetPrimaryFrameTree().root();
// Ensure that the pending frame policy is not set on the main frames, as it
// is usually set on frames by their parent frames.
if (prerender_root_ftn->pending_frame_policy() != blink::FramePolicy()) {
return false;
}
if (primary_root_ftn->pending_frame_policy() != blink::FramePolicy()) {
return false;
}
if (prerender_root_ftn->current_replication_state().frame_policy !=
primary_root_ftn->current_replication_state().frame_policy) {
return false;
}
return true;
}
bool PrerenderHost::AreInitialPrerenderNavigationParamsCompatibleWithNavigation(
NavigationRequest& navigation_request) {
// TODO(crbug.com/1181763): compare the rest of the navigation parameters. We
// should introduce compile-time parameter checks as well, to ensure how new
// fields should be compared for compatibility.
// As the initial prerender navigation is a) limited to HTTP(s) URLs and b)
// initiated by the PrerenderHost, we do not expect some navigation parameters
// connected to certain navigation types to be set and the DCHECKS below
// enforce that.
// The parameters of the potential activation, however, are coming from the
// renderer and we mostly don't have any guarantees what they are, so we
// should not DCHECK them. Instead, by default we compare them with initial
// prerender activation parameters and fail to activate when they differ.
// Note: some of those parameters should be never set (or should be ignored)
// for main-frame / HTTP(s) navigations, but we still compare them here as a
// defence-in-depth measure.
DCHECK(navigation_request.IsInPrimaryMainFrame());
// Compare BeginNavigationParams.
if (!AreBeginNavigationParamsCompatibleWithNavigation(
navigation_request.begin_params())) {
return false;
}
// Compare CommonNavigationParams.
if (!AreCommonNavigationParamsCompatibleWithNavigation(
navigation_request.common_params())) {
return false;
}
return true;
}
bool PrerenderHost::AreBeginNavigationParamsCompatibleWithNavigation(
const blink::mojom::BeginNavigationParams& potential_activation) {
if (potential_activation.initiator_frame_token !=
begin_params_->initiator_frame_token) {
return false;
}
if (!AreHttpRequestHeadersCompatible(potential_activation.headers,
begin_params_->headers)) {
return false;
}
// Don't activate a prerendered page if the potential activation request
// requires validation or bypass of the browser cache, as the prerendered page
// is a kind of caches.
// TODO(https://crbug.com/1213299): Instead of checking the load flags on
// activation, we should cancel prerendering when the prerender initial
// navigation has the flags.
int cache_load_flags = net::LOAD_VALIDATE_CACHE | net::LOAD_BYPASS_CACHE |
net::LOAD_DISABLE_CACHE;
if (potential_activation.load_flags & cache_load_flags) {
return false;
}
if (potential_activation.load_flags != begin_params_->load_flags) {
return false;
}
if (potential_activation.skip_service_worker !=
begin_params_->skip_service_worker) {
return false;
}
if (potential_activation.mixed_content_context_type !=
begin_params_->mixed_content_context_type) {
return false;
}
// Initial prerender navigation cannot be a form submission.
DCHECK(!begin_params_->is_form_submission);
if (potential_activation.is_form_submission !=
begin_params_->is_form_submission) {
return false;
}
if (potential_activation.searchable_form_url !=
begin_params_->searchable_form_url) {
return false;
}
if (potential_activation.searchable_form_encoding !=
begin_params_->searchable_form_encoding) {
return false;
}
// Trust token params can be set only on subframe navigations, so both values
// should be null here.
DCHECK(!begin_params_->trust_token_params);
if (potential_activation.trust_token_params !=
begin_params_->trust_token_params) {
return false;
}
// Web bundle token cannot be set due because it is only set for child
// frame navigations.
DCHECK(!begin_params_->web_bundle_token);
if (potential_activation.web_bundle_token) {
return false;
}
// Don't require equality for request_context_type because link clicks
// (HYPERLINK) should be allowed for activation, whereas prerender always has
// type LOCATION.
DCHECK_EQ(begin_params_->request_context_type,
blink::mojom::RequestContextType::LOCATION);
switch (potential_activation.request_context_type) {
case blink::mojom::RequestContextType::HYPERLINK:
case blink::mojom::RequestContextType::LOCATION:
break;
default:
return false;
}
// Since impression should not be set, no need to compare contents.
DCHECK(!begin_params_->impression);
if (potential_activation.impression.has_value()) {
return false;
}
// No need to test for devtools_initiator because this field is used for
// tracking what triggered a network request, and prerender activation will
// not use network requests.
return true;
}
bool PrerenderHost::AreCommonNavigationParamsCompatibleWithNavigation(
const blink::mojom::CommonNavigationParams& potential_activation) {
// The CommonNavigationParams::url field is expected to be the same for both
// initial and activation prerender navigations, as the PrerenderHost
// selection would have already checked for matching values. Adding a DCHECK
// here to be safe.
if (attributes_.url_match_predicate) {
DCHECK(
attributes_.url_match_predicate.value().Run(potential_activation.url));
} else {
DCHECK_EQ(potential_activation.url, common_params_->url);
}
if (potential_activation.initiator_origin !=
common_params_->initiator_origin) {
return false;
}
if (potential_activation.transition != common_params_->transition) {
return false;
}
DCHECK_EQ(common_params_->navigation_type,
blink::mojom::NavigationType::DIFFERENT_DOCUMENT);
if (potential_activation.navigation_type != common_params_->navigation_type) {
return false;
}
// We don't check download_policy as it affects whether the download triggered
// by the NavigationRequest is allowed to proceed (or logs metrics) and
// doesn't affect the behaviour of the document created by a non-download
// navigation after commit (e.g. it doesn't affect future downloads in child
// frames). PrerenderNavigationThrottle has already ensured that the initial
// prerendering navigation isn't a download and as prerendering activation
// won't reach out to the network, it won't turn into a navigation as well.
DCHECK(common_params_->base_url_for_data_url.is_empty());
if (potential_activation.base_url_for_data_url !=
common_params_->base_url_for_data_url) {
return false;
}
// The previews_state is always set to NO_PREVIEWS in BeginNavigation and the
// previews code was removed, so no need to compare it here as it's not used.
// TODO(crbug.com/1232909): remove this previews_state.
if (potential_activation.method != common_params_->method) {
return false;
}
// Initial prerender navigation can't be a form submission.
DCHECK(!common_params_->post_data);
if (potential_activation.post_data != common_params_->post_data) {
return false;
}
// No need to compare source_location, as it's only passed to the DevTools for
// debugging purposes and does not impact the properties of the document
// created by this navigation.
DCHECK(!common_params_->started_from_context_menu);
if (potential_activation.started_from_context_menu !=
common_params_->started_from_context_menu) {
return false;
}
// has_user_gesture doesn't affect any of the security properties of the
// document created by navigation, so equality of the values is not required.
// TODO(crbug.com/1232915): ensure that the user activation status is
// propagated to the activated document.
// text_fragment_token doesn't affect any of the security properties of the
// document created by navigation, so equality of the values is not required.
// TODO(crbug.com/1232919): ensure the activated document consumes
// text_fragment_token and scrolls to the corresponding viewport.
// No need to compare should_check_main_world_csp, as if the CSP blocks the
// initial navigation, it cancels prerendering, and we don't reach here for
// matching. So regardless of the activation's capability to bypass the main
// world CSP, the prerendered page is eligible for the activation. This also
// permits content scripts to activate the page.
if (potential_activation.initiator_origin_trial_features !=
common_params_->initiator_origin_trial_features) {
return false;
}
if (potential_activation.href_translate != common_params_->href_translate) {
return false;
}
// Initial prerender navigation can't be a history navigation.
DCHECK(!common_params_->is_history_navigation_in_new_child_frame);
if (potential_activation.is_history_navigation_in_new_child_frame !=
common_params_->is_history_navigation_in_new_child_frame) {
return false;
}
// The spec mandates matching the referrer policy, and not the referrer URL
// itself, so we only compare the referrer policy here. Referrer policy is a
// more predictable value to match than referrer URL.
// https://wicg.github.io/nav-speculation/prerendering.html#navigate-activation
if (potential_activation.referrer->policy !=
common_params_->referrer->policy) {
return false;
}
if (potential_activation.request_destination !=
common_params_->request_destination) {
return false;
}
return true;
}
RenderFrameHostImpl* PrerenderHost::GetPrerenderedMainFrameHost() {
DCHECK(page_holder_->frame_tree());
DCHECK(page_holder_->frame_tree()->root()->current_frame_host());
return page_holder_->frame_tree()->root()->current_frame_host();
}
FrameTree& PrerenderHost::GetPrerenderFrameTree() {
DCHECK(page_holder_->frame_tree());
return *page_holder_->frame_tree();
}
void PrerenderHost::RecordFinalStatus(base::PassKey<PrerenderHostRegistry>,
FinalStatus status) {
RecordFinalStatus(status, attributes_.initiator_ukm_id,
ukm::kInvalidSourceId);
// Set failure reason for this PreloadingAttempt specific to the
// FinalStatus.
SetFailureReason(status);
}
void PrerenderHost::CreatePageHolder(WebContentsImpl& web_contents) {
page_holder_ = std::make_unique<PageHolder>(web_contents);
frame_tree_node_id_ =
page_holder_->frame_tree()->root()->frame_tree_node_id();
}
PrerenderHost::LoadingOutcome PrerenderHost::WaitForLoadStopForTesting() {
return page_holder_->WaitForLoadCompletionForTesting(); // IN-TEST
}
void PrerenderHost::RecordFinalStatus(FinalStatus status,
ukm::SourceId initiator_ukm_id,
ukm::SourceId prerendered_ukm_id) {
DCHECK(!final_status_);
final_status_ = status;
RecordPrerenderHostFinalStatus(status, attributes_, prerendered_ukm_id);
}
const GURL& PrerenderHost::GetInitialUrl() const {
return attributes_.prerendering_url;
}
void PrerenderHost::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void PrerenderHost::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
absl::optional<int64_t> PrerenderHost::GetInitialNavigationId() const {
return initial_navigation_id_;
}
void PrerenderHost::SetInitialNavigation(NavigationRequest* navigation) {
DCHECK(!initial_navigation_id_.has_value());
initial_navigation_id_ = navigation->GetNavigationId();
begin_params_ = navigation->begin_params().Clone();
common_params_ = navigation->common_params().Clone();
// The prerendered page should be checked by the main world CSP. See also
// relevant comments in AreCommonNavigationParamsCompatibleWithNavigation().
DCHECK_EQ(common_params_->should_check_main_world_csp,
network::mojom::CSPDisposition::CHECK);
}
void PrerenderHost::SetTriggeringOutcome(PreloadingTriggeringOutcome outcome) {
if (!attempt_)
return;
attempt_->SetTriggeringOutcome(outcome);
}
void PrerenderHost::SetEligibility(PreloadingEligibility eligibility) {
if (!attempt_)
return;
attempt_->SetEligibility(eligibility);
}
void PrerenderHost::SetFailureReason(FinalStatus status) {
if (!attempt_)
return;
switch (status) {
// When adding a new failure reason, consider whether it should be
// propagated to `attempt_`. Most values should be propagated, but we
// explicitly do not propagate failure reasons if the prerender was actually
// successful (kActivated), or if prerender was successfully prepared but
// then destroyed because it wasn't needed for a subsequent navigation
// (kTriggerDestroyed, kEmbedderTriggeredAndDestroyed).
case FinalStatus::kActivated:
case FinalStatus::kEmbedderTriggeredAndDestroyed:
case FinalStatus::kTriggerDestroyed:
return;
case FinalStatus::kDestroyed:
case FinalStatus::kLowEndDevice:
case FinalStatus::kCrossOriginRedirect:
case FinalStatus::kCrossOriginNavigation:
case FinalStatus::kInvalidSchemeRedirect:
case FinalStatus::kInvalidSchemeNavigation:
case FinalStatus::kInProgressNavigation:
case FinalStatus::kNavigationRequestBlockedByCsp:
case FinalStatus::kMainFrameNavigation:
case FinalStatus::kMojoBinderPolicy:
case FinalStatus::kRendererProcessCrashed:
case FinalStatus::kRendererProcessKilled:
case FinalStatus::kDownload:
case FinalStatus::kNavigationNotCommitted:
case FinalStatus::kNavigationBadHttpStatus:
case FinalStatus::kClientCertRequested:
case FinalStatus::kNavigationRequestNetworkError:
case FinalStatus::kMaxNumOfRunningPrerendersExceeded:
case FinalStatus::kCancelAllHostsForTesting:
case FinalStatus::kDidFailLoad:
case FinalStatus::kStop:
case FinalStatus::kSslCertificateError:
case FinalStatus::kLoginAuthRequested:
case FinalStatus::kUaChangeRequiresReload:
case FinalStatus::kBlockedByClient:
case FinalStatus::kAudioOutputDeviceRequested:
case FinalStatus::kMixedContent:
case FinalStatus::kTriggerBackgrounded:
case FinalStatus::kEmbedderTriggeredAndSameOriginRedirected:
case FinalStatus::kEmbedderTriggeredAndCrossOriginRedirected:
attempt_->SetFailureReason(ToPreloadingFailureReason(status));
return;
}
}
bool PrerenderHost::IsUrlMatch(const GURL& url) const {
// If the trigger defines its predicate, respect it.
if (attributes_.url_match_predicate) {
// Triggers are not allowed to treat a cross-origin url as a matched url. It
// would cause security risks.
if (!url::IsSameOriginWith(attributes_.prerendering_url, url))
return false;
return attributes_.url_match_predicate.value().Run(url);
}
return GetInitialUrl() == url;
}
void PrerenderHost::Cancel(FinalStatus status) {
TRACE_EVENT("navigation", "PrerenderHost::Cancel", "final_status", status);
// Already cancelled.
if (final_status_)
return;
RenderFrameHostImpl* host = PrerenderHost::GetPrerenderedMainFrameHost();
DCHECK(host);
PrerenderHostRegistry* registry =
host->delegate()->GetPrerenderHostRegistry();
DCHECK(registry);
registry->CancelHost(frame_tree_node_id_, status);
}
} // namespace content