chromium/ios/web/navigation/crw_wk_navigation_handler.mm

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/web/navigation/crw_wk_navigation_handler.h"

#import "base/apple/foundation_util.h"
#import "base/feature_list.h"
#import "base/ios/ns_error_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "base/timer/timer.h"
#import "components/security_interstitials/core/insecure_form_util.h"
#import "ios/components/security_interstitials/https_only_mode/feature.h"
#import "ios/net/protocol_handler_util.h"
#import "ios/net/url_scheme_util.h"
#import "ios/web/common/features.h"
#import "ios/web/common/url_scheme_util.h"
#import "ios/web/download/download_native_task_bridge.h"
#import "ios/web/navigation/crw_error_page_helper.h"
#import "ios/web/navigation/crw_navigation_item_holder.h"
#import "ios/web/navigation/crw_pending_navigation_info.h"
#import "ios/web/navigation/crw_wk_navigation_states.h"
#import "ios/web/navigation/navigation_context_impl.h"
#import "ios/web/navigation/navigation_item_impl.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#import "ios/web/navigation/navigation_manager_util.h"
#import "ios/web/navigation/web_kit_constants.h"
#import "ios/web/navigation/wk_back_forward_list_item_holder.h"
#import "ios/web/navigation/wk_navigation_action_policy_util.h"
#import "ios/web/navigation/wk_navigation_action_util.h"
#import "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/browser_state.h"
#import "ios/web/public/download/download_controller.h"
#import "ios/web/public/navigation/form_warning_type.h"
#import "ios/web/public/web_client.h"
#import "ios/web/security/crw_cert_verification_controller.h"
#import "ios/web/security/wk_web_view_security_util.h"
#import "ios/web/session/session_certificate_policy_cache_impl.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/user_interaction_state.h"
#import "ios/web/web_state/web_state_impl.h"
#import "ios/web/web_view/content_type_util.h"
#import "ios/web/web_view/error_translation_util.h"
#import "ios/web/web_view/wk_security_origin_util.h"
#import "ios/web/web_view/wk_web_view_util.h"
#import "net/base/apple/http_response_headers_util.h"
#import "net/base/apple/url_conversions.h"
#import "net/base/net_errors.h"
#import "net/cert/x509_util_apple.h"
#import "net/http/http_content_disposition.h"
#import "url/gurl.h"

using web::wk_navigation_util::kReferrerHeaderName;
using web::wk_navigation_util::IsRestoreSessionUrl;
using web::wk_navigation_util::IsWKInternalUrl;

namespace {
// Maximum number of errors to store in cert verification errors cache.
// Cache holds errors only for pending navigations, so the actual number of
// stored errors is not expected to be high.
const web::CertVerificationErrorsCacheType::size_type kMaxCertErrorsCount = 100;

// Returns true if the navigation was upgraded to HTTPS but failed due to an
// SSL or net error. This can happen when HTTPS-Only Mode feature automatically
// upgrades a navigation to HTTPS.
web::HttpsUpgradeType GetFailedHttpsUpgradeType(
    NSError* error,
    web::NavigationContextImpl* context,
    NSError* cancellationError) {
  if (!context || !context->GetItem() ||
      context->GetItem()->GetHttpsUpgradeType() ==
          web::HttpsUpgradeType::kNone ||
      cancellationError) {
    return web::HttpsUpgradeType::kNone;
  }
  int error_code = 0;
  if (!web::GetNetErrorFromIOSErrorCode(
          error.code, &error_code, net::NSURLWithGURL(context->GetUrl()))) {
    error_code = net::ERR_FAILED;
  }
  if (error_code != net::OK || web::IsWKWebViewSSLCertError(error)) {
    return context->GetItem()->GetHttpsUpgradeType();
  }
  return web::HttpsUpgradeType::kNone;
}

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class ErrorPagePresentationFailed {
  kUnknown,
  kWebViewReleased,
  kJavaScriptExceptionOccurred,
  kOtherWKErrorDomain,
  kMaxValue = kOtherWKErrorDomain
};

void LogPresentingErrorPageFailedWithError(NSError* error) {
  ErrorPagePresentationFailed failure_type =
      ErrorPagePresentationFailed::kUnknown;

  if ([WKErrorDomain isEqualToString:error.domain]) {
    if (error.code == WKErrorWebViewInvalidated ||
        error.code == WKErrorWebContentProcessTerminated ||
        error.code == WKErrorJavaScriptResultTypeIsUnsupported) {
      failure_type = ErrorPagePresentationFailed::kWebViewReleased;
    } else if (error.code == WKErrorJavaScriptExceptionOccurred) {
      failure_type = ErrorPagePresentationFailed::kJavaScriptExceptionOccurred;
    } else {
      failure_type = ErrorPagePresentationFailed::kOtherWKErrorDomain;
    }
  }

  base::UmaHistogramEnumeration("IOS.Web.ErrorPagePresentationFailed",
                                failure_type);
}

}  // namespace

@interface CRWWKNavigationHandler () <DownloadNativeTaskBridgeDelegate> {
  // Referrer for the current page; does not include the fragment.
  NSString* _currentReferrerString;

  // CertVerification errors which happened inside
  // `webView:didReceiveAuthenticationChallenge:completionHandler:`.
  // Key is leaf-cert/host pair. This storage is used to carry calculated
  // cert status from `didReceiveAuthenticationChallenge:` to
  // `didFailProvisionalNavigation:` delegate method.
  std::unique_ptr<web::CertVerificationErrorsCacheType> _certVerificationErrors;

  // Used to keep track of newly created and current download
  // task objects created from
  // `webView:navigationAction:didBecomeDownload` and
  // `webView:navigationResponse:didBecomeDownload`. Respectively,
  // DownloadNativeTaskBridge objects will help provide a valid
  // `navigationAction` or `navigationResponse`.
  NSMutableSet<DownloadNativeTaskBridge*>* _nativeTaskBridges;

  // Stores navigation policy state of download task to indicate if a download
  // should be performed.
  BOOL _shouldPerformDownload;
}

@property(nonatomic, weak) id<CRWWKNavigationHandlerDelegate> delegate;

// Returns the WebStateImpl from self.delegate.
@property(nonatomic, readonly, assign) web::WebStateImpl* webStateImpl;
// Returns the NavigationManagerImpl from self.webStateImpl.
@property(nonatomic, readonly, assign)
    web::NavigationManagerImpl* navigationManagerImpl;
// Returns the UserInteractionState from self.delegate.
@property(nonatomic, readonly, assign)
    web::UserInteractionState* userInteractionState;
// Returns the CRWCertVerificationController from self.delegate.
@property(nonatomic, readonly, weak)
    CRWCertVerificationController* certVerificationController;
// Returns the document URL from self.delegate.
@property(nonatomic, readonly, assign) GURL documentURL;

@end

@implementation CRWWKNavigationHandler

- (instancetype)initWithDelegate:(id<CRWWKNavigationHandlerDelegate>)delegate {
  if ((self = [super init])) {
    _navigationStates = [[CRWWKNavigationStates alloc] init];
    // Load phase when no WebView present is 'loaded' because this represents
    // the idle state.
    _navigationState = web::WKNavigationState::FINISHED;

    _certVerificationErrors =
        std::make_unique<web::CertVerificationErrorsCacheType>(
            kMaxCertErrorsCount);

    _nativeTaskBridges = [[NSMutableSet alloc] init];

    _shouldPerformDownload = NO;

    _delegate = delegate;
  }
  return self;
}

- (void)dealloc {
  for (DownloadNativeTaskBridge* bridge in _nativeTaskBridges) {
    [bridge cancel];
  }
  _nativeTaskBridges = nil;
}

#pragma mark - WKNavigationDelegate

- (void)webView:(WKWebView*)webView
    decidePolicyForNavigationAction:(WKNavigationAction*)action
                        preferences:(WKWebpagePreferences*)preferences
                    decisionHandler:(void (^)(WKNavigationActionPolicy,
                                              WKWebpagePreferences*))handler {
  // Check if OS lockdown mode is enabled and update the preference value.
  if (!self.beingDestroyed) {
    static dispatch_once_t onceToken;
    web::BrowserState* browser_state = self.webStateImpl->GetBrowserState();
    dispatch_once(&onceToken, ^{
      if (@available(iOS 16.0, *)) {
        web::GetWebClient()->SetOSLockdownModeEnabled(
            browser_state, preferences.lockdownModeEnabled);
      }
    });
  }

  GURL requestURL = net::GURLWithNSURL(action.request.URL);
  const web::UserAgentType userAgentType =
      [self userAgentForNavigationAction:action webView:webView];

  if (userAgentType != web::UserAgentType::NONE) {
    if (action.navigationType == WKNavigationTypeBackForward &&
        self.webStateImpl->GetUserAgentForSessionRestoration() !=
            web::UserAgentType::AUTOMATIC) {
      // When navigating back to a page with a UserAgent that wasn't automatic,
      // let's reuse this user agent for next navigations.
      self.webStateImpl->SetUserAgent(userAgentType);
    }

    GURL URLForUserAgent = requestURL;
    if ([CRWErrorPageHelper isErrorPageFileURL:URLForUserAgent]) {
      URLForUserAgent = [CRWErrorPageHelper
          failedNavigationURLFromErrorPageFileURL:URLForUserAgent];
    }

    if (action.navigationType == WKNavigationTypeReload &&
        web::wk_navigation_util::URLNeedsUserAgentType(URLForUserAgent) &&
        webView.backForwardList.currentItem) {
      // When reloading the page, the UserAgent will be updated to the one for
      // the new page.
      web::NavigationItem* item = [[CRWNavigationItemHolder
          holderForBackForwardListItem:webView.backForwardList.currentItem]
          navigationItem];
      if (item) {
        item->SetUserAgentType(userAgentType);
        if (web::wk_navigation_util::IsRestoreSessionUrl(item->GetURL())) {
          self.webStateImpl->SetUserAgent(userAgentType);
        }
      }
    }

    NSString* userAgentString = base::SysUTF8ToNSString(
        web::GetWebClient()->GetUserAgent(userAgentType));
    if (![webView.customUserAgent isEqualToString:userAgentString]) {
      webView.customUserAgent = userAgentString;
    }
  }

  const WKContentMode contentMode = userAgentType == web::UserAgentType::DESKTOP
                                        ? WKContentModeDesktop
                                        : WKContentModeMobile;
  BOOL isMainFrameNavigationAction = [self isMainFrameNavigationAction:action];
  auto decisionHandler = ^(WKNavigationActionPolicy policy) {
    preferences.preferredContentMode = contentMode;
#if defined(__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
    if (@available(iOS 16.0, *)) {
      if ((policy == WKNavigationActionPolicyAllow) &&
          isMainFrameNavigationAction) {
        UMA_HISTOGRAM_BOOLEAN("IOS.MainFrameNavigationIsInLockdownMode",
                              preferences.lockdownModeEnabled);
      }

      if (!self.beingDestroyed) {
        web::BrowserState* browser_state = self.webStateImpl->GetBrowserState();
        bool browser_lockdown_mode_enabled =
            web::GetWebClient()->IsBrowserLockdownModeEnabled(browser_state);
        if ((policy == WKNavigationActionPolicyAllow) &&
            isMainFrameNavigationAction) {
          UMA_HISTOGRAM_BOOLEAN(
              "IOS.MainFrameNavigationIsInBrowserLockdownMode",
              browser_lockdown_mode_enabled);
        }
        if (browser_lockdown_mode_enabled) {
          preferences.lockdownModeEnabled = true;
        }
      }
    }
#endif  // defined (__IPHONE_16_0)
    handler(policy, preferences);
  };

  [self didReceiveWKNavigationDelegateCallback];

  BOOL forceBlockUniversalLinks = self.blockUniversalLinksOnNextDecidePolicy;
  self.blockUniversalLinksOnNextDecidePolicy = NO;

  _webProcessCrashed = NO;
  if (self.beingDestroyed) {
    decisionHandler(WKNavigationActionPolicyCancel);
    return;
  }

  // The page will not be changed until this navigation is committed, so the
  // retrieved state will be pending until `didCommitNavigation` callback.
  [self createPendingNavigationInfoFromNavigationAction:action];

  if (action.targetFrame.mainFrame &&
      action.navigationType == WKNavigationTypeBackForward) {
    web::NavigationContextImpl* context =
        [self contextForPendingMainFrameNavigationWithURL:requestURL];
    if (context) {
      // Context is null for renderer-initiated navigations.
      int index = web::GetCommittedItemIndexWithUniqueID(
          self.navigationManagerImpl, context->GetNavigationItemUniqueID());
      self.navigationManagerImpl->SetPendingItemIndex(index);
    }
  }

  // If this is a error navigation, pass through.
  if ([CRWErrorPageHelper isErrorPageFileURL:requestURL]) {
    if (action.sourceFrame.mainFrame) {
      // Disallow renderer initiated navigations to error URLs.
      decisionHandler(WKNavigationActionPolicyCancel);
    } else {
      decisionHandler(WKNavigationActionPolicyAllow);
    }
    return;
  }

  ui::PageTransition transition =
      [self pageTransitionFromNavigationType:action.navigationType];
  if (isMainFrameNavigationAction) {
    web::NavigationContextImpl* context =
        [self contextForPendingMainFrameNavigationWithURL:requestURL];
    // Theoretically if `context` can be found here, the navigation should be
    // either user-initiated or JS back/forward. The second part in the "if"
    // condition used to be a DCHECK, but it would fail in this case:
    // 1. Multiple render-initiated navigation with the same URL happens at the
    //    same time;
    // 2. One of these navigations gets the "didStartProvisonalNavigation"
    //    callback and creates a NavigationContext;
    // 3. Another navigation reaches here and retrieves that NavigationContext
    //    by matching URL.
    // The DCHECK is now turned into a "if" condition, but can be reverted if a
    // more reliable way of matching NavigationContext with WKNavigationAction
    // is found.
    if (context &&
        (!context->IsRendererInitiated() ||
         (context->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK))) {
      transition = context->GetPageTransition();
      if (context->IsLoadingErrorPage()) {
        // loadHTMLString: navigation which loads error page into WKWebView.
        decisionHandler(WKNavigationActionPolicyAllow);
        return;
      }
    }
  }

  // Invalid URLs should not be loaded.
  if (!requestURL.is_valid()) {
    // The HTML5 spec indicates that window.open with an invalid URL should open
    // about:blank.
    BOOL isFirstLoadInOpenedWindow =
        self.webStateImpl->HasOpener() &&
        !self.webStateImpl->GetNavigationManager()->GetLastCommittedItem();
    BOOL isMainFrame = action.targetFrame.mainFrame;
    if (isFirstLoadInOpenedWindow && isMainFrame) {
      decisionHandler(WKNavigationActionPolicyCancel);
      GURL aboutBlankURL(url::kAboutBlankURL);
      web::NavigationManager::WebLoadParams loadParams(aboutBlankURL);
      loadParams.referrer = self.currentReferrer;

      self.webStateImpl->GetNavigationManager()->LoadURLWithParams(loadParams);
      return;
    }
    // On iOS 12, we allow the navigation since cancelling it here causes
    // crbug.com/965067. The underlying issue is a WebKit bug that converts
    // valid URLs into invalid ones. This issue is fixed in iOS 13.
    decisionHandler(WKNavigationActionPolicyCancel);
    return;
  }

  // First check if the navigation action should be blocked by the controller
  // and make sure to update the controller in the case that the controller
  // can't handle the request URL. Then use the embedders' policyDeciders to
  // either: 1- Handle the URL it self and return false to stop the controller
  // from proceeding with the navigation if needed. or 2- return true to allow
  // the navigation to be proceeded by the web controller.
  web::WebStatePolicyDecider::PolicyDecision policyDecision =
      web::WebStatePolicyDecider::PolicyDecision::Allow();
  if (web::GetWebClient()->IsAppSpecificURL(requestURL)) {
    // `policyDecision` is initialized above this conditional to allow loads, so
    // it only needs to be overwritten if the load should be cancelled.
    if (![self shouldAllowAppSpecificURLNavigationAction:action
                                              transition:transition]) {
      policyDecision = web::WebStatePolicyDecider::PolicyDecision::Cancel();
    }
    if (policyDecision.ShouldAllowNavigation()) {
      [self.delegate navigationHandler:self createWebUIForURL:requestURL];
    }
  }

  const BOOL webControllerCanShow =
      web::UrlHasWebScheme(requestURL) ||
      web::GetWebClient()->IsAppSpecificURL(requestURL) ||
      requestURL.SchemeIs(url::kFileScheme) ||
      requestURL.SchemeIs(url::kAboutScheme) ||
      requestURL.SchemeIs(url::kBlobScheme);

  _shouldPerformDownload = NO;
  _shouldPerformDownload = action.shouldPerformDownload;

  __weak CRWWKNavigationHandler* weakSelf = self;
  auto callback =
      base::BindOnce(^(web::WebStatePolicyDecider::PolicyDecision decision) {
        __strong CRWWKNavigationHandler* strongSelf = weakSelf;
        // The WebState may have been closed in the ShouldAllowRequest callback.
        if (!strongSelf || strongSelf.beingDestroyed) {
          decisionHandler(WKNavigationActionPolicyCancel);
          return;
        }

        if (!webControllerCanShow) {
          decision = web::WebStatePolicyDecider::PolicyDecision::Cancel();
        }

        [strongSelf answerDecisionHandler:decisionHandler
                      forNavigationAction:action
                       withPolicyDecision:decision
                                  webView:webView
                 forceBlockUniversalLinks:forceBlockUniversalLinks];
      });

  if (!policyDecision.ShouldAllowNavigation()) {
    std::move(callback).Run(policyDecision);
    return;
  }

  BOOL isUserInitiated = [self.delegate isUserInitiatedAction:action];
  BOOL hasTappedRecently =
      self.userInteractionState->HasUserTappedRecently(webView);

  BOOL isCrossOriginTargetFrame = NO;
  if (action.sourceFrame && action.targetFrame &&
      action.sourceFrame.webView == action.targetFrame.webView &&
      action.sourceFrame != action.targetFrame) {
    isCrossOriginTargetFrame = !url::IsSameOriginWith(
        web::GURLOriginWithWKSecurityOrigin(action.sourceFrame.securityOrigin),
        web::GURLOriginWithWKSecurityOrigin(action.targetFrame.securityOrigin));
  }

  BOOL isCrossOriginCrossWindow = NO;
  if (action.sourceFrame && action.targetFrame &&
      action.sourceFrame.webView != action.targetFrame.webView) {
    GURL sourceOrigin =
        web::GURLOriginWithWKSecurityOrigin(action.sourceFrame.securityOrigin);
    GURL targetOrigin =
        web::GURLOriginWithWKSecurityOrigin(action.targetFrame.securityOrigin);
    isCrossOriginCrossWindow =
        !url::IsSameOriginWith(sourceOrigin, targetOrigin);
  }

  // Ref: crbug.com/1408799
  if (base::FeatureList::IsEnabled(
          web::features::kPreventNavigationWithoutUserInteraction) &&
      isMainFrameNavigationAction && isCrossOriginTargetFrame &&
      !isUserInitiated) {
    decisionHandler(WKNavigationActionPolicyCancel);
    return;
  }

  const web::WebStatePolicyDecider::RequestInfo requestInfo(
      transition, isMainFrameNavigationAction, isCrossOriginTargetFrame,
      isCrossOriginCrossWindow, isUserInitiated, hasTappedRecently);

  self.webStateImpl->ShouldAllowRequest(action.request, requestInfo,
                                        std::move(callback));
}

- (void)webView:(WKWebView*)webView
    decidePolicyForNavigationResponse:(WKNavigationResponse*)WKResponse
                      decisionHandler:
                          (void (^)(WKNavigationResponsePolicy))handler {
  [self didReceiveWKNavigationDelegateCallback];

  // If this is a error navigation, pass through.
  GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL);
  if ([CRWErrorPageHelper isErrorPageFileURL:responseURL]) {
    handler(WKNavigationResponsePolicyAllow);
    // Set the mime type for error pages. This allows them to be treated as
    // HTML.
    [self updatePendingNavigationInfoFromNavigationResponse:WKResponse
                                                HTTPHeaders:nullptr];
    return;
  }

  if (self.pendingNavigationInfo.unsafeRedirect) {
    self.pendingNavigationInfo.cancelled = YES;
    self.pendingNavigationInfo.cancellationError =
        [NSError errorWithDomain:net::kNSErrorDomain
                            code:net::ERR_UNSAFE_REDIRECT
                        userInfo:nil];
    handler(WKNavigationResponsePolicyCancel);
    return;
  }

  scoped_refptr<net::HttpResponseHeaders> headers;
  if ([WKResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
    headers = net::CreateHeadersFromNSHTTPURLResponse(
        static_cast<NSHTTPURLResponse*>(WKResponse.response));
  }

  // The page will not be changed until this navigation is committed, so the
  // retrieved state will be pending until `didCommitNavigation` callback.
  [self updatePendingNavigationInfoFromNavigationResponse:WKResponse
                                              HTTPHeaders:headers];

  __weak CRWPendingNavigationInfo* weakPendingNavigationInfo =
      self.pendingNavigationInfo;
  auto callback = base::BindOnce(
      ^(web::WebStatePolicyDecider::PolicyDecision policyDecision) {
        if (policyDecision.ShouldCancelNavigation() &&
            WKResponse.canShowMIMEType && WKResponse.forMainFrame) {
          weakPendingNavigationInfo.cancelled = YES;
          weakPendingNavigationInfo.cancellationError =
              policyDecision.GetDisplayError();
        }

        handler(policyDecision.ShouldAllowNavigation()
                    ? WKNavigationResponsePolicyAllow
                    : WKNavigationResponsePolicyCancel);
      });

  if ([self shouldRenderResponse:WKResponse HTTPHeaders:headers.get()]) {
    const web::WebStatePolicyDecider::ResponseInfo response_info(
        WKResponse.forMainFrame);
    self.webStateImpl->ShouldAllowResponse(WKResponse.response, response_info,
                                           std::move(callback));
    return;
  }

  handler(WKNavigationResponsePolicyDownload);
}

- (void)webView:(WKWebView*)webView
    didStartProvisionalNavigation:(WKNavigation*)navigation {
  [self didReceiveWKNavigationDelegateCallback];

  GURL webViewURL = net::GURLWithNSURL(webView.URL);

  [self.navigationStates setState:web::WKNavigationState::STARTED
                    forNavigation:navigation];

  if (webViewURL.is_empty()) {
    // URL starts empty for window.open(""), by didCommitNavigation: callback
    // the URL will be "about:blank".
    webViewURL = GURL(url::kAboutBlankURL);
  }

  web::NavigationContextImpl* context =
      [self.navigationStates contextForNavigation:navigation];

  if (context) {
    // This is already seen and registered navigation.

    if (context->IsLoadingErrorPage()) {
      // This is loadHTMLString: navigation to display error page in web view.
      self.navigationState = web::WKNavigationState::REQUESTED;
      return;
    }

    if (![CRWErrorPageHelper isErrorPageFileURL:webViewURL] &&
        !IsWKInternalUrl(webViewURL) && context->GetUrl() != webViewURL) {
      web::NavigationItem* item =
          web::GetItemWithUniqueID(self.navigationManagerImpl, context);

      // Update last seen URL because it may be changed by WKWebView (f.e. by
      // performing characters escaping).
      if (item) {
        // Item may not exist if navigation was stopped (see crbug.com/969915).
        item->SetURL(webViewURL);
        if ([CRWErrorPageHelper isErrorPageFileURL:webViewURL]) {
          item->SetVirtualURL([CRWErrorPageHelper
              failedNavigationURLFromErrorPageFileURL:webViewURL]);
        }
      }
      context->SetUrl(webViewURL);
    }

    self.webStateImpl->OnNavigationStarted(context);
    self.webStateImpl->GetNavigationManagerImpl().OnNavigationStarted(
        webViewURL);
    return;
  }

  // This is renderer-initiated navigation which was not seen before and
  // should be registered.

  // Renderer-initiated app-specific loads should only be allowed in these
  // specific cases:
  // 1) back/forward navigation to an app-specific URL should be allowed.
  // 2) navigation to an app-specific URL should be allowed from other
  //    app-specific URLs
  bool exemptedAppSpecificLoad = false;
  bool isBackForward =
      self.pendingNavigationInfo.navigationType == WKNavigationTypeBackForward;
  bool isRestoringSession = IsRestoreSessionUrl(self.documentURL);
  exemptedAppSpecificLoad =
      isBackForward || isRestoringSession || self.webStateImpl->HasWebUI();

  if (!web::GetWebClient()->IsAppSpecificURL(webViewURL) ||
      !exemptedAppSpecificLoad) {
    self.webStateImpl->ClearWebUI();
  }

  self.webStateImpl->GetNavigationManagerImpl().OnNavigationStarted(webViewURL);

  std::unique_ptr<web::NavigationContextImpl> navigationContext =
      [self.delegate navigationHandler:self
             registerLoadRequestForURL:webViewURL
                sameDocumentNavigation:NO
                        hasUserGesture:self.pendingNavigationInfo.hasUserGesture
                     rendererInitiated:YES];
  web::NavigationContextImpl* navigationContextPtr = navigationContext.get();

  // GetPendingItem which may be called inside OnNavigationStarted relies on
  // association between NavigationContextImpl and WKNavigation.
  [self.navigationStates setContext:std::move(navigationContext)
                      forNavigation:navigation];
  self.webStateImpl->OnNavigationStarted(navigationContextPtr);
  DCHECK_EQ(web::WKNavigationState::REQUESTED, self.navigationState);
}

- (void)webView:(WKWebView*)webView
    didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation {
  [self didReceiveWKNavigationDelegateCallback];

  GURL webViewURL = net::GURLWithNSURL(webView.URL);

  [self.navigationStates setState:web::WKNavigationState::REDIRECTED
                    forNavigation:navigation];

  web::NavigationContextImpl* context =
      [self.navigationStates contextForNavigation:navigation];
  if (!context)
    return;

  // Redirecting to a data url is always unsafe.
  if (webViewURL.SchemeIs(url::kDataScheme) ||
      // Block redirects to JavaScript schemes. Ref: crbug.com/1509267
      webViewURL.SchemeIs(url::kJavaScriptScheme)) {
    self.pendingNavigationInfo.unsafeRedirect = YES;
  } else {
    context->SetUrl(webViewURL);
  }
  web::NavigationItemImpl* item =
      web::GetItemWithUniqueID(self.navigationManagerImpl, context);

  // Associated item can be a pending item, previously discarded by another
  // navigation. WKWebView allows multiple provisional navigations, while
  // Navigation Manager has only one pending navigation.
  if (item) {
    if (!IsWKInternalUrl(webViewURL) &&
        !self.pendingNavigationInfo.unsafeRedirect) {
      item->SetVirtualURL(webViewURL);
      item->SetURL(webViewURL);
    }
    // Redirects (3xx response code), must change POST requests to GETs.
    item->SetPostData(nil);
    item->ResetHttpRequestHeaders();
  }

  self.userInteractionState->ResetLastTransferTime();
  self.webStateImpl->OnNavigationRedirected(context);
}

- (void)webView:(WKWebView*)webView
    didFailProvisionalNavigation:(WKNavigation*)navigation
                       withError:(NSError*)error {
  [self didReceiveWKNavigationDelegateCallback];

  BOOL wasRedirected = [self.navigationStates stateForNavigation:navigation] ==
                       web::WKNavigationState::REDIRECTED;

  [self.navigationStates setState:web::WKNavigationState::PROVISIONALY_FAILED
                    forNavigation:navigation];

  // Ignore provisional navigation failure if a new navigation has been started,
  // for example, if a page is reloaded after the start of the provisional
  // load but before the load has been committed.
  if (![[self.navigationStates lastAddedNavigation] isEqual:navigation]) {
    return;
  }

  web::NavigationContextImpl* navigationContext =
      [self.navigationStates contextForNavigation:navigation];
  if (wasRedirected && navigationContext) {
    // If there was a redirect, change the URL to have the URL of the first
    // page.
    NSMutableDictionary* userInfo = [error.userInfo mutableCopy];
    userInfo[NSURLErrorFailingURLStringErrorKey] =
        base::SysUTF8ToNSString(navigationContext->GetUrl().spec());
    error = [NSError errorWithDomain:error.domain
                                code:error.code
                            userInfo:userInfo];
  }

  // Handle load cancellation for directly cancelled navigations without
  // handling their potential errors. Otherwise, handle the error.
  if (self.pendingNavigationInfo.cancelled) {
    if (self.pendingNavigationInfo.cancellationError) {
      // If the navigation was cancelled for a CancelAndDisplayError() policy
      // decision, load the error in the failed navigation.
      [self handleLoadError:error
              forNavigation:navigation
                    webView:webView
            provisionalLoad:YES];
    } else {
      [self handleCancelledError:error
                   forNavigation:navigation
                 provisionalLoad:YES];
    }
  } else if (error.code == NSURLErrorUnsupportedURL &&
             self.webStateImpl->HasWebUI()) {
    // This is a navigation to WebUI page.
    DCHECK(web::GetWebClient()->IsAppSpecificURL(
        net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey])));
  } else {
    [self handleLoadError:error
            forNavigation:navigation
                  webView:webView
          provisionalLoad:YES];
  }

  self.webStateImpl->RemoveAllWebFrames();
  // This must be reset at the end, since code above may need information about
  // the pending load.
  self.pendingNavigationInfo = nil;
  if (!web::IsWKWebViewSSLCertError(error)) {
    _certVerificationErrors->Clear();
  }

  // Remove the navigation to immediately get rid of pending item.
  if (web::WKNavigationState::NONE !=
      [self.navigationStates stateForNavigation:navigation]) {
    [self.navigationStates removeNavigation:navigation];
  }
}

- (void)webView:(WKWebView*)webView
    didCommitNavigation:(WKNavigation*)navigation {
  [self didReceiveWKNavigationDelegateCallback];

  // For reasons not yet fully understood, sometimes WKWebView triggers
  // `webView:didFinishNavigation` before `webView:didCommitNavigation`. If a
  // navigation is already finished, stop processing
  // (https://crbug.com/818796#c2).
  if ([self.navigationStates stateForNavigation:navigation] ==
      web::WKNavigationState::FINISHED)
    return;

  BOOL committedNavigation =
      [self.navigationStates isCommittedNavigation:navigation];

  web::NavigationContextImpl* context =
      [self.navigationStates contextForNavigation:navigation];
  if (context && !web::IsWKWebViewSSLCertError(context->GetError())) {
    _certVerificationErrors->Clear();
  }

  // Invariant: Every `navigation` should have a `context`. Note that violation
  // of this invariant is currently observed in production, but the cause is not
  // well understood. Based on the current frequency with which this invariant
  // fails to hold, removing null-checks on `context` would lead to a top-crash.
  UMA_HISTOGRAM_BOOLEAN("IOS.CommittedNavigationHasContext", context);

  GURL webViewURL = net::GURLWithNSURL(webView.URL);
  GURL currentWKItemURL =
      net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
  UMA_HISTOGRAM_BOOLEAN("IOS.CommittedURLMatchesCurrentItem",
                        webViewURL == currentWKItemURL);

  // TODO(crbug.com/41356827): Always use
  // webView.backForwardList.currentItem.URL to obtain lastCommittedURL once
  // loadHTML: is no longer user for WebUI.
  if (webViewURL.is_empty()) {
    // It is possible for `webView.URL` to be nil, in which case
    // webView.backForwardList.currentItem.URL will return the right committed
    // URL (crbug.com/784480).
    webViewURL = currentWKItemURL;
  } else if (context &&
             context->GetUrl() == currentWKItemURL) {
    // If webView.backForwardList.currentItem.URL matches `context`, then this
    // is a known edge case where `webView.URL` is wrong.
    // TODO(crbug.com/41379040): Remove this workaround.
    webViewURL = currentWKItemURL;
  }

  if (context) {
    if (self.pendingNavigationInfo.HTTPHeaders)
      context->SetResponseHeaders(self.pendingNavigationInfo.HTTPHeaders);
  }

  [self.delegate navigationHandlerDisplayWebView:self];

  // `context` will be nil if this navigation has been already committed and
  // finished.
  if (context) {
    web::NavigationManager* navigationManager =
        self.webStateImpl->GetNavigationManager();
    GURL pendingURL;
    web::NavigationItem* pendingItem = nullptr;
    if (navigationManager->GetPendingItemIndex() == -1) {
      // Item may not exist if navigation was stopped (see
      // crbug.com/969915).
      pendingItem = context->GetItem();
    } else {
      pendingItem = navigationManager->GetPendingItem();
    }

    if (pendingItem) {
      pendingURL = pendingItem->GetURL();
    }

    if (self.pendingNavigationInfo.MIMEType) {
      context->SetMimeType(self.pendingNavigationInfo.MIMEType);
    } else if (pendingItem && !context->GetMimeType()) {
      // For navigations handled directly by WebKit's back/forward cache, such
      // as swipe-triggered navigations, `pendingNavigationInfo` will not get
      // populated since there is no `decidePolicyForNavigationResponse`
      // callback. The context's MIME type will also not get set when creating
      // the navigation, since swipe-triggered navigations are directly
      // initiated by WebKit. In this case, use the MIME type stored with the
      // navigation item.
      context->SetMimeType(
          web::WKBackForwardListItemHolder::FromNavigationItem(pendingItem)
              ->mime_type());
    }

    if ((pendingURL == webViewURL) || (context->IsLoadingHtmlString())) {
      // Commit navigation if at least one of these is true:
      //  - Navigation has pending item (this should always be true, but
      //    pending item may not exist due to crbug.com/925304).
      //  - Navigation is loadHTMLString:baseURL: navigation, which does not
      //    create a pending item, but modifies committed item instead.
      //  - Transition type is reload with Legacy Navigation Manager (Legacy
      //    Navigation Manager does not create pending item for reload due to
      //    crbug.com/676129)
      context->SetHasCommitted(true);
    }
    self.webStateImpl->SetContentsMimeType(
        base::SysNSStringToUTF8(context->GetMimeType()));
  }

  [self commitPendingNavigationInfoInWebView:webView];

  self.webStateImpl->RemoveAllWebFrames();

  // This point should closely approximate the document object change, so reset
  // the list of injected scripts to those that are automatically injected.  For
  // WebUI, let the window ID be injected when the `loadHTMLString:baseURL`
  // navigation is committed.
  const std::string& mime_type = self.webStateImpl->GetContentsMimeType();
  if (web::IsContentTypeHtml(mime_type) || web::IsContentTypeImage(mime_type) ||
      mime_type.empty()) {
    // In unit tests MIME type will be empty, because loadHTML:forURL: does
    // not notify web view delegate about received response, so web controller
    // does not get a chance to properly update MIME type.
    self.webStateImpl->RetrieveExistingFrames();
  }

  if (committedNavigation) {
    // WKWebView called didCommitNavigation: with incorrect WKNavigation object.
    // Correct WKNavigation object for this navigation was deallocated because
    // WKWebView mistakenly cancelled the navigation and called
    // didFailProvisionalNavigation. As a result web::NavigationContext for this
    // navigation does not exist anymore. Find correct navigation item and make
    // it committed.
    [self resetDocumentSpecificState];
    [self.delegate navigationHandlerDidStartLoading:self];
  } else if (context) {
    // If `navigation` is nil (which happens for windows open by DOM), then it
    // should be the first and the only pending navigation.
    BOOL isLastNavigation =
        !navigation ||
        [[self.navigationStates lastAddedNavigation] isEqual:navigation];
    if (isLastNavigation ||
        self.navigationManagerImpl->GetPendingItemIndex() == -1) {
      [self webPageChangedWithContext:context webView:webView];
    }
  }

  // The WebView URL can't always be trusted when multiple pending navigations
  // are occuring, as a navigation could commit after a new navigation has
  // started (and thus the WebView URL will be the URL of the new navigation).
  // See crbug.com/1127025.
  BOOL hasMultiplePendingNavigations =
      [self.navigationStates pendingNavigations].count > 1;

  // When loading an error page, the context has the correct URL whereas the
  // webView has the file URL.
  BOOL isErrorPage = [CRWErrorPageHelper isErrorPageFileURL:webViewURL];

  BOOL shouldUseContextURL =
      context && (isErrorPage || hasMultiplePendingNavigations);
  GURL documentURL = shouldUseContextURL ? context->GetUrl() : webViewURL;

  // This is the point where the document's URL has actually changed.
  [self.delegate navigationHandler:self
                    setDocumentURL:documentURL
                           context:context];

  // No further code relies an existance of pending item, so this navigation can
  // be marked as "committed".
  [self.navigationStates setState:web::WKNavigationState::COMMITTED
                    forNavigation:navigation];

  if (!committedNavigation && context && !context->IsLoadingErrorPage()) {
    self.webStateImpl->OnNavigationFinished(context);
  }

  // The actual navigation item will not be committed until the native content
  // or WebUI is shown.
  if (context &&
      !context->GetUrl().SchemeIs(url::kAboutScheme) &&
      !IsRestoreSessionUrl(context->GetUrl())) {
    [self.delegate webViewHandlerUpdateSSLStatusForCurrentNavigationItem:self];
    if (!context->IsLoadingErrorPage() && !IsRestoreSessionUrl(webViewURL)) {
      [self setLastCommittedNavigationItemTitle:webView.title];
    }
  }
}

- (void)webView:(WKWebView*)webView
    didFinishNavigation:(WKNavigation*)navigation {
  [self didReceiveWKNavigationDelegateCallback];

  // Sometimes `webView:didFinishNavigation` arrives before
  // `webView:didCommitNavigation`. Explicitly trigger post-commit processing.
  bool navigationCommitted =
      [self.navigationStates isCommittedNavigation:navigation];
  UMA_HISTOGRAM_BOOLEAN("IOS.WKWebViewFinishBeforeCommit",
                        !navigationCommitted);
  if (!navigationCommitted) {
    [self webView:webView didCommitNavigation:navigation];
    DCHECK_EQ(web::WKNavigationState::COMMITTED,
              [self.navigationStates stateForNavigation:navigation]);
  }

  // Sometimes `didFinishNavigation` callback arrives after `stopLoading` has
  // been called. Abort in this case.
  if ([self.navigationStates stateForNavigation:navigation] ==
      web::WKNavigationState::NONE) {
    return;
  }

  GURL webViewURL = net::GURLWithNSURL(webView.URL);
  GURL currentWKItemURL =
      net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
  UMA_HISTOGRAM_BOOLEAN("IOS.FinishedURLMatchesCurrentItem",
                        webViewURL == currentWKItemURL);

  web::NavigationContextImpl* context =
      [self.navigationStates contextForNavigation:navigation];
  web::NavigationItemImpl* item =
      context ? web::GetItemWithUniqueID(self.navigationManagerImpl, context)
              : nullptr;
  // Item may not exist if navigation was stopped (see crbug.com/969915).
  UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasContext", context);
  UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasItem", item);

  if (context && item) {
    if (context->GetUrl() == currentWKItemURL) {
      // If webView.backForwardList.currentItem.URL matches `context`, then this
      // is a known edge case where `webView.URL` is wrong.
      // TODO(crbug.com/41379040): Remove this workaround.
      webViewURL = currentWKItemURL;
    }

    if (!IsWKInternalUrl(currentWKItemURL) && currentWKItemURL == webViewURL &&
        currentWKItemURL != context->GetUrl() &&
        item == self.navigationManagerImpl->GetLastCommittedItem() &&
        item->GetURL().DeprecatedGetOriginAsURL() ==
            currentWKItemURL.DeprecatedGetOriginAsURL()) {
      // WKWebView sometimes changes URL on the same navigation, likely due to
      // location.replace() or history.replaceState in onload handler that does
      // not change the origin. It's safe to update `item` and `context` URL
      // because they are both associated to WKNavigation*, which is a stable ID
      // for the navigation. See https://crbug.com/869540 for a real-world case.
      item->SetURL(currentWKItemURL);
      context->SetUrl(currentWKItemURL);
    }

    if (context->GetError()) {
      [self loadErrorPageForNavigationItem:item
                         navigationContext:navigation
                                   webView:webView];
    }
  }

  [self updateStateForNavigation:navigation toFinishedWithContext:context];
}

- (void)updateStateForNavigation:(WKNavigation*)navigation
           toFinishedWithContext:(web::NavigationContextImpl*)context {
  [self.navigationStates setState:web::WKNavigationState::FINISHED
                    forNavigation:navigation];

  [self.delegate webViewHandler:self didFinishNavigation:context];

  // Remove the navigation to immediately get rid of pending item. Navigation
  // should not be cleared, however, in the case of a committed interstitial
  // for an SSL error.
  if (web::WKNavigationState::NONE !=
          [self.navigationStates stateForNavigation:navigation] &&
      !(context && web::IsWKWebViewSSLCertError(context->GetError()))) {
    [self.navigationStates removeNavigation:navigation];
  }
}

- (void)webView:(WKWebView*)webView
    didFailNavigation:(WKNavigation*)navigation
            withError:(NSError*)error {
  [self didReceiveWKNavigationDelegateCallback];

  // `webView:didFailNavigation:withError:` may be called after the document has
  // already loaded which should be ignored. This can happen when navigating
  // back to a page which loads from the back forward cache. See
  // crbug.com/1249735 for more details. This will also be called when the user
  // cancels an active page load.
  if (self.navigationState == web::WKNavigationState::FINISHED &&
      error.code == NSURLErrorCancelled &&
      [error.domain isEqualToString:NSURLErrorDomain]) {
    _certVerificationErrors->Clear();
    return;
  }

  // `webView:didFailNavigation:withError:` may be called when rendering an
  // office document which should be ignored because the `webView` will already
  // be displaying its own error. Additionally, in these cases, JavaScript can
  // not be run on these pages (in order to display our own error message). See
  // crbug.com/1489167 for more details.
  if ([error.domain isEqualToString:@"OfficeImportErrorDomain"]) {
    [self.navigationStates setState:web::WKNavigationState::FINISHED
                      forNavigation:navigation];
    self.webStateImpl->RemoveAllWebFrames();
    _certVerificationErrors->Clear();
    return;
  }

  if ([error.domain isEqualToString:@(web::kWebKitErrorDomain)] &&
      error.code == web::kWebKitErrorPlugInLoadFailed) {
    // In cases where a Plug-in handles the load, mark the navigation as
    // successful even though it is reported as a failed navigation.
    web::NavigationContextImpl* navigationContext =
        [self.navigationStates contextForNavigation:navigation];
    [self updateStateForNavigation:navigation
             toFinishedWithContext:navigationContext];
    return;
  }

  [self.navigationStates setState:web::WKNavigationState::FAILED
                    forNavigation:navigation];

  [self handleLoadError:error
          forNavigation:navigation
                webView:webView
        provisionalLoad:NO];
  self.webStateImpl->RemoveAllWebFrames();
  _certVerificationErrors->Clear();
  [self forgetNullWKNavigation:navigation];
}

- (void)webView:(WKWebView*)webView
    didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
                    completionHandler:
                        (void (^)(NSURLSessionAuthChallengeDisposition,
                                  NSURLCredential*))completionHandler {
  [self didReceiveWKNavigationDelegateCallback];

  NSString* authMethod = challenge.protectionSpace.authenticationMethod;
  if ([authMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic] ||
      [authMethod isEqualToString:NSURLAuthenticationMethodNTLM] ||
      [authMethod isEqualToString:NSURLAuthenticationMethodHTTPDigest]) {
    [self handleHTTPAuthForChallenge:challenge
                   completionHandler:completionHandler];
    return;
  }

  if (![authMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
    completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
    return;
  }

  SecTrustRef trust = challenge.protectionSpace.serverTrust;
  base::apple::ScopedCFTypeRef<SecTrustRef> scopedTrust(
      trust, base::scoped_policy::RETAIN);
  __weak CRWWKNavigationHandler* weakSelf = self;
  [self.certVerificationController
      decideLoadPolicyForTrust:scopedTrust
                          host:challenge.protectionSpace.host
             completionHandler:^(web::CertAcceptPolicy policy,
                                 net::CertStatus status) {
               CRWWKNavigationHandler* strongSelf = weakSelf;
               if (!strongSelf) {
                 completionHandler(
                     NSURLSessionAuthChallengeRejectProtectionSpace, nil);
                 return;
               }
               [strongSelf processAuthChallenge:challenge
                            forCertAcceptPolicy:policy
                                     certStatus:status
                              completionHandler:completionHandler];
             }];
}

- (void)webView:(WKWebView*)webView
     authenticationChallenge:(NSURLAuthenticationChallenge*)challenge
    shouldAllowDeprecatedTLS:(void (^)(BOOL))decisionHandler {
  [self didReceiveWKNavigationDelegateCallback];
  DCHECK(challenge);
  DCHECK(decisionHandler);

  // TLS 1.0/1.1 is no longer supported in Chrome. When a connection occurs that
  // uses deprecated TLS versions, reject it. For navigations (which are user
  // visible), cancel the navigation and set the error code to
  // ERR_SSL_VERSION_OR_CIPHER_MISMATCH.
  if (self.pendingNavigationInfo) {
    self.pendingNavigationInfo.cancelled = YES;
    self.pendingNavigationInfo.cancellationError =
        [NSError errorWithDomain:net::kNSErrorDomain
                            code:net::ERR_SSL_VERSION_OR_CIPHER_MISMATCH
                        userInfo:nil];
  }
  decisionHandler(NO);
}

- (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView {
  [self didReceiveWKNavigationDelegateCallback];

  _certVerificationErrors->Clear();
  _webProcessCrashed = YES;
  self.webStateImpl->RemoveAllWebFrames();

  [self.delegate navigationHandlerWebProcessDidCrash:self];
}

- (void)webView:(WKWebView*)webView
     navigationAction:(WKNavigationAction*)navigationAction
    didBecomeDownload:(WKDownload*)WKDownload {
  // As Chromium never return WKNavigationResponsePolicyDownload
  // when deciding the policy for an action, WebKit should never
  // invoke this delegate method.
  NOTREACHED_IN_MIGRATION();
}

- (void)webView:(WKWebView*)webView
    navigationResponse:(WKNavigationResponse*)navigationResponse
     didBecomeDownload:(WKDownload*)WKDownload {
  // Send navigation callback if the download occurs in the main frame.
  if (navigationResponse.forMainFrame) {
    const GURL responseURL =
        net::GURLWithNSURL(navigationResponse.response.URL);
    web::NavigationContextImpl* context =
        [self contextForPendingMainFrameNavigationWithURL:responseURL];

    // Context lookup can fail in rare cases (e.g. after certain redirects,
    // see https://crbug.com/820375 for details). In that case, it's not
    // possible to locate the correct context to call OnNavigationFinished().
    // Not sending this event does not cause any major issue, so do nothing
    // if `context` cannot be found (i.e. this is not a security issue).
    if (context) {
      context->SetIsDownload(true);
      context->ReleaseItem();
      self.webStateImpl->OnNavigationFinished(context);
    }
  }

  // Discard the pending item to ensure that the current URL is not different
  // from what is displayed on the view.
  self.navigationManagerImpl->DiscardNonCommittedItems();

  [_nativeTaskBridges
      addObject:[[DownloadNativeTaskBridge alloc] initWithDownload:WKDownload
                                                          delegate:self]];
}

// Used to set response url, content length, mimetype and http response headers
// in CRWWkNavigationHandler so method can interact with WKWebView. Returns NO
// if the download cannot be started.
- (BOOL)onDownloadNativeTaskBridgeReadyForDownload:
    (DownloadNativeTaskBridge*)bridge {
  __attribute__((objc_precise_lifetime))
  DownloadNativeTaskBridge* nativeTaskBridge = bridge;
  [_nativeTaskBridges removeObject:bridge];
  if (!self.webStateImpl)
    return NO;

  const GURL responseURL = net::GURLWithNSURL(bridge.response.URL);
  const int64_t contentLength = bridge.response.expectedContentLength;
  const std::string MIMEType =
      base::SysNSStringToUTF8(bridge.response.MIMEType);

  std::string contentDisposition;
  scoped_refptr<net::HttpResponseHeaders> headers;
  if ([bridge.response isKindOfClass:[NSHTTPURLResponse class]]) {
    headers = net::CreateHeadersFromNSHTTPURLResponse(
        static_cast<NSHTTPURLResponse*>(bridge.response));
    if (headers) {
      headers->GetNormalizedHeader("content-disposition", &contentDisposition);
    }
  }

  NSString* HTTPMethod = bridge.download.originalRequest.HTTPMethod;
  web::DownloadController::FromBrowserState(
      self.webStateImpl->GetBrowserState())
      ->CreateNativeDownloadTask(self.webStateImpl, [NSUUID UUID].UUIDString,
                                 responseURL, HTTPMethod, contentDisposition,
                                 contentLength, MIMEType, bridge);
  return YES;
}

- (void)resumeDownloadNativeTask:(NSData*)data
               completionHandler:(void (^)(WKDownload*))completionHandler {
  [self.delegate resumeDownloadWithData:data
                      completionHandler:completionHandler];
}

#pragma mark - Private methods

// Returns the UserAgent that needs to be used for the `navigationAction` from
// the `webView`.
- (web::UserAgentType)userAgentForNavigationAction:
                          (WKNavigationAction*)navigationAction
                                           webView:(WKWebView*)webView {
  web::NavigationItem* item = nullptr;
  web::UserAgentType userAgentType = web::UserAgentType::NONE;
  if (navigationAction.navigationType == WKNavigationTypeBackForward) {
    // Use the item associated with the back/forward item to have the same user
    // agent as the one used the first time.
    item = [[CRWNavigationItemHolder
        holderForBackForwardListItem:webView.backForwardList.currentItem]
        navigationItem];
    // In some cases, the associated item isn't found. In that case, follow the
    // code path for the non-backforward navigations. See crbug.com/1121950.
    if (item)
      userAgentType = item->GetUserAgentType();
  }
  if (!item) {
    // Get the visible item. There is no guarantee that the pending item belongs
    // to this navigation but it is very likely that it is the case. If there is
    // no pending item, it is probably a render initiated navigation. Use the
    // UserAgent of the previous navigation. This will also return the
    // navigation item of the restoration if a restoration occurs. Request the
    // pending item explicitly as the visible item might be the committed item
    // if the pending navigation isn't user triggered.
    item = self.navigationManagerImpl->GetPendingItem();
    if (!item)
      item = self.navigationManagerImpl->GetVisibleItem();

    if (item && item->GetTransitionType() & ui::PAGE_TRANSITION_FORWARD_BACK) {
      // When navigating forward to a restored page, the WKNavigationAction is
      // of type reload and not BackForward. The item is correctly set a
      // back/forward, so it is possible to use it.
      userAgentType = item->GetUserAgentType();
    } else {
      userAgentType = self.webStateImpl->GetUserAgentForNextNavigation(
          net::GURLWithNSURL(navigationAction.request.URL));
    }
  }

  if (item && web::GetWebClient()->IsAppSpecificURL(item->GetVirtualURL())) {
    // In case of app specific URL, no specificUser Agent needs to be used.
    // However, to have a custom User Agent and a WKContentMode, use mobile.
    userAgentType = web::UserAgentType::MOBILE;
  }
  return userAgentType;
}

- (web::NavigationManagerImpl*)navigationManagerImpl {
  return &(self.webStateImpl->GetNavigationManagerImpl());
}

- (web::WebStateImpl*)webStateImpl {
  return [self.delegate webStateImplForWebViewHandler:self];
}

- (web::UserInteractionState*)userInteractionState {
  return [self.delegate userInteractionStateForWebViewHandler:self];
}

- (CRWCertVerificationController*)certVerificationController {
  return [self.delegate certVerificationControllerForNavigationHandler:self];
}

- (GURL)documentURL {
  return [self.delegate documentURLForWebViewHandler:self];
}

- (web::NavigationItemImpl*)currentNavItem {
  return self.navigationManagerImpl
             ? self.navigationManagerImpl->GetCurrentItemImpl()
             : nullptr;
}

// This method should be called on receiving WKNavigationDelegate callbacks.
- (void)didReceiveWKNavigationDelegateCallback {
  DCHECK(!self.beingDestroyed);
}

// Extracts navigation info from WKNavigationAction and sets it as a pending.
// Some pieces of navigation information are only known in
// `decidePolicyForNavigationAction`, but must be in a pending state until
// `didgo/Navigation` where it becames current.
- (void)createPendingNavigationInfoFromNavigationAction:
    (WKNavigationAction*)action {
  if (action.targetFrame.mainFrame) {
    self.pendingNavigationInfo = [[CRWPendingNavigationInfo alloc] init];
    self.pendingNavigationInfo.referrer =
        [action.request valueForHTTPHeaderField:kReferrerHeaderName];
    self.pendingNavigationInfo.navigationType = action.navigationType;
    self.pendingNavigationInfo.HTTPMethod = action.request.HTTPMethod;
    self.pendingNavigationInfo.hasUserGesture =
        web::GetNavigationActionInitiationType(action) ==
        web::NavigationActionInitiationType::kUserInitiated;
  }
}

// Extracts navigation info from WKNavigationResponse and sets it as a pending.
// Some pieces of navigation information are only known in
// `decidePolicyForNavigationResponse`, but must be in a pending state until
// `didCommitNavigation` where it becames current.
- (void)
    updatePendingNavigationInfoFromNavigationResponse:
        (WKNavigationResponse*)response
                                          HTTPHeaders:
                                              (const scoped_refptr<
                                                  net::HttpResponseHeaders>&)
                                                  headers {
  if (response.isForMainFrame) {
    if (!self.pendingNavigationInfo) {
      self.pendingNavigationInfo = [[CRWPendingNavigationInfo alloc] init];
    }
    self.pendingNavigationInfo.MIMEType = response.response.MIMEType;
    self.pendingNavigationInfo.HTTPHeaders = headers;
  }
}

// Returns YES if the navigation action is associated with a main frame request.
- (BOOL)isMainFrameNavigationAction:(WKNavigationAction*)action {
  if (action.targetFrame) {
    return action.targetFrame.mainFrame;
  }
  // According to WKNavigationAction documentation, in the case of a new window
  // navigation, target frame will be nil. In this case check if the
  // `sourceFrame` is the mainFrame.
  return action.sourceFrame.mainFrame;
}

// Returns YES if the given `action` should be allowed to continue for app
// specific URL. If this returns NO, the navigation should be cancelled.
// App specific pages have elevated privileges and WKWebView uses the same
// renderer process for all page frames. With that Chromium does not allow
// running App specific pages in the same process as a web site from the
// internet. Allows navigation to app specific URL in the following cases:
//   - last committed URL is app specific
//   - navigation not a new navigation (back-forward or reload)
//   - navigation is typed, generated or bookmark
//   - navigation is performed in iframe and main frame is app-specific page
- (BOOL)shouldAllowAppSpecificURLNavigationAction:(WKNavigationAction*)action
                                       transition:
                                           (ui::PageTransition)pageTransition {
  GURL requestURL = net::GURLWithNSURL(action.request.URL);
  DCHECK(web::GetWebClient()->IsAppSpecificURL(requestURL));
  if (web::GetWebClient()->IsAppSpecificURL(
          self.webStateImpl->GetLastCommittedURL())) {
    // Last committed page is also app specific and navigation should be
    // allowed.
    return YES;
  }

  if (pageTransition & ui::PAGE_TRANSITION_FORWARD_BACK) {
    // Allow back-forward navigations.
    return YES;
  }

  if (ui::PageTransitionTypeIncludingQualifiersIs(pageTransition,
                                                  ui::PAGE_TRANSITION_TYPED)) {
    return YES;
  }

  if (ui::PageTransitionTypeIncludingQualifiersIs(
          pageTransition, ui::PAGE_TRANSITION_GENERATED)) {
    return YES;
  }

  if (ui::PageTransitionTypeIncludingQualifiersIs(
          pageTransition, ui::PAGE_TRANSITION_AUTO_BOOKMARK)) {
    return YES;
  }

  // If the session is being restored, allow the navigation.
  if (IsRestoreSessionUrl(self.documentURL)) {
    return YES;
  }

  // Allow navigation to WebUI pages from error pages.
  if ([CRWErrorPageHelper isErrorPageFileURL:self.documentURL]) {
    return YES;
  }

  GURL mainDocumentURL = net::GURLWithNSURL(action.request.mainDocumentURL);
  if (web::GetWebClient()->IsAppSpecificURL(mainDocumentURL) &&
      !action.sourceFrame.mainFrame) {
    // AppSpecific URLs are allowed inside iframe if the main frame is also
    // app specific page.
    return YES;
  }

  return NO;
}

// Caches request POST data in the given session entry.
- (void)cachePOSTDataForRequest:(NSURLRequest*)request
               inNavigationItem:(web::NavigationItemImpl*)item {
  NSUInteger maxPOSTDataSizeInBytes = 4096;
  NSString* cookieHeaderName = @"cookie";

  DCHECK(item);
  const bool shouldUpdateEntry =
      ui::PageTransitionCoreTypeIs(item->GetTransitionType(),
                                   ui::PAGE_TRANSITION_FORM_SUBMIT) &&
      ![request HTTPBodyStream] &&  // Don't cache streams.
      !item->HasPostData() &&
      item->GetURL() == net::GURLWithNSURL([request URL]);
  const bool belowSizeCap =
      [[request HTTPBody] length] < maxPOSTDataSizeInBytes;
  DLOG_IF(WARNING, shouldUpdateEntry && !belowSizeCap)
      << "Data in POST request exceeds the size cap (" << maxPOSTDataSizeInBytes
      << " bytes), and will not be cached.";

  if (shouldUpdateEntry && belowSizeCap) {
    item->SetPostData([request HTTPBody]);
    item->ResetHttpRequestHeaders();
    item->AddHttpRequestHeaders([request allHTTPHeaderFields]);
    // Don't cache the "Cookie" header.
    // According to NSURLRequest documentation, `-valueForHTTPHeaderField:` is
    // case insensitive, so it's enough to test the lower case only.
    if ([request valueForHTTPHeaderField:cookieHeaderName]) {
      // Case insensitive search in `headers`.
      NSSet<NSString*>* cookieKeys = [item->GetHttpRequestHeaders()
          keysOfEntriesPassingTest:^(NSString* key, NSString* obj, BOOL* stop) {
            const BOOL found =
                [key caseInsensitiveCompare:cookieHeaderName] == NSOrderedSame;
            *stop = found;
            return found;
          }];
      DCHECK_EQ(1u, [cookieKeys count]);
      item->RemoveHttpRequestHeaderForKey([cookieKeys anyObject]);
    }
  }
}

// If YES, the page should be closed if it successfully redirects to a native
// application, for example if a new tab redirects to the App Store.
- (BOOL)shouldClosePageOnNativeApplicationLoad {
  // The page should be closed if it was initiated by the DOM and there has been
  // no user interaction with the page since the web view was created, or if the
  // page has no navigation items. An exception to that when an URL redirect to
  // an application was initiated from another application (intent), in that
  // case a prompt will show and page initiating the redirect needs to stay
  // open to make sure that a prompt is properly owned and to give the user
  // context about that prompt.
  BOOL rendererInitiatedWithoutInteraction =
      self.webStateImpl->HasOpener() &&
      !self.userInteractionState
           ->UserInteractionRegisteredSinceWebViewCreated();
  BOOL noNavigationItems = !(self.navigationManagerImpl->GetItemCount());
  BOOL isIntent = !self.webStateImpl->HasOpener() && noNavigationItems;
  return !isIntent &&
         (rendererInitiatedWithoutInteraction || noNavigationItems);
}

// Returns YES if response should be rendered in WKWebView.
- (BOOL)shouldRenderResponse:(WKNavigationResponse*)WKResponse
                 HTTPHeaders:(net::HttpResponseHeaders*)headers {
  if (headers) {
    std::string contentDisposition;
    headers->GetNormalizedHeader("content-disposition", &contentDisposition);
    net::HttpContentDisposition parsedContentDisposition(contentDisposition,
                                                         std::string());
    if (parsedContentDisposition.is_attachment()) {
      return NO;
    }
  }

  if (_shouldPerformDownload) {
    return NO;
  }

  if (!WKResponse.canShowMIMEType) {
    return NO;
  }

  // TODO(crbug.com/40219220): Remove this when `canShowMIMEType` is fixed.
  // On iOS 15 `canShowMIMEType` returns true for AR files although WebKit is
  // not capable of displaying them natively.
  NSString* MIMEType = WKResponse.response.MIMEType;
  if ([MIMEType isEqualToString:@"model/vnd.pixar.usd"] ||
      [MIMEType isEqualToString:@"model/usd"] ||
      [MIMEType isEqualToString:@"model/vnd.usdz+zip"] ||
      [MIMEType isEqualToString:@"model/vnd.pixar.usd"] ||
      [MIMEType isEqualToString:@"model/vnd.reality"]) {
    return NO;
  }

  GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL);
  if (responseURL.SchemeIs(url::kDataScheme) && WKResponse.forMainFrame) {
    // Block rendering data URLs for renderer-initiated navigations in main
    // frame to prevent abusive behavior (crbug.com/890558).
    web::NavigationContext* context =
        [self contextForPendingMainFrameNavigationWithURL:responseURL];
    if (!context) {
      // If the data URL is originally data://foo/bar instead of data:foo/bar,
      // then the URL is transformed to data:///bar. Considering that the "foo"
      // part of the URL is lost, it doesn't really make sense to try to match
      // the URL as it would only work for text.
      return NO;
    }
    // If the server is doing a redirect on a user reload, the navigation is
    // treated as a reload instead of a redirect. See crbug.com/1165654.
    web::NavigationItem* lastCommittedItem =
        self.navigationManagerImpl->GetLastCommittedItem();
    BOOL isFakeReload = PageTransitionCoreTypeIs(context->GetPageTransition(),
                                                 ui::PAGE_TRANSITION_RELOAD) &&
                        lastCommittedItem &&
                        lastCommittedItem->GetURL() != responseURL;
    if (context->IsRendererInitiated() || isFakeReload) {
      return NO;
    }
  }

  return YES;
}

// WKNavigation objects are used as a weak key to store web::NavigationContext.
// WKWebView manages WKNavigation lifetime and destroys them after the
// navigation is finished. However for window opening navigations WKWebView
// passes null WKNavigation to WKNavigationDelegate callbacks and strong key is
// used to store web::NavigationContext. Those "null" navigations have to be
// cleaned up manually by calling this method.
- (void)forgetNullWKNavigation:(WKNavigation*)navigation {
  if (!navigation)
    [self.navigationStates removeNavigation:navigation];
}

// Returns the warning type to be shown for <form> posts, or kNone if no warning
// should be shown.
- (web::FormWarningType)formWarningType:(WKNavigationAction*)action {
  if (action.navigationType == WKNavigationTypeFormResubmitted) {
    return web::FormWarningType::kRepost;
  }
  if (web::GetWebClient()->IsInsecureFormWarningEnabled(
          self.webStateImpl->GetBrowserState()) &&
      action.navigationType == WKNavigationTypeFormSubmitted) {
    if (action.sourceFrame) {
      GURL source_url = web::GURLOriginWithWKSecurityOrigin(
          action.sourceFrame.securityOrigin);
      GURL form_action_url = net::GURLWithNSURL(action.request.URL);
      if (security_interstitials::IsInsecureFormActionOnSecureSource(
              source_url, form_action_url)) {
        return web::FormWarningType::kInsecureForm;
      }
    }
  }
  return web::FormWarningType::kNone;
}

// This method should be called on deciding policy for navigation action. It
// Answers the `decisionHandler` with a final decision caculated with passed
// `policyDecision`. The passed `policyDecision` should be determined by some
// conditions and policy deciders
- (void)answerDecisionHandler:
            (void (^)(WKNavigationActionPolicy))decisionHandler
          forNavigationAction:(WKNavigationAction*)action
           withPolicyDecision:
               (web::WebStatePolicyDecider::PolicyDecision)policyDecision
                      webView:(WKWebView*)webView
     forceBlockUniversalLinks:(BOOL)forceBlockUniversalLinks {
  if (policyDecision.ShouldAllowNavigation()) {
    if ([[action.request HTTPMethod] isEqualToString:@"POST"]) {
      web::FormWarningType warning_type = [self formWarningType:action];
      // Display the confirmation dialog if a form repost is detected.
      if (warning_type != web::FormWarningType::kNone) {
        self.webStateImpl->ShowRepostFormWarningDialog(
            warning_type, base::BindOnce(^(bool shouldContinue) {
              if (self.beingDestroyed) {
                decisionHandler(WKNavigationActionPolicyCancel);
              } else if (shouldContinue) {
                decisionHandler(WKNavigationActionPolicyAllow);
              } else {
                decisionHandler(WKNavigationActionPolicyCancel);
                if (action.targetFrame.mainFrame) {
                  [self.pendingNavigationInfo setCancelled:YES];
                }
              }
            }));
        return;
      }

      web::NavigationItemImpl* item =
          self.navigationManagerImpl->GetCurrentItemImpl();
      // TODO(crbug.com/40449786): Remove this check once it's no longer
      // possible to have no current entries.
      if (item)
        [self cachePOSTDataForRequest:action.request inNavigationItem:item];
    }
  } else {
    if (action.targetFrame.mainFrame) {
      if (!self.beingDestroyed && policyDecision.ShouldDisplayError()) {
        DCHECK(policyDecision.GetDisplayError());

        // Navigation was blocked by `ShouldProvisionallyFailRequest`. Cancel
        // load of page.
        decisionHandler(WKNavigationActionPolicyCancel);

        ui::PageTransition transition =
            [self pageTransitionFromNavigationType:action.navigationType];

        [self displayError:policyDecision.GetDisplayError()
            forCancelledNavigationToURL:action.request.URL
                              inWebView:webView
                         withTransition:transition];
        return;
      }

      [self.pendingNavigationInfo setCancelled:YES];
      if (self.navigationManagerImpl->GetPendingItemIndex() == -1) {
        // Discard the new pending item to ensure that the current URL is not
        // different from what is displayed on the view. There is no need to
        // reset pending item index for a different pending back-forward
        // navigation.
        self.navigationManagerImpl->DiscardNonCommittedItems();
      }

      web::NavigationContextImpl* context = [self
          contextForPendingMainFrameNavigationWithURL:net::GURLWithNSURL(
                                                          action.request.URL)];
      if (context) {
        // Destroy associated pending item, because this will be the last
        // WKWebView callback for this navigation context.
        context->ReleaseItem();
      }

      if (!self.beingDestroyed &&
          [self shouldClosePageOnNativeApplicationLoad]) {
        self.webStateImpl->CloseWebState();
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
      }
    }
  }

  if (policyDecision.ShouldCancelNavigation()) {
    decisionHandler(WKNavigationActionPolicyCancel);
    return;
  }

  BOOL isOffTheRecord = self.webStateImpl->GetBrowserState()->IsOffTheRecord();
  decisionHandler(web::GetAllowNavigationActionPolicy(
      isOffTheRecord || forceBlockUniversalLinks));
}

#pragma mark - Auth Challenge

// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to
// reply with NSURLSessionAuthChallengeDisposition and credentials.
- (void)processAuthChallenge:(NSURLAuthenticationChallenge*)challenge
         forCertAcceptPolicy:(web::CertAcceptPolicy)policy
                  certStatus:(net::CertStatus)certStatus
           completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
                                       NSURLCredential*))completionHandler {
  SecTrustRef trust = challenge.protectionSpace.serverTrust;
  if (policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER) {
    // Cert is invalid, but user agreed to proceed, override default behavior.
    completionHandler(NSURLSessionAuthChallengeUseCredential,
                      [NSURLCredential credentialForTrust:trust]);
    return;
  }

  if (policy != web::CERT_ACCEPT_POLICY_ALLOW &&
      SecTrustGetCertificateCount(trust)) {
    // The cert is invalid and the user has not agreed to proceed. Cache the
    // cert verification result in `_certVerificationErrors`, so that it can
    // later be reused inside `didFailProvisionalNavigation:`.
    // The leaf cert is used as the key, because the chain provided by
    // `didFailProvisionalNavigation:` will differ (it is the server-supplied
    // chain), thus if intermediates were considered, the keys would mismatch.

    scoped_refptr<net::X509Certificate> leafCert = nil;
    base::apple::ScopedCFTypeRef<CFArrayRef> certificateChain(
        SecTrustCopyCertificateChain(trust));
    SecCertificateRef secCertificate =
        base::apple::CFCastStrict<SecCertificateRef>(
            CFArrayGetValueAtIndex(certificateChain.get(), 0));
    leafCert = net::x509_util::CreateX509CertificateFromSecCertificate(
        base::apple::ScopedCFTypeRef<SecCertificateRef>(
            secCertificate, base::scoped_policy::RETAIN),
        {});

    if (leafCert) {
      bool is_recoverable =
          policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_UNDECIDED_BY_USER;
      std::string host =
          base::SysNSStringToUTF8(challenge.protectionSpace.host);
      _certVerificationErrors->Put(
          web::CertHostPair(leafCert, host),
          web::CertVerificationError(is_recoverable, certStatus));
    }
  }
  completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
}

// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
// with NSURLSessionAuthChallengeDisposition and credentials.
- (void)handleHTTPAuthForChallenge:(NSURLAuthenticationChallenge*)challenge
                 completionHandler:
                     (void (^)(NSURLSessionAuthChallengeDisposition,
                               NSURLCredential*))completionHandler {
  NSURLProtectionSpace* space = challenge.protectionSpace;
  DCHECK([space.authenticationMethod
             isEqualToString:NSURLAuthenticationMethodHTTPBasic] ||
         [space.authenticationMethod
             isEqualToString:NSURLAuthenticationMethodNTLM] ||
         [space.authenticationMethod
             isEqualToString:NSURLAuthenticationMethodHTTPDigest]);

  self.webStateImpl->OnAuthRequired(
      space, challenge.proposedCredential,
      base::BindRepeating(^(NSString* user, NSString* password) {
        [CRWWKNavigationHandler processHTTPAuthForUser:user
                                              password:password
                                     completionHandler:completionHandler];
      }));
}

// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
// with NSURLSessionAuthChallengeDisposition and credentials.
+ (void)processHTTPAuthForUser:(NSString*)user
                      password:(NSString*)password
             completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
                                         NSURLCredential*))completionHandler {
  DCHECK_EQ(user == nil, password == nil);
  if (!user || !password) {
    // Embedder cancelled authentication.
    completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
    return;
  }
  completionHandler(
      NSURLSessionAuthChallengeUseCredential,
      [NSURLCredential
          credentialWithUser:user
                    password:password
                 persistence:NSURLCredentialPersistenceForSession]);
}

// Called when a load ends in an error.
- (void)handleLoadError:(NSError*)error
          forNavigation:(WKNavigation*)navigation
                webView:(WKWebView*)webView
        provisionalLoad:(BOOL)provisionalLoad {
  NSError* policyDecisionCancellationError =
      self.pendingNavigationInfo.cancellationError;
  if (!policyDecisionCancellationError && error.code == NSURLErrorCancelled) {
    [self handleCancelledError:error
                 forNavigation:navigation
               provisionalLoad:provisionalLoad];
    // NSURLErrorCancelled errors that aren't handled by aborting the load will
    // automatically be retried by the web view, so early return in this case.
    return;
  }

  NSError* contextError = web::NetErrorFromError(error);
  if (policyDecisionCancellationError) {
    contextError = base::ios::ErrorWithAppendedUnderlyingError(
        contextError, policyDecisionCancellationError);
  }

  web::NavigationContextImpl* navigationContext =
      [self.navigationStates contextForNavigation:navigation];
  web::HttpsUpgradeType failed_upgrade_type = GetFailedHttpsUpgradeType(
      error, navigationContext, policyDecisionCancellationError);
  if (failed_upgrade_type != web::HttpsUpgradeType::kNone) {
    navigationContext->SetError(contextError);
    navigationContext->SetFailedHttpsUpgradeType(failed_upgrade_type);
    [self handleCancelledError:error
                 forNavigation:navigation
               provisionalLoad:provisionalLoad];
    return;
  }

  navigationContext->SetError(contextError);
  navigationContext->SetIsPost([self isCurrentNavigationItemPOST]);
  // TODO(crbug.com/41365797) DCHECK that self.currentNavItem is the navigation
  // item associated with navigationContext.

  if ([error.domain
          isEqualToString:base::SysUTF8ToNSString(web::kWebKitErrorDomain)]) {
    if (error.code == web::kWebKitErrorUrlBlockedByContentFilter) {
      DCHECK(provisionalLoad);
      // If URL is blocked due to Restriction, do not take any further
      // action as WKWebView will show a built-in error.

      // On iOS13, immediately following this navigation, WebKit will
      // navigate to an internal failure page. Unfortunately, due to how
      // session restoration works with same document navigations, this page
      // blocked by a content filter puts WebKit into a state where all
      // further restoration same-document navigations are 'stuck' on this
      // failure page.  Instead, avoid restoring this page completely.
      // Consider revisiting this once WKWebView's interactionSate is used
      // for session restoration everywhere.
      self.navigationManagerImpl->SetWKWebViewNextPendingUrlNotSerializable(
          navigationContext->GetUrl());
      return;
    }

    if (error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange &&
        !policyDecisionCancellationError) {
      // Handle Frame Load Interrupted errors from WebView. This block is
      // executed when web controller rejected the load inside
      // decidePolicyForNavigationResponse: to handle download or WKWebView
      // opened a Universal Link.
      if (!navigationContext->IsDownload()) {
        // Non-download navigation was cancelled because WKWebView has opened a
        // Universal Link and called webView:didFailProvisionalNavigation:.
        self.navigationManagerImpl->DiscardNonCommittedItems();
        [self.navigationStates removeNavigation:navigation];
      }
      return;
    }

    if (error.code == web::kWebKitErrorCannotShowUrl) {
      if (!navigationContext->GetUrl().is_valid()) {
        // It won't be possible to load an error page for invalid URL, because
        // WKWebView will revert the url to about:blank. Simply discard pending
        // item and fail the navigation.
        navigationContext->ReleaseItem();
        self.webStateImpl->OnNavigationFinished(navigationContext);
        self.webStateImpl->OnPageLoaded(navigationContext->GetUrl(), false);
        return;
      }
    }
  }

  web::NavigationManager* navManager =
      self.webStateImpl->GetNavigationManager();
  web::NavigationItem* lastCommittedItem = navManager->GetLastCommittedItem();
  if (lastCommittedItem && !web::IsWKWebViewSSLCertError(error)) {
    // Reset SSL status for last committed navigation to avoid showing security
    // status for error pages.
    if (!lastCommittedItem->GetSSL().Equals(web::SSLStatus())) {
      lastCommittedItem->GetSSL() = web::SSLStatus();
      self.webStateImpl->DidChangeVisibleSecurityState();
    }
  }

  web::NavigationItemImpl* item =
      web::GetItemWithUniqueID(self.navigationManagerImpl, navigationContext);

  if (!item) {
    return;
  }

  WKNavigation* errorNavigation =
      [self displayErrorPageWithError:error
                            inWebView:webView
                    isProvisionalLoad:provisionalLoad];

  std::unique_ptr<web::NavigationContextImpl> originalContext =
      [self.navigationStates removeNavigation:navigation];
  originalContext->SetLoadingErrorPage(true);
  [self.navigationStates setContext:std::move(originalContext)
                      forNavigation:errorNavigation];
}

// Displays an error page with details from `error` in `webView`. The error page
// is presented with `transition` and associated with `blockedNSURL`.
- (void)displayError:(NSError*)error
    forCancelledNavigationToURL:(NSURL*)blockedNSURL
                      inWebView:(WKWebView*)webView
                 withTransition:(ui::PageTransition)transition {
  const GURL blockedURL = net::GURLWithNSURL(blockedNSURL);

  // Error page needs the URL string in the error's userInfo for proper
  // display.
  if (!error.userInfo[NSURLErrorFailingURLStringErrorKey]) {
    NSMutableDictionary* updatedUserInfo = [[NSMutableDictionary alloc] init];
    [updatedUserInfo addEntriesFromDictionary:error.userInfo];
    [updatedUserInfo setObject:blockedNSURL.absoluteString
                        forKey:NSURLErrorFailingURLStringErrorKey];

    error = [NSError errorWithDomain:error.domain
                                code:error.code
                            userInfo:updatedUserInfo];
  }

  WKNavigation* errorNavigation = [self displayErrorPageWithError:error
                                                        inWebView:webView
                                                isProvisionalLoad:YES];

  // Create pending item.
  self.navigationManagerImpl->AddPendingItem(
      blockedURL, web::Referrer(), transition,
      web::NavigationInitiationType::BROWSER_INITIATED,
      /*is_post_navigation=*/false, /*is_error_navigation=*/true,
      web::HttpsUpgradeType::kNone);

  // Create context.
  std::unique_ptr<web::NavigationContextImpl> context =
      web::NavigationContextImpl::CreateNavigationContext(
          self.webStateImpl, blockedURL,
          /*has_user_gesture=*/true, transition,
          /*is_renderer_initiated=*/false);
  std::unique_ptr<web::NavigationItemImpl> item =
      self.navigationManagerImpl->ReleasePendingItem();
  CHECK(item);

  context->SetNavigationItemUniqueID(item->GetUniqueID());
  context->SetItem(std::move(item));
  context->SetError(error);
  context->SetLoadingErrorPage(true);

  self.webStateImpl->OnNavigationStarted(context.get());

  [self.navigationStates setContext:std::move(context)
                      forNavigation:errorNavigation];
}

// Creates and returns a new WKNavigation to load an error page displaying
// details of `error` inside `webView`. `provisionalLoad` should be set
// according to whether or not the error occurred during a provisionalLoad.
- (WKNavigation*)displayErrorPageWithError:(NSError*)error
                                 inWebView:(WKWebView*)webView
                         isProvisionalLoad:(BOOL)provisionalLoad {
  CRWErrorPageHelper* errorPage =
      [[CRWErrorPageHelper alloc] initWithError:error];
  WKBackForwardListItem* backForwardItem = webView.backForwardList.currentItem;
  GURL backForwardGURL = net::GURLWithNSURL(backForwardItem.URL);
  GURL failedURL = [CRWErrorPageHelper
      failedNavigationURLFromErrorPageFileURL:backForwardGURL];
  bool isSameURLFromWebClient = web::GetWebClient()->IsPointingToSameDocument(
      failedURL, net::GURLWithNSURL(errorPage.failedNavigationURL));
  // There are 4 possible scenarios here:
  //   1. Current nav item is an error page for failed URL;
  //   2. Current nav item has a failed URL. This may happen when
  //      back/forward/refresh on a loaded page;
  //   3. Current nav item is an irrelevant page.
  //   4. Current nav item is a session restoration.
  // For 1, 2 and 4, load an empty string to remove existing JS code. The URL is
  // also updated to the URL of the page that failed to allow back/forward
  // navigations even on navigations originating from pushstate. See
  // crbug.com/1153261.
  // For 3, load error page file to create a new nav item.
  // The actual error HTML will be loaded in didFinishNavigation callback.
  WKNavigation* errorNavigation = nil;
  if (provisionalLoad &&
      ![errorPage
          isErrorPageFileURLForFailedNavigationURL:backForwardItem.URL] &&
      !isSameURLFromWebClient &&
      ![backForwardItem.URL isEqual:errorPage.failedNavigationURL] &&
      !web::wk_navigation_util::IsRestoreSessionUrl(backForwardItem.URL)) {
    errorNavigation = [webView loadFileURL:errorPage.errorPageFileURL
                   allowingReadAccessToURL:errorPage.errorPageFileURL];
  } else {
    errorNavigation = [webView loadHTMLString:@""
                                      baseURL:errorPage.failedNavigationURL];
  }
  [self.navigationStates setState:web::WKNavigationState::REQUESTED
                    forNavigation:errorNavigation];

  return errorNavigation;
}

// Handles cancelled load in WKWebView (error with NSURLErrorCancelled code).
- (void)handleCancelledError:(NSError*)error
               forNavigation:(WKNavigation*)navigation
             provisionalLoad:(BOOL)provisionalLoad {
  web::HttpsUpgradeType failed_upgrade_type = GetFailedHttpsUpgradeType(
      error, [self.navigationStates contextForNavigation:navigation],
      self.pendingNavigationInfo.cancellationError);
  if (failed_upgrade_type == web::HttpsUpgradeType::kNone &&
      ![self shouldCancelLoadForCancelledError:error
                               provisionalLoad:provisionalLoad]) {
    return;
  }
  std::unique_ptr<web::NavigationContextImpl> navigationContext =
      [self.navigationStates removeNavigation:navigation];
  [self loadCancelled];
  web::NavigationItemImpl* item =
      navigationContext ? web::GetItemWithUniqueID(self.navigationManagerImpl,
                                                   navigationContext.get())
                        : nullptr;
  if (self.navigationManagerImpl->GetPendingItem() == item) {
    self.navigationManagerImpl->DiscardNonCommittedItems();
  }

    if (provisionalLoad) {
      // TODO(crbug.com/40631880): Remove this workaround when WebKit bug is
      // fixed.
      if (!navigationContext) {
        // It is likely that `navigationContext` is null because
        // didStartProvisionalNavigation: was not called with this WKNavigation
        // object. Do not call OnNavigationFinished() to avoid crash on null
        // pointer dereferencing. See crbug.com/973653 for details.
      } else {
        self.webStateImpl->OnNavigationFinished(navigationContext.get());
      }
    }
}

// Used to decide whether a load that generates errors with the
// NSURLErrorCancelled code should be cancelled.
- (BOOL)shouldCancelLoadForCancelledError:(NSError*)error
                          provisionalLoad:(BOOL)provisionalLoad {
  DCHECK(error.code == NSURLErrorCancelled ||
         error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange);
  // Do not cancel the load if it is for an app specific URL, as such errors
  // are produced during the app specific URL load process.
  const GURL errorURL =
      net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]);
  if (web::GetWebClient()->IsAppSpecificURL(errorURL))
    return NO;

  return provisionalLoad;
}

// Loads the error page.
- (void)loadErrorPageForNavigationItem:(web::NavigationItemImpl*)item
                     navigationContext:(WKNavigation*)navigation
                               webView:(WKWebView*)webView {
  web::NavigationContextImpl* context =
      [self.navigationStates contextForNavigation:navigation];
  NSError* error = context->GetError();
  DCHECK(error);
  DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID());

  net::SSLInfo info;
  std::optional<net::SSLInfo> ssl_info = std::nullopt;

  if (web::IsWKWebViewSSLCertError(error)) {
    web::GetSSLInfoFromWKWebViewSSLCertError(error, &info);
    if (info.cert) {
      // Retrieve verification results from _certVerificationErrors cache to
      // avoid unnecessary recalculations. Verification results are cached for
      // the leaf cert, because the cert chain in
      // `didReceiveAuthenticationChallenge:` is the OS constructed chain, while
      // `chain` is the chain from the server.
      NSArray* chain = error.userInfo[web::kNSErrorPeerCertificateChainKey];
      NSURL* requestURL = error.userInfo[web::kNSErrorFailingURLKey];
      NSString* host = requestURL.host;
      scoped_refptr<net::X509Certificate> leafCert;
      if (chain.count && host.length) {
        // The complete cert chain may not be available, so the leaf cert is
        // used as a key to retrieve _certVerificationErrors, as well as for
        // storing the cert decision.
        leafCert = web::CreateCertFromChain(@[ chain.firstObject ]);
        if (leafCert) {
          auto verificationError = _certVerificationErrors->Get(
              {leafCert, base::SysNSStringToUTF8(host)});
          bool cacheHit = verificationError != _certVerificationErrors->end();
          if (cacheHit) {
            info.is_fatal_cert_error = verificationError->second.is_recoverable;
            info.cert_status = verificationError->second.status;
          }
          UMA_HISTOGRAM_BOOLEAN("WebController.CertVerificationErrorsCacheHit",
                                cacheHit);
        }
      }
      ssl_info = info;
    }
  }
  NSString* failingURLString =
      error.userInfo[NSURLErrorFailingURLStringErrorKey];
  GURL failingURL(base::SysNSStringToUTF8(failingURLString));
  GURL itemURL = item->GetURL();
  if (itemURL != failingURL)
    item->SetVirtualURL(failingURL);
  web::GetWebClient()->PrepareErrorPage(
      self.webStateImpl, failingURL, error, context->IsPost(),
      self.webStateImpl->GetBrowserState()->IsOffTheRecord(), ssl_info,
      context->GetNavigationId(), base::BindOnce(^(NSString* errorHTML) {
        if (self.navigationStates.lastAddedNavigation != navigation) {
          // Do not show the stale error page if the page has since navigated.
          return;
        }
        if (errorHTML) {
          CRWErrorPageHelper* errorPageHelper =
              [[CRWErrorPageHelper alloc] initWithError:context->GetError()];
          [webView
              evaluateJavaScript:[errorPageHelper
                                     scriptForInjectingHTML:errorHTML
                                         addAutomaticReload:YES]
               completionHandler:^(id result, NSError* nserror) {
                 if (nserror) {
                   LogPresentingErrorPageFailedWithError(nserror);
                   // WKErrorJavaScriptResultTypeIsUnsupported can be received
                   // if the WKWebView is released during this call.
                   DCHECK(nserror.code == WKErrorWebViewInvalidated ||
                          nserror.code == WKErrorWebContentProcessTerminated ||
                          nserror.code ==
                              WKErrorJavaScriptResultTypeIsUnsupported)
                       << "Error injecting error page HTML: "
                       << base::SysNSStringToUTF8(nserror.description);
                 }
               }];
        }

        // TODO(crbug.com/41464714): This is a workaround because `item` might
        // get released after
        // `self.navigationManagerImpl->
        // CommitPendingItem(context->ReleaseItem()`.
        // Remove this once navigation refactor is done.
        web::NavigationContextImpl* navContext =
            [self.navigationStates contextForNavigation:navigation];
        self.navigationManagerImpl->CommitPendingItem(
            navContext->ReleaseItem());
        [self.delegate navigationHandler:self
                          setDocumentURL:itemURL
                                 context:navContext];

        // Rewrite the context URL to actual URL and trigger the deferred
        // `OnNavigationFinished` callback.
        navContext->SetUrl(failingURL);
        navContext->SetHasCommitted(true);
        self.webStateImpl->OnNavigationFinished(navContext);

        // For SSL cert error pages, SSLStatus needs to be set manually because
        // the placeholder navigation for the error page is committed and
        // there is no server trust (since there's no network navigation), which
        // is required to create a cert in CRWSSLStatusUpdater.
        if (web::IsWKWebViewSSLCertError(navContext->GetError()) && info.cert) {
          web::SSLStatus& SSLStatus =
              self.navigationManagerImpl->GetLastCommittedItem()->GetSSL();
          SSLStatus.cert_status = info.cert_status;
          SSLStatus.certificate = info.cert;
          SSLStatus.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN;
          self.webStateImpl->DidChangeVisibleSecurityState();
        }

        [self.delegate navigationHandler:self
              didCompleteLoadWithSuccess:NO
                              forContext:navContext];
        self.webStateImpl->OnPageLoaded(failingURL, NO);
      }));
}

// Resets any state that is associated with a specific document object (e.g.,
// page interaction tracking).
- (void)resetDocumentSpecificState {
  self.userInteractionState->SetLastUserInteraction(nullptr);
  self.userInteractionState->SetTapInProgress(false);
}

#pragma mark - Public methods

- (void)stopLoading {
  self.pendingNavigationInfo.cancelled = YES;
  [self loadCancelled];
  _certVerificationErrors->Clear();
}

- (void)loadCancelled {
  if (self.navigationState != web::WKNavigationState::FINISHED) {
    self.navigationState = web::WKNavigationState::FINISHED;
    if (!self.beingDestroyed) {
      self.webStateImpl->SetIsLoading(false);
    }
  }
}

// Returns context for pending navigation that has `URL`. null if there is no
// matching pending navigation.
- (web::NavigationContextImpl*)contextForPendingMainFrameNavigationWithURL:
    (const GURL&)URL {
  // Here the enumeration variable `navigation` is __strong to allow setting it
  // to nil.
  for (__strong id navigation in [self.navigationStates pendingNavigations]) {
    if (navigation == [NSNull null]) {
      // null is a valid navigation object passed to WKNavigationDelegate
      // callbacks and represents window opening action.
      navigation = nil;
    }

    web::NavigationContextImpl* context =
        [self.navigationStates contextForNavigation:navigation];
    if (context && context->GetUrl() == URL) {
      return context;
    }
  }
  return nullptr;
}

- (BOOL)isCurrentNavigationBackForward {
  if (!self.currentNavItem)
    return NO;
  WKNavigationType currentNavigationType =
      self.currentBackForwardListItemHolder->navigation_type();
  return currentNavigationType == WKNavigationTypeBackForward;
}

- (BOOL)isCurrentNavigationItemPOST {
  // `self.navigationHandler.pendingNavigationInfo` will be nil if the
  // decidePolicy* delegate methods were not called.
  NSString* HTTPMethod =
      self.pendingNavigationInfo
          ? self.pendingNavigationInfo.HTTPMethod
          : self.currentBackForwardListItemHolder->http_method();
  if ([HTTPMethod isEqualToString:@"POST"]) {
    return YES;
  }
  if (!self.currentNavItem) {
    return NO;
  }
  return self.currentNavItem->HasPostData();
}

// Returns the WKBackForwardListItemHolder for the current navigation item.
- (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder {
  web::NavigationItem* item = self.currentNavItem;
  DCHECK(item);
  web::WKBackForwardListItemHolder* holder =
      web::WKBackForwardListItemHolder::FromNavigationItem(item);
  DCHECK(holder);
  return holder;
}

// Updates current state with any pending information. Should be called when a
// navigation is committed.
- (void)commitPendingNavigationInfoInWebView:(WKWebView*)webView {
  if (self.pendingNavigationInfo.referrer) {
    _currentReferrerString = [self.pendingNavigationInfo.referrer copy];
  }
  [self updateCurrentBackForwardListItemHolderInWebView:webView];

  self.pendingNavigationInfo = nil;
}

// Updates the WKBackForwardListItemHolder navigation item.
- (void)updateCurrentBackForwardListItemHolderInWebView:(WKWebView*)webView {
  if (!self.currentNavItem) {
    // TODO(crbug.com/41437377): Pending item (which stores the holder) should
    // be owned by NavigationContext object. Pending item should never be null.
    return;
  }

  web::WKBackForwardListItemHolder* holder =
      self.currentBackForwardListItemHolder;

  WKNavigationType navigationType =
      self.pendingNavigationInfo ? self.pendingNavigationInfo.navigationType
                                 : WKNavigationTypeOther;
  holder->set_back_forward_list_item(webView.backForwardList.currentItem);
  holder->set_navigation_type(navigationType);
  holder->set_http_method(self.pendingNavigationInfo.HTTPMethod);

  // Only update the MIME type in the holder if there was MIME type information
  // as part of this pending load. It will be nil when doing a fast
  // back/forward navigation, for instance, because the callback that would
  // populate it is not called in that flow.
  if (self.pendingNavigationInfo.MIMEType)
    holder->set_mime_type(self.pendingNavigationInfo.MIMEType);
}

- (web::Referrer)currentReferrer {
  // Referrer string doesn't include the fragment, so in cases where the
  // previous URL is equal to the current referrer plus the fragment the
  // previous URL is returned as current referrer.
  NSString* referrerString = _currentReferrerString;

  // In case of an error evaluating the JavaScript simply return empty string.
  if (referrerString.length == 0)
    return web::Referrer();

  web::NavigationItem* item = self.currentNavItem;
  GURL navigationURL = item ? item->GetVirtualURL() : GURL();
  NSString* previousURLString = base::SysUTF8ToNSString(navigationURL.spec());
  // Check if the referrer is equal to the previous URL minus the hash symbol.
  // L'#' is used to convert the char '#' to a unichar.
  if ([previousURLString length] > referrerString.length &&
      [previousURLString hasPrefix:referrerString] &&
      [previousURLString characterAtIndex:referrerString.length] == L'#') {
    referrerString = previousURLString;
  }
  // Since referrer is being extracted from the destination page, the correct
  // policy from the origin has *already* been applied. Since the extracted URL
  // is the post-policy value, and the source policy is no longer available,
  // the policy is set to Always so that whatever WebKit decided to send will be
  // re-sent when replaying the entry.
  // TODO(crbug.com/41004475): When possible, get the real referrer and policy
  // in advance and use that instead.
  return web::Referrer(GURL(base::SysNSStringToUTF8(referrerString)),
                       web::ReferrerPolicyAlways);
}

- (void)setLastCommittedNavigationItemTitle:(NSString*)title {
  DCHECK(title);
  web::NavigationItem* item =
      self.navigationManagerImpl->GetLastCommittedItem();
  if (!item)
    return;

  item->SetTitle(base::SysNSStringToUTF16(title));
  self.webStateImpl->OnTitleChanged();
}

- (ui::PageTransition)pageTransitionFromNavigationType:
    (WKNavigationType)navigationType {
  switch (navigationType) {
    case WKNavigationTypeLinkActivated:
      return ui::PAGE_TRANSITION_LINK;
    case WKNavigationTypeFormSubmitted:
    case WKNavigationTypeFormResubmitted:
      return ui::PAGE_TRANSITION_FORM_SUBMIT;
    case WKNavigationTypeBackForward:
      return ui::PAGE_TRANSITION_FORWARD_BACK;
    case WKNavigationTypeReload:
      return ui::PAGE_TRANSITION_RELOAD;
    case WKNavigationTypeOther:
      // The "Other" type covers a variety of very different cases, which may
      // or may not be the result of user actions. For now, guess based on
      // whether there's been an interaction since the last URL change.
      // TODO(crbug.com/41213462): See if this heuristic can be improved.
      return self.userInteractionState
                     ->UserInteractionRegisteredSinceLastUrlChange()
                 ? ui::PAGE_TRANSITION_LINK
                 : ui::PAGE_TRANSITION_CLIENT_REDIRECT;
  }
}

- (void)webPageChangedWithContext:(web::NavigationContextImpl*)context
                          webView:(WKWebView*)webView {
  web::Referrer referrer = self.currentReferrer;
  // If no referrer was known in advance, record it now. (If there was one,
  // keep it since it will have a more accurate URL and policy than what can
  // be extracted from the landing page.)
  web::NavigationItem* currentItem = self.currentNavItem;

  // TODO(crbug.com/41437377): Pending item (which should be used here) should
  // be owned by NavigationContext object. Pending item should never be null.
  if (currentItem && !currentItem->GetReferrer().url.is_valid()) {
    currentItem->SetReferrer(referrer);
  }

  // TODO(crbug.com/40624624): This shouldn't be called for push/replaceState.
  [self resetDocumentSpecificState];

  [self.delegate navigationHandlerDidStartLoading:self];
  self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem());
  if (context->IsLoadingHtmlString()) {
    self.navigationManagerImpl->GetLastCommittedItem()->SetURL(
        context->GetUrl());
  }
}

@end