blob: c90e65fd4ae7870af74955fee8321bbc90929b1e [file] [log] [blame]
// Copyright 2018 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 "ios/web/navigation/error_retry_state_machine.h"
#include "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/web_client.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace web {
using wk_navigation_util::CreatePlaceholderUrlForUrl;
using wk_navigation_util::CreateRedirectUrl;
using wk_navigation_util::IsPlaceholderUrl;
using wk_navigation_util::IsRestoreSessionUrl;
using wk_navigation_util::ExtractTargetURL;
ErrorRetryStateMachine::ErrorRetryStateMachine()
: state_(ErrorRetryState::kNewRequest) {}
ErrorRetryStateMachine::ErrorRetryStateMachine(
const ErrorRetryStateMachine& machine)
: state_(machine.state_), url_(machine.url_) {}
void ErrorRetryStateMachine::SetURL(const GURL& url) {
url_ = url;
}
ErrorRetryState ErrorRetryStateMachine::state() const {
return state_;
}
void ErrorRetryStateMachine::SetNoNavigationError() {
state_ = ErrorRetryState::kNoNavigationError;
}
void ErrorRetryStateMachine::SetDisplayingWebError() {
// Web error is displayed in two scenarios:
// (1) Placeholder entry for network load error finished loading in web view.
// This is the common case.
// (2) Retry of a previously failed load failed in SSL error. This can happen
// when the first navigation failed in offline mode. SSL interstitial does
// not normally trigger ErrorRetryStateMachine because the error page is
// not to become part of the navigation history. This leaves the item
// stuck in the transient kRetryFailedNavigationItem state. So for this
// specific case, treat the SSL interstitial as a web error so that
// error retry works as expected on subsequent back/forward navigations.
DCHECK(state_ == ErrorRetryState::kReadyToDisplayError ||
state_ == ErrorRetryState::kRetryFailedNavigationItem)
<< "Unexpected error retry state: " << static_cast<int>(state_);
state_ = ErrorRetryState::kDisplayingError;
}
void ErrorRetryStateMachine::SetRetryPlaceholderNavigation() {
DCHECK(state_ == web::ErrorRetryState::kNoNavigationError);
state_ = ErrorRetryState::kRetryPlaceholderNavigation;
}
void ErrorRetryStateMachine::SetIgnorePlaceholderNavigation() {
state_ = ErrorRetryState::kIgnorePlaceholderNavigation;
}
ErrorRetryCommand ErrorRetryStateMachine::DidFailProvisionalNavigation(
const GURL& web_view_url,
const GURL& error_url) {
switch (state_) {
case ErrorRetryState::kNewRequest:
if (web_view_url == CreateRedirectUrl(error_url)) {
// Client redirect in restore_session.html failed. A placeholder is not
// needed here because a back/forward item already exists for
// restore_session.html.
state_ = ErrorRetryState::kReadyToDisplayError;
return ErrorRetryCommand::kLoadError;
}
// Provisional navigation failed on a new item.
state_ = ErrorRetryState::kLoadingPlaceholder;
return ErrorRetryCommand::kLoadPlaceholder;
// Reload of a previously successful load fails.
case ErrorRetryState::kNoNavigationError:
// Retry of a previous failure still fails.
case ErrorRetryState::kRetryFailedNavigationItem:
// This case happens for the second back/forward navigation in offline mode
// to a page that initially loaded successfully.
case ErrorRetryState::kDisplayingError:
if (web::GetWebClient()->IsSlimNavigationManagerEnabled()) {
// In this case, a back/forward item already exists. Rewriting the
// WebView's URL to the placeholder URL before loading the error page
// ensures that WebKit doesn't associate the (empty) contents of an
// immediately-preceding kRewriteToWebViewURL step with the actual URL
// in its page cache. See http://crbug.com/950489.
state_ = ErrorRetryState::kLoadingPlaceholder;
return ErrorRetryCommand::kRewriteToPlaceholderURL;
} else {
return BackForwardOrReloadFailed();
}
case ErrorRetryState::kLoadingPlaceholder:
case ErrorRetryState::kRetryPlaceholderNavigation:
case ErrorRetryState::kReadyToDisplayError:
case ErrorRetryState::kNavigatingToFailedNavigationItem:
case ErrorRetryState::kIgnorePlaceholderNavigation:
NOTREACHED() << "Unexpected error retry state: "
<< static_cast<unsigned>(state_);
}
return ErrorRetryCommand::kDoNothing;
}
ErrorRetryCommand ErrorRetryStateMachine::DidFailNavigation(
const GURL& web_view_url) {
switch (state_) {
case ErrorRetryState::kNewRequest:
state_ = ErrorRetryState::kReadyToDisplayError;
return ErrorRetryCommand::kLoadError;
// Reload of a previously successful load fails.
case ErrorRetryState::kNoNavigationError:
// Retry of a previous failure still fails.
case ErrorRetryState::kRetryFailedNavigationItem:
// Retry of a previous failure still fails.
case ErrorRetryState::kDisplayingError:
return BackForwardOrReloadFailed();
case ErrorRetryState::kLoadingPlaceholder:
case ErrorRetryState::kRetryPlaceholderNavigation:
case ErrorRetryState::kReadyToDisplayError:
case ErrorRetryState::kNavigatingToFailedNavigationItem:
case ErrorRetryState::kIgnorePlaceholderNavigation:
NOTREACHED() << "Unexpected error retry state: "
<< static_cast<unsigned>(state_);
}
return ErrorRetryCommand::kDoNothing;
}
ErrorRetryCommand ErrorRetryStateMachine::DidFinishNavigation(
const GURL& web_view_url) {
switch (state_) {
case ErrorRetryState::kLoadingPlaceholder:
// (1) Placeholder load for initial failure succeeded.
if (@available(iOS 13, *)) {
// This DCHECK is hit on iOS 12 when navigating to restricted URL. See
// crbug.com/1000366 for more details.
DCHECK_EQ(web_view_url, CreatePlaceholderUrlForUrl(url_));
}
state_ = ErrorRetryState::kReadyToDisplayError;
return ErrorRetryCommand::kLoadError;
case ErrorRetryState::kRetryPlaceholderNavigation:
if (IsPlaceholderUrl(web_view_url)) {
// (11) Explicitly keep the state the same so after rewriting to the non
// placeholder url the else block will trigger.
DCHECK_EQ(web_view_url, CreatePlaceholderUrlForUrl(url_));
state_ = ErrorRetryState::kRetryPlaceholderNavigation;
return ErrorRetryCommand::kRewriteToWebViewURL;
} else {
// The url was written by kRewriteToWebViewURL in the if block, so on
// this navigation load an error view.
state_ = ErrorRetryState::kReadyToDisplayError;
return ErrorRetryCommand::kLoadError;
}
case ErrorRetryState::kNewRequest:
if (IsRestoreSessionUrl(web_view_url)) {
// (8) Initial load of restore_session.html. Don't change state or
// issue command. Wait for the client-side redirect.
} else if (IsPlaceholderUrl(web_view_url) &&
web::GetWebClient()->IsSlimNavigationManagerEnabled()) {
state_ = ErrorRetryState::kNavigatingToFailedNavigationItem;
return ErrorRetryCommand::kRewriteToWebViewURL;
} else {
// (2) Initial load succeeded.
state_ = ErrorRetryState::kNoNavigationError;
}
break;
case ErrorRetryState::kReadyToDisplayError:
// (3) Finished loading error in web view.
DCHECK_EQ(web_view_url, url_);
state_ = ErrorRetryState::kDisplayingError;
break;
case ErrorRetryState::kDisplayingError:
if (web_view_url == CreatePlaceholderUrlForUrl(url_)) {
// (4) Back/forward to or reload of placeholder URL. Rewrite WebView URL
// to prepare for retry.
state_ = ErrorRetryState::kNavigatingToFailedNavigationItem;
return ErrorRetryCommand::kRewriteToWebViewURL;
}
if (IsRestoreSessionUrl(web_view_url)) {
GURL target_url;
if (ExtractTargetURL(web_view_url, &target_url) && target_url == url_) {
// (10) Back/forward navigation to a restored session entry in offline
// mode. It is OK to consider this load succeeded for now because the
// failure delegate will be triggered again if the load fails.
state_ = ErrorRetryState::kNoNavigationError;
return ErrorRetryCommand::kDoNothing;
}
}
// (5) This is either a reload of the original URL that succeeded in
// WebView (either because it was already in Page Cache or the network
// load succeded), or a back/forward of a previous WebUI error that is
// served from Page Cache. It's impossible to distinguish between the two
// because in both cases, |web_view_url| is the original URL. This can
// lead to network error being displayed even when network condition
// is regained. User has to reload explicitly to retry loading online.
DCHECK_EQ(web_view_url, url_);
state_ = ErrorRetryState::kNoNavigationError;
break;
// (6) Successfully rewritten the WebView URL from placeholder URL to
// original URL. Ready to try reload.
case ErrorRetryState::kNavigatingToFailedNavigationItem:
DCHECK_EQ(web_view_url, url_);
state_ = ErrorRetryState::kRetryFailedNavigationItem;
return ErrorRetryCommand::kReload;
// (7) Retry loading succeeded.
case ErrorRetryState::kRetryFailedNavigationItem:
DCHECK_EQ(web_view_url, url_);
state_ = ErrorRetryState::kNoNavigationError;
break;
case ErrorRetryState::kIgnorePlaceholderNavigation:
state_ = ErrorRetryState::kNoNavigationError;
break;
// (9) Back/forward to or reload of a previously successfull load succeeds
// again.
case ErrorRetryState::kNoNavigationError:
break;
}
return ErrorRetryCommand::kDoNothing;
}
ErrorRetryCommand ErrorRetryStateMachine::BackForwardOrReloadFailed() {
state_ = ErrorRetryState::kReadyToDisplayError;
return ErrorRetryCommand::kLoadError;
}
} // namespace web