| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/web/navigation/crw_wk_navigation_handler.h" |
| |
| #include "base/feature_list.h" |
| #include "base/ios/ios_util.h" |
| #import "base/ios/ns_error_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/timer/timer.h" |
| #import "ios/net/http_response_headers_util.h" |
| #import "ios/net/protocol_handler_util.h" |
| #import "ios/net/url_scheme_util.h" |
| #include "ios/web/common/features.h" |
| #import "ios/web/common/url_scheme_util.h" |
| #import "ios/web/js_messaging/crw_js_injector.h" |
| #import "ios/web/js_messaging/web_frames_manager_impl.h" |
| #import "ios/web/navigation/crw_error_page_helper.h" |
| #import "ios/web/navigation/crw_navigation_item_holder.h" |
| #import "ios/web/navigation/crw_pending_navigation_info.h" |
| #import "ios/web/navigation/crw_wk_navigation_states.h" |
| #include "ios/web/navigation/error_retry_state_machine.h" |
| #import "ios/web/navigation/navigation_context_impl.h" |
| #import "ios/web/navigation/navigation_manager_impl.h" |
| #include "ios/web/navigation/navigation_manager_util.h" |
| #import "ios/web/navigation/web_kit_constants.h" |
| #import "ios/web/navigation/wk_back_forward_list_item_holder.h" |
| #import "ios/web/navigation/wk_navigation_action_policy_util.h" |
| #import "ios/web/navigation/wk_navigation_action_util.h" |
| #import "ios/web/navigation/wk_navigation_util.h" |
| #include "ios/web/public/browser_state.h" |
| #import "ios/web/public/download/download_controller.h" |
| #import "ios/web/public/web_client.h" |
| #import "ios/web/security/crw_cert_verification_controller.h" |
| #import "ios/web/security/wk_web_view_security_util.h" |
| #import "ios/web/session/session_certificate_policy_cache_impl.h" |
| #import "ios/web/web_state/user_interaction_state.h" |
| #import "ios/web/web_state/web_state_impl.h" |
| #include "ios/web/web_view/content_type_util.h" |
| #import "ios/web/web_view/error_translation_util.h" |
| #import "ios/web/web_view/wk_security_origin_util.h" |
| #import "ios/web/web_view/wk_web_view_util.h" |
| #import "net/base/mac/url_conversions.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/x509_util_ios.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| // TODO(crbug.com/1038303): Remove references to "Placeholder". |
| using web::wk_navigation_util::IsPlaceholderUrl; |
| using web::wk_navigation_util::CreatePlaceholderUrlForUrl; |
| using web::wk_navigation_util::ExtractUrlFromPlaceholderUrl; |
| using web::wk_navigation_util::kReferrerHeaderName; |
| using web::wk_navigation_util::IsRestoreSessionUrl; |
| using web::wk_navigation_util::IsWKInternalUrl; |
| |
| namespace { |
| // Maximum number of errors to store in cert verification errors cache. |
| // Cache holds errors only for pending navigations, so the actual number of |
| // stored errors is not expected to be high. |
| const web::CertVerificationErrorsCacheType::size_type kMaxCertErrorsCount = 100; |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class OutOfSyncURLAction { |
| kNoAction = 0, |
| kGoBack = 1, |
| kGoForward = 2, |
| kMaxValue = kGoForward, |
| }; |
| |
| void ReportOutOfSyncURLInDidStartProvisionalNavigation( |
| OutOfSyncURLAction action) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "WebController.BackForwardListOutOfSyncInProvisionalNavigation", action); |
| } |
| |
| } // namespace |
| |
| @interface CRWWKNavigationHandler () { |
| // Referrer for the current page; does not include the fragment. |
| NSString* _currentReferrerString; |
| |
| // CertVerification errors which happened inside |
| // |webView:didReceiveAuthenticationChallenge:completionHandler:|. |
| // Key is leaf-cert/host pair. This storage is used to carry calculated |
| // cert status from |didReceiveAuthenticationChallenge:| to |
| // |didFailProvisionalNavigation:| delegate method. |
| std::unique_ptr<web::CertVerificationErrorsCacheType> _certVerificationErrors; |
| } |
| |
| @property(nonatomic, weak) id<CRWWKNavigationHandlerDelegate> delegate; |
| |
| // Returns the WebStateImpl from self.delegate. |
| @property(nonatomic, readonly, assign) web::WebStateImpl* webStateImpl; |
| // Returns the NavigationManagerImpl from self.webStateImpl. |
| @property(nonatomic, readonly, assign) |
| web::NavigationManagerImpl* navigationManagerImpl; |
| // Returns the UserInteractionState from self.delegate. |
| @property(nonatomic, readonly, assign) |
| web::UserInteractionState* userInteractionState; |
| // Returns the CRWCertVerificationController from self.delegate. |
| @property(nonatomic, readonly, weak) |
| CRWCertVerificationController* certVerificationController; |
| // Returns the docuemnt URL from self.delegate. |
| @property(nonatomic, readonly, assign) GURL documentURL; |
| // Returns the js injector from self.delegate. |
| @property(nonatomic, readonly, weak) CRWJSInjector* JSInjector; |
| |
| @end |
| |
| @implementation CRWWKNavigationHandler |
| |
| - (instancetype)initWithDelegate:(id<CRWWKNavigationHandlerDelegate>)delegate { |
| if (self = [super init]) { |
| _navigationStates = [[CRWWKNavigationStates alloc] init]; |
| // Load phase when no WebView present is 'loaded' because this represents |
| // the idle state. |
| _navigationState = web::WKNavigationState::FINISHED; |
| |
| _certVerificationErrors = |
| std::make_unique<web::CertVerificationErrorsCacheType>( |
| kMaxCertErrorsCount); |
| |
| _delegate = delegate; |
| } |
| return self; |
| } |
| |
| #pragma mark - WKNavigationDelegate |
| |
| - (void)webView:(WKWebView*)webView |
| decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction |
| preferences:(WKWebpagePreferences*)preferences |
| decisionHandler: |
| (void (^)(WKNavigationActionPolicy, |
| WKWebpagePreferences*))decisionHandler |
| API_AVAILABLE(ios(13)) { |
| web::UserAgentType userAgentType = |
| [self userAgentForNavigationAction:navigationAction webView:webView]; |
| |
| if (navigationAction.navigationType == WKNavigationTypeBackForward && |
| userAgentType != web::UserAgentType::NONE && |
| self.webStateImpl->GetUserAgentForSessionRestoration() != |
| web::UserAgentType::AUTOMATIC) { |
| // When navigating back to a page with a UserAgent that wasn't automatic, |
| // let's reuse this user agent for next navigations. |
| self.webStateImpl->SetUserAgent(userAgentType); |
| } |
| |
| if (navigationAction.navigationType == WKNavigationTypeReload && |
| userAgentType != web::UserAgentType::NONE && |
| web::wk_navigation_util::URLNeedsUserAgentType( |
| net::GURLWithNSURL(navigationAction.request.URL))) { |
| // When reloading the page, the UserAgent will be updated to the one for the |
| // new page. |
| web::NavigationItem* item = [[CRWNavigationItemHolder |
| holderForBackForwardListItem:webView.backForwardList.currentItem] |
| navigationItem]; |
| if (item) |
| item->SetUserAgentType(userAgentType); |
| } |
| |
| if (userAgentType != web::UserAgentType::NONE) { |
| NSString* userAgentString = base::SysUTF8ToNSString( |
| web::GetWebClient()->GetUserAgent(userAgentType)); |
| if (![webView.customUserAgent isEqualToString:userAgentString]) { |
| webView.customUserAgent = userAgentString; |
| } |
| } |
| |
| WKContentMode contentMode = userAgentType == web::UserAgentType::DESKTOP |
| ? WKContentModeDesktop |
| : WKContentModeMobile; |
| |
| [self webView:webView |
| decidePolicyForNavigationAction:navigationAction |
| decisionHandler:^(WKNavigationActionPolicy policy) { |
| preferences.preferredContentMode = contentMode; |
| decisionHandler(policy, preferences); |
| }]; |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| decidePolicyForNavigationAction:(WKNavigationAction*)action |
| decisionHandler: |
| (void (^)(WKNavigationActionPolicy))decisionHandler { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| if (@available(iOS 13, *)) { |
| } else { |
| // As webView:decidePolicyForNavigationAction:preferences:decisionHandler: |
| // is only called for iOS 13, the code is duplicated here to also have it |
| // for iOS 12. |
| web::UserAgentType userAgentType = |
| [self userAgentForNavigationAction:action webView:webView]; |
| |
| if (action.navigationType == WKNavigationTypeBackForward && |
| userAgentType != web::UserAgentType::NONE && |
| self.webStateImpl->GetUserAgentForSessionRestoration() != |
| web::UserAgentType::AUTOMATIC) { |
| // When navigating back to a page with a UserAgent that wasn't automatic, |
| // let's reuse this user agent for next navigations. |
| self.webStateImpl->SetUserAgent(userAgentType); |
| } |
| |
| if (action.navigationType == WKNavigationTypeReload && |
| userAgentType != web::UserAgentType::NONE && |
| web::wk_navigation_util::URLNeedsUserAgentType( |
| net::GURLWithNSURL(action.request.URL))) { |
| // When reloading the page, the UserAgent will be updated to the one for |
| // the new page. |
| web::NavigationItem* item = [[CRWNavigationItemHolder |
| holderForBackForwardListItem:webView.backForwardList.currentItem] |
| navigationItem]; |
| if (item) |
| item->SetUserAgentType(userAgentType); |
| } |
| |
| if (userAgentType != web::UserAgentType::NONE) { |
| NSString* userAgentString = base::SysUTF8ToNSString( |
| web::GetWebClient()->GetUserAgent(userAgentType)); |
| if (![webView.customUserAgent isEqualToString:userAgentString]) { |
| webView.customUserAgent = userAgentString; |
| } |
| } |
| } |
| |
| _webProcessCrashed = NO; |
| if (self.beingDestroyed) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| return; |
| } |
| |
| GURL requestURL = net::GURLWithNSURL(action.request.URL); |
| |
| // Workaround for a WKWebView bug where the web content loaded using |
| // |-loadHTMLString:baseURL| clobbers the next WKBackForwardListItem. It works |
| // by detecting back/forward navigation to a clobbered item and replacing the |
| // clobberred item and its forward history using a partial session restore in |
| // the current web view. There is an unfortunate caveat: if the workaround is |
| // triggered in a back navigation to a clobbered item, the restored forward |
| // session is inserted after the current item before the back navigation, so |
| // it doesn't fully replaces the "bad" history, even though user will be |
| // navigated to the expected URL and may not notice the issue until they |
| // review the back history by long pressing on "Back" button. |
| // |
| // TODO(crbug.com/887497): remove this workaround once iOS ships the fix. |
| if (action.targetFrame.mainFrame) { |
| GURL webViewURL = net::GURLWithNSURL(webView.URL); |
| GURL currentWKItemURL = |
| net::GURLWithNSURL(webView.backForwardList.currentItem.URL); |
| GURL backItemURL = net::GURLWithNSURL(webView.backForwardList.backItem.URL); |
| web::NavigationContextImpl* context = |
| [self contextForPendingMainFrameNavigationWithURL:webViewURL]; |
| bool willClobberHistory = |
| action.navigationType == WKNavigationTypeBackForward && |
| requestURL == backItemURL && webView.backForwardList.currentItem && |
| requestURL != currentWKItemURL && currentWKItemURL == webViewURL && |
| context && |
| (context->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK); |
| |
| UMA_HISTOGRAM_BOOLEAN("IOS.WKWebViewClobberedHistory", willClobberHistory); |
| |
| if (willClobberHistory && base::FeatureList::IsEnabled( |
| web::features::kHistoryClobberWorkaround)) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| self.navigationManagerImpl |
| ->ApplyWKWebViewForwardHistoryClobberWorkaround(); |
| return; |
| } |
| } |
| |
| // The page will not be changed until this navigation is committed, so the |
| // retrieved state will be pending until |didCommitNavigation| callback. |
| [self createPendingNavigationInfoFromNavigationAction:action]; |
| |
| if (action.targetFrame.mainFrame && |
| action.navigationType == WKNavigationTypeBackForward) { |
| web::NavigationContextImpl* context = |
| [self contextForPendingMainFrameNavigationWithURL:requestURL]; |
| if (context) { |
| // Context is null for renderer-initiated navigations. |
| int index = web::GetCommittedItemIndexWithUniqueID( |
| self.navigationManagerImpl, context->GetNavigationItemUniqueID()); |
| self.navigationManagerImpl->SetPendingItemIndex(index); |
| } |
| } |
| |
| // If this is a placeholder navigation, pass through. |
| if ((!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| IsPlaceholderUrl(requestURL)) || |
| (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| [CRWErrorPageHelper isErrorPageFileURL:requestURL])) { |
| if (action.sourceFrame.mainFrame) { |
| // Disallow renderer initiated navigations to placeholder URLs. |
| decisionHandler(WKNavigationActionPolicyCancel); |
| } else { |
| decisionHandler(WKNavigationActionPolicyAllow); |
| } |
| return; |
| } |
| |
| ui::PageTransition transition = |
| [self pageTransitionFromNavigationType:action.navigationType]; |
| BOOL isMainFrameNavigationAction = [self isMainFrameNavigationAction:action]; |
| if (isMainFrameNavigationAction) { |
| web::NavigationContextImpl* context = |
| [self contextForPendingMainFrameNavigationWithURL:requestURL]; |
| // Theoretically if |context| can be found here, the navigation should be |
| // either user-initiated or JS back/forward. The second part in the "if" |
| // condition used to be a DCHECK, but it would fail in this case: |
| // 1. Multiple render-initiated navigation with the same URL happens at the |
| // same time; |
| // 2. One of these navigations gets the "didStartProvisonalNavigation" |
| // callback and creates a NavigationContext; |
| // 3. Another navigation reaches here and retrieves that NavigationContext |
| // by matching URL. |
| // The DCHECK is now turned into a "if" condition, but can be reverted if a |
| // more reliable way of matching NavigationContext with WKNavigationAction |
| // is found. |
| if (context && |
| (!context->IsRendererInitiated() || |
| (context->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK))) { |
| transition = context->GetPageTransition(); |
| if (context->IsLoadingErrorPage()) { |
| // loadHTMLString: navigation which loads error page into WKWebView. |
| decisionHandler(WKNavigationActionPolicyAllow); |
| return; |
| } |
| } |
| } |
| |
| // Invalid URLs should not be loaded. |
| if (!requestURL.is_valid()) { |
| // The HTML5 spec indicates that window.open with an invalid URL should open |
| // about:blank. |
| BOOL isFirstLoadInOpenedWindow = |
| self.webStateImpl->HasOpener() && |
| !self.webStateImpl->GetNavigationManager()->GetLastCommittedItem(); |
| BOOL isMainFrame = action.targetFrame.mainFrame; |
| if (isFirstLoadInOpenedWindow && isMainFrame) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| GURL aboutBlankURL(url::kAboutBlankURL); |
| web::NavigationManager::WebLoadParams loadParams(aboutBlankURL); |
| loadParams.referrer = self.currentReferrer; |
| |
| self.webStateImpl->GetNavigationManager()->LoadURLWithParams(loadParams); |
| return; |
| } |
| } |
| |
| // First check if the navigation action should be blocked by the controller |
| // and make sure to update the controller in the case that the controller |
| // can't handle the request URL. Then use the embedders' policyDeciders to |
| // either: 1- Handle the URL it self and return false to stop the controller |
| // from proceeding with the navigation if needed. or 2- return true to allow |
| // the navigation to be proceeded by the web controller. |
| web::WebStatePolicyDecider::PolicyDecision policyDecision = |
| web::WebStatePolicyDecider::PolicyDecision::Allow(); |
| if (web::GetWebClient()->IsAppSpecificURL(requestURL)) { |
| // |policyDecision| is initialized above this conditional to allow loads, so |
| // it only needs to be overwritten if the load should be cancelled. |
| if (![self shouldAllowAppSpecificURLNavigationAction:action |
| transition:transition]) { |
| policyDecision = web::WebStatePolicyDecider::PolicyDecision::Cancel(); |
| } |
| if (policyDecision.ShouldAllowNavigation()) { |
| [self.delegate navigationHandler:self createWebUIForURL:requestURL]; |
| } |
| } |
| |
| BOOL webControllerCanShow = |
| web::UrlHasWebScheme(requestURL) || |
| web::GetWebClient()->IsAppSpecificURL(requestURL) || |
| requestURL.SchemeIs(url::kFileScheme) || |
| requestURL.SchemeIs(url::kAboutScheme) || |
| requestURL.SchemeIs(url::kBlobScheme); |
| |
| if (policyDecision.ShouldAllowNavigation()) { |
| BOOL userInteractedWithRequestMainFrame = |
| self.userInteractionState->HasUserTappedRecently(webView) && |
| net::GURLWithNSURL(action.request.mainDocumentURL) == |
| self.userInteractionState->LastUserInteraction()->main_document_url; |
| BOOL isCrossOriginTargetFrame = NO; |
| if (action.sourceFrame && action.targetFrame && |
| action.sourceFrame != action.targetFrame) { |
| url::Origin sourceOrigin = |
| url::Origin::Create(web::GURLOriginWithWKSecurityOrigin( |
| action.sourceFrame.securityOrigin)); |
| url::Origin targetOrigin = |
| url::Origin::Create(web::GURLOriginWithWKSecurityOrigin( |
| action.targetFrame.securityOrigin)); |
| isCrossOriginTargetFrame = !sourceOrigin.IsSameOriginWith(targetOrigin); |
| } |
| web::WebStatePolicyDecider::RequestInfo requestInfo( |
| transition, isMainFrameNavigationAction, isCrossOriginTargetFrame, |
| userInteractedWithRequestMainFrame); |
| |
| policyDecision = |
| self.webStateImpl->ShouldAllowRequest(action.request, requestInfo); |
| |
| // The WebState may have been closed in the ShouldAllowRequest callback. |
| if (self.beingDestroyed) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| return; |
| } |
| } |
| |
| if (!webControllerCanShow) { |
| policyDecision = web::WebStatePolicyDecider::PolicyDecision::Cancel(); |
| } |
| |
| if (policyDecision.ShouldAllowNavigation()) { |
| if ([[action.request HTTPMethod] isEqualToString:@"POST"]) { |
| // Display the confirmation dialog if a form repost is detected. |
| if (action.navigationType == WKNavigationTypeFormResubmitted) { |
| self.webStateImpl->ShowRepostFormWarningDialog( |
| base::BindOnce(^(bool shouldContinue) { |
| if (self.beingDestroyed) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| } else if (shouldContinue) { |
| decisionHandler(WKNavigationActionPolicyAllow); |
| } else { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| if (action.targetFrame.mainFrame) { |
| [self.pendingNavigationInfo setCancelled:YES]; |
| } |
| } |
| })); |
| return; |
| } |
| |
| web::NavigationItemImpl* item = |
| self.navigationManagerImpl->GetCurrentItemImpl(); |
| // TODO(crbug.com/570699): Remove this check once it's no longer possible |
| // to have no current entries. |
| if (item) |
| [self cachePOSTDataForRequest:action.request inNavigationItem:item]; |
| } |
| } else { |
| if (action.targetFrame.mainFrame) { |
| if (!self.beingDestroyed && policyDecision.ShouldDisplayError()) { |
| DCHECK(policyDecision.GetDisplayError()); |
| |
| // Navigation was blocked by |ShouldProvisionallyFailRequest|. Cancel |
| // load of page. |
| decisionHandler(WKNavigationActionPolicyCancel); |
| |
| // Handling presentation of policy decision error is dependent on |
| // |web::features::kUseJSForErrorPage| feature. |
| if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) { |
| return; |
| } |
| |
| [self displayError:policyDecision.GetDisplayError() |
| forCancelledNavigationToURL:action.request.URL |
| inWebView:webView |
| withTransition:transition]; |
| return; |
| } |
| |
| [self.pendingNavigationInfo setCancelled:YES]; |
| if (self.navigationManagerImpl->GetPendingItemIndex() == -1) { |
| // Discard the new pending item to ensure that the current URL is not |
| // different from what is displayed on the view. There is no need to |
| // reset pending item index for a different pending back-forward |
| // navigation. |
| self.navigationManagerImpl->DiscardNonCommittedItems(); |
| } |
| |
| web::NavigationContextImpl* context = |
| [self contextForPendingMainFrameNavigationWithURL:requestURL]; |
| if (context) { |
| // Destroy associated pending item, because this will be the last |
| // WKWebView callback for this navigation context. |
| context->ReleaseItem(); |
| } |
| |
| if (!self.beingDestroyed && |
| [self shouldClosePageOnNativeApplicationLoad]) { |
| self.webStateImpl->CloseWebState(); |
| decisionHandler(WKNavigationActionPolicyCancel); |
| return; |
| } |
| } |
| } |
| |
| if (policyDecision.ShouldCancelNavigation()) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| return; |
| } |
| BOOL isOffTheRecord = self.webStateImpl->GetBrowserState()->IsOffTheRecord(); |
| decisionHandler(web::GetAllowNavigationActionPolicy(isOffTheRecord)); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| decidePolicyForNavigationResponse:(WKNavigationResponse*)WKResponse |
| decisionHandler: |
| (void (^)(WKNavigationResponsePolicy))handler { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| // If this is a placeholder navigation, pass through. |
| GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL); |
| if ((!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| IsPlaceholderUrl(responseURL)) || |
| (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| [CRWErrorPageHelper isErrorPageFileURL:responseURL])) { |
| handler(WKNavigationResponsePolicyAllow); |
| return; |
| } |
| |
| if (self.pendingNavigationInfo.unsafeRedirect) { |
| self.pendingNavigationInfo.cancelled = YES; |
| self.pendingNavigationInfo.cancellationError = |
| [NSError errorWithDomain:net::kNSErrorDomain |
| code:net::ERR_UNSAFE_REDIRECT |
| userInfo:nil]; |
| handler(WKNavigationResponsePolicyCancel); |
| return; |
| } |
| |
| scoped_refptr<net::HttpResponseHeaders> headers; |
| if ([WKResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { |
| headers = net::CreateHeadersFromNSHTTPURLResponse( |
| static_cast<NSHTTPURLResponse*>(WKResponse.response)); |
| } |
| |
| // The page will not be changed until this navigation is committed, so the |
| // retrieved state will be pending until |didCommitNavigation| callback. |
| [self updatePendingNavigationInfoFromNavigationResponse:WKResponse |
| HTTPHeaders:headers]; |
| |
| web::WebStatePolicyDecider::PolicyDecision policyDecision = |
| web::WebStatePolicyDecider::PolicyDecision::Allow(); |
| |
| __weak CRWPendingNavigationInfo* weakPendingNavigationInfo = |
| self.pendingNavigationInfo; |
| auto callback = base::BindOnce( |
| ^(web::WebStatePolicyDecider::PolicyDecision policyDecision) { |
| if (policyDecision.ShouldCancelNavigation() && |
| WKResponse.canShowMIMEType && WKResponse.forMainFrame) { |
| weakPendingNavigationInfo.cancelled = YES; |
| weakPendingNavigationInfo.cancellationError = |
| policyDecision.GetDisplayError(); |
| } |
| |
| handler(policyDecision.ShouldAllowNavigation() |
| ? WKNavigationResponsePolicyAllow |
| : WKNavigationResponsePolicyCancel); |
| }); |
| |
| if ([self shouldRenderResponse:WKResponse]) { |
| self.webStateImpl->ShouldAllowResponse( |
| WKResponse.response, WKResponse.forMainFrame, std::move(callback)); |
| return; |
| } |
| |
| if (web::UrlHasWebScheme(responseURL)) { |
| [self createDownloadTaskForResponse:WKResponse HTTPHeaders:headers.get()]; |
| } else { |
| // DownloadTask only supports web schemes, so do nothing. |
| } |
| // Discard the pending item to ensure that the current URL is not different |
| // from what is displayed on the view. |
| self.navigationManagerImpl->DiscardNonCommittedItems(); |
| std::move(callback).Run(web::WebStatePolicyDecider::PolicyDecision::Cancel()); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didStartProvisionalNavigation:(WKNavigation*)navigation { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| GURL webViewURL = net::GURLWithNSURL(webView.URL); |
| |
| [self.navigationStates setState:web::WKNavigationState::STARTED |
| forNavigation:navigation]; |
| |
| if (webViewURL.is_empty()) { |
| // URL starts empty for window.open(""), by didCommitNavigation: callback |
| // the URL will be "about:blank". |
| webViewURL = GURL(url::kAboutBlankURL); |
| } |
| |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| |
| if (context) { |
| // This is already seen and registered navigation. |
| |
| if (context->IsLoadingErrorPage()) { |
| // This is loadHTMLString: navigation to display error page in web view. |
| self.navigationState = web::WKNavigationState::REQUESTED; |
| return; |
| } |
| |
| BOOL isErrorPageNavigation = |
| (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| [CRWErrorPageHelper isErrorPageFileURL:webViewURL]) || |
| (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| context->IsPlaceholderNavigation()); |
| |
| if (!isErrorPageNavigation && !IsWKInternalUrl(webViewURL)) { |
| web::NavigationItem* item = |
| web::GetItemWithUniqueID(self.navigationManagerImpl, context); |
| if (item) { |
| web::WKBackForwardListItemHolder* itemHolder = |
| web::WKBackForwardListItemHolder::FromNavigationItem(item); |
| if (itemHolder->navigation_type() == WKNavigationTypeBackForward && |
| ![webView.backForwardList.currentItem.URL isEqual:webView.URL]) { |
| // Sometimes on back/forward navigation, the backforward list is out |
| // of sync with the webView. Go back or forward to fix it. See |
| // crbug.com/968539. |
| if ([webView.backForwardList.backItem.URL isEqual:webView.URL]) { |
| ReportOutOfSyncURLInDidStartProvisionalNavigation( |
| OutOfSyncURLAction::kGoBack); |
| [webView goBack]; |
| return; |
| } |
| if ([webView.backForwardList.forwardItem.URL isEqual:webView.URL]) { |
| ReportOutOfSyncURLInDidStartProvisionalNavigation( |
| OutOfSyncURLAction::kGoForward); |
| [webView goForward]; |
| return; |
| } |
| ReportOutOfSyncURLInDidStartProvisionalNavigation( |
| OutOfSyncURLAction::kNoAction); |
| } |
| } |
| |
| if (context->GetUrl() != webViewURL) { |
| // Update last seen URL because it may be changed by WKWebView (f.e. |
| // by performing characters escaping). |
| if (item) { |
| // Item may not exist if navigation was stopped (see |
| // crbug.com/969915). |
| item->SetURL(webViewURL); |
| if ([CRWErrorPageHelper isErrorPageFileURL:webViewURL]) { |
| item->SetVirtualURL([CRWErrorPageHelper |
| failedNavigationURLFromErrorPageFileURL:webViewURL]); |
| } |
| } |
| context->SetUrl(webViewURL); |
| } |
| } |
| |
| self.webStateImpl->OnNavigationStarted(context); |
| self.webStateImpl->GetNavigationManagerImpl().OnNavigationStarted( |
| webViewURL); |
| return; |
| } |
| |
| // This is renderer-initiated navigation which was not seen before and |
| // should be registered. |
| |
| // Renderer-initiated app-specific loads should only be allowed in these |
| // specific cases: |
| // 1) if |backForwardList.currentItem| is a placeholder URL for the |
| // provisional load URL (i.e. webView.URL), then this is an in-progress |
| // app-specific load and should not be restarted. |
| // 2) back/forward navigation to an app-specific URL should be allowed. |
| // 3) navigation to an app-specific URL should be allowed from other |
| // app-specific URLs |
| bool exemptedAppSpecificLoad = false; |
| bool currentItemIsPlaceholder = |
| !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| CreatePlaceholderUrlForUrl(webViewURL) == |
| net::GURLWithNSURL(webView.backForwardList.currentItem.URL); |
| bool isBackForward = |
| self.pendingNavigationInfo.navigationType == WKNavigationTypeBackForward; |
| bool isRestoringSession = IsRestoreSessionUrl(self.documentURL); |
| exemptedAppSpecificLoad = currentItemIsPlaceholder || isBackForward || |
| isRestoringSession || self.webStateImpl->HasWebUI(); |
| |
| if (!web::GetWebClient()->IsAppSpecificURL(webViewURL) || |
| !exemptedAppSpecificLoad) { |
| self.webStateImpl->ClearWebUI(); |
| } |
| |
| self.webStateImpl->GetNavigationManagerImpl().OnNavigationStarted(webViewURL); |
| |
| BOOL isPlaceholderURL = |
| base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) |
| ? NO |
| : IsPlaceholderUrl(webViewURL); |
| std::unique_ptr<web::NavigationContextImpl> navigationContext = |
| [self.delegate navigationHandler:self |
| registerLoadRequestForURL:webViewURL |
| sameDocumentNavigation:NO |
| hasUserGesture:self.pendingNavigationInfo.hasUserGesture |
| rendererInitiated:YES |
| placeholderNavigation:isPlaceholderURL]; |
| web::NavigationContextImpl* navigationContextPtr = navigationContext.get(); |
| |
| // GetPendingItem which may be called inside OnNavigationStarted relies on |
| // association between NavigationContextImpl and WKNavigation. |
| [self.navigationStates setContext:std::move(navigationContext) |
| forNavigation:navigation]; |
| self.webStateImpl->OnNavigationStarted(navigationContextPtr); |
| DCHECK_EQ(web::WKNavigationState::REQUESTED, self.navigationState); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| GURL webViewURL = net::GURLWithNSURL(webView.URL); |
| |
| // This callback should never be triggered for placeholder navigations. |
| DCHECK(base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) || |
| !IsPlaceholderUrl(webViewURL)); |
| |
| [self.navigationStates setState:web::WKNavigationState::REDIRECTED |
| forNavigation:navigation]; |
| |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| if (!context) |
| return; |
| |
| if (webViewURL.SchemeIs(url::kDataScheme)) { |
| // Redirecting to a data url is always unsafe. |
| self.pendingNavigationInfo.unsafeRedirect = YES; |
| } else { |
| context->SetUrl(webViewURL); |
| } |
| web::NavigationItemImpl* item = |
| web::GetItemWithUniqueID(self.navigationManagerImpl, context); |
| |
| // Associated item can be a pending item, previously discarded by another |
| // navigation. WKWebView allows multiple provisional navigations, while |
| // Navigation Manager has only one pending navigation. |
| if (item) { |
| if (!IsWKInternalUrl(webViewURL) && |
| !self.pendingNavigationInfo.unsafeRedirect) { |
| item->SetVirtualURL(webViewURL); |
| item->SetURL(webViewURL); |
| } |
| // Redirects (3xx response code), must change POST requests to GETs. |
| item->SetPostData(nil); |
| item->ResetHttpRequestHeaders(); |
| } |
| |
| self.userInteractionState->ResetLastTransferTime(); |
| self.webStateImpl->OnNavigationRedirected(context); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didFailProvisionalNavigation:(WKNavigation*)navigation |
| withError:(NSError*)error { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| BOOL wasRedirected = [self.navigationStates stateForNavigation:navigation] == |
| web::WKNavigationState::REDIRECTED; |
| |
| [self.navigationStates setState:web::WKNavigationState::PROVISIONALY_FAILED |
| forNavigation:navigation]; |
| |
| // Ignore provisional navigation failure if a new navigation has been started, |
| // for example, if a page is reloaded after the start of the provisional |
| // load but before the load has been committed. |
| if (![[self.navigationStates lastAddedNavigation] isEqual:navigation]) { |
| return; |
| } |
| |
| web::NavigationContextImpl* navigationContext = |
| [self.navigationStates contextForNavigation:navigation]; |
| if (wasRedirected && navigationContext) { |
| // If there was a redirect, change the URL to have the URL of the first |
| // page. |
| NSMutableDictionary* userInfo = [error.userInfo mutableCopy]; |
| userInfo[NSURLErrorFailingURLStringErrorKey] = |
| base::SysUTF8ToNSString(navigationContext->GetUrl().spec()); |
| error = [NSError errorWithDomain:error.domain |
| code:error.code |
| userInfo:userInfo]; |
| } |
| |
| // Handle load cancellation for directly cancelled navigations without |
| // handling their potential errors. Otherwise, handle the error. |
| if (self.pendingNavigationInfo.cancelled) { |
| if (self.pendingNavigationInfo.cancellationError) { |
| // If the navigation was cancelled for a CancelAndDisplayError() policy |
| // decision, load the error in the failed navigation. |
| [self handleLoadError:error |
| forNavigation:navigation |
| webView:webView |
| provisionalLoad:YES]; |
| } else { |
| [self handleCancelledError:error |
| forNavigation:navigation |
| provisionalLoad:YES]; |
| } |
| } else if (error.code == NSURLErrorUnsupportedURL && |
| self.webStateImpl->HasWebUI()) { |
| // This is a navigation to WebUI page. |
| DCHECK(web::GetWebClient()->IsAppSpecificURL( |
| net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]))); |
| } else { |
| [self handleLoadError:error |
| forNavigation:navigation |
| webView:webView |
| provisionalLoad:YES]; |
| } |
| |
| self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames(); |
| // This must be reset at the end, since code above may need information about |
| // the pending load. |
| self.pendingNavigationInfo = nil; |
| if (!web::IsWKWebViewSSLCertError(error)) { |
| _certVerificationErrors->Clear(); |
| } |
| |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| |
| // Remove the navigation to immediately get rid of pending item. Navigation |
| // should not be cleared, however, in the case of a committed interstitial |
| // for an SSL error. |
| if (web::WKNavigationState::NONE != |
| [self.navigationStates stateForNavigation:navigation] && |
| !(context && web::IsWKWebViewSSLCertError(context->GetError()) && |
| !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))) { |
| [self.navigationStates removeNavigation:navigation]; |
| } |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didCommitNavigation:(WKNavigation*)navigation { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| // For reasons not yet fully understood, sometimes WKWebView triggers |
| // |webView:didFinishNavigation| before |webView:didCommitNavigation|. If a |
| // navigation is already finished, stop processing |
| // (https://crbug.com/818796#c2). |
| if ([self.navigationStates stateForNavigation:navigation] == |
| web::WKNavigationState::FINISHED) |
| return; |
| |
| BOOL committedNavigation = |
| [self.navigationStates isCommittedNavigation:navigation]; |
| |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| if (context && !web::IsWKWebViewSSLCertError(context->GetError())) { |
| _certVerificationErrors->Clear(); |
| } |
| |
| // Invariant: Every |navigation| should have a |context|. Note that violation |
| // of this invariant is currently observed in production, but the cause is not |
| // well understood. This DCHECK is meant to catch such cases in testing if |
| // they arise. |
| // TODO(crbug.com/864769): Remove nullptr checks on |context| in this method |
| // once the root cause of the invariant violation is found. |
| DCHECK(context); |
| UMA_HISTOGRAM_BOOLEAN("IOS.CommittedNavigationHasContext", context); |
| |
| GURL webViewURL = net::GURLWithNSURL(webView.URL); |
| GURL currentWKItemURL = |
| net::GURLWithNSURL(webView.backForwardList.currentItem.URL); |
| UMA_HISTOGRAM_BOOLEAN("IOS.CommittedURLMatchesCurrentItem", |
| webViewURL == currentWKItemURL); |
| |
| // TODO(crbug.com/787497): Always use webView.backForwardList.currentItem.URL |
| // to obtain lastCommittedURL once loadHTML: is no longer user for WebUI. |
| if (webViewURL.is_empty()) { |
| // It is possible for |webView.URL| to be nil, in which case |
| // webView.backForwardList.currentItem.URL will return the right committed |
| // URL (crbug.com/784480). |
| webViewURL = currentWKItemURL; |
| } else if (context && |
| (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) || |
| !context->IsPlaceholderNavigation()) && |
| context->GetUrl() == currentWKItemURL) { |
| // If webView.backForwardList.currentItem.URL matches |context|, then this |
| // is a known edge case where |webView.URL| is wrong. |
| // TODO(crbug.com/826013): Remove this workaround. |
| webViewURL = currentWKItemURL; |
| } |
| |
| if (context) { |
| if (self.pendingNavigationInfo.MIMEType) |
| context->SetMimeType(self.pendingNavigationInfo.MIMEType); |
| if (self.pendingNavigationInfo.HTTPHeaders) |
| context->SetResponseHeaders(self.pendingNavigationInfo.HTTPHeaders); |
| } |
| |
| // Don't show webview for placeholder navigation to avoid covering existing |
| // content. |
| if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) || |
| !IsPlaceholderUrl(webViewURL)) |
| [self.delegate navigationHandlerDisplayWebView:self]; |
| |
| // |context| will be nil if this navigation has been already committed and |
| // finished. |
| if (context) { |
| web::NavigationManager* navigationManager = |
| self.webStateImpl->GetNavigationManager(); |
| GURL pendingURL; |
| if (navigationManager->GetPendingItemIndex() == -1) { |
| if (context->GetItem()) { |
| // Item may not exist if navigation was stopped (see |
| // crbug.com/969915). |
| pendingURL = context->GetItem()->GetURL(); |
| } |
| } else { |
| if (navigationManager->GetPendingItem()) { |
| pendingURL = navigationManager->GetPendingItem()->GetURL(); |
| } |
| } |
| if ((pendingURL == webViewURL) || (context->IsLoadingHtmlString())) { |
| // Commit navigation if at least one of these is true: |
| // - Navigation has pending item (this should always be true, but |
| // pending item may not exist due to crbug.com/925304). |
| // - Navigation is loadHTMLString:baseURL: navigation, which does not |
| // create a pending item, but modifies committed item instead. |
| // - Transition type is reload with Legacy Navigation Manager (Legacy |
| // Navigation Manager does not create pending item for reload due to |
| // crbug.com/676129) |
| context->SetHasCommitted(true); |
| } |
| self.webStateImpl->SetContentsMimeType( |
| base::SysNSStringToUTF8(context->GetMimeType())); |
| } |
| |
| [self commitPendingNavigationInfoInWebView:webView]; |
| |
| self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames(); |
| |
| // This point should closely approximate the document object change, so reset |
| // the list of injected scripts to those that are automatically injected. |
| // Do not inject window ID if this is a placeholder URL. For WebUI, let the |
| // window ID be injected when the |loadHTMLString:baseURL| navigation is |
| // committed. |
| if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) || |
| !IsPlaceholderUrl(webViewURL)) { |
| const std::string& mime_type = self.webStateImpl->GetContentsMimeType(); |
| if (web::IsContentTypeHtml(mime_type) || |
| web::IsContentTypeImage(mime_type) || mime_type.empty()) { |
| // In unit tests MIME type will be empty, because loadHTML:forURL: does |
| // not notify web view delegate about received response, so web controller |
| // does not get a chance to properly update MIME type. |
| [self.JSInjector injectWindowID]; |
| self.webStateImpl->GetWebFramesManagerImpl().RegisterExistingFrames(); |
| } |
| } |
| |
| if (committedNavigation) { |
| // WKWebView called didCommitNavigation: with incorrect WKNavigation object. |
| // Correct WKNavigation object for this navigation was deallocated because |
| // WKWebView mistakenly cancelled the navigation and called |
| // didFailProvisionalNavigation. As a result web::NavigationContext for this |
| // navigation does not exist anymore. Find correct navigation item and make |
| // it committed. |
| [self resetDocumentSpecificState]; |
| [self.delegate navigationHandlerDidStartLoading:self]; |
| } else if (context) { |
| // If |navigation| is nil (which happens for windows open by DOM), then it |
| // should be the first and the only pending navigation. |
| BOOL isLastNavigation = |
| !navigation || |
| [[self.navigationStates lastAddedNavigation] isEqual:navigation]; |
| if (isLastNavigation || |
| self.navigationManagerImpl->GetPendingItemIndex() == -1) { |
| [self webPageChangedWithContext:context webView:webView]; |
| } |
| } |
| |
| // The WebView URL can't always be trusted when multiple pending navigations |
| // are occuring, as a navigation could commit after a new navigation has |
| // started (and thus the WebView URL will be the URL of the new navigation). |
| // See crbug.com/1127025. |
| BOOL hasMultiplePendingNavigations = |
| [self.navigationStates pendingNavigations].count > 1; |
| |
| // When loading an error page, the context has the correct URL whereas the |
| // webView has the file URL. |
| BOOL isErrorPage = |
| base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| [CRWErrorPageHelper isErrorPageFileURL:webViewURL]; |
| |
| // When loading an error page that is a placeholder (legacy), the webViewURL |
| // should be used as it is the actual URL we want to load. |
| BOOL isLegacyErrorPage = |
| !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| context && !context->IsPlaceholderNavigation(); |
| |
| BOOL shouldUseContextURL = |
| context |
| ? isErrorPage || (!isLegacyErrorPage && hasMultiplePendingNavigations) |
| : NO; |
| GURL documentURL = shouldUseContextURL ? context->GetUrl() : webViewURL; |
| |
| // This is the point where the document's URL has actually changed. |
| [self.delegate navigationHandler:self |
| setDocumentURL:documentURL |
| context:context]; |
| |
| // No further code relies an existance of pending item, so this navigation can |
| // be marked as "committed". |
| [self.navigationStates setState:web::WKNavigationState::COMMITTED |
| forNavigation:navigation]; |
| |
| if (!committedNavigation && context && !context->IsLoadingErrorPage()) { |
| self.webStateImpl->OnNavigationFinished(context); |
| } |
| |
| // Do not update the states of the last committed item for placeholder page |
| // because the actual navigation item will not be committed until the native |
| // content or WebUI is shown. |
| if (context && |
| (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) || |
| (!context->IsPlaceholderNavigation() && |
| !context->IsLoadingErrorPage())) && |
| !context->GetUrl().SchemeIs(url::kAboutScheme) && |
| !IsRestoreSessionUrl(context->GetUrl())) { |
| [self.delegate webViewHandlerUpdateSSLStatusForCurrentNavigationItem:self]; |
| if (!context->IsLoadingErrorPage() && !IsRestoreSessionUrl(webViewURL)) { |
| [self setLastCommittedNavigationItemTitle:webView.title]; |
| } |
| } |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didFinishNavigation:(WKNavigation*)navigation { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| // Sometimes |webView:didFinishNavigation| arrives before |
| // |webView:didCommitNavigation|. Explicitly trigger post-commit processing. |
| bool navigationCommitted = |
| [self.navigationStates isCommittedNavigation:navigation]; |
| UMA_HISTOGRAM_BOOLEAN("IOS.WKWebViewFinishBeforeCommit", |
| !navigationCommitted); |
| if (!navigationCommitted) { |
| [self webView:webView didCommitNavigation:navigation]; |
| DCHECK_EQ(web::WKNavigationState::COMMITTED, |
| [self.navigationStates stateForNavigation:navigation]); |
| } |
| |
| // Sometimes |didFinishNavigation| callback arrives after |stopLoading| has |
| // been called. Abort in this case. |
| if ([self.navigationStates stateForNavigation:navigation] == |
| web::WKNavigationState::NONE) { |
| return; |
| } |
| |
| GURL webViewURL = net::GURLWithNSURL(webView.URL); |
| GURL currentWKItemURL = |
| net::GURLWithNSURL(webView.backForwardList.currentItem.URL); |
| UMA_HISTOGRAM_BOOLEAN("IOS.FinishedURLMatchesCurrentItem", |
| webViewURL == currentWKItemURL); |
| |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| web::NavigationItemImpl* item = |
| context ? web::GetItemWithUniqueID(self.navigationManagerImpl, context) |
| : nullptr; |
| // Item may not exist if navigation was stopped (see crbug.com/969915). |
| |
| // Invariant: every |navigation| should have a |context| and a |item|. |
| // TODO(crbug.com/899383) Fix invariant violation when a new pending item is |
| // created before a placeholder load finishes. |
| if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| IsPlaceholderUrl(webViewURL)) { |
| GURL originalURL = ExtractUrlFromPlaceholderUrl(webViewURL); |
| if (self.currentNavItem != item && |
| self.currentNavItem->GetVirtualURL() != originalURL) { |
| // The |didFinishNavigation| callback for placeholder navigation can |
| // arrive after another navigation has started. Abort in this case. |
| return; |
| } |
| } |
| DCHECK(context); |
| UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasContext", context); |
| UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasItem", item); |
| |
| if (context && item) { |
| GURL navigationURL = |
| !base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| context->IsPlaceholderNavigation() |
| ? CreatePlaceholderUrlForUrl(context->GetUrl()) |
| : context->GetUrl(); |
| if (navigationURL == currentWKItemURL) { |
| // If webView.backForwardList.currentItem.URL matches |context|, then this |
| // is a known edge case where |webView.URL| is wrong. |
| // TODO(crbug.com/826013): Remove this workaround. |
| webViewURL = currentWKItemURL; |
| } |
| |
| if (!IsWKInternalUrl(currentWKItemURL) && currentWKItemURL == webViewURL && |
| currentWKItemURL != context->GetUrl() && |
| item == self.navigationManagerImpl->GetLastCommittedItem() && |
| item->GetURL().GetOrigin() == currentWKItemURL.GetOrigin()) { |
| // WKWebView sometimes changes URL on the same navigation, likely due to |
| // location.replace() or history.replaceState in onload handler that does |
| // not change the origin. It's safe to update |item| and |context| URL |
| // because they are both associated to WKNavigation*, which is a stable ID |
| // for the navigation. See https://crbug.com/869540 for a real-world case. |
| item->SetURL(currentWKItemURL); |
| context->SetUrl(currentWKItemURL); |
| } |
| |
| if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) { |
| if (IsPlaceholderUrl(webViewURL)) { |
| if (item->GetURL() == webViewURL) { |
| // Current navigation item is restored from a placeholder URL as part |
| // of session restoration. It is now safe to update the navigation |
| // item URL to the original app-specific URL. |
| item->SetURL(ExtractUrlFromPlaceholderUrl(webViewURL)); |
| } |
| |
| if (item->error_retry_state_machine().state() == |
| web::ErrorRetryState::kNoNavigationError) { |
| // Offline pages can leave the WKBackForwardList current item as a |
| // placeholder with no saved content. In this case, trigger a retry |
| // on that navigation with an update |item| url and |context| error. |
| item->SetURL( |
| ExtractUrlFromPlaceholderUrl(net::GURLWithNSURL(webView.URL))); |
| item->SetVirtualURL(item->GetURL()); |
| context->SetError([NSError |
| errorWithDomain:NSURLErrorDomain |
| code:NSURLErrorNetworkConnectionLost |
| userInfo:@{ |
| NSURLErrorFailingURLStringErrorKey : |
| base::SysUTF8ToNSString(item->GetURL().spec()) |
| }]); |
| item->error_retry_state_machine().SetRetryPlaceholderNavigation(); |
| } |
| } |
| |
| web::ErrorRetryCommand command = |
| item->error_retry_state_machine().DidFinishNavigation(webViewURL); |
| [self handleErrorRetryCommand:command |
| navigationItem:item |
| navigationContext:context |
| originalNavigation:navigation |
| webView:webView]; |
| } else if (context->GetError()) { |
| [self loadErrorPageForNavigationItem:item |
| navigationContext:navigation |
| webView:webView]; |
| } |
| } |
| |
| [self.navigationStates setState:web::WKNavigationState::FINISHED |
| forNavigation:navigation]; |
| |
| [self.delegate webViewHandler:self didFinishNavigation:context]; |
| |
| // Remove the navigation to immediately get rid of pending item. Navigation |
| // should not be cleared, however, in the case of a committed interstitial |
| // for an SSL error. |
| if (web::WKNavigationState::NONE != |
| [self.navigationStates stateForNavigation:navigation] && |
| !(context && web::IsWKWebViewSSLCertError(context->GetError()))) { |
| [self.navigationStates removeNavigation:navigation]; |
| } |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didFailNavigation:(WKNavigation*)navigation |
| withError:(NSError*)error { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| [self.navigationStates setState:web::WKNavigationState::FAILED |
| forNavigation:navigation]; |
| |
| [self handleLoadError:error |
| forNavigation:navigation |
| webView:webView |
| provisionalLoad:NO]; |
| self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames(); |
| _certVerificationErrors->Clear(); |
| [self forgetNullWKNavigation:navigation]; |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge |
| completionHandler: |
| (void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| NSString* authMethod = challenge.protectionSpace.authenticationMethod; |
| if ([authMethod isEqual:NSURLAuthenticationMethodHTTPBasic] || |
| [authMethod isEqual:NSURLAuthenticationMethodNTLM] || |
| [authMethod isEqual:NSURLAuthenticationMethodHTTPDigest]) { |
| [self handleHTTPAuthForChallenge:challenge |
| completionHandler:completionHandler]; |
| return; |
| } |
| |
| if (![authMethod isEqual:NSURLAuthenticationMethodServerTrust]) { |
| completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| return; |
| } |
| |
| SecTrustRef trust = challenge.protectionSpace.serverTrust; |
| base::ScopedCFTypeRef<SecTrustRef> scopedTrust(trust, |
| base::scoped_policy::RETAIN); |
| __weak CRWWKNavigationHandler* weakSelf = self; |
| [self.certVerificationController |
| decideLoadPolicyForTrust:scopedTrust |
| host:challenge.protectionSpace.host |
| completionHandler:^(web::CertAcceptPolicy policy, |
| net::CertStatus status) { |
| CRWWKNavigationHandler* strongSelf = weakSelf; |
| if (!strongSelf) { |
| completionHandler( |
| NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| return; |
| } |
| [strongSelf processAuthChallenge:challenge |
| forCertAcceptPolicy:policy |
| certStatus:status |
| completionHandler:completionHandler]; |
| }]; |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| authenticationChallenge:(NSURLAuthenticationChallenge*)challenge |
| shouldAllowDeprecatedTLS:(void (^)(BOOL))decisionHandler |
| API_AVAILABLE(ios(14)) { |
| [self didReceiveWKNavigationDelegateCallback]; |
| DCHECK(challenge); |
| DCHECK(decisionHandler); |
| |
| // If the legacy TLS interstitial is not enabled, don't cause errors. The |
| // interstitial is also dependent on committed interstitials being enabled. |
| if (!base::FeatureList::IsEnabled(web::features::kIOSLegacyTLSInterstitial)) { |
| decisionHandler(YES); |
| return; |
| } |
| |
| if (web::GetWebClient()->IsLegacyTLSAllowedForHost( |
| self.webStateImpl, |
| base::SysNSStringToUTF8(challenge.protectionSpace.host))) { |
| decisionHandler(YES); |
| return; |
| } |
| |
| if (self.pendingNavigationInfo) { |
| self.pendingNavigationInfo.cancelled = YES; |
| self.pendingNavigationInfo.cancellationError = |
| [NSError errorWithDomain:net::kNSErrorDomain |
| code:net::ERR_SSL_OBSOLETE_VERSION |
| userInfo:nil]; |
| } |
| decisionHandler(NO); |
| } |
| |
| - (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView { |
| [self didReceiveWKNavigationDelegateCallback]; |
| |
| _certVerificationErrors->Clear(); |
| _webProcessCrashed = YES; |
| self.webStateImpl->GetWebFramesManagerImpl().RemoveAllWebFrames(); |
| |
| [self.delegate navigationHandlerWebProcessDidCrash:self]; |
| } |
| |
| #pragma mark - Private methods |
| |
| // Returns the UserAgent that needs to be used for the |navigationAction| from |
| // the |webView|. |
| - (web::UserAgentType)userAgentForNavigationAction: |
| (WKNavigationAction*)navigationAction |
| webView:(WKWebView*)webView { |
| web::NavigationItem* item = nullptr; |
| web::UserAgentType userAgentType = web::UserAgentType::NONE; |
| if (navigationAction.navigationType == WKNavigationTypeBackForward) { |
| // Use the item associated with the back/forward item to have the same user |
| // agent as the one used the first time. |
| item = [[CRWNavigationItemHolder |
| holderForBackForwardListItem:webView.backForwardList.currentItem] |
| navigationItem]; |
| // In some cases, the associated item isn't found. In that case, follow the |
| // code path for the non-backforward navigations. See crbug.com/1121950. |
| if (item) |
| userAgentType = item->GetUserAgentType(); |
| } |
| if (!item) { |
| // Get the visible item. There is no guarantee that the pending item belongs |
| // to this navigation but it is very likely that it is the case. If there is |
| // no pending item, it is probably a render initiated navigation. Use the |
| // UserAgent of the previous navigation. This will also return the |
| // navigation item of the restoration if a restoration occurs. Request the |
| // pending item explicitly as the visible item might be the committed item |
| // if the pending navigation isn't user triggered. |
| item = self.navigationManagerImpl->GetPendingItem(); |
| if (!item) |
| item = self.navigationManagerImpl->GetVisibleItem(); |
| |
| if (item && item->GetTransitionType() & ui::PAGE_TRANSITION_FORWARD_BACK) { |
| // When navigating forward to a restored page, the WKNavigationAction is |
| // of type reload and not BackForward. The item is correctly set a |
| // back/forward, so it is possible to use it. |
| userAgentType = item->GetUserAgentType(); |
| } else { |
| userAgentType = self.webStateImpl->GetUserAgentForNextNavigation( |
| net::GURLWithNSURL(navigationAction.request.URL)); |
| } |
| } |
| |
| if (item && web::GetWebClient()->IsAppSpecificURL(item->GetVirtualURL())) { |
| // In case of app specific URL, no specificUser Agent needs to be used. |
| // However, to have a custom User Agent and a WKContentMode, use mobile. |
| userAgentType = web::UserAgentType::MOBILE; |
| } |
| return userAgentType; |
| } |
| |
| - (web::NavigationManagerImpl*)navigationManagerImpl { |
| return &(self.webStateImpl->GetNavigationManagerImpl()); |
| } |
| |
| - (web::WebStateImpl*)webStateImpl { |
| return [self.delegate webStateImplForWebViewHandler:self]; |
| } |
| |
| - (web::UserInteractionState*)userInteractionState { |
| return [self.delegate userInteractionStateForWebViewHandler:self]; |
| } |
| |
| - (CRWJSInjector*)JSInjector { |
| return [self.delegate JSInjectorForNavigationHandler:self]; |
| } |
| |
| - (CRWCertVerificationController*)certVerificationController { |
| return [self.delegate certVerificationControllerForNavigationHandler:self]; |
| } |
| |
| - (GURL)documentURL { |
| return [self.delegate documentURLForWebViewHandler:self]; |
| } |
| |
| - (web::NavigationItemImpl*)currentNavItem { |
| return self.navigationManagerImpl |
| ? self.navigationManagerImpl->GetCurrentItemImpl() |
| : nullptr; |
| } |
| |
| // This method should be called on receiving WKNavigationDelegate callbacks. |
| - (void)didReceiveWKNavigationDelegateCallback { |
| DCHECK(!self.beingDestroyed); |
| } |
| |
| // Extracts navigation info from WKNavigationAction and sets it as a pending. |
| // Some pieces of navigation information are only known in |
| // |decidePolicyForNavigationAction|, but must be in a pending state until |
| // |didgo/Navigation| where it becames current. |
| - (void)createPendingNavigationInfoFromNavigationAction: |
| (WKNavigationAction*)action { |
| if (action.targetFrame.mainFrame) { |
| self.pendingNavigationInfo = [[CRWPendingNavigationInfo alloc] init]; |
| self.pendingNavigationInfo.referrer = |
| [action.request valueForHTTPHeaderField:kReferrerHeaderName]; |
| self.pendingNavigationInfo.navigationType = action.navigationType; |
| self.pendingNavigationInfo.HTTPMethod = action.request.HTTPMethod; |
| self.pendingNavigationInfo.hasUserGesture = |
| web::GetNavigationActionInitiationType(action) == |
| web::NavigationActionInitiationType::kUserInitiated; |
| } |
| } |
| |
| // Extracts navigation info from WKNavigationResponse and sets it as a pending. |
| // Some pieces of navigation information are only known in |
| // |decidePolicyForNavigationResponse|, but must be in a pending state until |
| // |didCommitNavigation| where it becames current. |
| - (void) |
| updatePendingNavigationInfoFromNavigationResponse: |
| (WKNavigationResponse*)response |
| HTTPHeaders: |
| (const scoped_refptr< |
| net::HttpResponseHeaders>&) |
| headers { |
| if (response.isForMainFrame) { |
| if (!self.pendingNavigationInfo) { |
| self.pendingNavigationInfo = [[CRWPendingNavigationInfo alloc] init]; |
| } |
| self.pendingNavigationInfo.MIMEType = response.response.MIMEType; |
| self.pendingNavigationInfo.HTTPHeaders = headers; |
| } |
| } |
| |
| // Returns YES if the navigation action is associated with a main frame request. |
| - (BOOL)isMainFrameNavigationAction:(WKNavigationAction*)action { |
| if (action.targetFrame) { |
| return action.targetFrame.mainFrame; |
| } |
| // According to WKNavigationAction documentation, in the case of a new window |
| // navigation, target frame will be nil. In this case check if the |
| // |sourceFrame| is the mainFrame. |
| return action.sourceFrame.mainFrame; |
| } |
| |
| // Returns YES if the given |action| should be allowed to continue for app |
| // specific URL. If this returns NO, the navigation should be cancelled. |
| // App specific pages have elevated privileges and WKWebView uses the same |
| // renderer process for all page frames. With that Chromium does not allow |
| // running App specific pages in the same process as a web site from the |
| // internet. Allows navigation to app specific URL in the following cases: |
| // - last committed URL is app specific |
| // - navigation not a new navigation (back-forward or reload) |
| // - navigation is typed, generated or bookmark |
| // - navigation is performed in iframe and main frame is app-specific page |
| - (BOOL)shouldAllowAppSpecificURLNavigationAction:(WKNavigationAction*)action |
| transition: |
| (ui::PageTransition)pageTransition { |
| GURL requestURL = net::GURLWithNSURL(action.request.URL); |
| DCHECK(web::GetWebClient()->IsAppSpecificURL(requestURL)); |
| if (web::GetWebClient()->IsAppSpecificURL( |
| self.webStateImpl->GetLastCommittedURL())) { |
| // Last committed page is also app specific and navigation should be |
| // allowed. |
| return YES; |
| } |
| |
| if (pageTransition & ui::PAGE_TRANSITION_FORWARD_BACK) { |
| // Allow back-forward navigations. |
| return YES; |
| } |
| |
| if (ui::PageTransitionTypeIncludingQualifiersIs(pageTransition, |
| ui::PAGE_TRANSITION_TYPED)) { |
| return YES; |
| } |
| |
| if (ui::PageTransitionTypeIncludingQualifiersIs( |
| pageTransition, ui::PAGE_TRANSITION_GENERATED)) { |
| return YES; |
| } |
| |
| if (ui::PageTransitionTypeIncludingQualifiersIs( |
| pageTransition, ui::PAGE_TRANSITION_AUTO_BOOKMARK)) { |
| return YES; |
| } |
| |
| // If the session is being restored, allow the navigation. |
| if (IsRestoreSessionUrl(self.documentURL)) { |
| return YES; |
| } |
| |
| // Allow navigation to WebUI pages from error pages. |
| if ([CRWErrorPageHelper isErrorPageFileURL:self.documentURL]) { |
| return YES; |
| } |
| |
| GURL mainDocumentURL = net::GURLWithNSURL(action.request.mainDocumentURL); |
| if (web::GetWebClient()->IsAppSpecificURL(mainDocumentURL) && |
| !action.sourceFrame.mainFrame) { |
| // AppSpecific URLs are allowed inside iframe if the main frame is also |
| // app specific page. |
| return YES; |
| } |
| |
| return NO; |
| } |
| |
| // Caches request POST data in the given session entry. |
| - (void)cachePOSTDataForRequest:(NSURLRequest*)request |
| inNavigationItem:(web::NavigationItemImpl*)item { |
| NSUInteger maxPOSTDataSizeInBytes = 4096; |
| NSString* cookieHeaderName = @"cookie"; |
| |
| DCHECK(item); |
| const bool shouldUpdateEntry = |
| ui::PageTransitionCoreTypeIs(item->GetTransitionType(), |
| ui::PAGE_TRANSITION_FORM_SUBMIT) && |
| ![request HTTPBodyStream] && // Don't cache streams. |
| !item->HasPostData() && |
| item->GetURL() == net::GURLWithNSURL([request URL]); |
| const bool belowSizeCap = |
| [[request HTTPBody] length] < maxPOSTDataSizeInBytes; |
| DLOG_IF(WARNING, shouldUpdateEntry && !belowSizeCap) |
| << "Data in POST request exceeds the size cap (" << maxPOSTDataSizeInBytes |
| << " bytes), and will not be cached."; |
| |
| if (shouldUpdateEntry && belowSizeCap) { |
| item->SetPostData([request HTTPBody]); |
| item->ResetHttpRequestHeaders(); |
| item->AddHttpRequestHeaders([request allHTTPHeaderFields]); |
| // Don't cache the "Cookie" header. |
| // According to NSURLRequest documentation, |-valueForHTTPHeaderField:| is |
| // case insensitive, so it's enough to test the lower case only. |
| if ([request valueForHTTPHeaderField:cookieHeaderName]) { |
| // Case insensitive search in |headers|. |
| NSSet* cookieKeys = [item->GetHttpRequestHeaders() |
| keysOfEntriesPassingTest:^(id key, id obj, BOOL* stop) { |
| NSString* header = (NSString*)key; |
| const BOOL found = |
| [header caseInsensitiveCompare:cookieHeaderName] == |
| NSOrderedSame; |
| *stop = found; |
| return found; |
| }]; |
| DCHECK_EQ(1u, [cookieKeys count]); |
| item->RemoveHttpRequestHeaderForKey([cookieKeys anyObject]); |
| } |
| } |
| } |
| |
| // If YES, the page should be closed if it successfully redirects to a native |
| // application, for example if a new tab redirects to the App Store. |
| - (BOOL)shouldClosePageOnNativeApplicationLoad { |
| // The page should be closed if it was initiated by the DOM and there has been |
| // no user interaction with the page since the web view was created, or if the |
| // page has no navigation items. An exception to that when an URL redirect to |
| // an application was initiated from another application (intent), in that |
| // case a prompt will show and page initiating the redirect needs to stay |
| // open to make sure that a prompt is properly owned and to give the user |
| // context about that prompt. |
| BOOL rendererInitiatedWithoutInteraction = |
| self.webStateImpl->HasOpener() && |
| !self.userInteractionState |
| ->UserInteractionRegisteredSinceWebViewCreated(); |
| BOOL noNavigationItems = !(self.navigationManagerImpl->GetItemCount()); |
| BOOL isIntent = !self.webStateImpl->HasOpener() && noNavigationItems; |
| return !isIntent && |
| (rendererInitiatedWithoutInteraction || noNavigationItems); |
| } |
| |
| // Returns YES if response should be rendered in WKWebView. |
| - (BOOL)shouldRenderResponse:(WKNavigationResponse*)WKResponse { |
| if (!WKResponse.canShowMIMEType) { |
| return NO; |
| } |
| |
| GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL); |
| if (responseURL.SchemeIs(url::kDataScheme) && WKResponse.forMainFrame) { |
| // Block rendering data URLs for renderer-initiated navigations in main |
| // frame to prevent abusive behavior (crbug.com/890558). |
| web::NavigationContext* context = |
| [self contextForPendingMainFrameNavigationWithURL:responseURL]; |
| if (!context) { |
| // If the data URL is originally data://foo/bar instead of data:foo/bar, |
| // then the URL is transformed to data:///bar. Considering that the "foo" |
| // part of the URL is lost, it doesn't really make sense to try to match |
| // the URL as it would only work for text. |
| return NO; |
| } |
| // If the server is doing a redirect on a user reload, the navigation is |
| // treated as a reload instead of a redirect. See crbug.com/1165654. |
| web::NavigationItem* lastCommittedItem = |
| self.navigationManagerImpl->GetLastCommittedItem(); |
| BOOL isFakeReload = PageTransitionCoreTypeIs(context->GetPageTransition(), |
| ui::PAGE_TRANSITION_RELOAD) && |
| lastCommittedItem && |
| lastCommittedItem->GetURL() != responseURL; |
| if (context->IsRendererInitiated() || isFakeReload) { |
| return NO; |
| } |
| } |
| |
| return YES; |
| } |
| |
| // Creates DownloadTask for the given navigation response. Headers are passed |
| // as argument to avoid extra NSDictionary -> net::HttpResponseHeaders |
| // conversion. |
| - (void)createDownloadTaskForResponse:(WKNavigationResponse*)WKResponse |
| HTTPHeaders:(net::HttpResponseHeaders*)headers { |
| const GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL); |
| const int64_t contentLength = WKResponse.response.expectedContentLength; |
| const std::string MIMEType = |
| base::SysNSStringToUTF8(WKResponse.response.MIMEType); |
| |
| std::string contentDisposition; |
| if (headers) { |
| headers->GetNormalizedHeader("content-disposition", &contentDisposition); |
| } |
| |
| NSString* HTTPMethod = @"GET"; |
| if (WKResponse.forMainFrame) { |
| web::NavigationContextImpl* context = |
| [self contextForPendingMainFrameNavigationWithURL:responseURL]; |
| // Context lookup fails in rare cases (f.e. after certain redirects, |
| // when WKWebView.URL did not change to redirected page inside |
| // webView:didReceiveServerRedirectForProvisionalNavigation: |
| // as happened in crbug.com/820375). In that case it's not possible |
| // to locate correct context to update |HTTPMethod| and call |
| // WebStateObserver::DidFinishNavigation. Download will fail with incorrect |
| // HTTPMethod, which is better than a crash on null pointer dereferencing. |
| // Missing DidFinishNavigation for download navigation does not cause any |
| // major issues, and it's also better than a crash. |
| if (context) { |
| context->SetIsDownload(true); |
| context->ReleaseItem(); |
| if (context->IsPost()) { |
| HTTPMethod = @"POST"; |
| } |
| // Navigation callbacks can only be called for the main frame. |
| self.webStateImpl->OnNavigationFinished(context); |
| } |
| } |
| web::DownloadController::FromBrowserState( |
| self.webStateImpl->GetBrowserState()) |
| ->CreateDownloadTask(self.webStateImpl, [NSUUID UUID].UUIDString, |
| responseURL, HTTPMethod, contentDisposition, |
| contentLength, MIMEType); |
| } |
| |
| // WKNavigation objects are used as a weak key to store web::NavigationContext. |
| // WKWebView manages WKNavigation lifetime and destroys them after the |
| // navigation is finished. However for window opening navigations WKWebView |
| // passes null WKNavigation to WKNavigationDelegate callbacks and strong key is |
| // used to store web::NavigationContext. Those "null" navigations have to be |
| // cleaned up manually by calling this method. |
| - (void)forgetNullWKNavigation:(WKNavigation*)navigation { |
| if (!navigation) |
| [self.navigationStates removeNavigation:navigation]; |
| } |
| |
| #pragma mark - Auth Challenge |
| |
| // Used in webView:didReceiveAuthenticationChallenge:completionHandler: to |
| // reply with NSURLSessionAuthChallengeDisposition and credentials. |
| - (void)processAuthChallenge:(NSURLAuthenticationChallenge*)challenge |
| forCertAcceptPolicy:(web::CertAcceptPolicy)policy |
| certStatus:(net::CertStatus)certStatus |
| completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| SecTrustRef trust = challenge.protectionSpace.serverTrust; |
| if (policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER) { |
| // Cert is invalid, but user agreed to proceed, override default behavior. |
| completionHandler(NSURLSessionAuthChallengeUseCredential, |
| [NSURLCredential credentialForTrust:trust]); |
| return; |
| } |
| |
| if (policy != web::CERT_ACCEPT_POLICY_ALLOW && |
| SecTrustGetCertificateCount(trust)) { |
| // The cert is invalid and the user has not agreed to proceed. Cache the |
| // cert verification result in |_certVerificationErrors|, so that it can |
| // later be reused inside |didFailProvisionalNavigation:|. |
| // The leaf cert is used as the key, because the chain provided by |
| // |didFailProvisionalNavigation:| will differ (it is the server-supplied |
| // chain), thus if intermediates were considered, the keys would mismatch. |
| scoped_refptr<net::X509Certificate> leafCert = |
| net::x509_util::CreateX509CertificateFromSecCertificate( |
| base::ScopedCFTypeRef<SecCertificateRef>( |
| SecTrustGetCertificateAtIndex(trust, 0), |
| base::scoped_policy::RETAIN), |
| {}); |
| if (leafCert) { |
| bool is_recoverable = |
| policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_UNDECIDED_BY_USER; |
| std::string host = |
| base::SysNSStringToUTF8(challenge.protectionSpace.host); |
| _certVerificationErrors->Put( |
| web::CertHostPair(leafCert, host), |
| web::CertVerificationError(is_recoverable, certStatus)); |
| } |
| } |
| completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| } |
| |
| // Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply |
| // with NSURLSessionAuthChallengeDisposition and credentials. |
| - (void)handleHTTPAuthForChallenge:(NSURLAuthenticationChallenge*)challenge |
| completionHandler: |
| (void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| NSURLProtectionSpace* space = challenge.protectionSpace; |
| DCHECK( |
| [space.authenticationMethod isEqual:NSURLAuthenticationMethodHTTPBasic] || |
| [space.authenticationMethod isEqual:NSURLAuthenticationMethodNTLM] || |
| [space.authenticationMethod isEqual:NSURLAuthenticationMethodHTTPDigest]); |
| |
| self.webStateImpl->OnAuthRequired( |
| space, challenge.proposedCredential, |
| base::BindRepeating(^(NSString* user, NSString* password) { |
| [CRWWKNavigationHandler processHTTPAuthForUser:user |
| password:password |
| completionHandler:completionHandler]; |
| })); |
| } |
| |
| // Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply |
| // with NSURLSessionAuthChallengeDisposition and credentials. |
| + (void)processHTTPAuthForUser:(NSString*)user |
| password:(NSString*)password |
| completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| DCHECK_EQ(user == nil, password == nil); |
| if (!user || !password) { |
| // Embedder cancelled authentication. |
| completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| return; |
| } |
| completionHandler( |
| NSURLSessionAuthChallengeUseCredential, |
| [NSURLCredential |
| credentialWithUser:user |
| password:password |
| persistence:NSURLCredentialPersistenceForSession]); |
| } |
| |
| // Called when a load ends in an error. |
| - (void)handleLoadError:(NSError*)error |
| forNavigation:(WKNavigation*)navigation |
| webView:(WKWebView*)webView |
| provisionalLoad:(BOOL)provisionalLoad { |
| NSError* policyDecisionCancellationError = |
| self.pendingNavigationInfo.cancellationError; |
| if (!policyDecisionCancellationError && error.code == NSURLErrorCancelled) { |
| [self handleCancelledError:error |
| forNavigation:navigation |
| provisionalLoad:provisionalLoad]; |
| if (@available(iOS 13, *)) { |
| // The bug has been fixed on iOS 13. The workaround is only needed for |
| // other versions. |
| } else if (@available(iOS 12.2, *)) { |
| if (![webView.backForwardList.currentItem.URL isEqual:webView.URL] && |
| [self isCurrentNavigationItemPOST]) { |
| UMA_HISTOGRAM_BOOLEAN("WebController.BackForwardListOutOfSync", true); |
| // Sometimes on error the backForward list is out of sync with the |
| // webView, go back or forward to fix it. See crbug.com/951880. |
| if ([webView.backForwardList.backItem.URL isEqual:webView.URL]) { |
| [webView goBack]; |
| } else if ([webView.backForwardList.forwardItem.URL |
| isEqual:webView.URL]) { |
| [webView goForward]; |
| } |
| } |
| } |
| // NSURLErrorCancelled errors that aren't handled by aborting the load will |
| // automatically be retried by the web view, so early return in this case. |
| return; |
| } |
| |
| web::NavigationContextImpl* navigationContext = |
| [self.navigationStates contextForNavigation:navigation]; |
| |
| if (@available(iOS 13, *)) { |
| } else { |
| if (provisionalLoad && !navigationContext && |
| web::RequiresProvisionalNavigationFailureWorkaround()) { |
| // It is likely that |navigationContext| is null because |
| // didStartProvisionalNavigation: was not called with this WKNavigation |
| // object, which was pretty common on iOS 12 and fixed on iOS 13. |
| // See crbug.com/973653 and crbug.com/1004634 for details. |
| return; |
| } |
| } |
| |
| NSError* contextError = web::NetErrorFromError(error); |
| if (policyDecisionCancellationError) { |
| contextError = base::ios::ErrorWithAppendedUnderlyingError( |
| contextError, policyDecisionCancellationError); |
| } |
| |
| navigationContext->SetError(contextError); |
| navigationContext->SetIsPost([self isCurrentNavigationItemPOST]); |
| // TODO(crbug.com/803631) DCHECK that self.currentNavItem is the navigation |
| // item associated with navigationContext. |
| |
| if ([error.domain isEqual:base::SysUTF8ToNSString(web::kWebKitErrorDomain)]) { |
| if (error.code == web::kWebKitErrorPlugInLoadFailed) { |
| // In cases where a Plug-in handles the load do not take any further |
| // action. |
| return; |
| } |
| |
| ui::PageTransition transition = navigationContext->GetPageTransition(); |
| if (error.code == web::kWebKitErrorUrlBlockedByContentFilter) { |
| DCHECK(provisionalLoad); |
| // If URL is blocked due to Restriction, do not take any further |
| // action as WKWebView will show a built-in error. |
| if (!web::RequiresContentFilterBlockingWorkaround()) { |
| // On iOS13, immediately following this navigation, WebKit will |
| // navigate to an internal failure page. Unfortunately, due to how |
| // session restoration works with same document navigations, this page |
| // blocked by a content filter puts WebKit into a state where all |
| // further restoration same-document navigations are 'stuck' on this |
| // failure page. Instead, avoid restoring this page completely. |
| // Consider revisiting this if and when a proper session restoration |
| // API is provided by WKWebView. |
| self.navigationManagerImpl->SetWKWebViewNextPendingUrlNotSerializable( |
| navigationContext->GetUrl()); |
| return; |
| } else if (!PageTransitionIsNewNavigation(transition)) { |
| return; |
| } |
| } |
| |
| if (error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange && |
| !policyDecisionCancellationError) { |
| // Handle Frame Load Interrupted errors from WebView. This block is |
| // executed when web controller rejected the load inside |
| // decidePolicyForNavigationResponse: to handle download or WKWebView |
| // opened a Universal Link. |
| if (!navigationContext->IsDownload()) { |
| // Non-download navigation was cancelled because WKWebView has opened a |
| // Universal Link and called webView:didFailProvisionalNavigation:. |
| self.navigationManagerImpl->DiscardNonCommittedItems(); |
| [self.navigationStates removeNavigation:navigation]; |
| } |
| return; |
| } |
| |
| if (error.code == web::kWebKitErrorCannotShowUrl) { |
| if (!navigationContext->GetUrl().is_valid()) { |
| // It won't be possible to load an error page for invalid URL, because |
| // WKWebView will revert the url to about:blank. Simply discard pending |
| // item and fail the navigation. |
| navigationContext->ReleaseItem(); |
| self.webStateImpl->OnNavigationFinished(navigationContext); |
| self.webStateImpl->OnPageLoaded(navigationContext->GetUrl(), false); |
| return; |
| } |
| } |
| } |
| |
| web::NavigationManager* navManager = |
| self.webStateImpl->GetNavigationManager(); |
| web::NavigationItem* lastCommittedItem = navManager->GetLastCommittedItem(); |
| if (lastCommittedItem && !web::IsWKWebViewSSLCertError(error)) { |
| // Reset SSL status for last committed navigation to avoid showing security |
| // status for error pages. |
| if (!lastCommittedItem->GetSSL().Equals(web::SSLStatus())) { |
| lastCommittedItem->GetSSL() = web::SSLStatus(); |
| self.webStateImpl->DidChangeVisibleSecurityState(); |
| } |
| } |
| |
| web::NavigationItemImpl* item = |
| web::GetItemWithUniqueID(self.navigationManagerImpl, navigationContext); |
| |
| if (item) { |
| if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) { |
| WKNavigation* errorNavigation = |
| [self displayErrorPageWithError:error |
| inWebView:webView |
| isProvisionalLoad:provisionalLoad]; |
| |
| std::unique_ptr<web::NavigationContextImpl> originalContext = |
| [self.navigationStates removeNavigation:navigation]; |
| originalContext->SetLoadingErrorPage(true); |
| [self.navigationStates setContext:std::move(originalContext) |
| forNavigation:errorNavigation]; |
| // Return as the context was moved. |
| return; |
| } else { |
| GURL errorURL = |
| net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]); |
| web::ErrorRetryCommand command = web::ErrorRetryCommand::kDoNothing; |
| if (provisionalLoad) { |
| command = |
| item->error_retry_state_machine().DidFailProvisionalNavigation( |
| net::GURLWithNSURL(webView.URL), errorURL); |
| } else { |
| command = item->error_retry_state_machine().DidFailNavigation( |
| net::GURLWithNSURL(webView.URL)); |
| } |
| [self handleErrorRetryCommand:command |
| navigationItem:item |
| navigationContext:navigationContext |
| originalNavigation:navigation |
| webView:webView]; |
| } |
| } |
| |
| // Don't commit the pending item or call OnNavigationFinished until the |
| // placeholder navigation finishes loading. |
| } |
| |
| // Displays an error page with details from |error| in |webView| using JS error |
| // pages (associated with the kUseJSForErrorPage flag.) The error page is |
| // presented with |transition| and associated with |blockedNSURL|. |
| - (void)displayError:(NSError*)error |
| forCancelledNavigationToURL:(NSURL*)blockedNSURL |
| inWebView:(WKWebView*)webView |
| withTransition:(ui::PageTransition)transition { |
| DCHECK(base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)); |
| |
| const GURL blockedURL = net::GURLWithNSURL(blockedNSURL); |
| |
| // Error page needs the URL string in the error's userInfo for proper |
| // display. |
| if (!error.userInfo[NSURLErrorFailingURLStringErrorKey]) { |
| NSMutableDictionary* updatedUserInfo = [[NSMutableDictionary alloc] init]; |
| [updatedUserInfo addEntriesFromDictionary:error.userInfo]; |
| [updatedUserInfo setObject:blockedNSURL.absoluteString |
| forKey:NSURLErrorFailingURLStringErrorKey]; |
| |
| error = [NSError errorWithDomain:error.domain |
| code:error.code |
| userInfo:updatedUserInfo]; |
| } |
| |
| WKNavigation* errorNavigation = [self displayErrorPageWithError:error |
| inWebView:webView |
| isProvisionalLoad:YES]; |
| |
| // Create pending item. |
| self.navigationManagerImpl->AddPendingItem( |
| blockedURL, web::Referrer(), transition, |
| web::NavigationInitiationType::BROWSER_INITIATED); |
| |
| // Create context. |
| std::unique_ptr<web::NavigationContextImpl> context = |
| web::NavigationContextImpl::CreateNavigationContext( |
| self.webStateImpl, blockedURL, |
| /*has_user_gesture=*/true, transition, |
| /*is_renderer_initiated=*/false); |
| std::unique_ptr<web::NavigationItemImpl> item = |
| self.navigationManagerImpl->ReleasePendingItem(); |
| context->SetNavigationItemUniqueID(item->GetUniqueID()); |
| context->SetItem(std::move(item)); |
| context->SetError(error); |
| context->SetLoadingErrorPage(true); |
| |
| self.webStateImpl->OnNavigationStarted(context.get()); |
| |
| [self.navigationStates setContext:std::move(context) |
| forNavigation:errorNavigation]; |
| } |
| |
| // Creates and returns a new WKNavigation to load an error page displaying |
| // details of |error| inside |webView|. (Using JS error pages associated with |
| // the kUseJSForErrorPage flag.) |provisionalLoad| should be set according to |
| // whether or not the error occurred during a provisionalLoad. |
| - (WKNavigation*)displayErrorPageWithError:(NSError*)error |
| inWebView:(WKWebView*)webView |
| isProvisionalLoad:(BOOL)provisionalLoad { |
| DCHECK(base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)); |
| |
| CRWErrorPageHelper* errorPage = |
| [[CRWErrorPageHelper alloc] initWithError:error]; |
| WKBackForwardListItem* backForwardItem = webView.backForwardList.currentItem; |
| // There are 4 possible scenarios here: |
| // 1. Current nav item is an error page for failed URL; |
| // 2. Current nav item has a failed URL. This may happen when |
| // back/forward/refresh on a loaded page; |
| // 3. Current nav item is an irrelevant page. |
| // 4. Current nav item is a session restoration. |
| // For 1, 2 and 4, load an empty string to remove existing JS code. The URL is |
| // also updated to the URL of the page that failed to allow back/forward |
| // navigations even on navigations originating from pushstate. See |
| // crbug.com/1153261. |
| // For 3, load error page file to create a new nav item. |
| // The actual error HTML will be loaded in didFinishNavigation callback. |
| WKNavigation* errorNavigation = nil; |
| if (provisionalLoad && |
| ![errorPage |
| isErrorPageFileURLForFailedNavigationURL:backForwardItem.URL] && |
| ![backForwardItem.URL isEqual:errorPage.failedNavigationURL] && |
| !web::wk_navigation_util::IsRestoreSessionUrl(backForwardItem.URL)) { |
| errorNavigation = [webView loadFileURL:errorPage.errorPageFileURL |
| allowingReadAccessToURL:errorPage.errorPageFileURL]; |
| } else { |
| errorNavigation = [webView loadHTMLString:@"" |
| baseURL:errorPage.failedNavigationURL]; |
| } |
| [self.navigationStates setState:web::WKNavigationState::REQUESTED |
| forNavigation:errorNavigation]; |
| |
| return errorNavigation; |
| } |
| |
| // Handles cancelled load in WKWebView (error with NSURLErrorCancelled code). |
| - (void)handleCancelledError:(NSError*)error |
| forNavigation:(WKNavigation*)navigation |
| provisionalLoad:(BOOL)provisionalLoad { |
| if ([self shouldCancelLoadForCancelledError:error |
| provisionalLoad:provisionalLoad]) { |
| std::unique_ptr<web::NavigationContextImpl> navigationContext = |
| [self.navigationStates removeNavigation:navigation]; |
| [self loadCancelled]; |
| web::NavigationItemImpl* item = |
| navigationContext ? web::GetItemWithUniqueID(self.navigationManagerImpl, |
| navigationContext.get()) |
| : nullptr; |
| if (self.navigationManagerImpl->GetPendingItem() == item) { |
| self.navigationManagerImpl->DiscardNonCommittedItems(); |
| } |
| |
| if (provisionalLoad) { |
| if (base::ios::IsRunningOnIOS13OrLater() || navigationContext) { |
| // It is likely that |navigationContext| is null because |
| // didStartProvisionalNavigation: was not called with this WKNavigation |
| // object. See crbug.com/973653 for details. |
| self.webStateImpl->OnNavigationFinished(navigationContext.get()); |
| } |
| } |
| } else if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) && |
| !provisionalLoad) { |
| web::NavigationContextImpl* navigationContext = |
| [self.navigationStates contextForNavigation:navigation]; |
| web::NavigationItemImpl* item = |
| navigationContext ? web::GetItemWithUniqueID(self.navigationManagerImpl, |
| navigationContext) |
| : nullptr; |
| if (item) { |
| // Since the navigation has already been committed, it will retain its |
| // back / forward item even though the load has been cancelled. Update the |
| // error state machine so that if future loads of this item fail, the same |
| // item will be reused for the error view rather than loading a |
| // placeholder URL into a new navigation item, since the latter would |
| // destroy the forward list. |
| item->error_retry_state_machine().SetNoNavigationError(); |
| } |
| } |
| } |
| |
| // Executes the command specified by the ErrorRetryStateMachine. |
| - (void)handleErrorRetryCommand:(web::ErrorRetryCommand)command |
| navigationItem:(web::NavigationItemImpl*)item |
| navigationContext:(web::NavigationContextImpl*)context |
| originalNavigation:(WKNavigation*)originalNavigation |
| webView:(WKWebView*)webView { |
| DCHECK(!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)); |
| if (command == web::ErrorRetryCommand::kDoNothing) |
| return; |
| |
| DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID()); |
| switch (command) { |
| case web::ErrorRetryCommand::kLoadPlaceholder: { |
| // This case only happens when a new request failed in provisional |
| // navigation. Disassociate the navigation context from the original |
| // request and resuse it for the placeholder navigation. |
| std::unique_ptr<web::NavigationContextImpl> originalContext = |
| [self.navigationStates removeNavigation:originalNavigation]; |
| [self loadPlaceholderInWebViewForURL:item->GetURL() |
| rendererInitiated:context->IsRendererInitiated() |
| forContext:std::move(originalContext)]; |
| } break; |
| |
| case web::ErrorRetryCommand::kLoadError: |
| [self loadErrorPageForNavigationItem:item |
| navigationContext:originalNavigation |
| webView:webView]; |
| break; |
| |
| case web::ErrorRetryCommand::kReload: |
| [webView reload]; |
| break; |
| |
| case web::ErrorRetryCommand::kRewriteToWebViewURL: { |
| std::unique_ptr<web::NavigationContextImpl> navigationContext = |
| [self.delegate navigationHandler:self |
| registerLoadRequestForURL:item->GetURL() |
| sameDocumentNavigation:NO |
| hasUserGesture:NO |
| rendererInitiated:context->IsRendererInitiated() |
| placeholderNavigation:NO]; |
| WKNavigation* navigation = |
| [webView loadHTMLString:@"" |
| baseURL:net::NSURLWithGURL(item->GetURL())]; |
| navigationContext->SetError(context->GetError()); |
| navigationContext->SetIsPost(context->IsPost()); |
| [self.navigationStates setContext:std::move(navigationContext) |
| forNavigation:navigation]; |
| } break; |
| |
| case web::ErrorRetryCommand::kRewriteToPlaceholderURL: { |
| std::unique_ptr<web::NavigationContextImpl> originalContext = |
| [self.navigationStates removeNavigation:originalNavigation]; |
| originalContext->SetPlaceholderNavigation(YES); |
| GURL placeholderURL = CreatePlaceholderUrlForUrl(item->GetURL()); |
| |
| WKNavigation* navigation = |
| [webView loadHTMLString:@"" |
| baseURL:net::NSURLWithGURL(placeholderURL)]; |
| [self.navigationStates setContext:std::move(originalContext) |
| forNavigation:navigation]; |
| } break; |
| |
| case web::ErrorRetryCommand::kDoNothing: |
| NOTREACHED(); |
| } |
| } |
| |
| // Used to decide whether a load that generates errors with the |
| // NSURLErrorCancelled code should be cancelled. |
| - (BOOL)shouldCancelLoadForCancelledError:(NSError*)error |
| provisionalLoad:(BOOL)provisionalLoad { |
| DCHECK(error.code == NSURLErrorCancelled || |
| error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange); |
| // Do not cancel the load if it is for an app specific URL, as such errors |
| // are produced during the app specific URL load process. |
| const GURL errorURL = |
| net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]); |
| if (web::GetWebClient()->IsAppSpecificURL(errorURL)) |
| return NO; |
| |
| return provisionalLoad; |
| } |
| |
| // Loads the error page. |
| - (void)loadErrorPageForNavigationItem:(web::NavigationItemImpl*)item |
| navigationContext:(WKNavigation*)navigation |
| webView:(WKWebView*)webView { |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| NSError* error = context->GetError(); |
| DCHECK(error); |
| DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID()); |
| |
| net::SSLInfo info; |
| base::Optional<net::SSLInfo> ssl_info = base::nullopt; |
| |
| if (web::IsWKWebViewSSLCertError(error)) { |
| web::GetSSLInfoFromWKWebViewSSLCertError(error, &info); |
| if (info.cert) { |
| // Retrieve verification results from _certVerificationErrors cache to |
| // avoid unnecessary recalculations. Verification results are cached for |
| // the leaf cert, because the cert chain in |
| // |didReceiveAuthenticationChallenge:| is the OS constructed chain, while |
| // |chain| is the chain from the server. |
| NSArray* chain = error.userInfo[web::kNSErrorPeerCertificateChainKey]; |
| NSURL* requestURL = error.userInfo[web::kNSErrorFailingURLKey]; |
| NSString* host = requestURL.host; |
| scoped_refptr<net::X509Certificate> leafCert; |
| if (chain.count && host.length) { |
| // The complete cert chain may not be available, so the leaf cert is |
| // used as a key to retrieve _certVerificationErrors, as well as for |
| // storing the cert decision. |
| leafCert = web::CreateCertFromChain(@[ chain.firstObject ]); |
| if (leafCert) { |
| auto error = _certVerificationErrors->Get( |
| {leafCert, base::SysNSStringToUTF8(host)}); |
| bool cacheHit = error != _certVerificationErrors->end(); |
| if (cacheHit) { |
| info.is_fatal_cert_error = error->second.is_recoverable; |
| info.cert_status = error->second.status; |
| } |
| UMA_HISTOGRAM_BOOLEAN("WebController.CertVerificationErrorsCacheHit", |
| cacheHit); |
| } |
| } |
| } |
| ssl_info = base::make_optional<net::SSLInfo>(info); |
| } |
| NSString* failingURLString = |
| error.userInfo[NSURLErrorFailingURLStringErrorKey]; |
| GURL failingURL(base::SysNSStringToUTF8(failingURLString)); |
| GURL itemURL = item->GetURL(); |
| if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) { |
| if (itemURL != failingURL) |
| item->SetVirtualURL(failingURL); |
| } |
| int itemID = item->GetUniqueID(); |
| web::GetWebClient()->PrepareErrorPage( |
| self.webStateImpl, failingURL, error, context->IsPost(), |
| self.webStateImpl->GetBrowserState()->IsOffTheRecord(), ssl_info, |
| context->GetNavigationId(), base::BindOnce(^(NSString* errorHTML) { |
| if (errorHTML) { |
| if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) { |
| CRWErrorPageHelper* errorPageHelper = |
| [[CRWErrorPageHelper alloc] initWithError:context->GetError()]; |
| |
| [webView evaluateJavaScript:[errorPageHelper |
| scriptForInjectingHTML:errorHTML |
| addAutomaticReload:YES] |
| completionHandler:^(id result, NSError* error) { |
| DCHECK(!error) |
| << "Error injecting error page HTML: " |
| << base::SysNSStringToUTF8(error.description); |
| }]; |
| } else { |
| WKNavigation* navigation = |
| [webView loadHTMLString:errorHTML |
| baseURL:net::NSURLWithGURL(failingURL)]; |
| auto loadHTMLContext = |
| web::NavigationContextImpl::CreateNavigationContext( |
| self.webStateImpl, failingURL, |
| /*has_user_gesture=*/false, ui::PAGE_TRANSITION_FIRST, |
| /*is_renderer_initiated=*/false); |
| |
| if (!base::FeatureList::IsEnabled( |
| web::features::kUseJSForErrorPage)) |
| loadHTMLContext->SetLoadingErrorPage(true); |
| |
| loadHTMLContext->SetNavigationItemUniqueID(itemID); |
| |
| [self.navigationStates setContext:std::move(loadHTMLContext) |
| forNavigation:navigation]; |
| [self.navigationStates setState:web::WKNavigationState::REQUESTED |
| forNavigation:navigation]; |
| } |
| } |
| |
| if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) { |
| // TODO(crbug.com/803503): only call these for placeholder navigation |
| // because they should have already been triggered during navigation |
| // commit for failures that happen after commit. |
| [self.delegate navigationHandlerDidStartLoading:self]; |
| // TODO(crbug.com/973765): This is a workaround because |item| might |
| // get released after |
| // |self.navigationManagerImpl-> |
| // CommitPendingItem(context->ReleaseItem()|. |
| // Remove this once navigation refactor is done. |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem()); |
| [self.delegate navigationHandler:self |
| setDocumentURL:itemURL |
| context:context]; |
| |
| // If |context| is a placeholder navigation, this is the second part |
| // of the error page load for a provisional load failure. Rewrite the |
| // context URL to actual URL and trigger the deferred |
| // |OnNavigationFinished| callback. This is also needed if |context| |
| // is not yet committed, which can happen on a reload/back/forward |
| // load that failed in provisional navigation. |
| if ((!base::FeatureList::IsEnabled( |
| web::features::kUseJSForErrorPage) && |
| context->IsPlaceholderNavigation()) || |
| !context->HasCommitted()) { |
| context->SetUrl(itemURL); |
| if (!base::FeatureList::IsEnabled( |
| web::features::kUseJSForErrorPage)) |
| context->SetPlaceholderNavigation(false); |
| context->SetHasCommitted(true); |
| self.webStateImpl->OnNavigationFinished(context); |
| } |
| } else { |
| // TODO(crbug.com/973765): This is a workaround because |item| might |
| // get released after |
| // |self.navigationManagerImpl-> |
| // CommitPendingItem(context->ReleaseItem()|. |
| // Remove this once navigation refactor is done. |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem()); |
| [self.delegate navigationHandler:self |
| setDocumentURL:itemURL |
| context:context]; |
| |
| // Rewrite the context URL to actual URL and trigger the deferred |
| // |OnNavigationFinished| callback. |
| context->SetUrl(failingURL); |
| context->SetHasCommitted(true); |
| self.webStateImpl->OnNavigationFinished(context); |
| } |
| |
| // For SSL cert error pages, SSLStatus needs to be set manually because |
| // the placeholder navigation for the error page is committed and |
| // there is no server trust (since there's no network navigation), which |
| // is required to create a cert in CRWSSLStatusUpdater. |
| if (web::IsWKWebViewSSLCertError(context->GetError())) { |
| web::SSLStatus& SSLStatus = |
| self.navigationManagerImpl->GetLastCommittedItem()->GetSSL(); |
| SSLStatus.cert_status = info.cert_status; |
| SSLStatus.certificate = info.cert; |
| SSLStatus.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN; |
| self.webStateImpl->DidChangeVisibleSecurityState(); |
| } |
| |
| [self.delegate navigationHandler:self |
| didCompleteLoadWithSuccess:NO |
| forContext:context]; |
| self.webStateImpl->OnPageLoaded(failingURL, NO); |
| })); |
| } |
| |
| // Resets any state that is associated with a specific document object (e.g., |
| // page interaction tracking). |
| - (void)resetDocumentSpecificState { |
| self.userInteractionState->SetLastUserInteraction(nullptr); |
| self.userInteractionState->SetTapInProgress(false); |
| } |
| |
| #pragma mark - Public methods |
| |
| - (void)stopLoading { |
| self.pendingNavigationInfo.cancelled = YES; |
| [self loadCancelled]; |
| _certVerificationErrors->Clear(); |
| } |
| |
| - (void)loadCancelled { |
| // TODO(crbug.com/821995): Check if this function should be removed. |
| if (self.navigationState != web::WKNavigationState::FINISHED) { |
| self.navigationState = web::WKNavigationState::FINISHED; |
| if (!self.beingDestroyed) { |
| self.webStateImpl->SetIsLoading(false); |
| } |
| } |
| } |
| |
| // Returns context for pending navigation that has |URL|. null if there is no |
| // matching pending navigation. |
| - (web::NavigationContextImpl*)contextForPendingMainFrameNavigationWithURL: |
| (const GURL&)URL { |
| // Here the enumeration variable |navigation| is __strong to allow setting it |
| // to nil. |
| for (__strong id navigation in [self.navigationStates pendingNavigations]) { |
| if (navigation == [NSNull null]) { |
| // null is a valid navigation object passed to WKNavigationDelegate |
| // callbacks and represents window opening action. |
| navigation = nil; |
| } |
| |
| web::NavigationContextImpl* context = |
| [self.navigationStates contextForNavigation:navigation]; |
| if (context && context->GetUrl() == URL) { |
| return context; |
| } |
| } |
| return nullptr; |
| } |
| |
| - (BOOL)isCurrentNavigationBackForward { |
| if (!self.currentNavItem) |
| return NO; |
| WKNavigationType currentNavigationType = |
| self.currentBackForwardListItemHolder->navigation_type(); |
| return currentNavigationType == WKNavigationTypeBackForward; |
| } |
| |
| - (BOOL)isCurrentNavigationItemPOST { |
| // |self.navigationHandler.pendingNavigationInfo| will be nil if the |
| // decidePolicy* delegate methods were not called. |
| NSString* HTTPMethod = |
| self.pendingNavigationInfo |
| ? self.pendingNavigationInfo.HTTPMethod |
| : self.currentBackForwardListItemHolder->http_method(); |
| if ([HTTPMethod isEqual:@"POST"]) { |
| return YES; |
| } |
| if (!self.currentNavItem) { |
| return NO; |
| } |
| return self.currentNavItem->HasPostData(); |
| } |
| |
| // Returns the WKBackForwardListItemHolder for the current navigation item. |
| - (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder { |
| web::NavigationItem* item = self.currentNavItem; |
| DCHECK(item); |
| web::WKBackForwardListItemHolder* holder = |
| web::WKBackForwardListItemHolder::FromNavigationItem(item); |
| DCHECK(holder); |
| return holder; |
| } |
| |
| // Updates current state with any pending information. Should be called when a |
| // navigation is committed. |
| - (void)commitPendingNavigationInfoInWebView:(WKWebView*)webView { |
| if (self.pendingNavigationInfo.referrer) { |
| _currentReferrerString = [self.pendingNavigationInfo.referrer copy]; |
| } |
| [self updateCurrentBackForwardListItemHolderInWebView:webView]; |
| |
| self.pendingNavigationInfo = nil; |
| } |
| |
| // Updates the WKBackForwardListItemHolder navigation item. |
| - (void)updateCurrentBackForwardListItemHolderInWebView:(WKWebView*)webView { |
| if (!self.currentNavItem) { |
| // TODO(crbug.com/925304): Pending item (which stores the holder) should be |
| // owned by NavigationContext object. Pending item should never be null. |
| return; |
| } |
| |
| web::WKBackForwardListItemHolder* holder = |
| self.currentBackForwardListItemHolder; |
| |
| WKNavigationType navigationType = |
| self.pendingNavigationInfo ? self.pendingNavigationInfo.navigationType |
| : WKNavigationTypeOther; |
| holder->set_back_forward_list_item(webView.backForwardList.currentItem); |
| holder->set_navigation_type(navigationType); |
| holder->set_http_method(self.pendingNavigationInfo.HTTPMethod); |
| |
| // Only update the MIME type in the holder if there was MIME type information |
| // as part of this pending load. It will be nil when doing a fast |
| // back/forward navigation, for instance, because the callback that would |
| // populate it is not called in that flow. |
| if (self.pendingNavigationInfo.MIMEType) |
| holder->set_mime_type(self.pendingNavigationInfo.MIMEType); |
| } |
| |
| - (web::Referrer)currentReferrer { |
| // Referrer string doesn't include the fragment, so in cases where the |
| // previous URL is equal to the current referrer plus the fragment the |
| // previous URL is returned as current referrer. |
| NSString* referrerString = _currentReferrerString; |
| |
| // In case of an error evaluating the JavaScript simply return empty string. |
| if (referrerString.length == 0) |
| return web::Referrer(); |
| |
| web::NavigationItem* item = self.currentNavItem; |
| GURL navigationURL = item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| NSString* previousURLString = base::SysUTF8ToNSString(navigationURL.spec()); |
| // Check if the referrer is equal to the previous URL minus the hash symbol. |
| // L'#' is used to convert the char '#' to a unichar. |
| if ([previousURLString length] > referrerString.length && |
| [previousURLString hasPrefix:referrerString] && |
| [previousURLString characterAtIndex:referrerString.length] == L'#') { |
| referrerString = previousURLString; |
| } |
| // Since referrer is being extracted from the destination page, the correct |
| // policy from the origin has *already* been applied. Since the extracted URL |
| // is the post-policy value, and the source policy is no longer available, |
| // the policy is set to Always so that whatever WebKit decided to send will be |
| // re-sent when replaying the entry. |
| // TODO(crbug.com/227769): When possible, get the real referrer and policy in |
| // advance and use that instead. |
| return web::Referrer(GURL(base::SysNSStringToUTF8(referrerString)), |
| web::ReferrerPolicyAlways); |
| } |
| |
| - (void)setLastCommittedNavigationItemTitle:(NSString*)title { |
| DCHECK(title); |
| web::NavigationItem* item = |
| self.navigationManagerImpl->GetLastCommittedItem(); |
| if (!item) |
| return; |
| |
| item->SetTitle(base::SysNSStringToUTF16(title)); |
| self.webStateImpl->OnTitleChanged(); |
| } |
| |
| - (ui::PageTransition)pageTransitionFromNavigationType: |
| (WKNavigationType)navigationType { |
| switch (navigationType) { |
| case WKNavigationTypeLinkActivated: |
| return ui::PAGE_TRANSITION_LINK; |
| case WKNavigationTypeFormSubmitted: |
| case WKNavigationTypeFormResubmitted: |
| return ui::PAGE_TRANSITION_FORM_SUBMIT; |
| case WKNavigationTypeBackForward: |
| return ui::PAGE_TRANSITION_FORWARD_BACK; |
| case WKNavigationTypeReload: |
| return ui::PAGE_TRANSITION_RELOAD; |
| case WKNavigationTypeOther: |
| // The "Other" type covers a variety of very different cases, which may |
| // or may not be the result of user actions. For now, guess based on |
| // whether there's been an interaction since the last URL change. |
| // TODO(crbug.com/549301): See if this heuristic can be improved. |
| return self.userInteractionState |
| ->UserInteractionRegisteredSinceLastUrlChange() |
| ? ui::PAGE_TRANSITION_LINK |
| : ui::PAGE_TRANSITION_CLIENT_REDIRECT; |
| } |
| } |
| |
| - (web::NavigationContextImpl*) |
| loadPlaceholderInWebViewForURL:(const GURL&)originalURL |
| rendererInitiated:(BOOL)rendererInitiated |
| forContext:(std::unique_ptr<web::NavigationContextImpl>) |
| originalContext { |
| DCHECK(!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)); |
| GURL placeholderURL = CreatePlaceholderUrlForUrl(originalURL); |
| |
| WKWebView* webView = [self.delegate webViewForWebViewHandler:self]; |
| |
| NSURLRequest* request = |
| [NSURLRequest requestWithURL:net::NSURLWithGURL(placeholderURL)]; |
| WKNavigation* navigation = [webView loadRequest:request]; |
| |
| NSError* error = originalContext ? originalContext->GetError() : nil; |
| if (web::RequiresContentFilterBlockingWorkaround() && |
| [error.domain isEqual:base::SysUTF8ToNSString(web::kWebKitErrorDomain)] && |
| error.code == web::kWebKitErrorUrlBlockedByContentFilter) { |
| GURL currentWKItemURL = |
| net::GURLWithNSURL(webView.backForwardList.currentItem.URL); |
| if (currentWKItemURL.SchemeIs(url::kAboutScheme)) { |
| // WKWebView will pass nil WKNavigation objects to WKNavigationDelegate |
| // callback for this navigation. TODO(crbug.com/954332): Remove the |
| // workaround when https://bugs.webkit.org/show_bug.cgi?id=196930 is |
| // fixed. |
| navigation = nil; |
| } |
| } |
| |
| [self.navigationStates setState:web::WKNavigationState::REQUESTED |
| forNavigation:navigation]; |
| std::unique_ptr<web::NavigationContextImpl> navigationContext; |
| if (originalContext) { |
| navigationContext = std::move(originalContext); |
| navigationContext->SetPlaceholderNavigation(YES); |
| } else { |
| navigationContext = [self.delegate navigationHandler:self |
| registerLoadRequestForURL:originalURL |
| sameDocumentNavigation:NO |
| hasUserGesture:NO |
| rendererInitiated:rendererInitiated |
| placeholderNavigation:YES]; |
| } |
| [self.navigationStates setContext:std::move(navigationContext) |
| forNavigation:navigation]; |
| return [self.navigationStates contextForNavigation:navigation]; |
| } |
| |
| - (void)webPageChangedWithContext:(web::NavigationContextImpl*)context |
| webView:(WKWebView*)webView { |
| web::Referrer referrer = self.currentReferrer; |
| // If no referrer was known in advance, record it now. (If there was one, |
| // keep it since it will have a more accurate URL and policy than what can |
| // be extracted from the landing page.) |
| web::NavigationItem* currentItem = self.currentNavItem; |
| |
| // TODO(crbug.com/925304): Pending item (which should be used here) should be |
| // owned by NavigationContext object. Pending item should never be null. |
| if (currentItem && !currentItem->GetReferrer().url.is_valid()) { |
| currentItem->SetReferrer(referrer); |
| } |
| |
| // TODO(crbug.com/956511): This shouldn't be called for push/replaceState. |
| [self resetDocumentSpecificState]; |
| |
| [self.delegate navigationHandlerDidStartLoading:self]; |
| // Do not commit pending item in the middle of loading a placeholder URL. The |
| // item will be committed when webUI is displayed. |
| if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage) || |
| !context->IsPlaceholderNavigation()) { |
| self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem()); |
| if (context->IsLoadingHtmlString()) { |
| self.navigationManagerImpl->GetLastCommittedItem()->SetURL( |
| context->GetUrl()); |
| } |
| } |
| } |
| |
| @end |