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