blob: d9a48add24a7e8a6b262a671b79988747e22be65 [file] [log] [blame]
// 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"
#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"
#import "ios/web/navigation/navigation_context_impl.h"
#import "ios/web/navigation/navigation_item_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
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 (base::FeatureList::IsEnabled(
web::features::kCreatePendingItemForPostFormSubmission)) {
if (web::wk_navigation_util::IsRestoreSessionUrl(item->GetURL())) {
self.webStateImpl->SetUserAgent(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];
BOOL forceBlockUniversalLinks = self.blockUniversalLinksOnNextDecidePolicy;
self.blockUniversalLinksOnNextDecidePolicy = NO;
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 (base::FeatureList::IsEnabled(
web::features::kCreatePendingItemForPostFormSubmission)) {
if (web::wk_navigation_util::IsRestoreSessionUrl(item->GetURL())) {
self.webStateImpl->SetUserAgent(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 error navigation, pass through.
if ([CRWErrorPageHelper isErrorPageFileURL:requestURL]) {
if (action.sourceFrame.mainFrame) {
// Disallow renderer initiated navigations to error URLs.
decisionHandler(WKNavigationActionPolicyCancel);
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
return;
}
ui::PageTransition transition =
[self pageTransitionFromNavigationType:action.navigationType];
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;
}
// On iOS 12, we allow the navigation since cancelling it here causes
// crbug.com/965067. The underlying issue is a WebKit bug that converts
// valid URLs into invalid ones. This issue is fixed in iOS 13.
if (@available(iOS 13, *)) {
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
// First check if the navigation action should be blocked by the controller
// and make sure to update the controller in the case that the controller
// can't handle the request URL. Then use the embedders' policyDeciders to
// either: 1- Handle the URL it self and return false to stop the controller
// from proceeding with the navigation if needed. or 2- return true to allow
// the navigation to be proceeded by the web controller.
web::WebStatePolicyDecider::PolicyDecision policyDecision =
web::WebStatePolicyDecider::PolicyDecision::Allow();
if (web::GetWebClient()->IsAppSpecificURL(requestURL)) {
// |policyDecision| is initialized above this conditional to allow loads, so
// it only needs to be overwritten if the load should be cancelled.
if (![self shouldAllowAppSpecificURLNavigationAction:action
transition:transition]) {
policyDecision = web::WebStatePolicyDecider::PolicyDecision::Cancel();
}
if (policyDecision.ShouldAllowNavigation()) {
[self.delegate navigationHandler:self createWebUIForURL:requestURL];
}
}
BOOL webControllerCanShow =
web::UrlHasWebScheme(requestURL) ||
web::GetWebClient()->IsAppSpecificURL(requestURL) ||
requestURL.SchemeIs(url::kFileScheme) ||
requestURL.SchemeIs(url::kAboutScheme) ||
requestURL.SchemeIs(url::kBlobScheme);
__weak CRWWKNavigationHandler* weakSelf = self;
auto callback = base::BindOnce(
^(web::WebStatePolicyDecider::PolicyDecision policyDecision) {
__strong CRWWKNavigationHandler* strongSelf = weakSelf;
// The WebState may have been closed in the ShouldAllowRequest callback.
if (!strongSelf || strongSelf.beingDestroyed) {
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
if (!webControllerCanShow) {
policyDecision = web::WebStatePolicyDecider::PolicyDecision::Cancel();
}
[strongSelf answerDecisionHandler:decisionHandler
forNavigationAction:action
withPolicyDecision:policyDecision
webView:webView
forceBlockUniversalLinks:forceBlockUniversalLinks];
});
if (!policyDecision.ShouldAllowNavigation()) {
std::move(callback).Run(policyDecision);
return;
}
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);
self.webStateImpl->ShouldAllowRequest(action.request, requestInfo,
std::move(callback));
}
- (void)webView:(WKWebView*)webView
decidePolicyForNavigationResponse:(WKNavigationResponse*)WKResponse
decisionHandler:
(void (^)(WKNavigationResponsePolicy))handler {
[self didReceiveWKNavigationDelegateCallback];
// If this is a error navigation, pass through.
GURL responseURL = net::GURLWithNSURL(WKResponse.response.URL);
if ([CRWErrorPageHelper isErrorPageFileURL:responseURL]) {
if (self.webStateImpl->ShouldAllowErrorPageToBeDisplayed(
WKResponse.response, WKResponse.forMainFrame)) {
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;
}
if (![CRWErrorPageHelper isErrorPageFileURL:webViewURL] &&
!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) back/forward navigation to an app-specific URL should be allowed.
// 2) navigation to an app-specific URL should be allowed from other
// app-specific URLs
bool exemptedAppSpecificLoad = false;
bool isBackForward =
self.pendingNavigationInfo.navigationType == WKNavigationTypeBackForward;
bool isRestoringSession = IsRestoreSessionUrl(self.documentURL);
exemptedAppSpecificLoad =
isBackForward || isRestoringSession || self.webStateImpl->HasWebUI();
if (!web::GetWebClient()->IsAppSpecificURL(webViewURL) ||
!exemptedAppSpecificLoad) {
self.webStateImpl->ClearWebUI();
}
self.webStateImpl->GetNavigationManagerImpl().OnNavigationStarted(webViewURL);
std::unique_ptr<web::NavigationContextImpl> navigationContext =
[self.delegate navigationHandler:self
registerLoadRequestForURL:webViewURL
sameDocumentNavigation:NO
hasUserGesture:self.pendingNavigationInfo.hasUserGesture
rendererInitiated:YES];
web::NavigationContextImpl* navigationContextPtr = navigationContext.get();
// GetPendingItem which may be called inside OnNavigationStarted relies on
// association between NavigationContextImpl and WKNavigation.
[self.navigationStates setContext:std::move(navigationContext)
forNavigation:navigation];
self.webStateImpl->OnNavigationStarted(navigationContextPtr);
DCHECK_EQ(web::WKNavigationState::REQUESTED, self.navigationState);
}
- (void)webView:(WKWebView*)webView
didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation {
[self didReceiveWKNavigationDelegateCallback];
GURL webViewURL = net::GURLWithNSURL(webView.URL);
[self.navigationStates setState:web::WKNavigationState::REDIRECTED
forNavigation:navigation];
web::NavigationContextImpl* context =
[self.navigationStates contextForNavigation:navigation];
if (!context)
return;
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();
}
// Remove the navigation to immediately get rid of pending item.
if (web::WKNavigationState::NONE !=
[self.navigationStates stateForNavigation:navigation]) {
[self.navigationStates removeNavigation:navigation];
}
}
- (void)webView:(WKWebView*)webView
didCommitNavigation:(WKNavigation*)navigation {
[self didReceiveWKNavigationDelegateCallback];
// For reasons not yet fully understood, sometimes WKWebView triggers
// |webView:didFinishNavigation| before |webView:didCommitNavigation|. If a
// navigation is already finished, stop processing
// (https://crbug.com/818796#c2).
if ([self.navigationStates stateForNavigation:navigation] ==
web::WKNavigationState::FINISHED)
return;
BOOL committedNavigation =
[self.navigationStates isCommittedNavigation:navigation];
web::NavigationContextImpl* context =
[self.navigationStates contextForNavigation:navigation];
if (context && !web::IsWKWebViewSSLCertError(context->GetError())) {
_certVerificationErrors->Clear();
}
// Invariant: Every |navigation| should have a |context|. Note that violation
// of this invariant is currently observed in production, but the cause is not
// well understood. 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 &&
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);
}
[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. For
// WebUI, let the window ID be injected when the |loadHTMLString:baseURL|
// navigation is committed.
const std::string& mime_type = self.webStateImpl->GetContentsMimeType();
if (web::IsContentTypeHtml(mime_type) || web::IsContentTypeImage(mime_type) ||
mime_type.empty()) {
// In unit tests MIME type will be empty, because loadHTML:forURL: does
// not notify web view delegate about received response, so web controller
// does not get a chance to properly update MIME type.
[self.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 = [CRWErrorPageHelper isErrorPageFileURL:webViewURL];
BOOL shouldUseContextURL =
context && (isErrorPage || hasMultiplePendingNavigations);
GURL documentURL = shouldUseContextURL ? context->GetUrl() : webViewURL;
// This is the point where the document's URL has actually changed.
[self.delegate navigationHandler:self
setDocumentURL:documentURL
context:context];
// No further code relies an existance of pending item, so this navigation can
// be marked as "committed".
[self.navigationStates setState:web::WKNavigationState::COMMITTED
forNavigation:navigation];
if (!committedNavigation && context && !context->IsLoadingErrorPage()) {
self.webStateImpl->OnNavigationFinished(context);
}
// The actual navigation item will not be committed until the native content
// or WebUI is shown.
if (context &&
!context->GetUrl().SchemeIs(url::kAboutScheme) &&
!IsRestoreSessionUrl(context->GetUrl())) {
[self.delegate webViewHandlerUpdateSSLStatusForCurrentNavigationItem:self];
if (!context->IsLoadingErrorPage() && !IsRestoreSessionUrl(webViewURL)) {
[self setLastCommittedNavigationItemTitle:webView.title];
}
}
}
- (void)webView:(WKWebView*)webView
didFinishNavigation:(WKNavigation*)navigation {
[self didReceiveWKNavigationDelegateCallback];
// Sometimes |webView:didFinishNavigation| arrives before
// |webView:didCommitNavigation|. Explicitly trigger post-commit processing.
bool navigationCommitted =
[self.navigationStates isCommittedNavigation:navigation];
UMA_HISTOGRAM_BOOLEAN("IOS.WKWebViewFinishBeforeCommit",
!navigationCommitted);
if (!navigationCommitted) {
[self webView:webView didCommitNavigation:navigation];
DCHECK_EQ(web::WKNavigationState::COMMITTED,
[self.navigationStates stateForNavigation:navigation]);
}
// Sometimes |didFinishNavigation| callback arrives after |stopLoading| has
// been called. Abort in this case.
if ([self.navigationStates stateForNavigation:navigation] ==
web::WKNavigationState::NONE) {
return;
}
GURL webViewURL = net::GURLWithNSURL(webView.URL);
GURL currentWKItemURL =
net::GURLWithNSURL(webView.backForwardList.currentItem.URL);
UMA_HISTOGRAM_BOOLEAN("IOS.FinishedURLMatchesCurrentItem",
webViewURL == currentWKItemURL);
web::NavigationContextImpl* context =
[self.navigationStates contextForNavigation:navigation];
web::NavigationItemImpl* item =
context ? web::GetItemWithUniqueID(self.navigationManagerImpl, context)
: nullptr;
// Item may not exist if navigation was stopped (see crbug.com/969915).
DCHECK(context);
UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasContext", context);
UMA_HISTOGRAM_BOOLEAN("IOS.FinishedNavigationHasItem", item);
if (context && item) {
if (context->GetUrl() == currentWKItemURL) {
// If webView.backForwardList.currentItem.URL matches |context|, then this
// is a known edge case where |webView.URL| is wrong.
// TODO(crbug.com/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 (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];
}
// This method should be called on deciding policy for navigation action. It
// Answers the |decisionHandler| with a final decision caculated with passed
// |policyDecision|. The passed |policyDecision| should be determined by some
// conditions and policy deciders
- (void)answerDecisionHandler:
(void (^)(WKNavigationActionPolicy))decisionHandler
forNavigationAction:(WKNavigationAction*)action
withPolicyDecision:
(web::WebStatePolicyDecider::PolicyDecision)policyDecision
webView:(WKWebView*)webView
forceBlockUniversalLinks:(BOOL)forceBlockUniversalLinks {
if (policyDecision.ShouldAllowNavigation()) {
if ([[action.request HTTPMethod] isEqualToString:@"POST"]) {
// 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);
ui::PageTransition transition =
[self pageTransitionFromNavigationType:action.navigationType];
[self displayError:policyDecision.GetDisplayError()
forCancelledNavigationToURL:action.request.URL
inWebView:webView
withTransition:transition];
return;
}
[self.pendingNavigationInfo setCancelled:YES];
if (self.navigationManagerImpl->GetPendingItemIndex() == -1) {
// Discard the new pending item to ensure that the current URL is not
// different from what is displayed on the view. There is no need to
// reset pending item index for a different pending back-forward
// navigation.
self.navigationManagerImpl->DiscardNonCommittedItems();
}
web::NavigationContextImpl* context = [self
contextForPendingMainFrameNavigationWithURL:net::GURLWithNSURL(
action.request.URL)];
if (context) {
// Destroy associated pending item, because this will be the last
// WKWebView callback for this navigation context.
context->ReleaseItem();
}
if (!self.beingDestroyed &&
[self shouldClosePageOnNativeApplicationLoad]) {
self.webStateImpl->CloseWebState();
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
}
if (policyDecision.ShouldCancelNavigation()) {
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
BOOL isOffTheRecord = self.webStateImpl->GetBrowserState()->IsOffTheRecord();
decisionHandler(web::GetAllowNavigationActionPolicy(
isOffTheRecord || forceBlockUniversalLinks));
}
#pragma mark - Auth Challenge
// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to
// reply with NSURLSessionAuthChallengeDisposition and credentials.
- (void)processAuthChallenge:(NSURLAuthenticationChallenge*)challenge
forCertAcceptPolicy:(web::CertAcceptPolicy)policy
certStatus:(net::CertStatus)certStatus
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
NSURLCredential*))completionHandler {
SecTrustRef trust = challenge.protectionSpace.serverTrust;
if (policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER) {
// Cert is invalid, but user agreed to proceed, override default behavior.
completionHandler(NSURLSessionAuthChallengeUseCredential,
[NSURLCredential credentialForTrust:trust]);
return;
}
if (policy != web::CERT_ACCEPT_POLICY_ALLOW &&
SecTrustGetCertificateCount(trust)) {
// The cert is invalid and the user has not agreed to proceed. Cache the
// cert verification result in |_certVerificationErrors|, so that it can
// later be reused inside |didFailProvisionalNavigation:|.
// The leaf cert is used as the key, because the chain provided by
// |didFailProvisionalNavigation:| will differ (it is the server-supplied
// chain), thus if intermediates were considered, the keys would mismatch.
scoped_refptr<net::X509Certificate> leafCert =
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.
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;
}
if (error.code == web::kWebKitErrorUrlBlockedByContentFilter) {
DCHECK(provisionalLoad);
// If URL is blocked due to Restriction, do not take any further
// action as WKWebView will show a built-in error.
// On iOS13, immediately following this navigation, WebKit will
// navigate to an internal failure page. Unfortunately, due to how
// session restoration works with same document navigations, this page
// blocked by a content filter puts WebKit into a state where all
// further restoration same-document navigations are 'stuck' on this
// failure page. Instead, avoid restoring this page completely.
// Consider revisiting this once WKWebView's interactionSate is used
// for session restoration everywhere.
self.navigationManagerImpl->SetWKWebViewNextPendingUrlNotSerializable(
navigationContext->GetUrl());
return;
}
if (error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange &&
!policyDecisionCancellationError) {
// Handle Frame Load Interrupted errors from WebView. This block is
// executed when web controller rejected the load inside
// decidePolicyForNavigationResponse: to handle download or WKWebView
// opened a Universal Link.
if (!navigationContext->IsDownload()) {
// Non-download navigation was cancelled because WKWebView has opened a
// Universal Link and called webView:didFailProvisionalNavigation:.
self.navigationManagerImpl->DiscardNonCommittedItems();
[self.navigationStates removeNavigation:navigation];
}
return;
}
if (error.code == web::kWebKitErrorCannotShowUrl) {
if (!navigationContext->GetUrl().is_valid()) {
// It won't be possible to load an error page for invalid URL, because
// WKWebView will revert the url to about:blank. Simply discard pending
// item and fail the navigation.
navigationContext->ReleaseItem();
self.webStateImpl->OnNavigationFinished(navigationContext);
self.webStateImpl->OnPageLoaded(navigationContext->GetUrl(), false);
return;
}
}
}
web::NavigationManager* navManager =
self.webStateImpl->GetNavigationManager();
web::NavigationItem* lastCommittedItem = navManager->GetLastCommittedItem();
if (lastCommittedItem && !web::IsWKWebViewSSLCertError(error)) {
// Reset SSL status for last committed navigation to avoid showing security
// status for error pages.
if (!lastCommittedItem->GetSSL().Equals(web::SSLStatus())) {
lastCommittedItem->GetSSL() = web::SSLStatus();
self.webStateImpl->DidChangeVisibleSecurityState();
}
}
web::NavigationItemImpl* item =
web::GetItemWithUniqueID(self.navigationManagerImpl, navigationContext);
if (item) {
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;
}
}
// Displays an error page with details from |error| in |webView|. The error page
// is presented with |transition| and associated with |blockedNSURL|.
- (void)displayError:(NSError*)error
forCancelledNavigationToURL:(NSURL*)blockedNSURL
inWebView:(WKWebView*)webView
withTransition:(ui::PageTransition)transition {
const GURL blockedURL = net::GURLWithNSURL(blockedNSURL);
// Error page needs the URL string in the error's userInfo for proper
// display.
if (!error.userInfo[NSURLErrorFailingURLStringErrorKey]) {
NSMutableDictionary* updatedUserInfo = [[NSMutableDictionary alloc] init];
[updatedUserInfo addEntriesFromDictionary:error.userInfo];
[updatedUserInfo setObject:blockedNSURL.absoluteString
forKey:NSURLErrorFailingURLStringErrorKey];
error = [NSError errorWithDomain:error.domain
code:error.code
userInfo:updatedUserInfo];
}
WKNavigation* errorNavigation = [self displayErrorPageWithError:error
inWebView:webView
isProvisionalLoad:YES];
// Create pending item.
self.navigationManagerImpl->AddPendingItem(
blockedURL, web::Referrer(), transition,
web::NavigationInitiationType::BROWSER_INITIATED,
/*is_post_navigation=*/false,
/*is_using_https_as_default_scheme=*/false);
// 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|. |provisionalLoad| should be set
// according to whether or not the error occurred during a provisionalLoad.
- (WKNavigation*)displayErrorPageWithError:(NSError*)error
inWebView:(WKWebView*)webView
isProvisionalLoad:(BOOL)provisionalLoad {
CRWErrorPageHelper* errorPage =
[[CRWErrorPageHelper alloc] initWithError:error];
WKBackForwardListItem* backForwardItem = webView.backForwardList.currentItem;
// 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])
return;
std::unique_ptr<web::NavigationContextImpl> navigationContext =
[self.navigationStates removeNavigation:navigation];
[self loadCancelled];
web::NavigationItemImpl* item =
navigationContext ? web::GetItemWithUniqueID(self.navigationManagerImpl,
navigationContext.get())
: nullptr;
if (self.navigationManagerImpl->GetPendingItem() == item) {
self.navigationManagerImpl->DiscardNonCommittedItems();
}
if (provisionalLoad) {
if (!navigationContext &&
web::RequiresProvisionalNavigationFailureWorkaround()) {
// It is likely that |navigationContext| is null because
// didStartProvisionalNavigation: was not called with this WKNavigation
// object. Do not call OnNavigationFinished() to avoid crash on null
// pointer dereferencing. See crbug.com/973653 for details.
} else {
self.webStateImpl->OnNavigationFinished(navigationContext.get());
}
}
}
// Used to decide whether a load that generates errors with the
// NSURLErrorCancelled code should be cancelled.
- (BOOL)shouldCancelLoadForCancelledError:(NSError*)error
provisionalLoad:(BOOL)provisionalLoad {
DCHECK(error.code == NSURLErrorCancelled ||
error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange);
// Do not cancel the load if it is for an app specific URL, as such errors
// are produced during the app specific URL load process.
const GURL errorURL =
net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]);
if (web::GetWebClient()->IsAppSpecificURL(errorURL))
return NO;
return provisionalLoad;
}
// Loads the error page.
- (void)loadErrorPageForNavigationItem:(web::NavigationItemImpl*)item
navigationContext:(WKNavigation*)navigation
webView:(WKWebView*)webView {
web::NavigationContextImpl* context =
[self.navigationStates contextForNavigation:navigation];
NSError* error = context->GetError();
DCHECK(error);
DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID());
net::SSLInfo info;
absl::optional<net::SSLInfo> ssl_info = absl::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 = absl::make_optional<net::SSLInfo>(info);
}
}
NSString* failingURLString =
error.userInfo[NSURLErrorFailingURLStringErrorKey];
GURL failingURL(base::SysNSStringToUTF8(failingURLString));
GURL itemURL = item->GetURL();
if (itemURL != failingURL)
item->SetVirtualURL(failingURL);
web::GetWebClient()->PrepareErrorPage(
self.webStateImpl, failingURL, error, context->IsPost(),
self.webStateImpl->GetBrowserState()->IsOffTheRecord(), ssl_info,
context->GetNavigationId(), base::BindOnce(^(NSString* errorHTML) {
if (errorHTML) {
CRWErrorPageHelper* errorPageHelper =
[[CRWErrorPageHelper alloc] initWithError:context->GetError()];
[webView evaluateJavaScript:[errorPageHelper
scriptForInjectingHTML:errorHTML
addAutomaticReload:YES]
completionHandler:^(id result, NSError* error) {
if (error) {
DCHECK(error.code == WKErrorWebViewInvalidated ||
error.code == WKErrorWebContentProcessTerminated)
<< "Error injecting error page HTML: "
<< base::SysNSStringToUTF8(error.description);
}
}];
}
// 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()) && info.cert) {
web::SSLStatus& SSLStatus =
self.navigationManagerImpl->GetLastCommittedItem()->GetSSL();
SSLStatus.cert_status = info.cert_status;
SSLStatus.certificate = info.cert;
SSLStatus.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN;
self.webStateImpl->DidChangeVisibleSecurityState();
}
[self.delegate navigationHandler:self
didCompleteLoadWithSuccess:NO
forContext: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;
}
}
- (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];
self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem());
if (context->IsLoadingHtmlString()) {
self.navigationManagerImpl->GetLastCommittedItem()->SetURL(
context->GetUrl());
}
}
@end