blob: 487d946e2d85a00a1988970f408301b0353c2827 [file] [log] [blame]
// Copyright 2013 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 "chrome/renderer/net/net_error_helper_core.h"
#include <stddef.h>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/i18n/rtl.h"
#include "base/json/json_reader.h"
#include "base/json/json_value_converter.h"
#include "base/json/json_writer.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/common/chrome_features.h"
#include "components/error_page/common/error_page_params.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_formatter.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/renderer/render_thread.h"
#include "net/base/escape.h"
#include "net/base/net_errors.h"
#include "third_party/blink/public/platform/web_string.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
#include "url/url_constants.h"
using OfflineContentOnNetErrorFeatureState =
error_page::LocalizedError::OfflineContentOnNetErrorFeatureState;
namespace {
// |NetErrorNavigationCorrectionTypes| enum id for Web search query.
// Other correction types uses the |kCorrectionResourceTable| array order.
const int kWebSearchQueryUMAId = 100;
// Number of URL correction suggestions to display.
const int kMaxUrlCorrectionsToDisplay = 1;
struct CorrectionTypeToResourceTable {
int resource_id;
const char* correction_type;
};
// Note: Ordering should be the same as |NetErrorNavigationCorrectionTypes| enum
// in histograms.xml.
const CorrectionTypeToResourceTable kCorrectionResourceTable[] = {
{IDS_ERRORPAGES_SUGGESTION_VISIT_GOOGLE_CACHE, "cachedPage"},
// "reloadPage" is has special handling.
{IDS_ERRORPAGES_SUGGESTION_CORRECTED_URL, "urlCorrection"},
{IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL, "siteDomain"},
{IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL, "host"},
{IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL, "sitemap"},
{IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL, "pathParentFolder"},
// "siteSearchQuery" is not yet supported.
// TODO(mmenke): Figure out what format "siteSearchQuery" uses for its
// suggestions.
// "webSearchQuery" has special handling.
{IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL, "contentOverlap"},
{IDS_ERRORPAGES_SUGGESTION_CORRECTED_URL, "emphasizedUrlCorrection"},
};
struct NavigationCorrection {
NavigationCorrection() : is_porn(false), is_soft_porn(false) {}
static void RegisterJSONConverter(
base::JSONValueConverter<NavigationCorrection>* converter) {
converter->RegisterStringField("correctionType",
&NavigationCorrection::correction_type);
converter->RegisterStringField("urlCorrection",
&NavigationCorrection::url_correction);
converter->RegisterStringField("clickType",
&NavigationCorrection::click_type);
converter->RegisterStringField("clickData",
&NavigationCorrection::click_data);
converter->RegisterBoolField("isPorn", &NavigationCorrection::is_porn);
converter->RegisterBoolField("isSoftPorn",
&NavigationCorrection::is_soft_porn);
}
std::string correction_type;
std::string url_correction;
std::string click_type;
std::string click_data;
bool is_porn;
bool is_soft_porn;
};
struct NavigationCorrectionResponse {
std::string event_id;
std::string fingerprint;
std::vector<std::unique_ptr<NavigationCorrection>> corrections;
static void RegisterJSONConverter(
base::JSONValueConverter<NavigationCorrectionResponse>* converter) {
converter->RegisterStringField("result.eventId",
&NavigationCorrectionResponse::event_id);
converter->RegisterStringField("result.fingerprint",
&NavigationCorrectionResponse::fingerprint);
converter->RegisterRepeatedMessage(
"result.UrlCorrections", &NavigationCorrectionResponse::corrections);
}
};
base::TimeDelta GetAutoReloadTime(size_t reload_count) {
static const int kDelaysMs[] = {0, 5000, 30000, 60000,
300000, 600000, 1800000};
if (reload_count >= arraysize(kDelaysMs))
reload_count = arraysize(kDelaysMs) - 1;
return base::TimeDelta::FromMilliseconds(kDelaysMs[reload_count]);
}
// Returns whether |error| is a DNS-related error (and therefore whether
// the tab helper should start a DNS probe after receiving it).
bool IsNetDnsError(const error_page::Error& error) {
return error.domain() == error_page::Error::kNetErrorDomain &&
net::IsDnsError(error.reason());
}
GURL SanitizeURL(const GURL& url) {
GURL::Replacements remove_params;
remove_params.ClearUsername();
remove_params.ClearPassword();
remove_params.ClearQuery();
remove_params.ClearRef();
return url.ReplaceComponents(remove_params);
}
// Sanitizes and formats a URL for upload to the error correction service.
std::string PrepareUrlForUpload(const GURL& url) {
// TODO(yuusuke): Change to url_formatter::FormatUrl when Link Doctor becomes
// unicode-capable.
std::string spec_to_send = SanitizeURL(url).spec();
// Notify navigation correction service of the url truncation by sending of
// "?" at the end.
if (url.has_query())
spec_to_send.append("?");
return spec_to_send;
}
// Given an Error, returns true if the FixURL service should be used
// for that error. Also sets |error_param| to the string that should be sent to
// the FixURL service to identify the error type.
bool ShouldUseFixUrlServiceForError(const error_page::Error& error,
std::string* error_param) {
error_param->clear();
// Don't use the correction service for HTTPS (for privacy reasons).
GURL unreachable_url(error.url());
if (GURL(unreachable_url).SchemeIsCryptographic())
return false;
const auto& domain = error.domain();
if (domain == error_page::Error::kHttpErrorDomain && error.reason() == 404) {
*error_param = "http404";
return true;
}
if (IsNetDnsError(error)) {
*error_param = "dnserror";
return true;
}
if (domain == error_page::Error::kNetErrorDomain &&
(error.reason() == net::ERR_CONNECTION_FAILED ||
error.reason() == net::ERR_CONNECTION_REFUSED ||
error.reason() == net::ERR_ADDRESS_UNREACHABLE ||
error.reason() == net::ERR_CONNECTION_TIMED_OUT)) {
*error_param = "connectionFailure";
return true;
}
return false;
}
// Creates a request body for use with the fixurl service. Sets parameters
// shared by all types of requests to the service. |correction_params| must
// contain the parameters specific to the actual request type.
std::string CreateRequestBody(
const std::string& method,
const std::string& error_param,
const NetErrorHelperCore::NavigationCorrectionParams& correction_params,
std::unique_ptr<base::DictionaryValue> params_dict) {
// Set params common to all request types.
params_dict->SetString("key", correction_params.api_key);
params_dict->SetString("clientName", "chrome");
params_dict->SetString("error", error_param);
if (!correction_params.language.empty())
params_dict->SetString("language", correction_params.language);
if (!correction_params.country_code.empty())
params_dict->SetString("originCountry", correction_params.country_code);
base::DictionaryValue request_dict;
request_dict.SetString("method", method);
request_dict.SetString("apiVersion", "v1");
request_dict.Set("params", std::move(params_dict));
std::string request_body;
bool success = base::JSONWriter::Write(request_dict, &request_body);
DCHECK(success);
return request_body;
}
// If URL correction information should be retrieved remotely for a main frame
// load that failed with |error|, returns true and sets
// |correction_request_body| to be the body for the correction request.
std::string CreateFixUrlRequestBody(
const error_page::Error& error,
const NetErrorHelperCore::NavigationCorrectionParams& correction_params) {
std::string error_param;
bool result = ShouldUseFixUrlServiceForError(error, &error_param);
DCHECK(result);
// TODO(mmenke): Investigate open sourcing the relevant protocol buffers and
// using those directly instead.
std::unique_ptr<base::DictionaryValue> params(new base::DictionaryValue());
params->SetString("urlQuery", PrepareUrlForUpload(error.url()));
return CreateRequestBody("linkdoctor.fixurl.fixurl", error_param,
correction_params, std::move(params));
}
std::string CreateClickTrackingUrlRequestBody(
const error_page::Error& error,
const NetErrorHelperCore::NavigationCorrectionParams& correction_params,
const NavigationCorrectionResponse& response,
const NavigationCorrection& correction) {
std::string error_param;
bool result = ShouldUseFixUrlServiceForError(error, &error_param);
DCHECK(result);
std::unique_ptr<base::DictionaryValue> params(new base::DictionaryValue());
params->SetString("originalUrlQuery", PrepareUrlForUpload(error.url()));
params->SetString("clickedUrlCorrection", correction.url_correction);
params->SetString("clickType", correction.click_type);
params->SetString("clickData", correction.click_data);
params->SetString("eventId", response.event_id);
params->SetString("fingerprint", response.fingerprint);
return CreateRequestBody("linkdoctor.fixurl.clicktracking", error_param,
correction_params, std::move(params));
}
base::string16 FormatURLForDisplay(const GURL& url, bool is_rtl) {
// Translate punycode into UTF8, unescape UTF8 URLs.
base::string16 url_for_display(url_formatter::FormatUrl(
url, url_formatter::kFormatUrlOmitNothing, net::UnescapeRule::NORMAL,
nullptr, nullptr, nullptr));
// URLs are always LTR.
if (is_rtl)
base::i18n::WrapStringWithLTRFormatting(&url_for_display);
return url_for_display;
}
std::unique_ptr<NavigationCorrectionResponse> ParseNavigationCorrectionResponse(
const std::string raw_response) {
// TODO(mmenke): Open source related protocol buffers and use them directly.
std::unique_ptr<base::Value> parsed = base::JSONReader::Read(raw_response);
std::unique_ptr<NavigationCorrectionResponse> response(
new NavigationCorrectionResponse());
base::JSONValueConverter<NavigationCorrectionResponse> converter;
if (!parsed || !converter.Convert(*parsed, response.get()))
response.reset();
return response;
}
void LogCorrectionTypeShown(int type_id) {
UMA_HISTOGRAM_ENUMERATION(
"Net.ErrorPageCounts.NavigationCorrectionLinksShown", type_id,
kWebSearchQueryUMAId + 1);
}
std::unique_ptr<error_page::ErrorPageParams> CreateErrorPageParams(
const NavigationCorrectionResponse& response,
const error_page::Error& error,
const NetErrorHelperCore::NavigationCorrectionParams& correction_params,
bool is_rtl) {
// Version of URL for display in suggestions. It has to be sanitized first
// because any received suggestions will be relative to the sanitized URL.
base::string16 original_url_for_display =
FormatURLForDisplay(SanitizeURL(GURL(error.url())), is_rtl);
std::unique_ptr<error_page::ErrorPageParams> params(
new error_page::ErrorPageParams());
params->override_suggestions.reset(new base::ListValue());
std::unique_ptr<base::ListValue> parsed_corrections(new base::ListValue());
for (auto it = response.corrections.begin(); it != response.corrections.end();
++it) {
// Doesn't seem like a good idea to show these.
if ((*it)->is_porn || (*it)->is_soft_porn)
continue;
int tracking_id = it - response.corrections.begin();
if ((*it)->correction_type == "reloadPage") {
params->suggest_reload = true;
params->reload_tracking_id = tracking_id;
continue;
}
if ((*it)->correction_type == "webSearchQuery") {
// If there are multiple searches suggested, use the first suggestion.
if (params->search_terms.empty()) {
params->search_url = correction_params.search_url;
params->search_terms = (*it)->url_correction;
params->search_tracking_id = tracking_id;
LogCorrectionTypeShown(kWebSearchQueryUMAId);
}
continue;
}
// Allow reload page and web search query to be empty strings, but not
// links.
if ((*it)->url_correction.empty() ||
(params->override_suggestions->GetSize() >=
kMaxUrlCorrectionsToDisplay)) {
continue;
}
size_t correction_index;
for (correction_index = 0;
correction_index < arraysize(kCorrectionResourceTable);
++correction_index) {
if ((*it)->correction_type !=
kCorrectionResourceTable[correction_index].correction_type) {
continue;
}
std::unique_ptr<base::DictionaryValue> suggest(
new base::DictionaryValue());
suggest->SetString(
"summary",
l10n_util::GetStringUTF16(
kCorrectionResourceTable[correction_index].resource_id));
suggest->SetString("urlCorrection", (*it)->url_correction);
suggest->SetString(
"urlCorrectionForDisplay",
FormatURLForDisplay(GURL((*it)->url_correction), is_rtl));
suggest->SetString("originalUrlForDisplay", original_url_for_display);
suggest->SetInteger("trackingId", tracking_id);
suggest->SetInteger("type", static_cast<int>(correction_index));
params->override_suggestions->Append(std::move(suggest));
LogCorrectionTypeShown(static_cast<int>(correction_index));
break;
}
}
if (params->override_suggestions->empty() && !params->search_url.is_valid())
params.reset();
return params;
}
void ReportAutoReloadSuccess(const error_page::Error& error, size_t count) {
if (error.domain() != error_page::Error::kNetErrorDomain)
return;
base::UmaHistogramSparse("Net.AutoReload.ErrorAtSuccess", -error.reason());
UMA_HISTOGRAM_COUNTS_1M("Net.AutoReload.CountAtSuccess",
static_cast<base::HistogramBase::Sample>(count));
if (count == 1) {
base::UmaHistogramSparse("Net.AutoReload.ErrorAtFirstSuccess",
-error.reason());
}
}
void ReportAutoReloadFailure(const error_page::Error& error, size_t count) {
if (error.domain() != error_page::Error::kNetErrorDomain)
return;
base::UmaHistogramSparse("Net.AutoReload.ErrorAtStop", -error.reason());
UMA_HISTOGRAM_COUNTS_1M("Net.AutoReload.CountAtStop",
static_cast<base::HistogramBase::Sample>(count));
}
// Tracks navigation correction service usage in UMA to enable more in depth
// analysis.
void TrackClickUMA(std::string type_id) {
// Web search suggestion isn't in |kCorrectionResourceTable| array.
if (type_id == "webSearchQuery") {
UMA_HISTOGRAM_ENUMERATION(
"Net.ErrorPageCounts.NavigationCorrectionLinksUsed",
kWebSearchQueryUMAId, kWebSearchQueryUMAId + 1);
return;
}
size_t correction_index;
for (correction_index = 0;
correction_index < arraysize(kCorrectionResourceTable);
++correction_index) {
if (kCorrectionResourceTable[correction_index].correction_type == type_id) {
UMA_HISTOGRAM_ENUMERATION(
"Net.ErrorPageCounts.NavigationCorrectionLinksUsed",
static_cast<int>(correction_index), kWebSearchQueryUMAId + 1);
break;
}
}
}
} // namespace
struct NetErrorHelperCore::ErrorPageInfo {
ErrorPageInfo(error_page::Error error,
bool was_failed_post,
bool was_ignoring_cache)
: error(error),
was_failed_post(was_failed_post),
was_ignoring_cache(was_ignoring_cache),
needs_dns_updates(false),
needs_load_navigation_corrections(false),
reload_button_in_page(false),
show_saved_copy_button_in_page(false),
show_cached_copy_button_in_page(false),
download_button_in_page(false),
is_finished_loading(false),
auto_reload_triggered(false),
offline_content_feature_state(
OfflineContentOnNetErrorFeatureState::kDisabled),
auto_fetch_allowed(false) {}
// Information about the failed page load.
error_page::Error error;
bool was_failed_post;
bool was_ignoring_cache;
// Information about the status of the error page.
// True if a page is a DNS error page and has not yet received a final DNS
// probe status.
bool needs_dns_updates;
// True if a blank page was loaded, and navigation corrections need to be
// loaded to generate the real error page.
bool needs_load_navigation_corrections;
// Navigation correction service paramers, which will be used in response to
// certain types of network errors. They are all stored here in case they
// change over the course of displaying the error page.
std::unique_ptr<NetErrorHelperCore::NavigationCorrectionParams>
navigation_correction_params;
std::unique_ptr<NavigationCorrectionResponse> navigation_correction_response;
// All the navigation corrections that have been clicked, for tracking
// purposes.
std::set<int> clicked_corrections;
// Track if specific buttons are included in an error page, for statistics.
bool reload_button_in_page;
bool show_saved_copy_button_in_page;
bool show_cached_copy_button_in_page;
bool download_button_in_page;
// True if a page has completed loading, at which point it can receive
// updates.
bool is_finished_loading;
// True if the auto-reload timer has fired and a reload is or has been in
// flight.
bool auto_reload_triggered;
// State of the offline content on net error page feature. Only enabled if
// the feature is enabled, and the error page is an offline error.
OfflineContentOnNetErrorFeatureState offline_content_feature_state;
// True if auto-fetch-on-dino-page is enabled and allowed for this error page.
bool auto_fetch_allowed;
};
NetErrorHelperCore::NavigationCorrectionParams::NavigationCorrectionParams() {}
NetErrorHelperCore::NavigationCorrectionParams::NavigationCorrectionParams(
const NavigationCorrectionParams& other) = default;
NetErrorHelperCore::NavigationCorrectionParams::~NavigationCorrectionParams() {}
bool NetErrorHelperCore::IsReloadableError(
const NetErrorHelperCore::ErrorPageInfo& info) {
GURL url = info.error.url();
return info.error.domain() == error_page::Error::kNetErrorDomain &&
info.error.reason() != net::ERR_ABORTED &&
// For now, net::ERR_UNKNOWN_URL_SCHEME is only being displayed on
// Chrome for Android.
info.error.reason() != net::ERR_UNKNOWN_URL_SCHEME &&
// Do not trigger if the server rejects a client certificate.
// https://crbug.com/431387
!net::IsClientCertificateError(info.error.reason()) &&
// Some servers reject client certificates with a generic
// handshake_failure alert.
// https://crbug.com/431387
info.error.reason() != net::ERR_SSL_PROTOCOL_ERROR &&
// Do not trigger for XSS Auditor violations.
info.error.reason() != net::ERR_BLOCKED_BY_XSS_AUDITOR &&
// Do not trigger for blacklisted URLs.
// https://crbug.com/803839
info.error.reason() != net::ERR_BLOCKED_BY_ADMINISTRATOR &&
!info.was_failed_post &&
// Don't auto-reload non-http/https schemas.
// https://crbug.com/471713
url.SchemeIsHTTPOrHTTPS();
}
NetErrorHelperCore::NetErrorHelperCore(Delegate* delegate,
bool auto_reload_enabled,
bool auto_reload_visible_only,
bool is_visible)
: delegate_(delegate),
last_probe_status_(error_page::DNS_PROBE_POSSIBLE),
can_show_network_diagnostics_dialog_(false),
auto_reload_enabled_(auto_reload_enabled),
auto_reload_visible_only_(auto_reload_visible_only),
auto_reload_timer_(new base::OneShotTimer()),
auto_reload_paused_(false),
auto_reload_in_flight_(false),
uncommitted_load_started_(false),
online_(content::RenderThread::Get()->IsOnline()),
visible_(is_visible),
auto_reload_count_(0),
navigation_from_button_(NO_BUTTON)
#if defined(OS_ANDROID)
,
page_auto_fetcher_helper_(
std::make_unique<PageAutoFetcherHelper>(delegate->GetRenderFrame()))
#endif
{
}
NetErrorHelperCore::~NetErrorHelperCore() {
if (committed_error_page_info_ &&
committed_error_page_info_->auto_reload_triggered) {
ReportAutoReloadFailure(committed_error_page_info_->error,
auto_reload_count_);
}
}
void NetErrorHelperCore::CancelPendingFetches() {
// Cancel loading the alternate error page, and prevent any pending error page
// load from starting a new error page load. Swapping in the error page when
// it's finished loading could abort the navigation, otherwise.
if (committed_error_page_info_)
committed_error_page_info_->needs_load_navigation_corrections = false;
if (pending_error_page_info_)
pending_error_page_info_->needs_load_navigation_corrections = false;
delegate_->CancelFetchNavigationCorrections();
auto_reload_timer_->Stop();
auto_reload_paused_ = false;
}
void NetErrorHelperCore::OnStop() {
if (committed_error_page_info_ &&
committed_error_page_info_->auto_reload_triggered) {
ReportAutoReloadFailure(committed_error_page_info_->error,
auto_reload_count_);
}
CancelPendingFetches();
uncommitted_load_started_ = false;
auto_reload_count_ = 0;
auto_reload_in_flight_ = false;
}
void NetErrorHelperCore::OnWasShown() {
visible_ = true;
if (!auto_reload_visible_only_)
return;
if (auto_reload_paused_)
MaybeStartAutoReloadTimer();
}
void NetErrorHelperCore::OnWasHidden() {
visible_ = false;
if (!auto_reload_visible_only_)
return;
PauseAutoReloadTimer();
}
void NetErrorHelperCore::OnStartLoad(FrameType frame_type, PageType page_type) {
if (frame_type != MAIN_FRAME)
return;
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_ || page_type != ERROR_PAGE) {
CancelPendingFetches();
} else {
// Halt auto-reload if it's currently scheduled. OnFinishLoad will trigger
// auto-reload if appropriate.
PauseAutoReloadTimer();
}
}
void NetErrorHelperCore::OnCommitLoad(FrameType frame_type, const GURL& url) {
if (frame_type != MAIN_FRAME)
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;
#if defined(OS_ANDROID)
// Don't need this state. It will be refreshed if another error page is
// loaded.
available_content_helper_.Reset();
#endif
// Track if an error occurred due to a page button press.
// This isn't perfect; if (for instance), the server is slow responding
// to a request generated from the page reload button, and the user hits
// the browser reload button, this code will still believe the
// result is from the page reload button.
if (committed_error_page_info_ && pending_error_page_info_ &&
navigation_from_button_ != NO_BUTTON &&
committed_error_page_info_->error.url() ==
pending_error_page_info_->error.url()) {
DCHECK(navigation_from_button_ == RELOAD_BUTTON ||
navigation_from_button_ == SHOW_SAVED_COPY_BUTTON);
RecordEvent(
navigation_from_button_ == RELOAD_BUTTON
? error_page::NETWORK_ERROR_PAGE_RELOAD_BUTTON_ERROR
: error_page::NETWORK_ERROR_PAGE_SHOW_SAVED_COPY_BUTTON_ERROR);
}
navigation_from_button_ = NO_BUTTON;
if (committed_error_page_info_ && !pending_error_page_info_ &&
committed_error_page_info_->auto_reload_triggered) {
const error_page::Error& error = committed_error_page_info_->error;
const GURL& error_url = error.url();
if (url == error_url)
ReportAutoReloadSuccess(error, auto_reload_count_);
else if (url != content::kUnreachableWebDataURL)
ReportAutoReloadFailure(error, auto_reload_count_);
}
committed_error_page_info_ = std::move(pending_error_page_info_);
}
void NetErrorHelperCore::OnFinishLoad(FrameType frame_type) {
if (frame_type != MAIN_FRAME)
return;
if (!committed_error_page_info_) {
auto_reload_count_ = 0;
return;
}
committed_error_page_info_->is_finished_loading = true;
RecordEvent(error_page::NETWORK_ERROR_PAGE_SHOWN);
if (committed_error_page_info_->reload_button_in_page) {
RecordEvent(error_page::NETWORK_ERROR_PAGE_RELOAD_BUTTON_SHOWN);
}
if (committed_error_page_info_->show_saved_copy_button_in_page) {
RecordEvent(error_page::NETWORK_ERROR_PAGE_SHOW_SAVED_COPY_BUTTON_SHOWN);
}
if (committed_error_page_info_->download_button_in_page) {
RecordEvent(error_page::NETWORK_ERROR_PAGE_DOWNLOAD_BUTTON_SHOWN);
}
if (committed_error_page_info_->reload_button_in_page &&
committed_error_page_info_->show_saved_copy_button_in_page) {
RecordEvent(error_page::NETWORK_ERROR_PAGE_BOTH_BUTTONS_SHOWN);
}
if (committed_error_page_info_->show_cached_copy_button_in_page) {
RecordEvent(error_page::NETWORK_ERROR_PAGE_CACHED_COPY_BUTTON_SHOWN);
}
delegate_->SetIsShowingDownloadButton(
committed_error_page_info_->download_button_in_page);
delegate_->EnablePageHelperFunctions(
static_cast<net::Error>(committed_error_page_info_->error.reason()));
#if defined(OS_ANDROID)
if (committed_error_page_info_->offline_content_feature_state ==
OfflineContentOnNetErrorFeatureState::kEnabledList) {
available_content_helper_.FetchAvailableContent(base::BindOnce(
&Delegate::OfflineContentAvailable, base::Unretained(delegate_)));
} else if (committed_error_page_info_->offline_content_feature_state ==
OfflineContentOnNetErrorFeatureState::kEnabledSummary) {
available_content_helper_.FetchSummary(
base::BindOnce(&Delegate::OfflineContentSummaryAvailable,
base::Unretained(delegate_)));
}
if (committed_error_page_info_->auto_fetch_allowed) {
page_auto_fetcher_helper_->TrySchedule(
false, base::BindOnce(&Delegate::SetAutoFetchState,
base::Unretained(delegate_)));
}
#endif // OS_ANDROID
if (committed_error_page_info_->needs_load_navigation_corrections) {
// If there is another pending error page load, |fix_url| should have been
// cleared.
DCHECK(!pending_error_page_info_);
DCHECK(!committed_error_page_info_->needs_dns_updates);
delegate_->FetchNavigationCorrections(
committed_error_page_info_->navigation_correction_params->url,
CreateFixUrlRequestBody(
committed_error_page_info_->error,
*committed_error_page_info_->navigation_correction_params));
} else if (auto_reload_enabled_ &&
IsReloadableError(*committed_error_page_info_)) {
MaybeStartAutoReloadTimer();
}
if (!committed_error_page_info_->needs_dns_updates ||
last_probe_status_ == error_page::DNS_PROBE_POSSIBLE) {
return;
}
DVLOG(1) << "Error page finished loading; sending saved status.";
UpdateErrorPage();
}
void NetErrorHelperCore::PrepareErrorPage(FrameType frame_type,
const error_page::Error& error,
bool is_failed_post,
bool is_ignoring_cache,
std::string* error_html) {
if (frame_type == MAIN_FRAME) {
// If navigation corrections were needed before, that should have been
// cancelled earlier by starting a new page load (Which has now failed).
DCHECK(!committed_error_page_info_ ||
!committed_error_page_info_->needs_load_navigation_corrections);
pending_error_page_info_.reset(
new ErrorPageInfo(error, is_failed_post, is_ignoring_cache));
pending_error_page_info_->navigation_correction_params.reset(
new NavigationCorrectionParams(navigation_correction_params_));
PrepareErrorPageForMainFrame(pending_error_page_info_.get(), error_html);
} else {
// These values do not matter, as error pages in iframes hide the buttons.
bool reload_button_in_page;
bool show_saved_copy_button_in_page;
bool show_cached_copy_button_in_page;
bool download_button_in_page;
OfflineContentOnNetErrorFeatureState offline_content_feature_state;
bool auto_fetch_allowed;
if (error_html) {
delegate_->GenerateLocalizedErrorPage(
error, is_failed_post,
false /* No diagnostics dialogs allowed for subframes. */, nullptr,
&reload_button_in_page, &show_saved_copy_button_in_page,
&show_cached_copy_button_in_page, &download_button_in_page,
&offline_content_feature_state, &auto_fetch_allowed, error_html);
}
}
}
void NetErrorHelperCore::OnNetErrorInfo(error_page::DnsProbeStatus status) {
DCHECK_NE(error_page::DNS_PROBE_POSSIBLE, status);
last_probe_status_ = status;
if (!committed_error_page_info_ ||
!committed_error_page_info_->needs_dns_updates ||
!committed_error_page_info_->is_finished_loading) {
return;
}
UpdateErrorPage();
}
void NetErrorHelperCore::OnSetCanShowNetworkDiagnosticsDialog(
bool can_show_network_diagnostics_dialog) {
can_show_network_diagnostics_dialog_ = can_show_network_diagnostics_dialog;
}
void NetErrorHelperCore::OnSetNavigationCorrectionInfo(
const GURL& navigation_correction_url,
const std::string& language,
const std::string& country_code,
const std::string& api_key,
const GURL& search_url) {
navigation_correction_params_.url = navigation_correction_url;
navigation_correction_params_.language = language;
navigation_correction_params_.country_code = country_code;
navigation_correction_params_.api_key = api_key;
navigation_correction_params_.search_url = search_url;
}
void NetErrorHelperCore::OnEasterEggHighScoreReceived(int high_score) {
if (!committed_error_page_info_ ||
!committed_error_page_info_->is_finished_loading) {
return;
}
delegate_->InitializeErrorPageEasterEggHighScore(high_score);
}
void NetErrorHelperCore::PrepareErrorPageForMainFrame(
ErrorPageInfo* pending_error_page_info,
std::string* error_html) {
std::string error_param;
error_page::Error error = pending_error_page_info->error;
if (pending_error_page_info->navigation_correction_params &&
pending_error_page_info->navigation_correction_params->url.is_valid() &&
ShouldUseFixUrlServiceForError(error, &error_param)) {
pending_error_page_info->needs_load_navigation_corrections = true;
return;
}
if (IsNetDnsError(pending_error_page_info->error)) {
// The last probe status needs to be reset if this is a DNS error. This
// means that if a DNS error page is committed but has not yet finished
// loading, a DNS probe status scheduled to be sent to it may be thrown
// out, but since the new error page should trigger a new DNS probe, it
// will just get the results for the next page load.
last_probe_status_ = error_page::DNS_PROBE_POSSIBLE;
pending_error_page_info->needs_dns_updates = true;
error = GetUpdatedError(error);
}
if (error_html) {
delegate_->GenerateLocalizedErrorPage(
error, pending_error_page_info->was_failed_post,
can_show_network_diagnostics_dialog_, nullptr,
&pending_error_page_info->reload_button_in_page,
&pending_error_page_info->show_saved_copy_button_in_page,
&pending_error_page_info->show_cached_copy_button_in_page,
&pending_error_page_info->download_button_in_page,
&pending_error_page_info->offline_content_feature_state,
&pending_error_page_info->auto_fetch_allowed, error_html);
}
}
void NetErrorHelperCore::UpdateErrorPage() {
DCHECK(committed_error_page_info_->needs_dns_updates);
DCHECK(committed_error_page_info_->is_finished_loading);
DCHECK_NE(error_page::DNS_PROBE_POSSIBLE, last_probe_status_);
UMA_HISTOGRAM_ENUMERATION("DnsProbe.ErrorPageUpdateStatus",
last_probe_status_, error_page::DNS_PROBE_MAX);
// Every status other than error_page::DNS_PROBE_POSSIBLE and
// error_page::DNS_PROBE_STARTED is a final status code. Once one is reached,
// the page does not need further updates.
if (last_probe_status_ != error_page::DNS_PROBE_STARTED)
committed_error_page_info_->needs_dns_updates = false;
// There is no need to worry about the button display statistics here because
// the presentation of the reload and show saved copy buttons can't be changed
// by a DNS error update.
delegate_->UpdateErrorPage(GetUpdatedError(committed_error_page_info_->error),
committed_error_page_info_->was_failed_post,
can_show_network_diagnostics_dialog_);
}
void NetErrorHelperCore::OnNavigationCorrectionsFetched(
const std::string& corrections,
bool is_rtl) {
// Loading suggestions only starts when a blank error page finishes loading,
// and is cancelled with a new load.
DCHECK(!pending_error_page_info_);
DCHECK(committed_error_page_info_->is_finished_loading);
DCHECK(committed_error_page_info_->needs_load_navigation_corrections);
DCHECK(committed_error_page_info_->navigation_correction_params);
pending_error_page_info_.reset(
new ErrorPageInfo(committed_error_page_info_->error,
committed_error_page_info_->was_failed_post,
committed_error_page_info_->was_ignoring_cache));
pending_error_page_info_->navigation_correction_response =
ParseNavigationCorrectionResponse(corrections);
std::string error_html;
std::unique_ptr<error_page::ErrorPageParams> params;
if (pending_error_page_info_->navigation_correction_response) {
// Copy navigation correction parameters used for the request, so tracking
// requests can still be sent if the configuration changes.
pending_error_page_info_->navigation_correction_params.reset(
new NavigationCorrectionParams(
*committed_error_page_info_->navigation_correction_params));
params = CreateErrorPageParams(
*pending_error_page_info_->navigation_correction_response,
pending_error_page_info_->error,
*pending_error_page_info_->navigation_correction_params, is_rtl);
delegate_->GenerateLocalizedErrorPage(
pending_error_page_info_->error,
pending_error_page_info_->was_failed_post,
can_show_network_diagnostics_dialog_, std::move(params),
&pending_error_page_info_->reload_button_in_page,
&pending_error_page_info_->show_saved_copy_button_in_page,
&pending_error_page_info_->show_cached_copy_button_in_page,
&pending_error_page_info_->download_button_in_page,
&pending_error_page_info_->offline_content_feature_state,
&pending_error_page_info_->auto_fetch_allowed, &error_html);
} else {
// Since |navigation_correction_params| in |pending_error_page_info_| is
// NULL, this won't trigger another attempt to load corrections.
PrepareErrorPageForMainFrame(pending_error_page_info_.get(), &error_html);
}
// TODO(mmenke): Once the new API is in place, look into replacing this
// double page load by just updating the error page, like DNS
// probes do.
delegate_->LoadErrorPage(error_html, pending_error_page_info_->error.url());
}
error_page::Error NetErrorHelperCore::GetUpdatedError(
const error_page::Error& error) const {
// If a probe didn't run or wasn't conclusive, restore the original error.
if (last_probe_status_ == error_page::DNS_PROBE_NOT_RUN ||
last_probe_status_ == error_page::DNS_PROBE_FINISHED_INCONCLUSIVE) {
return error;
}
return error_page::Error::DnsProbeError(error.url(), last_probe_status_,
error.stale_copy_in_cache());
}
void NetErrorHelperCore::Reload(bool bypass_cache) {
if (!committed_error_page_info_) {
return;
}
delegate_->ReloadPage(bypass_cache);
}
bool NetErrorHelperCore::MaybeStartAutoReloadTimer() {
// Automation tools expect to be in control of reloads.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableAutomation)) {
return false;
}
if (!committed_error_page_info_ ||
!committed_error_page_info_->is_finished_loading ||
pending_error_page_info_ || uncommitted_load_started_) {
return false;
}
StartAutoReloadTimer();
return true;
}
void NetErrorHelperCore::StartAutoReloadTimer() {
DCHECK(committed_error_page_info_);
DCHECK(IsReloadableError(*committed_error_page_info_));
committed_error_page_info_->auto_reload_triggered = true;
if (!online_ || (!visible_ && auto_reload_visible_only_)) {
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::Bind(&NetErrorHelperCore::AutoReloadTimerFired,
base::Unretained(this)));
}
void NetErrorHelperCore::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 OnStartLoad stops the
// auto-reload timer.
DCHECK(committed_error_page_info_);
auto_reload_count_++;
auto_reload_in_flight_ = true;
Reload(committed_error_page_info_->was_ignoring_cache);
}
void NetErrorHelperCore::PauseAutoReloadTimer() {
if (!auto_reload_timer_->IsRunning())
return;
DCHECK(committed_error_page_info_);
DCHECK(!auto_reload_paused_);
DCHECK(committed_error_page_info_->auto_reload_triggered);
auto_reload_timer_->Stop();
auto_reload_paused_ = true;
}
void NetErrorHelperCore::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();
}
}
bool NetErrorHelperCore::ShouldSuppressErrorPage(FrameType frame_type,
const GURL& url) {
// Don't suppress child frame errors.
if (frame_type != MAIN_FRAME)
return false;
// 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;
}
#if defined(OS_ANDROID)
void NetErrorHelperCore::SetPageAutoFetcherHelperForTesting(
std::unique_ptr<PageAutoFetcherHelper> page_auto_fetcher_helper) {
page_auto_fetcher_helper_ = std::move(page_auto_fetcher_helper);
}
#endif
void NetErrorHelperCore::ExecuteButtonPress(Button button) {
// If there's no committed error page, should not be invoked.
DCHECK(committed_error_page_info_);
switch (button) {
case RELOAD_BUTTON:
RecordEvent(error_page::NETWORK_ERROR_PAGE_RELOAD_BUTTON_CLICKED);
if (committed_error_page_info_->show_saved_copy_button_in_page) {
RecordEvent(error_page::NETWORK_ERROR_PAGE_BOTH_BUTTONS_RELOAD_CLICKED);
}
navigation_from_button_ = RELOAD_BUTTON;
Reload(false);
return;
case SHOW_SAVED_COPY_BUTTON:
RecordEvent(
error_page::NETWORK_ERROR_PAGE_SHOW_SAVED_COPY_BUTTON_CLICKED);
navigation_from_button_ = SHOW_SAVED_COPY_BUTTON;
if (committed_error_page_info_->reload_button_in_page) {
RecordEvent(
error_page::
NETWORK_ERROR_PAGE_BOTH_BUTTONS_SHOWN_SAVED_COPY_CLICKED);
}
delegate_->LoadPageFromCache(committed_error_page_info_->error.url());
return;
case MORE_BUTTON:
// Visual effects on page are handled in Javascript code.
RecordEvent(error_page::NETWORK_ERROR_PAGE_MORE_BUTTON_CLICKED);
return;
case EASTER_EGG:
RecordEvent(error_page::NETWORK_ERROR_EASTER_EGG_ACTIVATED);
delegate_->RequestEasterEggHighScore();
return;
case SHOW_CACHED_COPY_BUTTON:
RecordEvent(error_page::NETWORK_ERROR_PAGE_CACHED_COPY_BUTTON_CLICKED);
return;
case DIAGNOSE_ERROR:
RecordEvent(error_page::NETWORK_ERROR_DIAGNOSE_BUTTON_CLICKED);
delegate_->DiagnoseError(committed_error_page_info_->error.url());
return;
case DOWNLOAD_BUTTON:
RecordEvent(error_page::NETWORK_ERROR_PAGE_DOWNLOAD_BUTTON_CLICKED);
delegate_->DownloadPageLater();
return;
case NO_BUTTON:
NOTREACHED();
return;
}
}
void NetErrorHelperCore::TrackClick(int tracking_id) {
// It's technically possible for |navigation_correction_params| to be NULL but
// for |navigation_correction_response| not to be NULL, if the paramters
// changed between loading the original error page and loading the error page
if (!committed_error_page_info_ ||
!committed_error_page_info_->navigation_correction_response) {
return;
}
NavigationCorrectionResponse* response =
committed_error_page_info_->navigation_correction_response.get();
// |tracking_id| is less than 0 when the error page was not generated by the
// navigation correction service. |tracking_id| should never be greater than
// the array size, but best to be safe, since it contains data from a remote
// site, though none of that data should make it into Javascript callbacks.
if (tracking_id < 0 ||
static_cast<size_t>(tracking_id) >= response->corrections.size()) {
return;
}
// Only report a clicked link once.
if (committed_error_page_info_->clicked_corrections.count(tracking_id))
return;
TrackClickUMA(response->corrections[tracking_id]->correction_type);
committed_error_page_info_->clicked_corrections.insert(tracking_id);
std::string request_body = CreateClickTrackingUrlRequestBody(
committed_error_page_info_->error,
*committed_error_page_info_->navigation_correction_params, *response,
*response->corrections[tracking_id]);
delegate_->SendTrackingRequest(
committed_error_page_info_->navigation_correction_params->url,
request_body);
}
void NetErrorHelperCore::LaunchOfflineItem(const std::string& id,
const std::string& name_space) {
#if defined(OS_ANDROID)
available_content_helper_.LaunchItem(id, name_space);
#endif
}
void NetErrorHelperCore::LaunchDownloadsPage() {
#if defined(OS_ANDROID)
available_content_helper_.LaunchDownloadsPage();
#endif
}
void NetErrorHelperCore::SavePageForLater() {
#if defined(OS_ANDROID)
page_auto_fetcher_helper_->TrySchedule(
/*user_requested=*/true, base::BindOnce(&Delegate::SetAutoFetchState,
base::Unretained(delegate_)));
#endif
}
void NetErrorHelperCore::CancelSavePage() {
#if defined(OS_ANDROID)
page_auto_fetcher_helper_->CancelSchedule();
#endif
}
void NetErrorHelperCore::ListVisibilityChanged(bool is_visible) {
#if defined(OS_ANDROID)
available_content_helper_.ListVisibilityChanged(is_visible);
#endif
}