blob: f2f8dc5ff921e290f7625303be8c893aa3f2453b [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/no_state_prefetch/browser/no_state_prefetch_contents.h"
#include <stddef.h>
#include <functional>
#include <utility>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_contents_delegate.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
#include "components/no_state_prefetch/common/no_state_prefetch_final_status.h"
#include "components/no_state_prefetch/common/no_state_prefetch_utils.h"
#include "components/no_state_prefetch/common/render_frame_prerender_messages.mojom.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/preloading.h"
#include "content/public/browser/preloading_data.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/session_storage_namespace.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "net/http/http_response_headers.h"
#include "services/resource_coordinator/public/cpp/memory_instrumentation/memory_instrumentation.h"
#include "services/service_manager/public/cpp/binder_registry.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "ui/base/page_transition_types.h"
#include "ui/gfx/geometry/size.h"
using content::BrowserThread;
using content::OpenURLParams;
using content::RenderFrameHost;
using content::SessionStorageNamespace;
using content::WebContents;
namespace prerender {
class NoStatePrefetchContentsFactoryImpl
: public NoStatePrefetchContents::Factory {
public:
NoStatePrefetchContents* CreateNoStatePrefetchContents(
std::unique_ptr<NoStatePrefetchContentsDelegate> delegate,
NoStatePrefetchManager* no_state_prefetch_manager,
content::BrowserContext* browser_context,
const GURL& url,
const content::Referrer& referrer,
const absl::optional<url::Origin>& initiator_origin,
Origin origin) override {
return new NoStatePrefetchContents(
std::move(delegate), no_state_prefetch_manager, browser_context, url,
referrer, initiator_origin, origin);
}
};
void SetPreloadingTriggeringOutcome(
content::PreloadingAttempt* attempt,
content::PreloadingTriggeringOutcome outcome) {
if (!attempt)
return;
attempt->SetTriggeringOutcome(outcome);
}
content::PreloadingFailureReason ToPreloadingFailureReason(FinalStatus status) {
return static_cast<content::PreloadingFailureReason>(
static_cast<int>(status) +
static_cast<int>(content::PreloadingFailureReason::
kPreloadingFailureReasonContentEnd));
}
// WebContentsDelegateImpl -----------------------------------------------------
class NoStatePrefetchContents::WebContentsDelegateImpl
: public content::WebContentsDelegate {
public:
explicit WebContentsDelegateImpl(
NoStatePrefetchContents* no_state_prefetch_contents)
: no_state_prefetch_contents_(no_state_prefetch_contents) {}
// content::WebContentsDelegate implementation:
WebContents* OpenURLFromTab(WebContents* source,
const OpenURLParams& params) override {
// |OpenURLFromTab| is typically called when a frame performs a navigation
// that requires the browser to perform the transition instead of WebKit.
// Examples include client redirects to hosted app URLs.
// TODO(cbentzel): Consider supporting this for CURRENT_TAB dispositions, if
// it is a common case during prerenders.
no_state_prefetch_contents_->Destroy(FINAL_STATUS_OPEN_URL);
return NULL;
}
bool ShouldAllowRendererInitiatedCrossProcessNavigation(
bool is_outermost_main_frame_navigation) override {
// Cancel the prerender if the navigation attempts to transfer to a
// different process. Examples include server redirects to privileged pages
// or cross-site subframe navigations in --site-per-process.
no_state_prefetch_contents_->Destroy(FINAL_STATUS_OPEN_URL);
return false;
}
void CanDownload(const GURL& url,
const std::string& request_method,
base::OnceCallback<void(bool)> callback) override {
no_state_prefetch_contents_->Destroy(FINAL_STATUS_DOWNLOAD);
// Cancel the download.
std::move(callback).Run(false);
}
bool OnGoToEntryOffset(int offset) override {
// This isn't allowed because the history merge operation
// does not work if there are renderer issued challenges.
// TODO(cbentzel): Cancel in this case? May not need to do
// since render-issued offset navigations are not guaranteed,
// but indicates that the page cares about the history.
return false;
}
gfx::Size GetSizeForNewRenderView(WebContents* web_contents) override {
// Have to set the size of the RenderView on initialization to be sure it is
// set before the RenderView is hidden on all platforms (esp. Android).
return no_state_prefetch_contents_->bounds_.size();
}
private:
raw_ptr<NoStatePrefetchContents> no_state_prefetch_contents_;
};
NoStatePrefetchContents::Observer::~Observer() {}
NoStatePrefetchContents::NoStatePrefetchContents(
std::unique_ptr<NoStatePrefetchContentsDelegate> delegate,
NoStatePrefetchManager* no_state_prefetch_manager,
content::BrowserContext* browser_context,
const GURL& url,
const content::Referrer& referrer,
const absl::optional<url::Origin>& initiator_origin,
Origin origin)
: prerendering_has_started_(false),
no_state_prefetch_manager_(no_state_prefetch_manager),
delegate_(std::move(delegate)),
prerender_url_(url),
referrer_(referrer),
initiator_origin_(initiator_origin),
browser_context_(browser_context),
has_finished_loading_(false),
final_status_(FINAL_STATUS_UNKNOWN),
prerendering_has_been_cancelled_(false),
process_pid_(base::kNullProcessId),
origin_(origin),
network_bytes_(0) {
switch (origin) {
case ORIGIN_OMNIBOX:
case ORIGIN_EXTERNAL_REQUEST:
case ORIGIN_EXTERNAL_REQUEST_FORCED_PRERENDER:
case ORIGIN_NAVIGATION_PREDICTOR:
case ORIGIN_ISOLATED_PRERENDER:
DCHECK(!initiator_origin_.has_value());
break;
case ORIGIN_GWS_PRERENDER:
case ORIGIN_LINK_REL_PRERENDER_SAMEDOMAIN:
case ORIGIN_LINK_REL_PRERENDER_CROSSDOMAIN:
case ORIGIN_LINK_REL_NEXT:
case ORIGIN_SAME_ORIGIN_SPECULATION:
DCHECK(initiator_origin_.has_value());
break;
case ORIGIN_NONE:
case ORIGIN_MAX:
NOTREACHED();
}
DCHECK(no_state_prefetch_manager);
}
bool NoStatePrefetchContents::Init() {
return AddAliasURL(prerender_url_);
}
// static
NoStatePrefetchContents::Factory* NoStatePrefetchContents::CreateFactory() {
return new NoStatePrefetchContentsFactoryImpl();
}
void NoStatePrefetchContents::SetPreloadingFailureReason(FinalStatus status) {
if (!attempt_)
return;
switch (status) {
case FINAL_STATUS_USED:
case FINAL_STATUS_NOSTATE_PREFETCH_FINISHED:
// 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 no state prefetch was actually successful (USED OR
// PREFETCH_FINISHED).
return;
case FINAL_STATUS_TIMED_OUT:
case FINAL_STATUS_PROFILE_DESTROYED:
case FINAL_STATUS_APP_TERMINATING:
case FINAL_STATUS_AUTH_NEEDED:
case FINAL_STATUS_DOWNLOAD:
case FINAL_STATUS_MEMORY_LIMIT_EXCEEDED:
case FINAL_STATUS_TOO_MANY_PROCESSES:
case FINAL_STATUS_RATE_LIMIT_EXCEEDED:
case FINAL_STATUS_RENDERER_CRASHED:
case FINAL_STATUS_UNSUPPORTED_SCHEME:
case FINAL_STATUS_RECENTLY_VISITED:
case FINAL_STATUS_SAFE_BROWSING:
case FINAL_STATUS_SSL_CLIENT_CERTIFICATE_REQUESTED:
case FINAL_STATUS_CACHE_OR_HISTORY_CLEARED:
case FINAL_STATUS_CANCELLED:
case FINAL_STATUS_SSL_ERROR:
case FINAL_STATUS_DUPLICATE:
case FINAL_STATUS_OPEN_URL:
case FINAL_STATUS_NAVIGATION_INTERCEPTED:
case FINAL_STATUS_PRERENDERING_DISABLED:
case FINAL_STATUS_BLOCK_THIRD_PARTY_COOKIES:
case FINAL_STATUS_LOW_END_DEVICE:
case FINAL_STATUS_BROWSER_SWITCH:
case FINAL_STATUS_GWS_HOLDBACK:
case FINAL_STATUS_UNKNOWN:
case FINAL_STATUS_NAVIGATION_PREDICTOR_HOLDBACK:
case FINAL_STATUS_SINGLE_PROCESS:
case FINAL_STATUS_LINK_REL_NEXT_NOT_ALLOWED:
case FINAL_STATUS_PREFETCH_HOLDBACK:
case FINAL_STATUS_MAX:
attempt_->SetFailureReason(ToPreloadingFailureReason(status));
// We reset the attempt to ensure we don't update once we have reported it
// as failure or accidentally use it for any other prerender attempts as
// PrerenderHost deletion is async.
attempt_.reset();
return;
}
}
void NoStatePrefetchContents::StartPrerendering(
const gfx::Rect& bounds,
SessionStorageNamespace* session_storage_namespace,
base::WeakPtr<content::PreloadingAttempt> attempt) {
DCHECK(browser_context_);
DCHECK(!bounds.IsEmpty());
DCHECK(!prerendering_has_started_);
DCHECK(!no_state_prefetch_contents_);
DCHECK_EQ(1U, alias_urls_.size());
if (session_storage_namespace)
session_storage_namespace_id_ = session_storage_namespace->id();
bounds_ = bounds;
DCHECK(load_start_time_.is_null());
load_start_time_ = base::TimeTicks::Now();
prerendering_has_started_ = true;
attempt_ = std::move(attempt);
SetPreloadingTriggeringOutcome(
attempt_.get(), content::PreloadingTriggeringOutcome::kRunning);
no_state_prefetch_contents_ = CreateWebContents(session_storage_namespace);
content::WebContentsObserver::Observe(no_state_prefetch_contents_.get());
delegate_->OnNoStatePrefetchContentsCreated(
no_state_prefetch_contents_.get());
web_contents_delegate_ = std::make_unique<WebContentsDelegateImpl>(this);
no_state_prefetch_contents_->SetDelegate(web_contents_delegate_.get());
// Set the size of the prerender WebContents.
no_state_prefetch_contents_->Resize(bounds_);
no_state_prefetch_contents_->WasHidden();
// TODO(davidben): This logic assumes each prerender has at most one
// process. https://crbug.com/440544
no_state_prefetch_manager()->AddPrerenderProcessHost(
GetPrimaryMainFrame()->GetProcess());
NotifyPrefetchStart();
content::NavigationController::LoadURLParams load_url_params(prerender_url_);
load_url_params.referrer = referrer_;
load_url_params.initiator_origin = initiator_origin_;
load_url_params.transition_type = ui::PAGE_TRANSITION_LINK;
if (origin_ == ORIGIN_OMNIBOX) {
load_url_params.transition_type = ui::PageTransitionFromInt(
ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
} else if (origin_ == ORIGIN_NAVIGATION_PREDICTOR) {
load_url_params.transition_type =
ui::PageTransitionFromInt(ui::PAGE_TRANSITION_GENERATED);
}
no_state_prefetch_contents_->GetController().LoadURLWithParams(
load_url_params);
}
void NoStatePrefetchContents::SetFinalStatus(FinalStatus final_status) {
DCHECK_GE(final_status, FINAL_STATUS_USED);
DCHECK_LT(final_status, FINAL_STATUS_MAX);
DCHECK_EQ(FINAL_STATUS_UNKNOWN, final_status_);
final_status_ = final_status;
SetPreloadingFailureReason(final_status);
}
NoStatePrefetchContents::~NoStatePrefetchContents() {
DCHECK_NE(FINAL_STATUS_UNKNOWN, final_status());
DCHECK(prerendering_has_been_cancelled() ||
final_status() == FINAL_STATUS_USED);
DCHECK_NE(ORIGIN_MAX, origin());
no_state_prefetch_manager_->RecordFinalStatus(origin(), final_status());
no_state_prefetch_manager_->RecordNetworkBytesConsumed(origin(),
network_bytes_);
if (no_state_prefetch_contents_) {
no_state_prefetch_contents_->SetDelegate(nullptr);
content::WebContentsObserver::Observe(nullptr);
delegate_->ReleaseNoStatePrefetchContents(
no_state_prefetch_contents_.get());
}
}
void NoStatePrefetchContents::AddObserver(Observer* observer) {
DCHECK_EQ(FINAL_STATUS_UNKNOWN, final_status_);
observer_list_.AddObserver(observer);
}
void NoStatePrefetchContents::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
std::unique_ptr<WebContents> NoStatePrefetchContents::CreateWebContents(
SessionStorageNamespace* session_storage_namespace) {
// TODO(ajwong): Remove the temporary map once prerendering is aware of
// multiple session storage namespaces per tab.
return WebContents::CreateWithSessionStorage(
WebContents::CreateParams(browser_context_),
CreateMapWithDefaultSessionStorageNamespace(browser_context_,
session_storage_namespace));
}
void NoStatePrefetchContents::NotifyPrefetchStart() {
DCHECK_EQ(FINAL_STATUS_UNKNOWN, final_status_);
for (Observer& observer : observer_list_)
observer.OnPrefetchStart(this);
}
void NoStatePrefetchContents::NotifyPrefetchStopLoading() {
// Set the status to Ready once the prefetch stops loading. For
// NoStatePrefetch we don't know if the final resource was used from cache
// later on or not. kReady doesn't mean it is a success.
SetPreloadingTriggeringOutcome(attempt_.get(),
content::PreloadingTriggeringOutcome::kReady);
for (Observer& observer : observer_list_)
observer.OnPrefetchStopLoading(this);
}
void NoStatePrefetchContents::NotifyPrefetchStop() {
DCHECK_NE(FINAL_STATUS_UNKNOWN, final_status_);
for (Observer& observer : observer_list_)
observer.OnPrefetchStop(this);
observer_list_.Clear();
}
bool NoStatePrefetchContents::CheckURL(const GURL& url) {
if (!url.SchemeIsHTTPOrHTTPS()) {
Destroy(FINAL_STATUS_UNSUPPORTED_SCHEME);
return false;
}
if (no_state_prefetch_manager_->HasRecentlyBeenNavigatedTo(origin(), url)) {
Destroy(FINAL_STATUS_RECENTLY_VISITED);
return false;
}
return true;
}
bool NoStatePrefetchContents::AddAliasURL(const GURL& url) {
if (!CheckURL(url))
return false;
alias_urls_.push_back(url);
return true;
}
bool NoStatePrefetchContents::Matches(
const GURL& url,
SessionStorageNamespace* session_storage_namespace) const {
// TODO(davidben): Remove any consumers that pass in a NULL
// session_storage_namespace and only test with matches.
if (session_storage_namespace &&
session_storage_namespace_id_ != session_storage_namespace->id()) {
return false;
}
return base::Contains(alias_urls_, url);
}
void NoStatePrefetchContents::PrimaryMainFrameRenderProcessGone(
base::TerminationStatus status) {
if (status == base::TERMINATION_STATUS_STILL_RUNNING) {
// The renderer process is being killed because of the browser/test
// shutdown, before the termination notification is received.
Destroy(FINAL_STATUS_APP_TERMINATING);
}
Destroy(FINAL_STATUS_RENDERER_CRASHED);
}
void NoStatePrefetchContents::RenderFrameCreated(
content::RenderFrameHost* render_frame_host) {
// When a new RenderFrame is created for a prerendering WebContents, tell the
// new RenderFrame it's being used for prerendering before any navigations
// occur. Note that this is always triggered before the first navigation, so
// there's no need to send the message just after the WebContents is created.
mojo::AssociatedRemote<prerender::mojom::PrerenderMessages>
prerender_render_frame;
render_frame_host->GetRemoteAssociatedInterfaces()->GetInterface(
&prerender_render_frame);
prerender_render_frame->SetIsPrerendering(
PrerenderHistograms::GetHistogramPrefix(origin_));
}
void NoStatePrefetchContents::DidStopLoading() {
NotifyPrefetchStopLoading();
}
void NoStatePrefetchContents::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
if (!CheckURL(navigation_handle->GetURL()))
return;
// Usually, this event fires if the user clicks or enters a new URL.
// Neither of these can happen in the case of an invisible prerender.
// So the cause is: Some JavaScript caused a new URL to be loaded. In that
// case, the spinner would start again in the browser, so we must reset
// has_finished_loading_ so that the spinner won't be stopped.
has_finished_loading_ = false;
}
void NoStatePrefetchContents::DidRedirectNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame())
return;
// If it's a redirect on the top-level resource, the name needs to be
// remembered for future matching, and if it redirects to an https resource,
// it needs to be canceled. If a subresource is redirected, nothing changes.
CheckURL(navigation_handle->GetURL());
}
void NoStatePrefetchContents::DidFinishLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
if (render_frame_host->IsInPrimaryMainFrame())
has_finished_loading_ = true;
}
void NoStatePrefetchContents::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
!navigation_handle->HasCommitted()) {
return;
}
if (navigation_handle->IsErrorPage()) {
// Maintain same behavior as old navigation API when the URL is unreachable
// and leads to an error page. Also maintain same final status code that
// previous navigation API returned, which was reached because the URL for
// the error page was kUnreachableWebDataURL and that was interpreted as
// unsupported scheme.
Destroy(FINAL_STATUS_UNSUPPORTED_SCHEME);
return;
}
// Add each redirect as an alias. |navigation_handle->GetURL()| is included in
// |navigation_handle->GetRedirectChain()|.
//
// TODO(davidben): We do not correctly patch up history for renderer-initated
// navigations which add history entries. http://crbug.com/305660.
for (const auto& redirect : navigation_handle->GetRedirectChain()) {
if (!AddAliasURL(redirect))
return;
}
}
void NoStatePrefetchContents::Destroy(FinalStatus final_status) {
DCHECK_NE(final_status, FINAL_STATUS_USED);
if (prerendering_has_been_cancelled_)
return;
SetFinalStatus(final_status);
prerendering_has_been_cancelled_ = true;
no_state_prefetch_manager_->AddToHistory(this);
no_state_prefetch_manager_->SetPrefetchFinalStatusForUrl(prerender_url_,
final_status);
no_state_prefetch_manager_->MoveEntryToPendingDelete(this, final_status);
if (prerendering_has_started())
NotifyPrefetchStop();
}
void NoStatePrefetchContents::DestroyWhenUsingTooManyResources() {
if (process_pid_ == base::kNullProcessId) {
RenderFrameHost* rfh = GetPrimaryMainFrame();
if (!rfh)
return;
content::RenderProcessHost* rph = rfh->GetProcess();
if (!rph)
return;
base::ProcessHandle handle = rph->GetProcess().Handle();
if (handle == base::kNullProcessHandle)
return;
process_pid_ = rph->GetProcess().Pid();
}
if (process_pid_ == base::kNullProcessId)
return;
memory_instrumentation::MemoryInstrumentation::GetInstance()
->RequestPrivateMemoryFootprint(
process_pid_,
base::BindOnce(&NoStatePrefetchContents::DidGetMemoryUsage,
weak_factory_.GetWeakPtr()));
}
void NoStatePrefetchContents::DidGetMemoryUsage(
bool success,
std::unique_ptr<memory_instrumentation::GlobalMemoryDump> global_dump) {
if (!success)
return;
for (const memory_instrumentation::GlobalMemoryDump::ProcessDump& dump :
global_dump->process_dumps()) {
if (dump.pid() != process_pid_)
continue;
// If |final_status_| == |FINAL_STATUS_USED|, then destruction will be
// handled by the entity that set final_status_.
if (dump.os_dump().private_footprint_kb * 1024 >
no_state_prefetch_manager_->config().max_bytes &&
final_status_ != FINAL_STATUS_USED) {
Destroy(FINAL_STATUS_MEMORY_LIMIT_EXCEEDED);
}
return;
}
}
RenderFrameHost* NoStatePrefetchContents::GetPrimaryMainFrame() {
return no_state_prefetch_contents_
? no_state_prefetch_contents_->GetPrimaryMainFrame()
: nullptr;
}
absl::optional<base::Value::Dict> NoStatePrefetchContents::GetAsDict() const {
if (!no_state_prefetch_contents_)
return absl::nullopt;
base::Value::Dict dict;
dict.Set("url", prerender_url_.spec());
base::TimeTicks current_time = base::TimeTicks::Now();
base::TimeDelta duration = current_time - load_start_time_;
dict.Set("duration", static_cast<int>(duration.InSeconds()));
dict.Set("is_loaded", no_state_prefetch_contents_ &&
!no_state_prefetch_contents_->IsLoading());
return dict;
}
void NoStatePrefetchContents::MarkAsUsedForTesting() {
SetFinalStatus(FINAL_STATUS_USED);
NotifyPrefetchStop();
}
void NoStatePrefetchContents::CancelPrerenderForUnsupportedScheme() {
Destroy(FINAL_STATUS_UNSUPPORTED_SCHEME);
}
void NoStatePrefetchContents::CancelPrerenderForNoStatePrefetch() {
Destroy(FINAL_STATUS_NOSTATE_PREFETCH_FINISHED);
}
void NoStatePrefetchContents::AddPrerenderCancelerReceiver(
mojo::PendingReceiver<prerender::mojom::PrerenderCanceler> receiver) {
prerender_canceler_receiver_set_.Add(this, std::move(receiver));
}
void NoStatePrefetchContents::AddNetworkBytes(int64_t bytes) {
network_bytes_ += bytes;
for (Observer& observer : observer_list_)
observer.OnPrefetchNetworkBytesChanged(this);
}
} // namespace prerender