1// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "ios/web/navigation/crw_wk_navigation_handler.h"
6
7#include "base/feature_list.h"
8#import "base/ios/ns_error_util.h"
9#include "base/metrics/histogram_functions.h"
10#include "base/metrics/histogram_macros.h"
11#include "base/strings/sys_string_conversions.h"
12#include "base/timer/timer.h"
13#import "ios/net/http_response_headers_util.h"
14#import "ios/net/protocol_handler_util.h"
15#include "ios/web/common/features.h"
16#import "ios/web/common/url_scheme_util.h"
17#import "ios/web/js_messaging/crw_js_injector.h"
18#import "ios/web/js_messaging/web_frames_manager_impl.h"
19#import "ios/web/navigation/crw_navigation_item_holder.h"
20#import "ios/web/navigation/crw_pending_navigation_info.h"
21#import "ios/web/navigation/crw_text_fragments_handler.h"
22#import "ios/web/navigation/crw_wk_navigation_states.h"
23#import "ios/web/navigation/error_page_helper.h"
24#include "ios/web/navigation/error_retry_state_machine.h"
25#import "ios/web/navigation/navigation_context_impl.h"
26#import "ios/web/navigation/navigation_manager_impl.h"
27#include "ios/web/navigation/navigation_manager_util.h"
28#import "ios/web/navigation/web_kit_constants.h"
29#import "ios/web/navigation/wk_back_forward_list_item_holder.h"
30#import "ios/web/navigation/wk_navigation_action_policy_util.h"
31#import "ios/web/navigation/wk_navigation_action_util.h"
32#import "ios/web/navigation/wk_navigation_util.h"
33#include "ios/web/public/browser_state.h"
34#import "ios/web/public/download/download_controller.h"
35#import "ios/web/public/web_client.h"
36#import "ios/web/security/crw_cert_verification_controller.h"
37#import "ios/web/security/wk_web_view_security_util.h"
38#import "ios/web/session/session_certificate_policy_cache_impl.h"
39#import "ios/web/web_state/user_interaction_state.h"
40#import "ios/web/web_state/web_state_impl.h"
41#include "ios/web/web_view/content_type_util.h"
42#import "ios/web/web_view/error_translation_util.h"
43#import "ios/web/web_view/wk_web_view_util.h"
44#import "net/base/mac/url_conversions.h"
45#include "net/base/net_errors.h"
46#include "net/cert/x509_util_ios.h"
47#include "url/gurl.h"
48
49#if !defined(__has_feature) || !__has_feature(objc_arc)
50#error "This file requires ARC support."
51#endif
52
53// TODO(crbug.com/1038303): Remove references to "Placeholder".
54using web::wk_navigation_util::IsPlaceholderUrl;
55using web::wk_navigation_util::CreatePlaceholderUrlForUrl;
56using web::wk_navigation_util::ExtractUrlFromPlaceholderUrl;
57using web::wk_navigation_util::kReferrerHeaderName;
58using web::wk_navigation_util::IsRestoreSessionUrl;
59using web::wk_navigation_util::IsWKInternalUrl;
60
61namespace {
62// Maximum number of errors to store in cert verification errors cache.
63// Cache holds errors only for pending navigations, so the actual number of
64// stored errors is not expected to be high.
65const web::CertVerificationErrorsCacheType::size_type kMaxCertErrorsCount = 100;
66
67// These values are persisted to logs. Entries should not be renumbered and
68// numeric values should never be reused.
69enum class OutOfSyncURLAction {
70  kNoAction = 0,
71  kGoBack = 1,
72  kGoForward = 2,
73  kMaxValue = kGoForward,
74};
75
76void ReportOutOfSyncURLInDidStartProvisionalNavigation(
77    OutOfSyncURLAction action) {
78  UMA_HISTOGRAM_ENUMERATION(
79      "WebController.BackForwardListOutOfSyncInProvisionalNavigation", action);
80}
81
82}  // namespace
83
84@interface CRWWKNavigationHandler () {
85  // Referrer for the current page; does not include the fragment.
86  NSString* _currentReferrerString;
87
88  // CertVerification errors which happened inside
89  // |webView:didReceiveAuthenticationChallenge:completionHandler:|.
90  // Key is leaf-cert/host pair. This storage is used to carry calculated
91  // cert status from |didReceiveAuthenticationChallenge:| to
92  // |didFailProvisionalNavigation:| delegate method.
93  std::unique_ptr<web::CertVerificationErrorsCacheType> _certVerificationErrors;
94}
95
96@property(nonatomic, weak) id<CRWWKNavigationHandlerDelegate> delegate;
97
98// Returns the WebStateImpl from self.delegate.
99@property(nonatomic, readonly, assign) web::WebStateImpl* webStateImpl;
100// Returns the NavigationManagerImpl from self.webStateImpl.
101@property(nonatomic, readonly, assign)
102    web::NavigationManagerImpl* navigationManagerImpl;
103// Returns the UserInteractionState from self.delegate.
104@property(nonatomic, readonly, assign)
105    web::UserInteractionState* userInteractionState;
106// Returns the CRWCertVerificationController from self.delegate.
107@property(nonatomic, readonly, weak)
108    CRWCertVerificationController* certVerificationController;
109// Returns the docuemnt URL from self.delegate.
110@property(nonatomic, readonly, assign) GURL documentURL;
111// Returns the js injector from self.delegate.
112@property(nonatomic, readonly, weak) CRWJSInjector* JSInjector;
113// Will handle highlighting text fragments on the page when necessary.
114@property(nonatomic, strong) CRWTextFragmentsHandler* textFragmentsHandler;
115
116@end
117
118@implementation CRWWKNavigationHandler
119
120- (instancetype)initWithDelegate:(id<CRWWKNavigationHandlerDelegate>)delegate {
121  if (self = [super init]) {
122    _navigationStates = [[CRWWKNavigationStates alloc] init];
123    // Load phase when no WebView present is 'loaded' because this represents
124    // the idle state.
125    _navigationState = web::WKNavigationState::FINISHED;
126
127    _certVerificationErrors =
128        std::make_unique<web::CertVerificationErrorsCacheType>(
129            kMaxCertErrorsCount);
130
131    _delegate = delegate;
132
133    _textFragmentsHandler =
134        [[CRWTextFragmentsHandler alloc] initWithDelegate:_delegate];
135  }
136  return self;
137}
138
139#pragma mark - WKNavigationDelegate
140
141- (void)webView:(WKWebView*)webView
142    decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction
143                        preferences:(WKWebpagePreferences*)preferences
144                    decisionHandler:
145                        (void (^)(WKNavigationActionPolicy,
146                                  WKWebpagePreferences*))decisionHandler
147    API_AVAILABLE(ios(13)) {
148  web::UserAgentType userAgentType =
149      [self userAgentForNavigationAction:navigationAction webView:webView];
150
151  if (navigationAction.navigationType == WKNavigationTypeBackForward &&
152      userAgentType != web::UserAgentType::NONE &&
153      self.webStateImpl->GetUserAgentForSessionRestoration() !=
154          web::UserAgentType::AUTOMATIC) {
155    // When navigating back to a page with a UserAgent that wasn't automatic,
156    // let's reuse this user agent for next navigations.
157    self.webStateImpl->SetUserAgent(userAgentType);
158  }
159
160  if (navigationAction.navigationType == WKNavigationTypeReload &&
161      userAgentType != web::UserAgentType::NONE &&
162      web::wk_navigation_util::URLNeedsUserAgentType(
163          net::GURLWithNSURL(navigationAction.request.URL))) {
164    // When reloading the page, the UserAgent will be updated to the one for the
165    // new page.
166    web::NavigationItem* item = [[CRWNavigationItemHolder
167        holderForBackForwardListItem:webView.backForwardList.currentItem]
168        navigationItem];
169    if (item)
170      item->SetUserAgentType(userAgentType);
171  }
172
173  if (userAgentType != web::UserAgentType::NONE) {
174    NSString* userAgentString = base::SysUTF8ToNSString(
175        web::GetWebClient()->GetUserAgent(userAgentType));
176    if (![webView.customUserAgent isEqualToString:userAgentString]) {
177      webView.customUserAgent = userAgentString;
178    }
179  }
180
181  WKContentMode contentMode = userAgentType == web::UserAgentType::DESKTOP
182                                  ? WKContentModeDesktop
183                                  : WKContentModeMobile;
184
185  [self webView:webView
186      decidePolicyForNavigationAction:navigationAction
187                      decisionHandler:^(WKNavigationActionPolicy policy) {
188                        preferences.preferredContentMode = contentMode;
189                        decisionHandler(policy, preferences);
190                      }];
191}
192
193- (void)webView:(WKWebView*)webView
194    decidePolicyForNavigationAction:(WKNavigationAction*)action
195                    decisionHandler:
196                        (void (^)(WKNavigationActionPolicy))decisionHandler {
197  [self didReceiveWKNavigationDelegateCallback];
198
199  if (@available(iOS 13, *)) {
200  } else {
201    // As webView:decidePolicyForNavigationAction:preferences:decisionHandler:
202    // is only called for iOS 13, the code is duplicated here to also have it
203    // for iOS 12.
204    web::UserAgentType userAgentType =
205        [self userAgentForNavigationAction:action webView:webView];
206
207    if (action.navigationType == WKNavigationTypeBackForward &&
208        userAgentType != web::UserAgentType::NONE &&
209        self.webStateImpl->GetUserAgentForSessionRestoration() !=
210            web::UserAgentType::AUTOMATIC) {
211      // When navigating back to a page with a UserAgent that wasn't automatic,
212      // let's reuse this user agent for next navigations.
213      self.webStateImpl->SetUserAgent(userAgentType);
214    }
215
216    if (action.navigationType == WKNavigationTypeReload &&
217        userAgentType != web::UserAgentType::NONE &&
218        web::wk_navigation_util::URLNeedsUserAgentType(
219            net::GURLWithNSURL(action.request.URL))) {
220      // When reloading the page, the UserAgent will be updated to the one for
221      // the new page.
222      web::NavigationItem* item = [[CRWNavigationItemHolder
223          holderForBackForwardListItem:webView.backForwardList.currentItem]
224          navigationItem];
225      if (item)
226        item->SetUserAgentType(userAgentType);
227    }
228
229    if (userAgentType != web::UserAgentType::NONE) {
230      NSString* userAgentString = base::SysUTF8ToNSString(
231          web::GetWebClient()->GetUserAgent(userAgentType));
232      if (![webView.customUserAgent isEqualToString:userAgentString]) {
233        webView.customUserAgent = userAgentString;
234      }
235    }
236  }
237
238  _webProcessCrashed = NO;
239  if (self.beingDestroyed) {
240    decisionHandler(WKNavigationActionPolicyCancel);
241    return;
242  }
243
244  GURL requestURL = net::GURLWithNSURL(action.request.URL);
245
246  // Workaround for a WKWebView bug where the web content loaded using
247  // |-loadHTMLString:baseURL| clobbers the next WKBackForwardListItem. It works
248  // by detecting back/forward navigation to a clobbered item and replacing the
249  // clobberred item and its forward history using a partial session restore in
250  // the current web view. There is an unfortunate caveat: if the workaround is
251  // triggered in a back navigation to a clobbered item, the restored forward
252  // session is inserted after the current item before the back navigation, so
253  // it doesn't fully replaces the "bad" history, even though user will be
254  // navigated to the expected URL and may not notice the issue until they
255  // review the back history by long pressing on "Back" button.
256  //
257  // TODO(crbug.com/887497): remove this workaround once iOS ships the fix.
258  if (action.targetFrame.mainFrame) {
259    GURL webViewURL = net::GURLWithNSURL(webView.URL);
260    GURL currentWKItemURL =
261        net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
262    GURL backItemURL = net::GURLWithNSURL(webView.backForwardList.backItem.URL);
263    web::NavigationContextImpl* context =
264        [self contextForPendingMainFrameNavigationWithURL:webViewURL];
265    bool willClobberHistory =
266        action.navigationType == WKNavigationTypeBackForward &&
267        requestURL == backItemURL && webView.backForwardList.currentItem &&
268        requestURL != currentWKItemURL && currentWKItemURL == webViewURL &&
269        context &&
270        (context->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK);
271
272    UMA_HISTOGRAM_BOOLEAN("IOS.WKWebViewClobberedHistory", willClobberHistory);
273
274    if (willClobberHistory && base::FeatureList::IsEnabled(
275                                  web::features::kHistoryClobberWorkaround)) {
276      decisionHandler(WKNavigationActionPolicyCancel);
277      self.navigationManagerImpl
278          ->ApplyWKWebViewForwardHistoryClobberWorkaround();
279      return;
280    }
281  }
282
283  // The page will not be changed until this navigation is committed, so the
284  // retrieved state will be pending until |didCommitNavigation| callback.
285  [self createPendingNavigationInfoFromNavigationAction:action];
286
287  if (action.targetFrame.mainFrame &&
288      action.navigationType == WKNavigationTypeBackForward) {
289    web::NavigationContextImpl* context =
290        [self contextForPendingMainFrameNavigationWithURL:requestURL];
291    if (context) {
292      // Context is null for renderer-initiated navigations.
293      int index = web::GetCommittedItemIndexWithUniqueID(
294          self.navigationManagerImpl, context->GetNavigationItemUniqueID());
295      self.navigationManagerImpl->SetPendingItemIndex(index);
296    }
297  }
298
299  // If this is a placeholder navigation, pass through.
300  if ((!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
301       IsPlaceholderUrl(requestURL)) ||
302      (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
303       [ErrorPageHelper isErrorPageFileURL:requestURL])) {
304    if (action.sourceFrame.mainFrame) {
305      // Disallow renderer initiated navigations to placeholder URLs.
306      decisionHandler(WKNavigationActionPolicyCancel);
307    } else {
308      decisionHandler(WKNavigationActionPolicyAllow);
309    }
310    return;
311  }
312
313  ui::PageTransition transition =
314      [self pageTransitionFromNavigationType:action.navigationType];
315  BOOL isMainFrameNavigationAction = [self isMainFrameNavigationAction:action];
316  if (isMainFrameNavigationAction) {
317    web::NavigationContextImpl* context =
318        [self contextForPendingMainFrameNavigationWithURL:requestURL];
319    // Theoretically if |context| can be found here, the navigation should be
320    // either user-initiated or JS back/forward. The second part in the "if"
321    // condition used to be a DCHECK, but it would fail in this case:
322    // 1. Multiple render-initiated navigation with the same URL happens at the
323    //    same time;
324    // 2. One of these navigations gets the "didStartProvisonalNavigation"
325    //    callback and creates a NavigationContext;
326    // 3. Another navigation reaches here and retrieves that NavigationContext
327    //    by matching URL.
328    // The DCHECK is now turned into a "if" condition, but can be reverted if a
329    // more reliable way of matching NavigationContext with WKNavigationAction
330    // is found.
331    if (context &&
332        (!context->IsRendererInitiated() ||
333         (context->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK))) {
334      transition = context->GetPageTransition();
335      if (context->IsLoadingErrorPage()) {
336        // loadHTMLString: navigation which loads error page into WKWebView.
337        decisionHandler(WKNavigationActionPolicyAllow);
338        return;
339      }
340    }
341  }
342
343  // Invalid URLs should not be loaded.
344  if (!requestURL.is_valid()) {
345    // The HTML5 spec indicates that window.open with an invalid URL should open
346    // about:blank.
347    BOOL isFirstLoadInOpenedWindow =
348        self.webStateImpl->HasOpener() &&
349        !self.webStateImpl->GetNavigationManager()->GetLastCommittedItem();
350    BOOL isMainFrame = action.targetFrame.mainFrame;
351    if (isFirstLoadInOpenedWindow && isMainFrame) {
352      decisionHandler(WKNavigationActionPolicyCancel);
353      GURL aboutBlankURL(url::kAboutBlankURL);
354      web::NavigationManager::WebLoadParams loadParams(aboutBlankURL);
355      loadParams.referrer = self.currentReferrer;
356
357      self.webStateImpl->GetNavigationManager()->LoadURLWithParams(loadParams);
358      return;
359    }
360  }
361
362  // First check if the navigation action should be blocked by the controller
363  // and make sure to update the controller in the case that the controller
364  // can't handle the request URL. Then use the embedders' policyDeciders to
365  // either: 1- Handle the URL it self and return false to stop the controller
366  // from proceeding with the navigation if needed. or 2- return true to allow
367  // the navigation to be proceeded by the web controller.
368  web::WebStatePolicyDecider::PolicyDecision policyDecision =
369      web::WebStatePolicyDecider::PolicyDecision::Allow();
370  if (web::GetWebClient()->IsAppSpecificURL(requestURL)) {
371    // |policyDecision| is initialized above this conditional to allow loads, so
372    // it only needs to be overwritten if the load should be cancelled.
373    if (![self shouldAllowAppSpecificURLNavigationAction:action
374                                              transition:transition]) {
375      policyDecision = web::WebStatePolicyDecider::PolicyDecision::Cancel();
376    }
377    if (policyDecision.ShouldAllowNavigation()) {
378      [self.delegate navigationHandler:self createWebUIForURL:requestURL];
379    }
380  }
381
382  BOOL webControllerCanShow =
383      web::UrlHasWebScheme(requestURL) ||
384      web::GetWebClient()->IsAppSpecificURL(requestURL) ||
385      requestURL.SchemeIs(url::kFileScheme) ||
386      requestURL.SchemeIs(url::kAboutScheme) ||
387      requestURL.SchemeIs(url::kBlobScheme);
388
389  if (policyDecision.ShouldAllowNavigation()) {
390    BOOL userInteractedWithRequestMainFrame =
391        self.userInteractionState->HasUserTappedRecently(webView) &&
392        net::GURLWithNSURL(action.request.mainDocumentURL) ==
393            self.userInteractionState->LastUserInteraction()->main_document_url;
394    web::WebStatePolicyDecider::RequestInfo requestInfo(
395        transition, isMainFrameNavigationAction,
396        userInteractedWithRequestMainFrame);
397
398    policyDecision =
399        self.webStateImpl->ShouldAllowRequest(action.request, requestInfo);
400
401    // The WebState may have been closed in the ShouldAllowRequest callback.
402    if (self.beingDestroyed) {
403      decisionHandler(WKNavigationActionPolicyCancel);
404      return;
405    }
406  }
407
408  if (!webControllerCanShow) {
409    policyDecision = web::WebStatePolicyDecider::PolicyDecision::Cancel();
410  }
411
412  if (policyDecision.ShouldAllowNavigation()) {
413    if ([[action.request HTTPMethod] isEqualToString:@"POST"]) {
414      // Display the confirmation dialog if a form repost is detected.
415      if (action.navigationType == WKNavigationTypeFormResubmitted) {
416        self.webStateImpl->ShowRepostFormWarningDialog(
417            base::BindOnce(^(bool shouldContinue) {
418              if (self.beingDestroyed) {
419                decisionHandler(WKNavigationActionPolicyCancel);
420              } else if (shouldContinue) {
421                decisionHandler(WKNavigationActionPolicyAllow);
422              } else {
423                decisionHandler(WKNavigationActionPolicyCancel);
424                if (action.targetFrame.mainFrame) {
425                  [self.pendingNavigationInfo setCancelled:YES];
426                }
427              }
428            }));
429        return;
430      }
431
432      web::NavigationItemImpl* item =
433          self.navigationManagerImpl->GetCurrentItemImpl();
434      // TODO(crbug.com/570699): Remove this check once it's no longer possible
435      // to have no current entries.
436      if (item)
437        [self cachePOSTDataForRequest:action.request inNavigationItem:item];
438    }
439  } else {
440    if (action.targetFrame.mainFrame) {
441      if (!self.beingDestroyed && policyDecision.ShouldDisplayError()) {
442        DCHECK(policyDecision.GetDisplayError());
443
444        // Navigation was blocked by |ShouldProvisionallyFailRequest|. Cancel
445        // load of page.
446        decisionHandler(WKNavigationActionPolicyCancel);
447
448        // Handling presentation of policy decision error is dependent on
449        // |web::features::kUseJSForErrorPage| feature.
450        if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) {
451          return;
452        }
453
454        [self displayError:policyDecision.GetDisplayError()
455            forCancelledNavigationToURL:action.request.URL
456                              inWebView:webView
457                         withTransition:transition];
458        return;
459      }
460
461      [self.pendingNavigationInfo setCancelled:YES];
462      if (self.navigationManagerImpl->GetPendingItemIndex() == -1) {
463        // Discard the new pending item to ensure that the current URL is not
464        // different from what is displayed on the view. There is no need to
465        // reset pending item index for a different pending back-forward
466        // navigation.
467        self.navigationManagerImpl->DiscardNonCommittedItems();
468      }
469
470      web::NavigationContextImpl* context =
471          [self contextForPendingMainFrameNavigationWithURL:requestURL];
472      if (context) {
473        // Destroy associated pending item, because this will be the last
474        // WKWebView callback for this navigation context.
475        context->ReleaseItem();
476      }
477
478      if (!self.beingDestroyed &&
479          [self shouldClosePageOnNativeApplicationLoad]) {
480        self.webStateImpl->CloseWebState();
481        decisionHandler(WKNavigationActionPolicyCancel);
482        return;
483      }
484    }
485  }
486
487  if (policyDecision.ShouldCancelNavigation()) {
488    decisionHandler(WKNavigationActionPolicyCancel);
489    return;
490  }
491  BOOL isOffTheRecord = self.webStateImpl->GetBrowserState()->IsOffTheRecord();
492  decisionHandler(web::GetAllowNavigationActionPolicy(isOffTheRecord));
493}
494
495- (void)webView:(WKWebView*)webView
496    decidePolicyForNavigationResponse:(WKNavigationResponse*)WKResponse
497                      decisionHandler:
498                          (void (^)(WKNavigationResponsePolicy))handler {
499  [self didReceiveWKNavigationDelegateCallback];
500
501  // If this is a placeholder navigation, pass through.
502  GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL);
503  if ((!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
504       IsPlaceholderUrl(responseURL)) ||
505      (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
506       [ErrorPageHelper isErrorPageFileURL:responseURL])) {
507    handler(WKNavigationResponsePolicyAllow);
508    return;
509  }
510
511  scoped_refptr<net::HttpResponseHeaders> headers;
512  if ([WKResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
513    headers = net::CreateHeadersFromNSHTTPURLResponse(
514        static_cast<NSHTTPURLResponse*>(WKResponse.response));
515  }
516
517  // The page will not be changed until this navigation is committed, so the
518  // retrieved state will be pending until |didCommitNavigation| callback.
519  [self updatePendingNavigationInfoFromNavigationResponse:WKResponse
520                                              HTTPHeaders:headers];
521
522  web::WebStatePolicyDecider::PolicyDecision policyDecision =
523      web::WebStatePolicyDecider::PolicyDecision::Allow();
524
525  __weak CRWPendingNavigationInfo* weakPendingNavigationInfo =
526      self.pendingNavigationInfo;
527  auto callback = base::BindOnce(
528      ^(web::WebStatePolicyDecider::PolicyDecision policyDecision) {
529        if (policyDecision.ShouldCancelNavigation() &&
530            WKResponse.canShowMIMEType && WKResponse.forMainFrame) {
531          weakPendingNavigationInfo.cancelled = YES;
532          weakPendingNavigationInfo.cancellationError =
533              policyDecision.GetDisplayError();
534        }
535
536        handler(policyDecision.ShouldAllowNavigation()
537                    ? WKNavigationResponsePolicyAllow
538                    : WKNavigationResponsePolicyCancel);
539      });
540
541  if ([self shouldRenderResponse:WKResponse]) {
542    self.webStateImpl->ShouldAllowResponse(
543        WKResponse.response, WKResponse.forMainFrame, std::move(callback));
544    return;
545  }
546
547  if (web::UrlHasWebScheme(responseURL)) {
548    [self createDownloadTaskForResponse:WKResponse HTTPHeaders:headers.get()];
549  } else {
550    // DownloadTask only supports web schemes, so do nothing.
551  }
552  // Discard the pending item to ensure that the current URL is not different
553  // from what is displayed on the view.
554  self.navigationManagerImpl->DiscardNonCommittedItems();
555  std::move(callback).Run(web::WebStatePolicyDecider::PolicyDecision::Cancel());
556}
557
558- (void)webView:(WKWebView*)webView
559    didStartProvisionalNavigation:(WKNavigation*)navigation {
560  [self didReceiveWKNavigationDelegateCallback];
561
562  GURL webViewURL = net::GURLWithNSURL(webView.URL);
563
564  [self.navigationStates setState:web::WKNavigationState::STARTED
565                    forNavigation:navigation];
566
567  if (webViewURL.is_empty()) {
568    // URL starts empty for window.open(""), by didCommitNavigation: callback
569    // the URL will be "about:blank".
570    webViewURL = GURL(url::kAboutBlankURL);
571  }
572
573  web::NavigationContextImpl* context =
574      [self.navigationStates contextForNavigation:navigation];
575
576  if (context) {
577    // This is already seen and registered navigation.
578
579    if (context->IsLoadingErrorPage()) {
580      // This is loadHTMLString: navigation to display error page in web view.
581      self.navigationState = web::WKNavigationState::REQUESTED;
582      return;
583    }
584
585    BOOL isErrorPageNavigation =
586        (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
587         [ErrorPageHelper isErrorPageFileURL:webViewURL]) ||
588        (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
589         context->IsPlaceholderNavigation());
590
591    if (!isErrorPageNavigation && !IsWKInternalUrl(webViewURL)) {
592      web::NavigationItem* item =
593          web::GetItemWithUniqueID(self.navigationManagerImpl, context);
594      if (item) {
595        web::WKBackForwardListItemHolder* itemHolder =
596            web::WKBackForwardListItemHolder::FromNavigationItem(item);
597        if (itemHolder->navigation_type() == WKNavigationTypeBackForward &&
598            ![webView.backForwardList.currentItem.URL isEqual:webView.URL]) {
599          // Sometimes on back/forward navigation, the backforward list is out
600          // of sync with the webView. Go back or forward to fix it. See
601          // crbug.com/968539.
602          if ([webView.backForwardList.backItem.URL isEqual:webView.URL]) {
603            ReportOutOfSyncURLInDidStartProvisionalNavigation(
604                OutOfSyncURLAction::kGoBack);
605            [webView goBack];
606            return;
607          }
608          if ([webView.backForwardList.forwardItem.URL isEqual:webView.URL]) {
609            ReportOutOfSyncURLInDidStartProvisionalNavigation(
610                OutOfSyncURLAction::kGoForward);
611            [webView goForward];
612            return;
613          }
614          ReportOutOfSyncURLInDidStartProvisionalNavigation(
615              OutOfSyncURLAction::kNoAction);
616        }
617      }
618
619      if (context->GetUrl() != webViewURL) {
620        // Update last seen URL because it may be changed by WKWebView (f.e.
621        // by performing characters escaping).
622        if (item) {
623          // Item may not exist if navigation was stopped (see
624          // crbug.com/969915).
625          item->SetURL(webViewURL);
626          if ([ErrorPageHelper isErrorPageFileURL:webViewURL]) {
627            item->SetVirtualURL([ErrorPageHelper
628                failedNavigationURLFromErrorPageFileURL:webViewURL]);
629          }
630        }
631        context->SetUrl(webViewURL);
632      }
633    }
634
635    self.webStateImpl->OnNavigationStarted(context);
636    self.webStateImpl->GetNavigationManagerImpl().OnNavigationStarted(
637        webViewURL);
638    return;
639  }
640
641  // This is renderer-initiated navigation which was not seen before and
642  // should be registered.
643
644  // When using WKBasedNavigationManager, renderer-initiated app-specific loads
645  // should only be allowed in these specific cases:
646  // 1) if |backForwardList.currentItem| is a placeholder URL for the
647  //    provisional load URL (i.e. webView.URL), then this is an in-progress
648  //    app-specific load and should not be restarted.
649  // 2) back/forward navigation to an app-specific URL should be allowed.
650  // 3) navigation to an app-specific URL should be allowed from other
651  //    app-specific URLs
652  bool exemptedAppSpecificLoad = false;
653  bool currentItemIsPlaceholder =
654      !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
655      CreatePlaceholderUrlForUrl(webViewURL) ==
656          net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
657  bool isBackForward =
658      self.pendingNavigationInfo.navigationType == WKNavigationTypeBackForward;
659  bool isRestoringSession = IsRestoreSessionUrl(self.documentURL);
660  exemptedAppSpecificLoad = currentItemIsPlaceholder || isBackForward ||
661                            isRestoringSession || self.webStateImpl->HasWebUI();
662
663  if (!web::GetWebClient()->IsAppSpecificURL(webViewURL) ||
664      !exemptedAppSpecificLoad) {
665    self.webStateImpl->ClearWebUI();
666  }
667
668  self.webStateImpl->GetNavigationManagerImpl().OnNavigationStarted(webViewURL);
669
670  // When a client-side redirect occurs while an interstitial warning is
671  // displayed, clear the warning and its navigation item, so that a new
672  // pending item is created for |context| in |registerLoadRequestForURL|. See
673  // crbug.com/861836.
674  self.webStateImpl->ClearTransientContent();
675
676  BOOL isPlaceholderURL =
677      base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)
678          ? NO
679          : IsPlaceholderUrl(webViewURL);
680  std::unique_ptr<web::NavigationContextImpl> navigationContext =
681      [self.delegate navigationHandler:self
682             registerLoadRequestForURL:webViewURL
683                sameDocumentNavigation:NO
684                        hasUserGesture:self.pendingNavigationInfo.hasUserGesture
685                     rendererInitiated:YES
686                 placeholderNavigation:isPlaceholderURL];
687  web::NavigationContextImpl* navigationContextPtr = navigationContext.get();
688
689  // GetPendingItem which may be called inside OnNavigationStarted relies on
690  // association between NavigationContextImpl and WKNavigation.
691  [self.navigationStates setContext:std::move(navigationContext)
692                      forNavigation:navigation];
693  self.webStateImpl->OnNavigationStarted(navigationContextPtr);
694  DCHECK_EQ(web::WKNavigationState::REQUESTED, self.navigationState);
695}
696
697- (void)webView:(WKWebView*)webView
698    didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation {
699  [self didReceiveWKNavigationDelegateCallback];
700
701  GURL webViewURL = net::GURLWithNSURL(webView.URL);
702
703  // This callback should never be triggered for placeholder navigations.
704  DCHECK(base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) ||
705         !IsPlaceholderUrl(webViewURL));
706
707  [self.navigationStates setState:web::WKNavigationState::REDIRECTED
708                    forNavigation:navigation];
709
710  web::NavigationContextImpl* context =
711      [self.navigationStates contextForNavigation:navigation];
712  [self didReceiveRedirectForNavigation:context withURL:webViewURL];
713}
714
715- (void)webView:(WKWebView*)webView
716    didFailProvisionalNavigation:(WKNavigation*)navigation
717                       withError:(NSError*)error {
718  [self didReceiveWKNavigationDelegateCallback];
719
720  [self.navigationStates setState:web::WKNavigationState::PROVISIONALY_FAILED
721                    forNavigation:navigation];
722
723  // Ignore provisional navigation failure if a new navigation has been started,
724  // for example, if a page is reloaded after the start of the provisional
725  // load but before the load has been committed.
726  if (![[self.navigationStates lastAddedNavigation] isEqual:navigation]) {
727    return;
728  }
729
730  // Handle load cancellation for directly cancelled navigations without
731  // handling their potential errors. Otherwise, handle the error.
732  if (self.pendingNavigationInfo.cancelled) {
733    if (self.pendingNavigationInfo.cancellationError) {
734      // If the navigation was cancelled for a CancelAndDisplayError() policy
735      // decision, load the error in the failed navigation.
736      [self handleLoadError:error
737              forNavigation:navigation
738                    webView:webView
739            provisionalLoad:YES];
740    } else {
741      [self handleCancelledError:error
742                   forNavigation:navigation
743                 provisionalLoad:YES];
744    }
745  } else if (error.code == NSURLErrorUnsupportedURL &&
746             self.webStateImpl->HasWebUI()) {
747    // This is a navigation to WebUI page.
748    DCHECK(web::GetWebClient()->IsAppSpecificURL(
749        net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey])));
750  } else {
751    [self handleLoadError:error
752            forNavigation:navigation
753                  webView:webView
754          provisionalLoad:YES];
755  }
756
757  self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames();
758  // This must be reset at the end, since code above may need information about
759  // the pending load.
760  self.pendingNavigationInfo = nil;
761  if (!web::IsWKWebViewSSLCertError(error)) {
762    _certVerificationErrors->Clear();
763  }
764
765  web::NavigationContextImpl* context =
766      [self.navigationStates contextForNavigation:navigation];
767
768  // Remove the navigation to immediately get rid of pending item. Navigation
769  // should not be cleared, however, in the case of a committed interstitial
770  // for an SSL error.
771  if (web::WKNavigationState::NONE !=
772          [self.navigationStates stateForNavigation:navigation] &&
773      !(context && web::IsWKWebViewSSLCertError(context->GetError()) &&
774        !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))) {
775    [self.navigationStates removeNavigation:navigation];
776  }
777}
778
779- (void)webView:(WKWebView*)webView
780    didCommitNavigation:(WKNavigation*)navigation {
781  [self didReceiveWKNavigationDelegateCallback];
782
783  // For reasons not yet fully understood, sometimes WKWebView triggers
784  // |webView:didFinishNavigation| before |webView:didCommitNavigation|. If a
785  // navigation is already finished, stop processing
786  // (https://crbug.com/818796#c2).
787  if ([self.navigationStates stateForNavigation:navigation] ==
788      web::WKNavigationState::FINISHED)
789    return;
790
791  BOOL committedNavigation =
792      [self.navigationStates isCommittedNavigation:navigation];
793
794  web::NavigationContextImpl* context =
795      [self.navigationStates contextForNavigation:navigation];
796  if (context && !web::IsWKWebViewSSLCertError(context->GetError())) {
797    _certVerificationErrors->Clear();
798  }
799
800  // Invariant: Every |navigation| should have a |context|. Note that violation
801  // of this invariant is currently observed in production, but the cause is not
802  // well understood. This DCHECK is meant to catch such cases in testing if
803  // they arise.
804  // TODO(crbug.com/864769): Remove nullptr checks on |context| in this method
805  // once the root cause of the invariant violation is found.
806  DCHECK(context);
807  UMA_HISTOGRAM_BOOLEAN("IOS.CommittedNavigationHasContext", context);
808
809  GURL webViewURL = net::GURLWithNSURL(webView.URL);
810  GURL currentWKItemURL =
811      net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
812  UMA_HISTOGRAM_BOOLEAN("IOS.CommittedURLMatchesCurrentItem",
813                        webViewURL == currentWKItemURL);
814
815  // TODO(crbug.com/787497): Always use webView.backForwardList.currentItem.URL
816  // to obtain lastCommittedURL once loadHTML: is no longer user for WebUI.
817  if (webViewURL.is_empty()) {
818    // It is possible for |webView.URL| to be nil, in which case
819    // webView.backForwardList.currentItem.URL will return the right committed
820    // URL (crbug.com/784480).
821    webViewURL = currentWKItemURL;
822  } else if (context &&
823             (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) ||
824              !context->IsPlaceholderNavigation()) &&
825             context->GetUrl() == currentWKItemURL) {
826    // If webView.backForwardList.currentItem.URL matches |context|, then this
827    // is a known edge case where |webView.URL| is wrong.
828    // TODO(crbug.com/826013): Remove this workaround.
829    webViewURL = currentWKItemURL;
830  }
831
832  if (context) {
833    if (self.pendingNavigationInfo.MIMEType)
834      context->SetMimeType(self.pendingNavigationInfo.MIMEType);
835    if (self.pendingNavigationInfo.HTTPHeaders)
836      context->SetResponseHeaders(self.pendingNavigationInfo.HTTPHeaders);
837  }
838
839  // Don't show webview for placeholder navigation to avoid covering existing
840  // content.
841  if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) ||
842      !IsPlaceholderUrl(webViewURL))
843    [self.delegate navigationHandlerDisplayWebView:self];
844
845  if (@available(iOS 11.3, *)) {
846    // On iOS 11.3 didReceiveServerRedirectForProvisionalNavigation: is not
847    // always called. So if URL was unexpectedly changed then it's probably
848    // because redirect callback was not called.
849    if (@available(iOS 12, *)) {
850      // rdar://37547029 was fixed on iOS 12.
851    } else if (context &&
852               (base::FeatureList::IsEnabled(
853                    web::features::kUseJSForErrorPage) ||
854                !context->IsPlaceholderNavigation()) &&
855               context->GetUrl() != webViewURL) {
856      [self didReceiveRedirectForNavigation:context withURL:webViewURL];
857    }
858  }
859
860  // |context| will be nil if this navigation has been already committed and
861  // finished.
862  if (context) {
863    web::NavigationManager* navigationManager =
864        self.webStateImpl->GetNavigationManager();
865    GURL pendingURL;
866    if (navigationManager->GetPendingItemIndex() == -1) {
867      if (context->GetItem()) {
868        // Item may not exist if navigation was stopped (see
869        // crbug.com/969915).
870        pendingURL = context->GetItem()->GetURL();
871      }
872    } else {
873      if (navigationManager->GetPendingItem()) {
874        pendingURL = navigationManager->GetPendingItem()->GetURL();
875      }
876    }
877    if ((pendingURL == webViewURL) || (context->IsLoadingHtmlString())) {
878      // Commit navigation if at least one of these is true:
879      //  - Navigation has pending item (this should always be true, but
880      //    pending item may not exist due to crbug.com/925304).
881      //  - Navigation is loadHTMLString:baseURL: navigation, which does not
882      //    create a pending item, but modifies committed item instead.
883      //  - Transition type is reload with Legacy Navigation Manager (Legacy
884      //    Navigation Manager does not create pending item for reload due to
885      //    crbug.com/676129)
886      context->SetHasCommitted(true);
887    }
888    self.webStateImpl->SetContentsMimeType(
889        base::SysNSStringToUTF8(context->GetMimeType()));
890  }
891
892  [self commitPendingNavigationInfoInWebView:webView];
893
894  self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames();
895
896  // This point should closely approximate the document object change, so reset
897  // the list of injected scripts to those that are automatically injected.
898  // Do not inject window ID if this is a placeholder URL. For WebUI, let the
899  // window ID be injected when the |loadHTMLString:baseURL| navigation is
900  // committed.
901  if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) ||
902      !IsPlaceholderUrl(webViewURL)) {
903    [self.JSInjector resetInjectedScriptSet];
904
905    const std::string& mime_type = self.webStateImpl->GetContentsMimeType();
906    if (web::IsContentTypeHtml(mime_type) ||
907        web::IsContentTypeImage(mime_type) || mime_type.empty()) {
908      // In unit tests MIME type will be empty, because loadHTML:forURL: does
909      // not notify web view delegate about received response, so web controller
910      // does not get a chance to properly update MIME type.
911      [self.JSInjector injectWindowID];
912      self.webStateImpl->GetWebFramesManagerImpl().RegisterExistingFrames();
913    }
914  }
915
916  if (committedNavigation) {
917    // WKWebView called didCommitNavigation: with incorrect WKNavigation object.
918    // Correct WKNavigation object for this navigation was deallocated because
919    // WKWebView mistakenly cancelled the navigation and called
920    // didFailProvisionalNavigation. As a result web::NavigationContext for this
921    // navigation does not exist anymore. Find correct navigation item and make
922    // it committed.
923    [self resetDocumentSpecificState];
924    [self.delegate navigationHandlerDidStartLoading:self];
925  } else if (context) {
926    // If |navigation| is nil (which happens for windows open by DOM), then it
927    // should be the first and the only pending navigation.
928    BOOL isLastNavigation =
929        !navigation ||
930        [[self.navigationStates lastAddedNavigation] isEqual:navigation];
931    if (isLastNavigation ||
932        self.navigationManagerImpl->GetPendingItemIndex() == -1) {
933      [self webPageChangedWithContext:context webView:webView];
934    }
935  }
936
937  // The WebView URL can't always be trusted when multiple pending navigations
938  // are occuring, as a navigation could commit after a new navigation has
939  // started (and thus the WebView URL will be the URL of the new navigation).
940  // See crbug.com/1127025.
941  BOOL hasMultiplePendingNavigations =
942      [self.navigationStates pendingNavigations].count > 1;
943
944  // When loading an error page, the context has the correct URL whereas the
945  // webView has the file URL.
946  BOOL isErrorPage =
947      base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
948      [ErrorPageHelper isErrorPageFileURL:webViewURL];
949
950  // When loading an error page that is a placeholder (legacy), the webViewURL
951  // should be used as it is the actual URL we want to load.
952  BOOL isLegacyErrorPage =
953      !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
954      context && !context->IsPlaceholderNavigation();
955
956  BOOL shouldUseContextURL =
957      context
958          ? isErrorPage || (!isLegacyErrorPage && hasMultiplePendingNavigations)
959          : NO;
960  GURL documentURL = shouldUseContextURL ? context->GetUrl() : webViewURL;
961
962  // This is the point where the document's URL has actually changed.
963  [self.delegate navigationHandler:self
964                    setDocumentURL:documentURL
965                           context:context];
966
967  // No further code relies an existance of pending item, so this navigation can
968  // be marked as "committed".
969  [self.navigationStates setState:web::WKNavigationState::COMMITTED
970                    forNavigation:navigation];
971
972  if (!committedNavigation && context && !context->IsLoadingErrorPage()) {
973    self.webStateImpl->OnNavigationFinished(context);
974  }
975
976  // Do not update the states of the last committed item for placeholder page
977  // because the actual navigation item will not be committed until the native
978  // content or WebUI is shown.
979  if (context &&
980      (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) ||
981       (!context->IsPlaceholderNavigation() &&
982        !context->IsLoadingErrorPage())) &&
983      !context->GetUrl().SchemeIs(url::kAboutScheme) &&
984      !IsRestoreSessionUrl(context->GetUrl())) {
985    [self.delegate webViewHandlerUpdateSSLStatusForCurrentNavigationItem:self];
986    if ((base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) ||
987         !context->IsLoadingErrorPage()) &&
988        !IsRestoreSessionUrl(webViewURL)) {
989      [self setLastCommittedNavigationItemTitle:webView.title];
990    }
991  }
992}
993
994- (void)webView:(WKWebView*)webView
995    didFinishNavigation:(WKNavigation*)navigation {
996  [self didReceiveWKNavigationDelegateCallback];
997
998  // Sometimes |webView:didFinishNavigation| arrives before
999  // |webView:didCommitNavigation|. Explicitly trigger post-commit processing.
1000  bool navigationCommitted =
1001      [self.navigationStates isCommittedNavigation:navigation];
1002  UMA_HISTOGRAM_BOOLEAN("IOS.WKWebViewFinishBeforeCommit",
1003                        !navigationCommitted);
1004  if (!navigationCommitted) {
1005    [self webView:webView didCommitNavigation:navigation];
1006    DCHECK_EQ(web::WKNavigationState::COMMITTED,
1007              [self.navigationStates stateForNavigation:navigation]);
1008  }
1009
1010  // Sometimes |didFinishNavigation| callback arrives after |stopLoading| has
1011  // been called. Abort in this case.
1012  if ([self.navigationStates stateForNavigation:navigation] ==
1013      web::WKNavigationState::NONE) {
1014    return;
1015  }
1016
1017  GURL webViewURL = net::GURLWithNSURL(webView.URL);
1018  GURL currentWKItemURL =
1019      net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
1020  UMA_HISTOGRAM_BOOLEAN("IOS.FinishedURLMatchesCurrentItem",
1021                        webViewURL == currentWKItemURL);
1022
1023  web::NavigationContextImpl* context =
1024      [self.navigationStates contextForNavigation:navigation];
1025  web::NavigationItemImpl* item =
1026      context ? web::GetItemWithUniqueID(self.navigationManagerImpl, context)
1027              : nullptr;
1028  // Item may not exist if navigation was stopped (see crbug.com/969915).
1029
1030  // Invariant: every |navigation| should have a |context| and a |item|.
1031  // TODO(crbug.com/899383) Fix invariant violation when a new pending item is
1032  // created before a placeholder load finishes.
1033  if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
1034      IsPlaceholderUrl(webViewURL)) {
1035    GURL originalURL = ExtractUrlFromPlaceholderUrl(webViewURL);
1036    if (self.currentNavItem != item &&
1037        self.currentNavItem->GetVirtualURL() != originalURL) {
1038      // The |didFinishNavigation| callback for placeholder navigation can
1039      // arrive after another navigation has started. Abort in this case.
1040      return;
1041    }
1042  }
1043  DCHECK(context);
1044  UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasContext", context);
1045  UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasItem", item);
1046
1047  if (context && item) {
1048    GURL navigationURL =
1049        !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
1050                context->IsPlaceholderNavigation()
1051            ? CreatePlaceholderUrlForUrl(context->GetUrl())
1052            : context->GetUrl();
1053    if (navigationURL == currentWKItemURL) {
1054      // If webView.backForwardList.currentItem.URL matches |context|, then this
1055      // is a known edge case where |webView.URL| is wrong.
1056      // TODO(crbug.com/826013): Remove this workaround.
1057      webViewURL = currentWKItemURL;
1058    }
1059
1060    if (!IsWKInternalUrl(currentWKItemURL) && currentWKItemURL == webViewURL &&
1061        currentWKItemURL != context->GetUrl() &&
1062        item == self.navigationManagerImpl->GetLastCommittedItem() &&
1063        item->GetURL().GetOrigin() == currentWKItemURL.GetOrigin()) {
1064      // WKWebView sometimes changes URL on the same navigation, likely due to
1065      // location.replace() or history.replaceState in onload handler that does
1066      // not change the origin. It's safe to update |item| and |context| URL
1067      // because they are both associated to WKNavigation*, which is a stable ID
1068      // for the navigation. See https://crbug.com/869540 for a real-world case.
1069      item->SetURL(currentWKItemURL);
1070      context->SetUrl(currentWKItemURL);
1071    }
1072
1073    if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) {
1074      if (IsPlaceholderUrl(webViewURL)) {
1075        if (item->GetURL() == webViewURL) {
1076          // Current navigation item is restored from a placeholder URL as part
1077          // of session restoration. It is now safe to update the navigation
1078          // item URL to the original app-specific URL.
1079          item->SetURL(ExtractUrlFromPlaceholderUrl(webViewURL));
1080        }
1081
1082        if (item->error_retry_state_machine().state() ==
1083            web::ErrorRetryState::kNoNavigationError) {
1084          // Offline pages can leave the WKBackForwardList current item as a
1085          // placeholder with no saved content.  In this case, trigger a retry
1086          // on that navigation with an update |item| url and |context| error.
1087          item->SetURL(
1088              ExtractUrlFromPlaceholderUrl(net::GURLWithNSURL(webView.URL)));
1089          item->SetVirtualURL(item->GetURL());
1090          context->SetError([NSError
1091              errorWithDomain:NSURLErrorDomain
1092                         code:NSURLErrorNetworkConnectionLost
1093                     userInfo:@{
1094                       NSURLErrorFailingURLStringErrorKey :
1095                           base::SysUTF8ToNSString(item->GetURL().spec())
1096                     }]);
1097          item->error_retry_state_machine().SetRetryPlaceholderNavigation();
1098        }
1099      }
1100
1101      web::ErrorRetryCommand command =
1102          item->error_retry_state_machine().DidFinishNavigation(webViewURL);
1103      [self handleErrorRetryCommand:command
1104                     navigationItem:item
1105                  navigationContext:context
1106                 originalNavigation:navigation
1107                            webView:webView];
1108    } else if (context->GetError()) {
1109      [self loadErrorPageForNavigationItem:item
1110                         navigationContext:navigation
1111                                   webView:webView];
1112    }
1113  }
1114
1115  [self.textFragmentsHandler
1116      processTextFragmentsWithContext:context
1117                             referrer:self.currentReferrer];
1118
1119  [self.navigationStates setState:web::WKNavigationState::FINISHED
1120                    forNavigation:navigation];
1121
1122  [self.delegate webViewHandler:self didFinishNavigation:context];
1123
1124  // Remove the navigation to immediately get rid of pending item. Navigation
1125  // should not be cleared, however, in the case of a committed interstitial
1126  // for an SSL error.
1127  if (web::WKNavigationState::NONE !=
1128          [self.navigationStates stateForNavigation:navigation] &&
1129      !(context && web::IsWKWebViewSSLCertError(context->GetError()))) {
1130    [self.navigationStates removeNavigation:navigation];
1131  }
1132}
1133
1134- (void)webView:(WKWebView*)webView
1135    didFailNavigation:(WKNavigation*)navigation
1136            withError:(NSError*)error {
1137  [self didReceiveWKNavigationDelegateCallback];
1138
1139  [self.navigationStates setState:web::WKNavigationState::FAILED
1140                    forNavigation:navigation];
1141
1142  [self handleLoadError:error
1143          forNavigation:navigation
1144                webView:webView
1145        provisionalLoad:NO];
1146  self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames();
1147  _certVerificationErrors->Clear();
1148  [self forgetNullWKNavigation:navigation];
1149}
1150
1151- (void)webView:(WKWebView*)webView
1152    didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
1153                    completionHandler:
1154                        (void (^)(NSURLSessionAuthChallengeDisposition,
1155                                  NSURLCredential*))completionHandler {
1156  [self didReceiveWKNavigationDelegateCallback];
1157
1158  NSString* authMethod = challenge.protectionSpace.authenticationMethod;
1159  if ([authMethod isEqual:NSURLAuthenticationMethodHTTPBasic] ||
1160      [authMethod isEqual:NSURLAuthenticationMethodNTLM] ||
1161      [authMethod isEqual:NSURLAuthenticationMethodHTTPDigest]) {
1162    [self handleHTTPAuthForChallenge:challenge
1163                   completionHandler:completionHandler];
1164    return;
1165  }
1166
1167  if (![authMethod isEqual:NSURLAuthenticationMethodServerTrust]) {
1168    completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
1169    return;
1170  }
1171
1172  SecTrustRef trust = challenge.protectionSpace.serverTrust;
1173  base::ScopedCFTypeRef<SecTrustRef> scopedTrust(trust,
1174                                                 base::scoped_policy::RETAIN);
1175  __weak CRWWKNavigationHandler* weakSelf = self;
1176  [self.certVerificationController
1177      decideLoadPolicyForTrust:scopedTrust
1178                          host:challenge.protectionSpace.host
1179             completionHandler:^(web::CertAcceptPolicy policy,
1180                                 net::CertStatus status) {
1181               CRWWKNavigationHandler* strongSelf = weakSelf;
1182               if (!strongSelf) {
1183                 completionHandler(
1184                     NSURLSessionAuthChallengeRejectProtectionSpace, nil);
1185                 return;
1186               }
1187               [strongSelf processAuthChallenge:challenge
1188                            forCertAcceptPolicy:policy
1189                                     certStatus:status
1190                              completionHandler:completionHandler];
1191             }];
1192}
1193
1194- (void)webView:(WKWebView*)webView
1195     authenticationChallenge:(NSURLAuthenticationChallenge*)challenge
1196    shouldAllowDeprecatedTLS:(void (^)(BOOL))decisionHandler
1197    API_AVAILABLE(ios(14)) {
1198  [self didReceiveWKNavigationDelegateCallback];
1199  DCHECK(challenge);
1200  DCHECK(decisionHandler);
1201
1202  // If the legacy TLS interstitial is not enabled, don't cause errors. The
1203  // interstitial is also dependent on committed interstitials being enabled.
1204  if (!base::FeatureList::IsEnabled(web::features::kIOSLegacyTLSInterstitial)) {
1205    decisionHandler(YES);
1206    return;
1207  }
1208
1209  if (web::GetWebClient()->IsLegacyTLSAllowedForHost(
1210          self.webStateImpl,
1211          base::SysNSStringToUTF8(challenge.protectionSpace.host))) {
1212    decisionHandler(YES);
1213    return;
1214  }
1215
1216  if (self.pendingNavigationInfo) {
1217    self.pendingNavigationInfo.cancelled = YES;
1218    self.pendingNavigationInfo.cancellationError =
1219        [NSError errorWithDomain:net::kNSErrorDomain
1220                            code:net::ERR_SSL_OBSOLETE_VERSION
1221                        userInfo:nil];
1222  }
1223  decisionHandler(NO);
1224}
1225
1226- (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView {
1227  [self didReceiveWKNavigationDelegateCallback];
1228
1229  _certVerificationErrors->Clear();
1230  _webProcessCrashed = YES;
1231  self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames();
1232
1233  [self.delegate navigationHandlerWebProcessDidCrash:self];
1234}
1235
1236#pragma mark - Private methods
1237
1238// Returns the UserAgent that needs to be used for the |navigationAction| from
1239// the |webView|.
1240- (web::UserAgentType)userAgentForNavigationAction:
1241                          (WKNavigationAction*)navigationAction
1242                                           webView:(WKWebView*)webView {
1243  web::NavigationItem* item = nullptr;
1244  web::UserAgentType userAgentType = web::UserAgentType::NONE;
1245  if (navigationAction.navigationType == WKNavigationTypeBackForward) {
1246    // Use the item associated with the back/forward item to have the same user
1247    // agent as the one used the first time.
1248    item = [[CRWNavigationItemHolder
1249        holderForBackForwardListItem:webView.backForwardList.currentItem]
1250        navigationItem];
1251    // In some cases, the associated item isn't found. In that case, follow the
1252    // code path for the non-backforward navigations. See crbug.com/1121950.
1253    if (item)
1254      userAgentType = item->GetUserAgentType();
1255  }
1256  if (!item) {
1257    // Get the visible item. There is no guarantee that the pending item belongs
1258    // to this navigation but it is very likely that it is the case. If there is
1259    // no pending item, it is probably a render initiated navigation. Use the
1260    // UserAgent of the previous navigation. This will also return the
1261    // navigation item of the restoration if a restoration occurs. Request the
1262    // pending item explicitly as the visible item might be the committed item
1263    // if the pending navigation isn't user triggered.
1264    item = self.navigationManagerImpl->GetPendingItem();
1265    if (!item)
1266      item = self.navigationManagerImpl->GetVisibleItem();
1267
1268    if (item && item->GetTransitionType() & ui::PAGE_TRANSITION_FORWARD_BACK) {
1269      // When navigating forward to a restored page, the WKNavigationAction is
1270      // of type reload and not BackForward. The item is correctly set a
1271      // back/forward, so it is possible to use it.
1272      userAgentType = item->GetUserAgentType();
1273    } else {
1274      userAgentType = self.webStateImpl->GetUserAgentForNextNavigation(
1275          net::GURLWithNSURL(navigationAction.request.URL));
1276    }
1277  }
1278
1279  if (item && web::GetWebClient()->IsAppSpecificURL(item->GetVirtualURL())) {
1280    // In case of app specific URL, no specificUser Agent needs to be used.
1281    // However, to have a custom User Agent and a WKContentMode, use mobile.
1282    userAgentType = web::UserAgentType::MOBILE;
1283  }
1284  return userAgentType;
1285}
1286
1287- (web::NavigationManagerImpl*)navigationManagerImpl {
1288  return &(self.webStateImpl->GetNavigationManagerImpl());
1289}
1290
1291- (web::WebStateImpl*)webStateImpl {
1292  return [self.delegate webStateImplForWebViewHandler:self];
1293}
1294
1295- (web::UserInteractionState*)userInteractionState {
1296  return [self.delegate userInteractionStateForWebViewHandler:self];
1297}
1298
1299- (CRWJSInjector*)JSInjector {
1300  return [self.delegate JSInjectorForNavigationHandler:self];
1301}
1302
1303- (CRWCertVerificationController*)certVerificationController {
1304  return [self.delegate certVerificationControllerForNavigationHandler:self];
1305}
1306
1307- (GURL)documentURL {
1308  return [self.delegate documentURLForWebViewHandler:self];
1309}
1310
1311- (web::NavigationItemImpl*)currentNavItem {
1312  return self.navigationManagerImpl
1313             ? self.navigationManagerImpl->GetCurrentItemImpl()
1314             : nullptr;
1315}
1316
1317// This method should be called on receiving WKNavigationDelegate callbacks.
1318- (void)didReceiveWKNavigationDelegateCallback {
1319  DCHECK(!self.beingDestroyed);
1320}
1321
1322// Extracts navigation info from WKNavigationAction and sets it as a pending.
1323// Some pieces of navigation information are only known in
1324// |decidePolicyForNavigationAction|, but must be in a pending state until
1325// |didgo/Navigation| where it becames current.
1326- (void)createPendingNavigationInfoFromNavigationAction:
1327    (WKNavigationAction*)action {
1328  if (action.targetFrame.mainFrame) {
1329    self.pendingNavigationInfo = [[CRWPendingNavigationInfo alloc] init];
1330    self.pendingNavigationInfo.referrer =
1331        [action.request valueForHTTPHeaderField:kReferrerHeaderName];
1332    self.pendingNavigationInfo.navigationType = action.navigationType;
1333    self.pendingNavigationInfo.HTTPMethod = action.request.HTTPMethod;
1334    self.pendingNavigationInfo.hasUserGesture =
1335        web::GetNavigationActionInitiationType(action) ==
1336        web::NavigationActionInitiationType::kUserInitiated;
1337  }
1338}
1339
1340// Extracts navigation info from WKNavigationResponse and sets it as a pending.
1341// Some pieces of navigation information are only known in
1342// |decidePolicyForNavigationResponse|, but must be in a pending state until
1343// |didCommitNavigation| where it becames current.
1344- (void)
1345    updatePendingNavigationInfoFromNavigationResponse:
1346        (WKNavigationResponse*)response
1347                                          HTTPHeaders:
1348                                              (const scoped_refptr<
1349                                                  net::HttpResponseHeaders>&)
1350                                                  headers {
1351  if (response.isForMainFrame) {
1352    if (!self.pendingNavigationInfo) {
1353      self.pendingNavigationInfo = [[CRWPendingNavigationInfo alloc] init];
1354    }
1355    self.pendingNavigationInfo.MIMEType = response.response.MIMEType;
1356    self.pendingNavigationInfo.HTTPHeaders = headers;
1357  }
1358}
1359
1360// Returns YES if the navigation action is associated with a main frame request.
1361- (BOOL)isMainFrameNavigationAction:(WKNavigationAction*)action {
1362  if (action.targetFrame) {
1363    return action.targetFrame.mainFrame;
1364  }
1365  // According to WKNavigationAction documentation, in the case of a new window
1366  // navigation, target frame will be nil. In this case check if the
1367  // |sourceFrame| is the mainFrame.
1368  return action.sourceFrame.mainFrame;
1369}
1370
1371// Returns YES if the given |action| should be allowed to continue for app
1372// specific URL. If this returns NO, the navigation should be cancelled.
1373// App specific pages have elevated privileges and WKWebView uses the same
1374// renderer process for all page frames. With that Chromium does not allow
1375// running App specific pages in the same process as a web site from the
1376// internet. Allows navigation to app specific URL in the following cases:
1377//   - last committed URL is app specific
1378//   - navigation not a new navigation (back-forward or reload)
1379//   - navigation is typed, generated or bookmark
1380//   - navigation is performed in iframe and main frame is app-specific page
1381- (BOOL)shouldAllowAppSpecificURLNavigationAction:(WKNavigationAction*)action
1382                                       transition:
1383                                           (ui::PageTransition)pageTransition {
1384  GURL requestURL = net::GURLWithNSURL(action.request.URL);
1385  DCHECK(web::GetWebClient()->IsAppSpecificURL(requestURL));
1386  if (web::GetWebClient()->IsAppSpecificURL(
1387          self.webStateImpl->GetLastCommittedURL())) {
1388    // Last committed page is also app specific and navigation should be
1389    // allowed.
1390    return YES;
1391  }
1392
1393  if (!ui::PageTransitionIsNewNavigation(pageTransition)) {
1394    // Allow reloads and back-forward navigations.
1395    return YES;
1396  }
1397
1398  if (ui::PageTransitionTypeIncludingQualifiersIs(pageTransition,
1399                                                  ui::PAGE_TRANSITION_TYPED)) {
1400    return YES;
1401  }
1402
1403  if (ui::PageTransitionTypeIncludingQualifiersIs(
1404          pageTransition, ui::PAGE_TRANSITION_GENERATED)) {
1405    return YES;
1406  }
1407
1408  if (ui::PageTransitionTypeIncludingQualifiersIs(
1409          pageTransition, ui::PAGE_TRANSITION_AUTO_BOOKMARK)) {
1410    return YES;
1411  }
1412
1413  // If the session is being restored, allow the navigation.
1414  if (IsRestoreSessionUrl(self.documentURL)) {
1415    return YES;
1416  }
1417
1418  // Allow navigation to WebUI pages from error pages.
1419  if ([ErrorPageHelper isErrorPageFileURL:self.documentURL]) {
1420    return YES;
1421  }
1422
1423  GURL mainDocumentURL = net::GURLWithNSURL(action.request.mainDocumentURL);
1424  if (web::GetWebClient()->IsAppSpecificURL(mainDocumentURL) &&
1425      !action.sourceFrame.mainFrame) {
1426    // AppSpecific URLs are allowed inside iframe if the main frame is also
1427    // app specific page.
1428    return YES;
1429  }
1430
1431  return NO;
1432}
1433
1434// Caches request POST data in the given session entry.
1435- (void)cachePOSTDataForRequest:(NSURLRequest*)request
1436               inNavigationItem:(web::NavigationItemImpl*)item {
1437  NSUInteger maxPOSTDataSizeInBytes = 4096;
1438  NSString* cookieHeaderName = @"cookie";
1439
1440  DCHECK(item);
1441  const bool shouldUpdateEntry =
1442      ui::PageTransitionCoreTypeIs(item->GetTransitionType(),
1443                                   ui::PAGE_TRANSITION_FORM_SUBMIT) &&
1444      ![request HTTPBodyStream] &&  // Don't cache streams.
1445      !item->HasPostData() &&
1446      item->GetURL() == net::GURLWithNSURL([request URL]);
1447  const bool belowSizeCap =
1448      [[request HTTPBody] length] < maxPOSTDataSizeInBytes;
1449  DLOG_IF(WARNING, shouldUpdateEntry && !belowSizeCap)
1450      << "Data in POST request exceeds the size cap (" << maxPOSTDataSizeInBytes
1451      << " bytes), and will not be cached.";
1452
1453  if (shouldUpdateEntry && belowSizeCap) {
1454    item->SetPostData([request HTTPBody]);
1455    item->ResetHttpRequestHeaders();
1456    item->AddHttpRequestHeaders([request allHTTPHeaderFields]);
1457    // Don't cache the "Cookie" header.
1458    // According to NSURLRequest documentation, |-valueForHTTPHeaderField:| is
1459    // case insensitive, so it's enough to test the lower case only.
1460    if ([request valueForHTTPHeaderField:cookieHeaderName]) {
1461      // Case insensitive search in |headers|.
1462      NSSet* cookieKeys = [item->GetHttpRequestHeaders()
1463          keysOfEntriesPassingTest:^(id key, id obj, BOOL* stop) {
1464            NSString* header = (NSString*)key;
1465            const BOOL found =
1466                [header caseInsensitiveCompare:cookieHeaderName] ==
1467                NSOrderedSame;
1468            *stop = found;
1469            return found;
1470          }];
1471      DCHECK_EQ(1u, [cookieKeys count]);
1472      item->RemoveHttpRequestHeaderForKey([cookieKeys anyObject]);
1473    }
1474  }
1475}
1476
1477// If YES, the page should be closed if it successfully redirects to a native
1478// application, for example if a new tab redirects to the App Store.
1479- (BOOL)shouldClosePageOnNativeApplicationLoad {
1480  // The page should be closed if it was initiated by the DOM and there has been
1481  // no user interaction with the page since the web view was created, or if
1482  // the page has no navigation items, as occurs when an App Store link is
1483  // opened from another application.
1484  BOOL rendererInitiatedWithoutInteraction =
1485      self.webStateImpl->HasOpener() &&
1486      !self.userInteractionState
1487           ->UserInteractionRegisteredSinceWebViewCreated();
1488  BOOL noNavigationItems = !(self.navigationManagerImpl->GetItemCount());
1489  return rendererInitiatedWithoutInteraction || noNavigationItems;
1490}
1491
1492// Returns YES if response should be rendered in WKWebView.
1493- (BOOL)shouldRenderResponse:(WKNavigationResponse*)WKResponse {
1494  if (!WKResponse.canShowMIMEType) {
1495    return NO;
1496  }
1497
1498  GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL);
1499  if (responseURL.SchemeIs(url::kDataScheme) && WKResponse.forMainFrame) {
1500    // Block rendering data URLs for renderer-initiated navigations in main
1501    // frame to prevent abusive behavior (crbug.com/890558).
1502    web::NavigationContext* context =
1503        [self contextForPendingMainFrameNavigationWithURL:responseURL];
1504    if (context->IsRendererInitiated()) {
1505      return NO;
1506    }
1507  }
1508
1509  return YES;
1510}
1511
1512// Creates DownloadTask for the given navigation response. Headers are passed
1513// as argument to avoid extra NSDictionary -> net::HttpResponseHeaders
1514// conversion.
1515- (void)createDownloadTaskForResponse:(WKNavigationResponse*)WKResponse
1516                          HTTPHeaders:(net::HttpResponseHeaders*)headers {
1517  const GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL);
1518  const int64_t contentLength = WKResponse.response.expectedContentLength;
1519  const std::string MIMEType =
1520      base::SysNSStringToUTF8(WKResponse.response.MIMEType);
1521
1522  std::string contentDisposition;
1523  if (headers) {
1524    headers->GetNormalizedHeader("content-disposition", &contentDisposition);
1525  }
1526
1527  NSString* HTTPMethod = @"GET";
1528  if (WKResponse.forMainFrame) {
1529    web::NavigationContextImpl* context =
1530        [self contextForPendingMainFrameNavigationWithURL:responseURL];
1531    // Context lookup fails in rare cases (f.e. after certain redirects,
1532    // when WKWebView.URL did not change to redirected page inside
1533    // webView:didReceiveServerRedirectForProvisionalNavigation:
1534    // as happened in crbug.com/820375). In that case it's not possible
1535    // to locate correct context to update |HTTPMethod| and call
1536    // WebStateObserver::DidFinishNavigation. Download will fail with incorrect
1537    // HTTPMethod, which is better than a crash on null pointer dereferencing.
1538    // Missing DidFinishNavigation for download navigation does not cause any
1539    // major issues, and it's also better than a crash.
1540    if (context) {
1541      context->SetIsDownload(true);
1542      context->ReleaseItem();
1543      if (context->IsPost()) {
1544        HTTPMethod = @"POST";
1545      }
1546      // Navigation callbacks can only be called for the main frame.
1547      self.webStateImpl->OnNavigationFinished(context);
1548    }
1549  }
1550  web::DownloadController::FromBrowserState(
1551      self.webStateImpl->GetBrowserState())
1552      ->CreateDownloadTask(self.webStateImpl, [NSUUID UUID].UUIDString,
1553                           responseURL, HTTPMethod, contentDisposition,
1554                           contentLength, MIMEType);
1555}
1556
1557// Updates URL for navigation context and navigation item.
1558- (void)didReceiveRedirectForNavigation:(web::NavigationContextImpl*)context
1559                                withURL:(const GURL&)URL {
1560  context->SetUrl(URL);
1561  web::NavigationItemImpl* item =
1562      web::GetItemWithUniqueID(self.navigationManagerImpl, context);
1563
1564  // Associated item can be a pending item, previously discarded by another
1565  // navigation. WKWebView allows multiple provisional navigations, while
1566  // Navigation Manager has only one pending navigation.
1567  if (item) {
1568    if (!IsWKInternalUrl(URL)) {
1569      item->SetVirtualURL(URL);
1570      item->SetURL(URL);
1571    }
1572    // Redirects (3xx response code), must change POST requests to GETs.
1573    item->SetPostData(nil);
1574    item->ResetHttpRequestHeaders();
1575  }
1576
1577  self.userInteractionState->ResetLastTransferTime();
1578  self.webStateImpl->OnNavigationRedirected(context);
1579}
1580
1581// WKNavigation objects are used as a weak key to store web::NavigationContext.
1582// WKWebView manages WKNavigation lifetime and destroys them after the
1583// navigation is finished. However for window opening navigations WKWebView
1584// passes null WKNavigation to WKNavigationDelegate callbacks and strong key is
1585// used to store web::NavigationContext. Those "null" navigations have to be
1586// cleaned up manually by calling this method.
1587- (void)forgetNullWKNavigation:(WKNavigation*)navigation {
1588  if (!navigation)
1589    [self.navigationStates removeNavigation:navigation];
1590}
1591
1592#pragma mark - Auth Challenge
1593
1594// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to
1595// reply with NSURLSessionAuthChallengeDisposition and credentials.
1596- (void)processAuthChallenge:(NSURLAuthenticationChallenge*)challenge
1597         forCertAcceptPolicy:(web::CertAcceptPolicy)policy
1598                  certStatus:(net::CertStatus)certStatus
1599           completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
1600                                       NSURLCredential*))completionHandler {
1601  SecTrustRef trust = challenge.protectionSpace.serverTrust;
1602  if (policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER) {
1603    // Cert is invalid, but user agreed to proceed, override default behavior.
1604    completionHandler(NSURLSessionAuthChallengeUseCredential,
1605                      [NSURLCredential credentialForTrust:trust]);
1606    return;
1607  }
1608
1609  if (policy != web::CERT_ACCEPT_POLICY_ALLOW &&
1610      SecTrustGetCertificateCount(trust)) {
1611    // The cert is invalid and the user has not agreed to proceed. Cache the
1612    // cert verification result in |_certVerificationErrors|, so that it can
1613    // later be reused inside |didFailProvisionalNavigation:|.
1614    // The leaf cert is used as the key, because the chain provided by
1615    // |didFailProvisionalNavigation:| will differ (it is the server-supplied
1616    // chain), thus if intermediates were considered, the keys would mismatch.
1617    scoped_refptr<net::X509Certificate> leafCert =
1618        net::x509_util::CreateX509CertificateFromSecCertificate(
1619            SecTrustGetCertificateAtIndex(trust, 0),
1620            std::vector<SecCertificateRef>());
1621    if (leafCert) {
1622      bool is_recoverable =
1623          policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_UNDECIDED_BY_USER;
1624      std::string host =
1625          base::SysNSStringToUTF8(challenge.protectionSpace.host);
1626      _certVerificationErrors->Put(
1627          web::CertHostPair(leafCert, host),
1628          web::CertVerificationError(is_recoverable, certStatus));
1629    }
1630  }
1631  completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
1632}
1633
1634// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
1635// with NSURLSessionAuthChallengeDisposition and credentials.
1636- (void)handleHTTPAuthForChallenge:(NSURLAuthenticationChallenge*)challenge
1637                 completionHandler:
1638                     (void (^)(NSURLSessionAuthChallengeDisposition,
1639                               NSURLCredential*))completionHandler {
1640  NSURLProtectionSpace* space = challenge.protectionSpace;
1641  DCHECK(
1642      [space.authenticationMethod isEqual:NSURLAuthenticationMethodHTTPBasic] ||
1643      [space.authenticationMethod isEqual:NSURLAuthenticationMethodNTLM] ||
1644      [space.authenticationMethod isEqual:NSURLAuthenticationMethodHTTPDigest]);
1645
1646  self.webStateImpl->OnAuthRequired(
1647      space, challenge.proposedCredential,
1648      base::BindRepeating(^(NSString* user, NSString* password) {
1649        [CRWWKNavigationHandler processHTTPAuthForUser:user
1650                                              password:password
1651                                     completionHandler:completionHandler];
1652      }));
1653}
1654
1655// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
1656// with NSURLSessionAuthChallengeDisposition and credentials.
1657+ (void)processHTTPAuthForUser:(NSString*)user
1658                      password:(NSString*)password
1659             completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
1660                                         NSURLCredential*))completionHandler {
1661  DCHECK_EQ(user == nil, password == nil);
1662  if (!user || !password) {
1663    // Embedder cancelled authentication.
1664    completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
1665    return;
1666  }
1667  completionHandler(
1668      NSURLSessionAuthChallengeUseCredential,
1669      [NSURLCredential
1670          credentialWithUser:user
1671                    password:password
1672                 persistence:NSURLCredentialPersistenceForSession]);
1673}
1674
1675// Called when a load ends in an SSL error and certificate chain.
1676- (void)handleSSLCertError:(NSError*)error
1677             forNavigation:(WKNavigation*)navigation
1678                   webView:(WKWebView*)webView {
1679  CHECK(web::IsWKWebViewSSLCertError(error));
1680
1681  net::SSLInfo info;
1682  web::GetSSLInfoFromWKWebViewSSLCertError(error, &info);
1683
1684  if (!info.cert) {
1685    // |info.cert| can be null if certChain in NSError is empty or can not be
1686    // parsed, in this case do not ask delegate if error should be allowed, it
1687    // should not be.
1688    [self handleLoadError:error
1689            forNavigation:navigation
1690                  webView:webView
1691          provisionalLoad:YES];
1692    return;
1693  }
1694
1695  // Retrieve verification results from _certVerificationErrors cache to avoid
1696  // unnecessary recalculations. Verification results are cached for the leaf
1697  // cert, because the cert chain in |didReceiveAuthenticationChallenge:| is
1698  // the OS constructed chain, while |chain| is the chain from the server.
1699  NSArray* chain = error.userInfo[web::kNSErrorPeerCertificateChainKey];
1700  NSURL* requestURL = error.userInfo[web::kNSErrorFailingURLKey];
1701  NSString* host = requestURL.host;
1702  scoped_refptr<net::X509Certificate> leafCert;
1703  bool recoverable = false;
1704  if (chain.count && host.length) {
1705    // The complete cert chain may not be available, so the leaf cert is used
1706    // as a key to retrieve _certVerificationErrors, as well as for storing the
1707    // cert decision.
1708    leafCert = web::CreateCertFromChain(@[ chain.firstObject ]);
1709    if (leafCert) {
1710      auto error = _certVerificationErrors->Get(
1711          {leafCert, base::SysNSStringToUTF8(host)});
1712      bool cacheHit = error != _certVerificationErrors->end();
1713      if (cacheHit) {
1714        recoverable = error->second.is_recoverable;
1715        info.cert_status = error->second.status;
1716      }
1717      UMA_HISTOGRAM_BOOLEAN("WebController.CertVerificationErrorsCacheHit",
1718                            cacheHit);
1719    }
1720  }
1721
1722  // If the current navigation item is in error state, update the error retry
1723  // state machine to indicate that SSL interstitial error will be displayed to
1724  // make sure subsequent back/forward navigation to this item starts with the
1725  // correct error retry state.
1726  web::NavigationContextImpl* context =
1727      [self.navigationStates contextForNavigation:navigation];
1728  if (context) {
1729    // This NavigationContext will be destroyed, so return pending item
1730    // ownership to NavigationManager. NavigationContext can only own pending
1731    // item until the navigation has committed or aborted.
1732    self.navigationManagerImpl->SetPendingItem(context->ReleaseItem());
1733    web::NavigationItemImpl* item =
1734        web::GetItemWithUniqueID(self.navigationManagerImpl, context);
1735    if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
1736        item &&
1737        item->error_retry_state_machine().state() ==
1738            web::ErrorRetryState::kRetryFailedNavigationItem) {
1739      item->error_retry_state_machine().SetDisplayingWebError();
1740    }
1741  }
1742
1743  // Ask web client if this cert error should be allowed.
1744  web::GetWebClient()->AllowCertificateError(
1745      self.webStateImpl, net::MapCertStatusToNetError(info.cert_status), info,
1746      net::GURLWithNSURL(requestURL), recoverable, context->GetNavigationId(),
1747      base::BindRepeating(^(bool proceed) {
1748        if (proceed) {
1749          DCHECK(recoverable);
1750          [self.certVerificationController allowCert:leafCert
1751                                             forHost:host
1752                                              status:info.cert_status];
1753          self.webStateImpl->GetSessionCertificatePolicyCacheImpl()
1754              .RegisterAllowedCertificate(
1755                  leafCert, base::SysNSStringToUTF8(host), info.cert_status);
1756          // New navigation is a different navigation from the original one.
1757          // The new navigation is always browser-initiated and happens when
1758          // the browser allows to proceed with the load.
1759          [self.delegate navigationHandler:self
1760              loadCurrentURLWithRendererInitiatedNavigation:NO];
1761        }
1762      }));
1763
1764  [self loadCancelled];
1765}
1766
1767// Called when a load ends in an error.
1768- (void)handleLoadError:(NSError*)error
1769          forNavigation:(WKNavigation*)navigation
1770                webView:(WKWebView*)webView
1771        provisionalLoad:(BOOL)provisionalLoad {
1772  NSError* policyDecisionCancellationError =
1773      self.pendingNavigationInfo.cancellationError;
1774  if (!policyDecisionCancellationError && error.code == NSURLErrorCancelled) {
1775    [self handleCancelledError:error
1776                 forNavigation:navigation
1777               provisionalLoad:provisionalLoad];
1778    if (@available(iOS 13, *)) {
1779      // The bug has been fixed on iOS 13. The workaround is only needed for
1780      // other versions.
1781    } else if (@available(iOS 12.2, *)) {
1782      if (![webView.backForwardList.currentItem.URL isEqual:webView.URL] &&
1783          [self isCurrentNavigationItemPOST]) {
1784        UMA_HISTOGRAM_BOOLEAN("WebController.BackForwardListOutOfSync", true);
1785        // Sometimes on error the backForward list is out of sync with the
1786        // webView, go back or forward to fix it. See crbug.com/951880.
1787        if ([webView.backForwardList.backItem.URL isEqual:webView.URL]) {
1788          [webView goBack];
1789        } else if ([webView.backForwardList.forwardItem.URL
1790                       isEqual:webView.URL]) {
1791          [webView goForward];
1792        }
1793      }
1794    }
1795    // NSURLErrorCancelled errors that aren't handled by aborting the load will
1796    // automatically be retried by the web view, so early return in this case.
1797    return;
1798  }
1799
1800  web::NavigationContextImpl* navigationContext =
1801      [self.navigationStates contextForNavigation:navigation];
1802
1803  if (@available(iOS 13, *)) {
1804  } else {
1805    if (provisionalLoad && !navigationContext &&
1806        web::RequiresProvisionalNavigationFailureWorkaround()) {
1807      // It is likely that |navigationContext| is null because
1808      // didStartProvisionalNavigation: was not called with this WKNavigation
1809      // object. Log UMA to know when this workaround can be removed and
1810      // do not call OnNavigationFinished() to avoid crash on null pointer
1811      // dereferencing. See crbug.com/973653 and crbug.com/1004634 for details.
1812      UMA_HISTOGRAM_BOOLEAN(
1813          "Navigation.IOSNullContextInDidFailProvisionalNavigation", true);
1814      return;
1815    }
1816  }
1817
1818  NSError* contextError = web::NetErrorFromError(error);
1819  if (policyDecisionCancellationError) {
1820    contextError = base::ios::ErrorWithAppendedUnderlyingError(
1821        contextError, policyDecisionCancellationError);
1822  }
1823
1824  navigationContext->SetError(contextError);
1825  navigationContext->SetIsPost([self isCurrentNavigationItemPOST]);
1826  // TODO(crbug.com/803631) DCHECK that self.currentNavItem is the navigation
1827  // item associated with navigationContext.
1828
1829  if ([error.domain isEqual:base::SysUTF8ToNSString(web::kWebKitErrorDomain)]) {
1830    if (error.code == web::kWebKitErrorPlugInLoadFailed) {
1831      // In cases where a Plug-in handles the load do not take any further
1832      // action.
1833      return;
1834    }
1835
1836    ui::PageTransition transition = navigationContext->GetPageTransition();
1837    if (error.code == web::kWebKitErrorUrlBlockedByContentFilter) {
1838      DCHECK(provisionalLoad);
1839        // If URL is blocked due to Restriction, do not take any further
1840        // action as WKWebView will show a built-in error.
1841        if (!web::RequiresContentFilterBlockingWorkaround()) {
1842          // On iOS13, immediately following this navigation, WebKit will
1843          // navigate to an internal failure page. Unfortunately, due to how
1844          // session restoration works with same document navigations, this page
1845          // blocked by a content filter puts WebKit into a state where all
1846          // further restoration same-document navigations are 'stuck' on this
1847          // failure page.  Instead, avoid restoring this page completely.
1848          // Consider revisiting this if and when a proper session restoration
1849          // API is provided by WKWebView.
1850          self.navigationManagerImpl->SetWKWebViewNextPendingUrlNotSerializable(
1851              navigationContext->GetUrl());
1852          return;
1853        } else if (!PageTransitionIsNewNavigation(transition)) {
1854          return;
1855        }
1856    }
1857
1858    if (error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange &&
1859        !policyDecisionCancellationError) {
1860      // Handle Frame Load Interrupted errors from WebView. This block is
1861      // executed when web controller rejected the load inside
1862      // decidePolicyForNavigationResponse: to handle download or WKWebView
1863      // opened a Universal Link.
1864      if (!navigationContext->IsDownload()) {
1865        // Non-download navigation was cancelled because WKWebView has opened a
1866        // Universal Link and called webView:didFailProvisionalNavigation:.
1867        self.navigationManagerImpl->DiscardNonCommittedItems();
1868        [self.navigationStates removeNavigation:navigation];
1869      }
1870      return;
1871    }
1872
1873    if (error.code == web::kWebKitErrorCannotShowUrl) {
1874      if (!navigationContext->GetUrl().is_valid()) {
1875        // It won't be possible to load an error page for invalid URL, because
1876        // WKWebView will revert the url to about:blank. Simply discard pending
1877        // item and fail the navigation.
1878        navigationContext->ReleaseItem();
1879        self.webStateImpl->OnNavigationFinished(navigationContext);
1880        self.webStateImpl->OnPageLoaded(navigationContext->GetUrl(), false);
1881        return;
1882      }
1883    }
1884  }
1885
1886  web::NavigationManager* navManager =
1887      self.webStateImpl->GetNavigationManager();
1888  web::NavigationItem* lastCommittedItem = navManager->GetLastCommittedItem();
1889  if (lastCommittedItem && !web::IsWKWebViewSSLCertError(error)) {
1890    // Reset SSL status for last committed navigation to avoid showing security
1891    // status for error pages.
1892    if (!lastCommittedItem->GetSSL().Equals(web::SSLStatus())) {
1893      lastCommittedItem->GetSSL() = web::SSLStatus();
1894      self.webStateImpl->DidChangeVisibleSecurityState();
1895    }
1896  }
1897
1898  web::NavigationItemImpl* item =
1899      web::GetItemWithUniqueID(self.navigationManagerImpl, navigationContext);
1900
1901  if (item) {
1902    if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) {
1903      WKNavigation* errorNavigation =
1904          [self displayErrorPageWithError:error
1905                                inWebView:webView
1906                        isProvisionalLoad:provisionalLoad];
1907
1908      std::unique_ptr<web::NavigationContextImpl> originalContext =
1909          [self.navigationStates removeNavigation:navigation];
1910      originalContext->SetLoadingErrorPage(true);
1911      [self.navigationStates setContext:std::move(originalContext)
1912                          forNavigation:errorNavigation];
1913      // Return as the context was moved.
1914      return;
1915    } else {
1916      GURL errorURL =
1917          net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]);
1918      web::ErrorRetryCommand command = web::ErrorRetryCommand::kDoNothing;
1919      if (provisionalLoad) {
1920        command =
1921            item->error_retry_state_machine().DidFailProvisionalNavigation(
1922                net::GURLWithNSURL(webView.URL), errorURL);
1923      } else {
1924        command = item->error_retry_state_machine().DidFailNavigation(
1925            net::GURLWithNSURL(webView.URL));
1926      }
1927      [self handleErrorRetryCommand:command
1928                     navigationItem:item
1929                  navigationContext:navigationContext
1930                 originalNavigation:navigation
1931                            webView:webView];
1932    }
1933  }
1934
1935  // Don't commit the pending item or call OnNavigationFinished until the
1936  // placeholder navigation finishes loading.
1937}
1938
1939// Displays an error page with details from |error| in |webView| using JS error
1940// pages (associated with the kUseJSForErrorPage flag.) The error page is
1941// presented with |transition| and associated with |blockedNSURL|.
1942- (void)displayError:(NSError*)error
1943    forCancelledNavigationToURL:(NSURL*)blockedNSURL
1944                      inWebView:(WKWebView*)webView
1945                 withTransition:(ui::PageTransition)transition {
1946  DCHECK(base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage));
1947
1948  const GURL blockedURL = net::GURLWithNSURL(blockedNSURL);
1949
1950  // Error page needs the URL string in the error's userInfo for proper
1951  // display.
1952  if (!error.userInfo[NSURLErrorFailingURLStringErrorKey]) {
1953    NSMutableDictionary* updatedUserInfo = [[NSMutableDictionary alloc] init];
1954    [updatedUserInfo addEntriesFromDictionary:error.userInfo];
1955    [updatedUserInfo setObject:blockedNSURL.absoluteString
1956                        forKey:NSURLErrorFailingURLStringErrorKey];
1957
1958    error = [NSError errorWithDomain:error.domain
1959                                code:error.code
1960                            userInfo:updatedUserInfo];
1961  }
1962
1963  WKNavigation* errorNavigation = [self displayErrorPageWithError:error
1964                                                        inWebView:webView
1965                                                isProvisionalLoad:YES];
1966
1967  // Create pending item.
1968  self.navigationManagerImpl->AddPendingItem(
1969      blockedURL, web::Referrer(), transition,
1970      web::NavigationInitiationType::BROWSER_INITIATED);
1971
1972  // Create context.
1973  std::unique_ptr<web::NavigationContextImpl> context =
1974      web::NavigationContextImpl::CreateNavigationContext(
1975          self.webStateImpl, blockedURL,
1976          /*has_user_gesture=*/true, transition,
1977          /*is_renderer_initiated=*/false);
1978  std::unique_ptr<web::NavigationItemImpl> item =
1979      self.navigationManagerImpl->ReleasePendingItem();
1980  context->SetNavigationItemUniqueID(item->GetUniqueID());
1981  context->SetItem(std::move(item));
1982  context->SetError(error);
1983  context->SetLoadingErrorPage(true);
1984
1985  self.webStateImpl->OnNavigationStarted(context.get());
1986
1987  [self.navigationStates setContext:std::move(context)
1988                      forNavigation:errorNavigation];
1989}
1990
1991// Creates and returns a new WKNavigation to load an error page displaying
1992// details of |error| inside |webView|. (Using JS error pages associated with
1993// the kUseJSForErrorPage flag.) |provisionalLoad| should be set according to
1994// whether or not the error occurred during a provisionalLoad.
1995- (WKNavigation*)displayErrorPageWithError:(NSError*)error
1996                                 inWebView:(WKWebView*)webView
1997                         isProvisionalLoad:(BOOL)provisionalLoad {
1998  DCHECK(base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage));
1999
2000  ErrorPageHelper* errorPage = [[ErrorPageHelper alloc] initWithError:error];
2001  WKBackForwardListItem* backForwardItem = webView.backForwardList.currentItem;
2002  // There are 4 possible scenarios here:
2003  //   1. Current nav item is an error page for failed URL;
2004  //   2. Current nav item has a failed URL. This may happen when
2005  //      back/forward/refresh on a loaded page;
2006  //   3. Current nav item is an irrelevant page.
2007  //   4. Current nav item is a session restoration.
2008  // For 1, 2 and 4, load an empty string to remove existing JS code. The URL is
2009  // also updated to the URL of the page that failed to allow back/forward
2010  // navigations even on navigations originating from pushstate. See
2011  // crbug.com/1153261.
2012  // For 3, load error page file to create a new nav item.
2013  // The actual error HTML will be loaded in didFinishNavigation callback.
2014  WKNavigation* errorNavigation = nil;
2015  if (provisionalLoad &&
2016      ![errorPage
2017          isErrorPageFileURLForFailedNavigationURL:backForwardItem.URL] &&
2018      ![backForwardItem.URL isEqual:errorPage.failedNavigationURL] &&
2019      !web::wk_navigation_util::IsRestoreSessionUrl(backForwardItem.URL)) {
2020    errorNavigation = [webView loadFileURL:errorPage.errorPageFileURL
2021                   allowingReadAccessToURL:errorPage.errorPageFileURL];
2022  } else {
2023    errorNavigation = [webView loadHTMLString:@""
2024                                      baseURL:errorPage.failedNavigationURL];
2025  }
2026  [self.navigationStates setState:web::WKNavigationState::REQUESTED
2027                    forNavigation:errorNavigation];
2028
2029  return errorNavigation;
2030}
2031
2032// Handles cancelled load in WKWebView (error with NSURLErrorCancelled code).
2033- (void)handleCancelledError:(NSError*)error
2034               forNavigation:(WKNavigation*)navigation
2035             provisionalLoad:(BOOL)provisionalLoad {
2036  if ([self shouldCancelLoadForCancelledError:error
2037                              provisionalLoad:provisionalLoad]) {
2038    std::unique_ptr<web::NavigationContextImpl> navigationContext =
2039        [self.navigationStates removeNavigation:navigation];
2040    [self loadCancelled];
2041    web::NavigationItemImpl* item =
2042        navigationContext ? web::GetItemWithUniqueID(self.navigationManagerImpl,
2043                                                     navigationContext.get())
2044                          : nullptr;
2045    if (self.navigationManagerImpl->GetPendingItem() == item) {
2046      self.navigationManagerImpl->DiscardNonCommittedItems();
2047    }
2048
2049    if (provisionalLoad) {
2050      if (!navigationContext &&
2051          web::RequiresProvisionalNavigationFailureWorkaround()) {
2052        // It is likely that |navigationContext| is null because
2053        // didStartProvisionalNavigation: was not called with this WKNavigation
2054        // object. Log UMA to know when this workaround can be removed and
2055        // do not call OnNavigationFinished() to avoid crash on null pointer
2056        // dereferencing. See crbug.com/973653 for details.
2057        UMA_HISTOGRAM_BOOLEAN(
2058            "Navigation.IOSNullContextInDidFailProvisionalNavigation", true);
2059      } else {
2060        self.webStateImpl->OnNavigationFinished(navigationContext.get());
2061      }
2062    }
2063  } else if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) &&
2064             !provisionalLoad) {
2065    web::NavigationContextImpl* navigationContext =
2066        [self.navigationStates contextForNavigation:navigation];
2067    web::NavigationItemImpl* item =
2068        navigationContext ? web::GetItemWithUniqueID(self.navigationManagerImpl,
2069                                                     navigationContext)
2070                          : nullptr;
2071    if (item) {
2072      // Since the navigation has already been committed, it will retain its
2073      // back / forward item even though the load has been cancelled. Update the
2074      // error state machine so that if future loads of this item fail, the same
2075      // item will be reused for the error view rather than loading a
2076      // placeholder URL into a new navigation item, since the latter would
2077      // destroy the forward list.
2078      item->error_retry_state_machine().SetNoNavigationError();
2079    }
2080  }
2081}
2082
2083// Executes the command specified by the ErrorRetryStateMachine.
2084- (void)handleErrorRetryCommand:(web::ErrorRetryCommand)command
2085                 navigationItem:(web::NavigationItemImpl*)item
2086              navigationContext:(web::NavigationContextImpl*)context
2087             originalNavigation:(WKNavigation*)originalNavigation
2088                        webView:(WKWebView*)webView {
2089  DCHECK(!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage));
2090  if (command == web::ErrorRetryCommand::kDoNothing)
2091    return;
2092
2093  DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID());
2094  switch (command) {
2095    case web::ErrorRetryCommand::kLoadPlaceholder: {
2096      // This case only happens when a new request failed in provisional
2097      // navigation. Disassociate the navigation context from the original
2098      // request and resuse it for the placeholder navigation.
2099      std::unique_ptr<web::NavigationContextImpl> originalContext =
2100          [self.navigationStates removeNavigation:originalNavigation];
2101      [self loadPlaceholderInWebViewForURL:item->GetURL()
2102                         rendererInitiated:context->IsRendererInitiated()
2103                                forContext:std::move(originalContext)];
2104    } break;
2105
2106    case web::ErrorRetryCommand::kLoadError:
2107      [self loadErrorPageForNavigationItem:item
2108                         navigationContext:originalNavigation
2109                                   webView:webView];
2110      break;
2111
2112    case web::ErrorRetryCommand::kReload:
2113      [webView reload];
2114      break;
2115
2116    case web::ErrorRetryCommand::kRewriteToWebViewURL: {
2117      std::unique_ptr<web::NavigationContextImpl> navigationContext =
2118          [self.delegate navigationHandler:self
2119                 registerLoadRequestForURL:item->GetURL()
2120                    sameDocumentNavigation:NO
2121                            hasUserGesture:NO
2122                         rendererInitiated:context->IsRendererInitiated()
2123                     placeholderNavigation:NO];
2124      WKNavigation* navigation =
2125          [webView loadHTMLString:@""
2126                          baseURL:net::NSURLWithGURL(item->GetURL())];
2127      navigationContext->SetError(context->GetError());
2128      navigationContext->SetIsPost(context->IsPost());
2129      [self.navigationStates setContext:std::move(navigationContext)
2130                          forNavigation:navigation];
2131    } break;
2132
2133    case web::ErrorRetryCommand::kRewriteToPlaceholderURL: {
2134      std::unique_ptr<web::NavigationContextImpl> originalContext =
2135          [self.navigationStates removeNavigation:originalNavigation];
2136      originalContext->SetPlaceholderNavigation(YES);
2137      GURL placeholderURL = CreatePlaceholderUrlForUrl(item->GetURL());
2138
2139      WKNavigation* navigation =
2140          [webView loadHTMLString:@""
2141                          baseURL:net::NSURLWithGURL(placeholderURL)];
2142      [self.navigationStates setContext:std::move(originalContext)
2143                          forNavigation:navigation];
2144    } break;
2145
2146    case web::ErrorRetryCommand::kDoNothing:
2147      NOTREACHED();
2148  }
2149}
2150
2151// Used to decide whether a load that generates errors with the
2152// NSURLErrorCancelled code should be cancelled.
2153- (BOOL)shouldCancelLoadForCancelledError:(NSError*)error
2154                          provisionalLoad:(BOOL)provisionalLoad {
2155  DCHECK(error.code == NSURLErrorCancelled ||
2156         error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange);
2157  // Do not cancel the load if it is for an app specific URL, as such errors
2158  // are produced during the app specific URL load process.
2159  const GURL errorURL =
2160      net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]);
2161  if (web::GetWebClient()->IsAppSpecificURL(errorURL))
2162    return NO;
2163
2164  return provisionalLoad;
2165}
2166
2167// Loads the error page.
2168- (void)loadErrorPageForNavigationItem:(web::NavigationItemImpl*)item
2169                     navigationContext:(WKNavigation*)navigation
2170                               webView:(WKWebView*)webView {
2171  web::NavigationContextImpl* context =
2172      [self.navigationStates contextForNavigation:navigation];
2173  NSError* error = context->GetError();
2174  DCHECK(error);
2175  DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID());
2176
2177  net::SSLInfo info;
2178  base::Optional<net::SSLInfo> ssl_info = base::nullopt;
2179
2180  if (web::IsWKWebViewSSLCertError(error)) {
2181    web::GetSSLInfoFromWKWebViewSSLCertError(error, &info);
2182    if (info.cert) {
2183      // Retrieve verification results from _certVerificationErrors cache to
2184      // avoid unnecessary recalculations. Verification results are cached for
2185      // the leaf cert, because the cert chain in
2186      // |didReceiveAuthenticationChallenge:| is the OS constructed chain, while
2187      // |chain| is the chain from the server.
2188      NSArray* chain = error.userInfo[web::kNSErrorPeerCertificateChainKey];
2189      NSURL* requestURL = error.userInfo[web::kNSErrorFailingURLKey];
2190      NSString* host = requestURL.host;
2191      scoped_refptr<net::X509Certificate> leafCert;
2192      if (chain.count && host.length) {
2193        // The complete cert chain may not be available, so the leaf cert is
2194        // used as a key to retrieve _certVerificationErrors, as well as for
2195        // storing the cert decision.
2196        leafCert = web::CreateCertFromChain(@[ chain.firstObject ]);
2197        if (leafCert) {
2198          auto error = _certVerificationErrors->Get(
2199              {leafCert, base::SysNSStringToUTF8(host)});
2200          bool cacheHit = error != _certVerificationErrors->end();
2201          if (cacheHit) {
2202            info.is_fatal_cert_error = error->second.is_recoverable;
2203            info.cert_status = error->second.status;
2204          }
2205          UMA_HISTOGRAM_BOOLEAN("WebController.CertVerificationErrorsCacheHit",
2206                                cacheHit);
2207        }
2208      }
2209    }
2210    ssl_info = base::make_optional<net::SSLInfo>(info);
2211  }
2212  NSString* failingURLString =
2213      error.userInfo[NSURLErrorFailingURLStringErrorKey];
2214  GURL failingURL(base::SysNSStringToUTF8(failingURLString));
2215  GURL itemURL = item->GetURL();
2216  if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) {
2217    if (itemURL != failingURL)
2218      item->SetVirtualURL(failingURL);
2219  }
2220  int itemID = item->GetUniqueID();
2221  web::GetWebClient()->PrepareErrorPage(
2222      self.webStateImpl, failingURL, error, context->IsPost(),
2223      self.webStateImpl->GetBrowserState()->IsOffTheRecord(), ssl_info,
2224      context->GetNavigationId(), base::BindOnce(^(NSString* errorHTML) {
2225        if (errorHTML) {
2226          if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) {
2227            ErrorPageHelper* errorPageHelper =
2228                [[ErrorPageHelper alloc] initWithError:context->GetError()];
2229
2230            [webView evaluateJavaScript:[errorPageHelper
2231                                            scriptForInjectingHTML:errorHTML
2232                                                addAutomaticReload:YES]
2233                      completionHandler:^(id result, NSError* error) {
2234                        DCHECK(!error)
2235                            << "Error injecting error page HTML: "
2236                            << base::SysNSStringToUTF8(error.description);
2237                      }];
2238          } else {
2239            WKNavigation* navigation =
2240                [webView loadHTMLString:errorHTML
2241                                baseURL:net::NSURLWithGURL(failingURL)];
2242            auto loadHTMLContext =
2243                web::NavigationContextImpl::CreateNavigationContext(
2244                    self.webStateImpl, failingURL,
2245                    /*has_user_gesture=*/false, ui::PAGE_TRANSITION_FIRST,
2246                    /*is_renderer_initiated=*/false);
2247
2248            if (!base::FeatureList::IsEnabled(
2249                    web::features::kUseJSForErrorPage))
2250              loadHTMLContext->SetLoadingErrorPage(true);
2251
2252            loadHTMLContext->SetNavigationItemUniqueID(itemID);
2253
2254            [self.navigationStates setContext:std::move(loadHTMLContext)
2255                                forNavigation:navigation];
2256            [self.navigationStates setState:web::WKNavigationState::REQUESTED
2257                              forNavigation:navigation];
2258          }
2259        }
2260
2261        if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) {
2262          // TODO(crbug.com/803503): only call these for placeholder navigation
2263          // because they should have already been triggered during navigation
2264          // commit for failures that happen after commit.
2265          [self.delegate navigationHandlerDidStartLoading:self];
2266          // TODO(crbug.com/973765): This is a workaround because |item| might
2267          // get released after
2268          // |self.navigationManagerImpl->
2269          // CommitPendingItem(context->ReleaseItem()|.
2270          // Remove this once navigation refactor is done.
2271          web::NavigationContextImpl* context =
2272              [self.navigationStates contextForNavigation:navigation];
2273          self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem());
2274          [self.delegate navigationHandler:self
2275                            setDocumentURL:itemURL
2276                                   context:context];
2277
2278          // If |context| is a placeholder navigation, this is the second part
2279          // of the error page load for a provisional load failure. Rewrite the
2280          // context URL to actual URL and trigger the deferred
2281          // |OnNavigationFinished| callback. This is also needed if |context|
2282          // is not yet committed, which can happen on a reload/back/forward
2283          // load that failed in provisional navigation.
2284          if ((!base::FeatureList::IsEnabled(
2285                   web::features::kUseJSForErrorPage) &&
2286               context->IsPlaceholderNavigation()) ||
2287              !context->HasCommitted()) {
2288            context->SetUrl(itemURL);
2289            if (!base::FeatureList::IsEnabled(
2290                    web::features::kUseJSForErrorPage))
2291              context->SetPlaceholderNavigation(false);
2292            context->SetHasCommitted(true);
2293            self.webStateImpl->OnNavigationFinished(context);
2294          }
2295        } else {
2296          // TODO(crbug.com/973765): This is a workaround because |item| might
2297          // get released after
2298          // |self.navigationManagerImpl->
2299          // CommitPendingItem(context->ReleaseItem()|.
2300          // Remove this once navigation refactor is done.
2301          web::NavigationContextImpl* context =
2302              [self.navigationStates contextForNavigation:navigation];
2303          self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem());
2304          [self.delegate navigationHandler:self
2305                            setDocumentURL:itemURL
2306                                   context:context];
2307
2308          // Rewrite the context URL to actual URL and trigger the deferred
2309          // |OnNavigationFinished| callback.
2310          context->SetUrl(failingURL);
2311          context->SetHasCommitted(true);
2312          self.webStateImpl->OnNavigationFinished(context);
2313        }
2314
2315        // For SSL cert error pages, SSLStatus needs to be set manually because
2316        // the placeholder navigation for the error page is committed and
2317        // there is no server trust (since there's no network navigation), which
2318        // is required to create a cert in CRWSSLStatusUpdater.
2319        if (web::IsWKWebViewSSLCertError(context->GetError())) {
2320          web::SSLStatus& SSLStatus =
2321              self.navigationManagerImpl->GetLastCommittedItem()->GetSSL();
2322          SSLStatus.cert_status = info.cert_status;
2323          SSLStatus.certificate = info.cert;
2324          SSLStatus.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN;
2325          self.webStateImpl->DidChangeVisibleSecurityState();
2326        }
2327
2328        [self.delegate navigationHandler:self
2329              didCompleteLoadWithSuccess:NO
2330                              forContext:context];
2331        self.webStateImpl->OnPageLoaded(failingURL, NO);
2332      }));
2333}
2334
2335// Resets any state that is associated with a specific document object (e.g.,
2336// page interaction tracking).
2337- (void)resetDocumentSpecificState {
2338  self.userInteractionState->SetLastUserInteraction(nullptr);
2339  self.userInteractionState->SetTapInProgress(false);
2340}
2341
2342#pragma mark - Public methods
2343
2344- (void)stopLoading {
2345  self.pendingNavigationInfo.cancelled = YES;
2346  [self loadCancelled];
2347  _certVerificationErrors->Clear();
2348}
2349
2350- (void)loadCancelled {
2351  // TODO(crbug.com/821995):  Check if this function should be removed.
2352  if (self.navigationState != web::WKNavigationState::FINISHED) {
2353    self.navigationState = web::WKNavigationState::FINISHED;
2354    if (!self.beingDestroyed) {
2355      self.webStateImpl->SetIsLoading(false);
2356    }
2357  }
2358}
2359
2360// Returns context for pending navigation that has |URL|. null if there is no
2361// matching pending navigation.
2362- (web::NavigationContextImpl*)contextForPendingMainFrameNavigationWithURL:
2363    (const GURL&)URL {
2364  // Here the enumeration variable |navigation| is __strong to allow setting it
2365  // to nil.
2366  for (__strong id navigation in [self.navigationStates pendingNavigations]) {
2367    if (navigation == [NSNull null]) {
2368      // null is a valid navigation object passed to WKNavigationDelegate
2369      // callbacks and represents window opening action.
2370      navigation = nil;
2371    }
2372
2373    web::NavigationContextImpl* context =
2374        [self.navigationStates contextForNavigation:navigation];
2375    if (context && context->GetUrl() == URL) {
2376      return context;
2377    }
2378  }
2379  return nullptr;
2380}
2381
2382- (BOOL)isCurrentNavigationBackForward {
2383  if (!self.currentNavItem)
2384    return NO;
2385  WKNavigationType currentNavigationType =
2386      self.currentBackForwardListItemHolder->navigation_type();
2387  return currentNavigationType == WKNavigationTypeBackForward;
2388}
2389
2390- (BOOL)isCurrentNavigationItemPOST {
2391  // |self.navigationHandler.pendingNavigationInfo| will be nil if the
2392  // decidePolicy* delegate methods were not called.
2393  NSString* HTTPMethod =
2394      self.pendingNavigationInfo
2395          ? self.pendingNavigationInfo.HTTPMethod
2396          : self.currentBackForwardListItemHolder->http_method();
2397  if ([HTTPMethod isEqual:@"POST"]) {
2398    return YES;
2399  }
2400  if (!self.currentNavItem) {
2401    return NO;
2402  }
2403  return self.currentNavItem->HasPostData();
2404}
2405
2406// Returns the WKBackForwardListItemHolder for the current navigation item.
2407- (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder {
2408  web::NavigationItem* item = self.currentNavItem;
2409  DCHECK(item);
2410  web::WKBackForwardListItemHolder* holder =
2411      web::WKBackForwardListItemHolder::FromNavigationItem(item);
2412  DCHECK(holder);
2413  return holder;
2414}
2415
2416// Updates current state with any pending information. Should be called when a
2417// navigation is committed.
2418- (void)commitPendingNavigationInfoInWebView:(WKWebView*)webView {
2419  if (self.pendingNavigationInfo.referrer) {
2420    _currentReferrerString = [self.pendingNavigationInfo.referrer copy];
2421  }
2422  [self updateCurrentBackForwardListItemHolderInWebView:webView];
2423
2424  self.pendingNavigationInfo = nil;
2425}
2426
2427// Updates the WKBackForwardListItemHolder navigation item.
2428- (void)updateCurrentBackForwardListItemHolderInWebView:(WKWebView*)webView {
2429  if (!self.currentNavItem) {
2430    // TODO(crbug.com/925304): Pending item (which stores the holder) should be
2431    // owned by NavigationContext object. Pending item should never be null.
2432    return;
2433  }
2434
2435  web::WKBackForwardListItemHolder* holder =
2436      self.currentBackForwardListItemHolder;
2437
2438  WKNavigationType navigationType =
2439      self.pendingNavigationInfo ? self.pendingNavigationInfo.navigationType
2440                                 : WKNavigationTypeOther;
2441  holder->set_back_forward_list_item(webView.backForwardList.currentItem);
2442  holder->set_navigation_type(navigationType);
2443  holder->set_http_method(self.pendingNavigationInfo.HTTPMethod);
2444
2445  // Only update the MIME type in the holder if there was MIME type information
2446  // as part of this pending load. It will be nil when doing a fast
2447  // back/forward navigation, for instance, because the callback that would
2448  // populate it is not called in that flow.
2449  if (self.pendingNavigationInfo.MIMEType)
2450    holder->set_mime_type(self.pendingNavigationInfo.MIMEType);
2451}
2452
2453- (web::Referrer)currentReferrer {
2454  // Referrer string doesn't include the fragment, so in cases where the
2455  // previous URL is equal to the current referrer plus the fragment the
2456  // previous URL is returned as current referrer.
2457  NSString* referrerString = _currentReferrerString;
2458
2459  // In case of an error evaluating the JavaScript simply return empty string.
2460  if (referrerString.length == 0)
2461    return web::Referrer();
2462
2463  web::NavigationItem* item = self.currentNavItem;
2464  GURL navigationURL = item ? item->GetVirtualURL() : GURL::EmptyGURL();
2465  NSString* previousURLString = base::SysUTF8ToNSString(navigationURL.spec());
2466  // Check if the referrer is equal to the previous URL minus the hash symbol.
2467  // L'#' is used to convert the char '#' to a unichar.
2468  if ([previousURLString length] > referrerString.length &&
2469      [previousURLString hasPrefix:referrerString] &&
2470      [previousURLString characterAtIndex:referrerString.length] == L'#') {
2471    referrerString = previousURLString;
2472  }
2473  // Since referrer is being extracted from the destination page, the correct
2474  // policy from the origin has *already* been applied. Since the extracted URL
2475  // is the post-policy value, and the source policy is no longer available,
2476  // the policy is set to Always so that whatever WebKit decided to send will be
2477  // re-sent when replaying the entry.
2478  // TODO(crbug.com/227769): When possible, get the real referrer and policy in
2479  // advance and use that instead.
2480  return web::Referrer(GURL(base::SysNSStringToUTF8(referrerString)),
2481                       web::ReferrerPolicyAlways);
2482}
2483
2484- (void)setLastCommittedNavigationItemTitle:(NSString*)title {
2485  DCHECK(title);
2486  web::NavigationItem* item =
2487      self.navigationManagerImpl->GetLastCommittedItem();
2488  if (!item)
2489    return;
2490
2491  item->SetTitle(base::SysNSStringToUTF16(title));
2492  self.webStateImpl->OnTitleChanged();
2493}
2494
2495- (ui::PageTransition)pageTransitionFromNavigationType:
2496    (WKNavigationType)navigationType {
2497  switch (navigationType) {
2498    case WKNavigationTypeLinkActivated:
2499      return ui::PAGE_TRANSITION_LINK;
2500    case WKNavigationTypeFormSubmitted:
2501    case WKNavigationTypeFormResubmitted:
2502      return ui::PAGE_TRANSITION_FORM_SUBMIT;
2503    case WKNavigationTypeBackForward:
2504      return ui::PAGE_TRANSITION_FORWARD_BACK;
2505    case WKNavigationTypeReload:
2506      return ui::PAGE_TRANSITION_RELOAD;
2507    case WKNavigationTypeOther:
2508      // The "Other" type covers a variety of very different cases, which may
2509      // or may not be the result of user actions. For now, guess based on
2510      // whether there's been an interaction since the last URL change.
2511      // TODO(crbug.com/549301): See if this heuristic can be improved.
2512      return self.userInteractionState
2513                     ->UserInteractionRegisteredSinceLastUrlChange()
2514                 ? ui::PAGE_TRANSITION_LINK
2515                 : ui::PAGE_TRANSITION_CLIENT_REDIRECT;
2516  }
2517}
2518
2519- (web::NavigationContextImpl*)
2520    loadPlaceholderInWebViewForURL:(const GURL&)originalURL
2521                 rendererInitiated:(BOOL)rendererInitiated
2522                        forContext:(std::unique_ptr<web::NavigationContextImpl>)
2523                                       originalContext {
2524  DCHECK(!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage));
2525  GURL placeholderURL = CreatePlaceholderUrlForUrl(originalURL);
2526
2527  WKWebView* webView = [self.delegate webViewForWebViewHandler:self];
2528
2529  NSURLRequest* request =
2530      [NSURLRequest requestWithURL:net::NSURLWithGURL(placeholderURL)];
2531  WKNavigation* navigation = [webView loadRequest:request];
2532
2533  NSError* error = originalContext ? originalContext->GetError() : nil;
2534  if (web::RequiresContentFilterBlockingWorkaround() &&
2535      [error.domain isEqual:base::SysUTF8ToNSString(web::kWebKitErrorDomain)] &&
2536      error.code == web::kWebKitErrorUrlBlockedByContentFilter) {
2537    GURL currentWKItemURL =
2538        net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
2539    if (currentWKItemURL.SchemeIs(url::kAboutScheme)) {
2540      // WKWebView will pass nil WKNavigation objects to WKNavigationDelegate
2541      // callback for this navigation. TODO(crbug.com/954332): Remove the
2542      // workaround when https://bugs.webkit.org/show_bug.cgi?id=196930 is
2543      // fixed.
2544      navigation = nil;
2545    }
2546  }
2547
2548  [self.navigationStates setState:web::WKNavigationState::REQUESTED
2549                    forNavigation:navigation];
2550  std::unique_ptr<web::NavigationContextImpl> navigationContext;
2551  if (originalContext) {
2552    navigationContext = std::move(originalContext);
2553    navigationContext->SetPlaceholderNavigation(YES);
2554  } else {
2555    navigationContext = [self.delegate navigationHandler:self
2556                               registerLoadRequestForURL:originalURL
2557                                  sameDocumentNavigation:NO
2558                                          hasUserGesture:NO
2559                                       rendererInitiated:rendererInitiated
2560                                   placeholderNavigation:YES];
2561  }
2562  [self.navigationStates setContext:std::move(navigationContext)
2563                      forNavigation:navigation];
2564  return [self.navigationStates contextForNavigation:navigation];
2565}
2566
2567- (void)webPageChangedWithContext:(web::NavigationContextImpl*)context
2568                          webView:(WKWebView*)webView {
2569  web::Referrer referrer = self.currentReferrer;
2570  // If no referrer was known in advance, record it now. (If there was one,
2571  // keep it since it will have a more accurate URL and policy than what can
2572  // be extracted from the landing page.)
2573  web::NavigationItem* currentItem = self.currentNavItem;
2574
2575  // TODO(crbug.com/925304): Pending item (which should be used here) should be
2576  // owned by NavigationContext object. Pending item should never be null.
2577  if (currentItem && !currentItem->GetReferrer().url.is_valid()) {
2578    currentItem->SetReferrer(referrer);
2579  }
2580
2581  // TODO(crbug.com/956511): This shouldn't be called for push/replaceState.
2582  [self resetDocumentSpecificState];
2583
2584  [self.delegate navigationHandlerDidStartLoading:self];
2585  // Do not commit pending item in the middle of loading a placeholder URL. The
2586  // item will be committed when webUI is displayed.
2587  if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) ||
2588      !context->IsPlaceholderNavigation()) {
2589    self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem());
2590    if (context->IsLoadingHtmlString()) {
2591      self.navigationManagerImpl->GetLastCommittedItem()->SetURL(
2592          context->GetUrl());
2593    }
2594  }
2595}
2596
2597@end
2598