blob: 0109c4bdf822b9772041be6f9c78c897218a0143 [file] [log] [blame]
// Copyright 2019 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 "weblayer/renderer/error_page_helper.h"
#include "components/error_page/common/error.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "weblayer/common/features.h"
namespace weblayer {
namespace {
base::TimeDelta GetAutoReloadTime(size_t reload_count) {
static const int kDelaysMs[] = {0, 5000, 30000, 60000,
300000, 600000, 1800000};
if (reload_count >= base::size(kDelaysMs))
reload_count = base::size(kDelaysMs) - 1;
return base::TimeDelta::FromMilliseconds(kDelaysMs[reload_count]);
}
bool IsErrorPage(const GURL& url) {
return url.is_valid() && url.spec() == content::kUnreachableWebDataURL;
}
bool IsReloadableError(const error_page::Error& error, bool was_failed_post) {
const GURL& url = error.url();
return error.domain() == error_page::Error::kNetErrorDomain &&
error.reason() != net::ERR_ABORTED &&
// For now, net::ERR_UNKNOWN_URL_SCHEME is only being displayed on
// Chrome for Android.
error.reason() != net::ERR_UNKNOWN_URL_SCHEME &&
// Do not trigger if the server rejects a client certificate.
// https://crbug.com/431387
!net::IsClientCertificateError(error.reason()) &&
// Some servers reject client certificates with a generic
// handshake_failure alert.
// https://crbug.com/431387
error.reason() != net::ERR_SSL_PROTOCOL_ERROR &&
// Do not trigger for blacklisted URLs.
// https://crbug.com/803839
error.reason() != net::ERR_BLOCKED_BY_ADMINISTRATOR &&
// Do not trigger for requests that were blocked by the browser itself.
error.reason() != net::ERR_BLOCKED_BY_CLIENT && !was_failed_post &&
// Do not trigger for this error code because it is used by Chrome
// while an auth prompt is being displayed.
error.reason() != net::ERR_INVALID_AUTH_CREDENTIALS &&
// Don't auto-reload non-http/https schemas.
// https://crbug.com/471713
url.SchemeIsHTTPOrHTTPS() &&
// Don't auto reload if the error was a secure DNS network error, since
// the reload may interfere with the captive portal probe state.
// TODO(crbug.com/1016164): Explore how to allow reloads for secure DNS
// network errors without interfering with the captive portal probe
// state.
!error.resolve_error_info().is_secure_network_error;
}
} // namespace
struct ErrorPageHelper::ErrorPageInfo {
ErrorPageInfo(const error_page::Error& error, bool was_failed_post)
: error(error), was_failed_post(was_failed_post) {}
// Information about the failed page load.
error_page::Error error;
bool was_failed_post = false;
// True if a page has completed loading, at which point it can receive
// updates.
bool is_finished_loading = false;
};
// static
void ErrorPageHelper::Create(content::RenderFrame* render_frame) {
if (render_frame->IsMainFrame())
new ErrorPageHelper(render_frame);
}
// static
ErrorPageHelper* ErrorPageHelper::GetForFrame(
content::RenderFrame* render_frame) {
return render_frame->IsMainFrame() ? Get(render_frame) : nullptr;
}
void ErrorPageHelper::PrepareErrorPage(const error_page::Error& error,
bool was_failed_post) {
pending_error_page_info_ =
std::make_unique<ErrorPageInfo>(error, was_failed_post);
}
bool ErrorPageHelper::ShouldSuppressErrorPage() {
// If there's no auto reload attempt in flight, this error page didn't come
// from auto reload, so don't suppress it.
if (!auto_reload_in_flight_)
return false;
uncommitted_load_started_ = false;
// This serves to terminate the auto-reload in flight attempt. If
// ShouldSuppressErrorPage is called, the auto-reload yielded an error, which
// means the request was already sent.
auto_reload_in_flight_ = false;
MaybeStartAutoReloadTimer();
return true;
}
void ErrorPageHelper::DidStartNavigation(
const GURL& url,
base::Optional<blink::WebNavigationType> navigation_type) {
uncommitted_load_started_ = true;
// If there's no pending error page information associated with the page load,
// or the new page is not an error page, then reset pending error page state.
if (!pending_error_page_info_ || !IsErrorPage(url)) {
CancelPendingReload();
} else {
// Halt auto-reload if it's currently scheduled. OnFinishLoad will trigger
// auto-reload if appropriate.
PauseAutoReloadTimer();
}
}
void ErrorPageHelper::DidCommitProvisionalLoad(bool is_same_document_navigation,
ui::PageTransition transition) {
if (is_same_document_navigation)
return;
// If a page is committing, either it's an error page and autoreload will be
// started again below, or it's a success page and we need to clear autoreload
// state.
auto_reload_in_flight_ = false;
// uncommitted_load_started_ could already be false, since RenderFrameImpl
// calls OnCommitLoad once for each in-page navigation (like a fragment
// change) with no corresponding OnStartLoad.
uncommitted_load_started_ = false;
committed_error_page_info_ = std::move(pending_error_page_info_);
weak_factory_.InvalidateWeakPtrs();
}
void ErrorPageHelper::DidFinishLoad() {
if (!committed_error_page_info_) {
auto_reload_count_ = 0;
return;
}
security_interstitials::SecurityInterstitialPageController::Install(
render_frame(), weak_factory_.GetWeakPtr());
committed_error_page_info_->is_finished_loading = true;
if (IsReloadableError(committed_error_page_info_->error,
committed_error_page_info_->was_failed_post)) {
MaybeStartAutoReloadTimer();
}
}
void ErrorPageHelper::OnStop() {
CancelPendingReload();
uncommitted_load_started_ = false;
auto_reload_count_ = 0;
auto_reload_in_flight_ = false;
}
void ErrorPageHelper::WasShown() {
if (auto_reload_paused_)
MaybeStartAutoReloadTimer();
}
void ErrorPageHelper::WasHidden() {
PauseAutoReloadTimer();
}
void ErrorPageHelper::OnDestruct() {
delete this;
}
void ErrorPageHelper::SendCommand(
security_interstitials::SecurityInterstitialCommand command) {
mojo::AssociatedRemote<security_interstitials::mojom::InterstitialCommands>
interface = GetInterface();
switch (command) {
case security_interstitials::CMD_DONT_PROCEED:
interface->DontProceed();
break;
case security_interstitials::CMD_PROCEED:
interface->Proceed();
break;
case security_interstitials::CMD_SHOW_MORE_SECTION:
interface->ShowMoreSection();
break;
case security_interstitials::CMD_OPEN_HELP_CENTER:
interface->OpenHelpCenter();
break;
case security_interstitials::CMD_OPEN_DIAGNOSTIC:
// Used by safebrowsing interstials.
interface->OpenDiagnostic();
break;
case security_interstitials::CMD_RELOAD:
interface->Reload();
break;
case security_interstitials::CMD_OPEN_LOGIN:
interface->OpenLogin();
break;
case security_interstitials::CMD_OPEN_DATE_SETTINGS:
interface->OpenDateSettings();
break;
case security_interstitials::CMD_REPORT_PHISHING_ERROR:
// Used by safebrowsing phishing interstitial.
interface->ReportPhishingError();
break;
case security_interstitials::CMD_DO_REPORT:
case security_interstitials::CMD_DONT_REPORT:
case security_interstitials::CMD_OPEN_REPORTING_PRIVACY:
case security_interstitials::CMD_OPEN_WHITEPAPER:
// Commands not used by the generic SSL error pages.
// Also not currently used by the safebrowsing error pages.
NOTREACHED();
break;
case security_interstitials::CMD_ERROR:
case security_interstitials::CMD_TEXT_FOUND:
case security_interstitials::CMD_TEXT_NOT_FOUND:
// Commands for testing.
NOTREACHED();
break;
}
}
mojo::AssociatedRemote<security_interstitials::mojom::InterstitialCommands>
ErrorPageHelper::GetInterface() {
mojo::AssociatedRemote<security_interstitials::mojom::InterstitialCommands>
interface;
render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(&interface);
return interface;
}
ErrorPageHelper::ErrorPageHelper(content::RenderFrame* render_frame)
: RenderFrameObserver(render_frame),
RenderFrameObserverTracker<ErrorPageHelper>(render_frame),
online_(content::RenderThread::Get()->IsOnline()) {
content::RenderThread::Get()->AddObserver(this);
}
ErrorPageHelper::~ErrorPageHelper() {
content::RenderThread::Get()->RemoveObserver(this);
}
void ErrorPageHelper::Reload() {
if (!committed_error_page_info_)
return;
render_frame()->GetWebFrame()->StartReload(blink::WebFrameLoadType::kReload);
}
void ErrorPageHelper::MaybeStartAutoReloadTimer() {
// Automation tools expect to be in control of reloads.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableAutomation) ||
!base::FeatureList::IsEnabled(features::kEnableAutoReload)) {
return;
}
if (!committed_error_page_info_ ||
!committed_error_page_info_->is_finished_loading ||
pending_error_page_info_ || uncommitted_load_started_) {
return;
}
StartAutoReloadTimer();
}
void ErrorPageHelper::StartAutoReloadTimer() {
DCHECK(committed_error_page_info_);
DCHECK(IsReloadableError(committed_error_page_info_->error,
committed_error_page_info_->was_failed_post));
if (!online_ || render_frame()->IsHidden()) {
auto_reload_paused_ = true;
return;
}
auto_reload_paused_ = false;
base::TimeDelta delay = GetAutoReloadTime(auto_reload_count_);
auto_reload_timer_.Stop();
auto_reload_timer_.Start(
FROM_HERE, delay,
base::BindOnce(&ErrorPageHelper::AutoReloadTimerFired,
base::Unretained(this)));
}
void ErrorPageHelper::AutoReloadTimerFired() {
// AutoReloadTimerFired only runs if:
// 1. StartAutoReloadTimer was previously called, which requires that
// committed_error_page_info_ is populated;
// 2. No other page load has started since (1), since DidStartNavigation stops
// the auto-reload timer.
DCHECK(committed_error_page_info_);
auto_reload_count_++;
auto_reload_in_flight_ = true;
Reload();
}
void ErrorPageHelper::PauseAutoReloadTimer() {
if (!auto_reload_timer_.IsRunning())
return;
DCHECK(committed_error_page_info_);
DCHECK(!auto_reload_paused_);
auto_reload_timer_.Stop();
auto_reload_paused_ = true;
}
void ErrorPageHelper::NetworkStateChanged(bool online) {
bool was_online = online_;
online_ = online;
if (!was_online && online) {
// Transitioning offline -> online
if (auto_reload_paused_)
MaybeStartAutoReloadTimer();
} else if (was_online && !online) {
// Transitioning online -> offline
if (auto_reload_timer_.IsRunning())
auto_reload_count_ = 0;
PauseAutoReloadTimer();
}
}
void ErrorPageHelper::CancelPendingReload() {
auto_reload_timer_.Stop();
auto_reload_paused_ = false;
}
} // namespace weblayer