1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/renderer/net/net_error_helper_core.h"
10 #include "base/callback.h"
11 #include "base/i18n/rtl.h"
12 #include "base/json/json_reader.h"
13 #include "base/json/json_writer.h"
14 #include "base/location.h"
15 #include "base/metrics/histogram.h"
16 #include "base/strings/string16.h"
17 #include "base/values.h"
18 #include "chrome/common/localized_error.h"
19 #include "grit/generated_resources.h"
20 #include "net/base/escape.h"
21 #include "net/base/net_errors.h"
22 #include "net/base/net_util.h"
23 #include "third_party/WebKit/public/platform/WebString.h"
24 #include "third_party/WebKit/public/platform/WebURLError.h"
25 #include "ui/base/l10n/l10n_util.h"
30 struct CorrectionTypeToResourceTable
{
32 const char* correction_type
;
35 const CorrectionTypeToResourceTable kCorrectionResourceTable
[] = {
36 {IDS_ERRORPAGES_SUGGESTION_VISIT_GOOGLE_CACHE
, "cachedPage"},
37 // "reloadPage" is has special handling.
38 {IDS_ERRORPAGES_SUGGESTION_CORRECTED_URL
, "urlCorrection"},
39 {IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL
, "siteDomain"},
40 {IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL
, "host"},
41 {IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL
, "sitemap"},
42 {IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL
, "pathParentFolder"},
43 // "siteSearchQuery" is not yet supported.
44 // TODO(mmenke): Figure out what format "siteSearchQuery" uses for its
46 // "webSearchQuery" has special handling.
47 {IDS_ERRORPAGES_SUGGESTION_ALTERNATE_URL
, "contentOverlap"},
48 {IDS_ERRORPAGES_SUGGESTION_CORRECTED_URL
, "emphasizedUrlCorrection"},
51 base::TimeDelta
GetAutoReloadTime(size_t reload_count
) {
52 static const int kDelaysMs
[] = {
53 0, 5000, 30000, 60000, 300000, 600000, 1800000
55 if (reload_count
>= arraysize(kDelaysMs
))
56 reload_count
= arraysize(kDelaysMs
) - 1;
57 return base::TimeDelta::FromMilliseconds(kDelaysMs
[reload_count
]);
60 // Returns whether |net_error| is a DNS-related error (and therefore whether
61 // the tab helper should start a DNS probe after receiving it.)
62 bool IsDnsError(const blink::WebURLError
& error
) {
63 return error
.domain
.utf8() == net::kErrorDomain
&&
64 (error
.reason
== net::ERR_NAME_NOT_RESOLVED
||
65 error
.reason
== net::ERR_NAME_RESOLUTION_FAILED
);
68 GURL
SanitizeURL(const GURL
& url
) {
69 GURL::Replacements remove_params
;
70 remove_params
.ClearUsername();
71 remove_params
.ClearPassword();
72 remove_params
.ClearQuery();
73 remove_params
.ClearRef();
74 return url
.ReplaceComponents(remove_params
);
77 // If URL correction information should be retrieved remotely for a main frame
78 // load that failed with |error|, returns true and sets
79 // |correction_request_body| to be the body for the correction request.
80 bool GetFixUrlRequestBody(const blink::WebURLError
& error
,
81 const std::string
& language
,
82 const std::string
& country_code
,
83 const std::string
& api_key
,
84 std::string
* correction_request_body
) {
85 // Parameter to send to the correction service indicating the error type.
86 std::string error_param
;
88 std::string domain
= error
.domain
.utf8();
89 if (domain
== "http" && error
.reason
== 404) {
90 error_param
= "http404";
91 } else if (IsDnsError(error
)) {
92 error_param
= "dnserror";
93 } else if (domain
== net::kErrorDomain
&&
94 (error
.reason
== net::ERR_CONNECTION_FAILED
||
95 error
.reason
== net::ERR_CONNECTION_REFUSED
||
96 error
.reason
== net::ERR_ADDRESS_UNREACHABLE
||
97 error
.reason
== net::ERR_CONNECTION_TIMED_OUT
)) {
98 error_param
= "connectionFailure";
103 // Don't use the correction service for HTTPS (for privacy reasons).
104 GURL
unreachable_url(error
.unreachableURL
);
105 if (unreachable_url
.SchemeIsSecure())
108 // TODO(yuusuke): Change to net::FormatUrl when Link Doctor becomes
110 std::string spec_to_send
= SanitizeURL(unreachable_url
).spec();
112 // Notify navigation correction service of the url truncation by sending of
114 if (unreachable_url
.has_query())
115 spec_to_send
.append("?");
117 // Assemble request body, which is a JSON string.
118 // TODO(mmenke): Investigate open sourcing the relevant protocol buffers and
119 // using those directly instead.
121 base::DictionaryValue request_dict
;
122 request_dict
.SetString("method", "linkdoctor.fixurl.fixurl");
123 request_dict
.SetString("apiVersion", "v1");
125 base::DictionaryValue
* params_dict
= new base::DictionaryValue();
126 request_dict
.Set("params", params_dict
);
128 params_dict
->SetString("key", api_key
);
129 params_dict
->SetString("urlQuery", spec_to_send
);
130 params_dict
->SetString("clientName", "chrome");
131 params_dict
->SetString("error", error_param
);
133 if (!language
.empty())
134 params_dict
->SetString("language", language
);
136 if (!country_code
.empty())
137 params_dict
->SetString("originCountry", country_code
);
139 base::JSONWriter::Write(&request_dict
, correction_request_body
);
143 base::string16
FormatURLForDisplay(const GURL
& url
, bool is_rtl
,
144 const std::string accept_languages
) {
145 // Translate punycode into UTF8, unescape UTF8 URLs.
146 base::string16
url_for_display(net::FormatUrl(
147 url
, accept_languages
, net::kFormatUrlOmitNothing
,
148 net::UnescapeRule::NORMAL
, NULL
, NULL
, NULL
));
149 // URLs are always LTR.
151 base::i18n::WrapStringWithLTRFormatting(&url_for_display
);
152 return url_for_display
;
155 LocalizedError::ErrorPageParams
* ParseAdditionalSuggestions(
156 const std::string
& data
,
157 const GURL
& original_url
,
158 const GURL
& search_url
,
159 const std::string
& accept_languages
,
161 scoped_ptr
<base::Value
> parsed(base::JSONReader::Read(data
));
164 // TODO(mmenke): Open source related protocol buffers and use them directly.
165 base::DictionaryValue
* parsed_dict
;
166 base::ListValue
* corrections
;
167 if (!parsed
->GetAsDictionary(&parsed_dict
))
169 if (!parsed_dict
->GetList("result.UrlCorrections", &corrections
))
172 // Version of URL for display in suggestions. It has to be sanitized first
173 // because any received suggestions will be relative to the sanitized URL.
174 base::string16 original_url_for_display
=
175 FormatURLForDisplay(SanitizeURL(original_url
), is_rtl
, accept_languages
);
177 scoped_ptr
<LocalizedError::ErrorPageParams
> params(
178 new LocalizedError::ErrorPageParams());
179 params
->override_suggestions
.reset(new base::ListValue());
180 scoped_ptr
<base::ListValue
> parsed_corrections(new base::ListValue());
181 for (base::ListValue::iterator it
= corrections
->begin();
182 it
!= corrections
->end(); ++it
) {
183 base::DictionaryValue
* correction
;
184 if (!(*it
)->GetAsDictionary(&correction
))
187 // Doesn't seem like a good idea to show these.
189 if (correction
->GetBoolean("isPorn", &is_porn
) && is_porn
)
191 if (correction
->GetBoolean("isSoftPorn", &is_porn
) && is_porn
)
194 std::string correction_type
;
195 std::string url_correction
;
196 if (!correction
->GetString("correctionType", &correction_type
) ||
197 !correction
->GetString("urlCorrection", &url_correction
)) {
201 std::string click_tracking_url
;
202 correction
->GetString("clickTrackingUrl", &click_tracking_url
);
204 if (correction_type
== "reloadPage") {
205 params
->suggest_reload
= true;
209 if (correction_type
== "webSearchQuery") {
210 // If there are mutliple searches suggested, use the first suggestion.
211 if (params
->search_terms
.empty()) {
212 params
->search_url
= search_url
;
213 params
->search_terms
= url_correction
;
218 size_t correction_index
;
219 for (correction_index
= 0;
220 correction_index
< arraysize(kCorrectionResourceTable
);
221 ++correction_index
) {
222 if (correction_type
!=
223 kCorrectionResourceTable
[correction_index
].correction_type
) {
226 base::DictionaryValue
* suggest
= new base::DictionaryValue();
227 suggest
->SetString("header",
228 l10n_util::GetStringUTF16(
229 kCorrectionResourceTable
[correction_index
].resource_id
));
230 suggest
->SetString("urlCorrection",
231 !click_tracking_url
.empty() ? click_tracking_url
:
234 "urlCorrectionForDisplay",
235 FormatURLForDisplay(GURL(url_correction
), is_rtl
, accept_languages
));
236 suggest
->SetString("originalUrlForDisplay", original_url_for_display
);
237 params
->override_suggestions
->Append(suggest
);
242 if (params
->override_suggestions
->empty() &&
243 !params
->search_url
.is_valid()) {
246 return params
.release();
251 struct NetErrorHelperCore::ErrorPageInfo
{
252 ErrorPageInfo(blink::WebURLError error
, bool was_failed_post
)
254 was_failed_post(was_failed_post
),
255 needs_dns_updates(false),
256 is_finished_loading(false) {
259 // Information about the failed page load.
260 blink::WebURLError error
;
261 bool was_failed_post
;
263 // Information about the status of the error page.
265 // True if a page is a DNS error page and has not yet received a final DNS
267 bool needs_dns_updates
;
269 // Navigation correction service url, which will be used in response to
270 // certain types of network errors. This is also stored by the
271 // NetErrorHelperCore itself, but it stored here as well in case its modified
272 // in the middle of an error page load. Empty when no error page should be
273 // fetched, or if there's already a fetch in progress.
274 GURL navigation_correction_url
;
276 // Request body to use when requesting corrections from a web service.
277 // TODO(mmenke): Investigate loading the error page at the same time as
278 // the blank page is loading, to get rid of these.
279 std::string navigation_correction_request_body
;
281 // True if a page has completed loading, at which point it can receive
283 bool is_finished_loading
;
286 bool NetErrorHelperCore::IsReloadableError(
287 const NetErrorHelperCore::ErrorPageInfo
& info
) {
288 return info
.error
.domain
.utf8() == net::kErrorDomain
&&
289 info
.error
.reason
!= net::ERR_ABORTED
&&
290 !info
.was_failed_post
;
293 NetErrorHelperCore::NetErrorHelperCore(Delegate
* delegate
)
294 : delegate_(delegate
),
295 last_probe_status_(chrome_common_net::DNS_PROBE_POSSIBLE
),
296 auto_reload_enabled_(false),
297 auto_reload_timer_(new base::Timer(false, false)),
298 // TODO(ellyjones): Make online_ accurate at object creation.
300 auto_reload_count_(0),
301 can_auto_reload_page_(false) {
304 NetErrorHelperCore::~NetErrorHelperCore() {
305 if (committed_error_page_info_
&& can_auto_reload_page_
) {
306 UMA_HISTOGRAM_CUSTOM_ENUMERATION("Net.AutoReload.ErrorAtStop",
307 -committed_error_page_info_
->error
.reason
,
308 net::GetAllErrorCodesForUma());
309 UMA_HISTOGRAM_COUNTS("Net.AutoReload.CountAtStop", auto_reload_count_
);
313 void NetErrorHelperCore::CancelPendingFetches() {
314 // Cancel loading the alternate error page, and prevent any pending error page
315 // load from starting a new error page load. Swapping in the error page when
316 // it's finished loading could abort the navigation, otherwise.
317 if (committed_error_page_info_
&& can_auto_reload_page_
) {
318 UMA_HISTOGRAM_CUSTOM_ENUMERATION("Net.AutoReload.ErrorAtStop",
319 -committed_error_page_info_
->error
.reason
,
320 net::GetAllErrorCodesForUma());
321 UMA_HISTOGRAM_COUNTS("Net.AutoReload.CountAtStop", auto_reload_count_
);
323 if (committed_error_page_info_
) {
324 committed_error_page_info_
->navigation_correction_url
= GURL();
325 committed_error_page_info_
->navigation_correction_request_body
.clear();
327 if (pending_error_page_info_
) {
328 pending_error_page_info_
->navigation_correction_url
= GURL();
329 pending_error_page_info_
->navigation_correction_request_body
.clear();
331 delegate_
->CancelFetchNavigationCorrections();
332 auto_reload_timer_
->Stop();
333 can_auto_reload_page_
= false;
336 void NetErrorHelperCore::OnStop() {
337 CancelPendingFetches();
338 auto_reload_count_
= 0;
341 void NetErrorHelperCore::OnStartLoad(FrameType frame_type
, PageType page_type
) {
342 if (frame_type
!= MAIN_FRAME
)
345 // If there's no pending error page information associated with the page load,
346 // or the new page is not an error page, then reset pending error page state.
347 if (!pending_error_page_info_
|| page_type
!= ERROR_PAGE
) {
348 CancelPendingFetches();
349 } else if (auto_reload_enabled_
) {
350 // If an error load is starting, the resulting error page is autoreloadable.
351 can_auto_reload_page_
= IsReloadableError(*pending_error_page_info_
);
355 void NetErrorHelperCore::OnCommitLoad(FrameType frame_type
) {
356 if (frame_type
!= MAIN_FRAME
)
359 if (committed_error_page_info_
&& !pending_error_page_info_
&&
360 can_auto_reload_page_
) {
361 int reason
= committed_error_page_info_
->error
.reason
;
362 UMA_HISTOGRAM_CUSTOM_ENUMERATION("Net.AutoReload.ErrorAtSuccess",
364 net::GetAllErrorCodesForUma());
365 UMA_HISTOGRAM_COUNTS("Net.AutoReload.CountAtSuccess", auto_reload_count_
);
366 if (auto_reload_count_
== 1) {
367 UMA_HISTOGRAM_CUSTOM_ENUMERATION("Net.AutoReload.ErrorAtFirstSuccess",
369 net::GetAllErrorCodesForUma());
373 committed_error_page_info_
.reset(pending_error_page_info_
.release());
376 void NetErrorHelperCore::OnFinishLoad(FrameType frame_type
) {
377 if (frame_type
!= MAIN_FRAME
)
380 if (!committed_error_page_info_
) {
381 auto_reload_count_
= 0;
385 committed_error_page_info_
->is_finished_loading
= true;
387 // Only enable stale cache JS bindings if this wasn't a post.
388 if (!committed_error_page_info_
->was_failed_post
) {
389 delegate_
->EnableStaleLoadBindings(
390 committed_error_page_info_
->error
.unreachableURL
);
393 if (committed_error_page_info_
->navigation_correction_url
.is_valid()) {
394 // If there is another pending error page load, |fix_url| should have been
396 DCHECK(!pending_error_page_info_
);
397 DCHECK(!committed_error_page_info_
->needs_dns_updates
);
398 delegate_
->FetchNavigationCorrections(
399 committed_error_page_info_
->navigation_correction_url
,
400 committed_error_page_info_
->navigation_correction_request_body
);
401 } else if (auto_reload_enabled_
&&
402 IsReloadableError(*committed_error_page_info_
)) {
403 MaybeStartAutoReloadTimer();
406 if (!committed_error_page_info_
->needs_dns_updates
||
407 last_probe_status_
== chrome_common_net::DNS_PROBE_POSSIBLE
) {
410 DVLOG(1) << "Error page finished loading; sending saved status.";
414 void NetErrorHelperCore::GetErrorHTML(
415 FrameType frame_type
,
416 const blink::WebURLError
& error
,
418 std::string
* error_html
) {
419 if (frame_type
== MAIN_FRAME
) {
420 // If navigation corrections were needed before, that should have been
421 // cancelled earlier by starting a new page load (Which has now failed).
422 DCHECK(!committed_error_page_info_
||
423 !committed_error_page_info_
->navigation_correction_url
.is_valid());
425 std::string navigation_correction_request_body
;
427 if (navigation_correction_url_
.is_valid() &&
428 GetFixUrlRequestBody(error
, language_
, country_code_
, api_key_
,
429 &navigation_correction_request_body
)) {
430 pending_error_page_info_
.reset(new ErrorPageInfo(error
, is_failed_post
));
431 pending_error_page_info_
->navigation_correction_url
=
432 navigation_correction_url_
;
433 pending_error_page_info_
->navigation_correction_request_body
=
434 navigation_correction_request_body
;
438 // The last probe status needs to be reset if this is a DNS error. This
439 // means that if a DNS error page is committed but has not yet finished
440 // loading, a DNS probe status scheduled to be sent to it may be thrown
441 // out, but since the new error page should trigger a new DNS probe, it
442 // will just get the results for the next page load.
443 if (IsDnsError(error
))
444 last_probe_status_
= chrome_common_net::DNS_PROBE_POSSIBLE
;
447 GenerateLocalErrorPage(frame_type
, error
, is_failed_post
,
448 scoped_ptr
<LocalizedError::ErrorPageParams
>(),
452 void NetErrorHelperCore::GenerateLocalErrorPage(
453 FrameType frame_type
,
454 const blink::WebURLError
& error
,
456 scoped_ptr
<LocalizedError::ErrorPageParams
> params
,
457 std::string
* error_html
) {
458 if (frame_type
== MAIN_FRAME
) {
459 pending_error_page_info_
.reset(new ErrorPageInfo(error
, is_failed_post
));
460 // Skip DNS logic if suggestions were received from a remote server.
461 if (IsDnsError(error
) && !params
) {
462 // This is not strictly necessary, but waiting for a new status to be
463 // sent as a result of the DidFinishLoading call keeps the histograms
464 // consistent with older versions of the code, at no real cost.
465 last_probe_status_
= chrome_common_net::DNS_PROBE_POSSIBLE
;
467 delegate_
->GenerateLocalizedErrorPage(
468 GetUpdatedError(error
), is_failed_post
, params
.Pass(),
470 pending_error_page_info_
->needs_dns_updates
= true;
475 delegate_
->GenerateLocalizedErrorPage(error
, is_failed_post
,
476 params
.Pass(), error_html
);
479 void NetErrorHelperCore::OnNetErrorInfo(
480 chrome_common_net::DnsProbeStatus status
) {
481 DCHECK_NE(chrome_common_net::DNS_PROBE_POSSIBLE
, status
);
483 last_probe_status_
= status
;
485 if (!committed_error_page_info_
||
486 !committed_error_page_info_
->needs_dns_updates
||
487 !committed_error_page_info_
->is_finished_loading
) {
494 void NetErrorHelperCore::OnSetNavigationCorrectionInfo(
495 const GURL
& navigation_correction_url
,
496 const std::string
& language
,
497 const std::string
& country_code
,
498 const std::string
& api_key
,
499 const GURL
& search_url
) {
500 navigation_correction_url_
= navigation_correction_url
;
501 language_
= language
;
502 country_code_
= country_code
;
504 search_url_
= search_url
;
507 void NetErrorHelperCore::UpdateErrorPage() {
508 DCHECK(committed_error_page_info_
->needs_dns_updates
);
509 DCHECK(committed_error_page_info_
->is_finished_loading
);
510 DCHECK_NE(chrome_common_net::DNS_PROBE_POSSIBLE
, last_probe_status_
);
512 UMA_HISTOGRAM_ENUMERATION("DnsProbe.ErrorPageUpdateStatus",
514 chrome_common_net::DNS_PROBE_MAX
);
515 // Every status other than DNS_PROBE_POSSIBLE and DNS_PROBE_STARTED is a
516 // final status code. Once one is reached, the page does not need further
518 if (last_probe_status_
!= chrome_common_net::DNS_PROBE_STARTED
)
519 committed_error_page_info_
->needs_dns_updates
= false;
521 delegate_
->UpdateErrorPage(
522 GetUpdatedError(committed_error_page_info_
->error
),
523 committed_error_page_info_
->was_failed_post
);
526 void NetErrorHelperCore::OnNavigationCorrectionsFetched(
527 const std::string
& corrections
,
528 const std::string
& accept_languages
,
530 // Loading suggestions only starts when a blank error page finishes loading,
531 // and is cancelled with a new load.
532 DCHECK(!pending_error_page_info_
);
533 DCHECK(committed_error_page_info_
->is_finished_loading
);
535 scoped_ptr
<LocalizedError::ErrorPageParams
> params(
536 ParseAdditionalSuggestions(
537 corrections
, GURL(committed_error_page_info_
->error
.unreachableURL
),
538 search_url_
, accept_languages
, is_rtl
));
539 std::string error_html
;
540 GenerateLocalErrorPage(MAIN_FRAME
,
541 committed_error_page_info_
->error
,
542 committed_error_page_info_
->was_failed_post
,
546 // |error_page_info| may have been destroyed by this point, since
547 // |pending_error_page_info_| was set to a new ErrorPageInfo.
549 // TODO(mmenke): Once the new API is in place, look into replacing this
550 // double page load by just updating the error page, like DNS
552 delegate_
->LoadErrorPageInMainFrame(
554 pending_error_page_info_
->error
.unreachableURL
);
557 blink::WebURLError
NetErrorHelperCore::GetUpdatedError(
558 const blink::WebURLError
& error
) const {
559 // If a probe didn't run or wasn't conclusive, restore the original error.
560 if (last_probe_status_
== chrome_common_net::DNS_PROBE_NOT_RUN
||
561 last_probe_status_
==
562 chrome_common_net::DNS_PROBE_FINISHED_INCONCLUSIVE
) {
566 blink::WebURLError updated_error
;
567 updated_error
.domain
= blink::WebString::fromUTF8(
568 chrome_common_net::kDnsProbeErrorDomain
);
569 updated_error
.reason
= last_probe_status_
;
570 updated_error
.unreachableURL
= error
.unreachableURL
;
571 updated_error
.staleCopyInCache
= error
.staleCopyInCache
;
573 return updated_error
;
576 void NetErrorHelperCore::Reload() {
577 if (!committed_error_page_info_
) {
580 delegate_
->ReloadPage();
583 bool NetErrorHelperCore::MaybeStartAutoReloadTimer() {
584 if (!committed_error_page_info_
||
585 !committed_error_page_info_
->is_finished_loading
||
586 !can_auto_reload_page_
||
587 pending_error_page_info_
) {
591 DCHECK(IsReloadableError(*committed_error_page_info_
));
596 StartAutoReloadTimer();
600 void NetErrorHelperCore::StartAutoReloadTimer() {
601 DCHECK(committed_error_page_info_
);
602 DCHECK(can_auto_reload_page_
);
603 base::TimeDelta delay
= GetAutoReloadTime(auto_reload_count_
);
604 auto_reload_count_
++;
605 auto_reload_timer_
->Stop();
606 auto_reload_timer_
->Start(FROM_HERE
, delay
,
607 base::Bind(&NetErrorHelperCore::Reload
,
608 base::Unretained(this)));
611 void NetErrorHelperCore::NetworkStateChanged(bool online
) {
613 if (auto_reload_timer_
->IsRunning()) {
614 DCHECK(committed_error_page_info_
);
615 // If there's an existing timer running, stop it and reset the retry count.
616 auto_reload_timer_
->Stop();
617 auto_reload_count_
= 0;
620 // If the network state changed to online, maybe start auto-reloading again.
622 MaybeStartAutoReloadTimer();
625 bool NetErrorHelperCore::ShouldSuppressErrorPage(FrameType frame_type
,
627 // Don't suppress child frame errors.
628 if (frame_type
!= MAIN_FRAME
)
631 // If |auto_reload_timer_| is still running, this error page isn't from an
633 if (auto_reload_timer_
->IsRunning())
636 // If there's no committed error page, this error page wasn't from an auto
638 if (!committed_error_page_info_
|| !can_auto_reload_page_
)
641 GURL error_url
= committed_error_page_info_
->error
.unreachableURL
;
642 // TODO(ellyjones): also plumb the error code down to CCRC and check that
643 if (error_url
!= url
)
646 // The first iteration of the timer is started by OnFinishLoad calling
647 // MaybeStartAutoReloadTimer, but since error pages for subsequent loads are
648 // suppressed in this function, subsequent iterations of the timer have to be
650 MaybeStartAutoReloadTimer();