blob: ba56e46b1f460468af87285b52667a1eff435035 [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"
#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"
#include "ios/web/common/features.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_manager_impl.h"
#include "ios/web/navigation/navigation_manager_util.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/url_scheme_util.h"
#import "ios/web/public/web_client.h"
#import "ios/web/web_state/user_interaction_state.h"
#import "ios/web/web_state/web_state_impl.h"
#import "ios/web/web_view/wk_web_view_util.h"
#import "net/base/mac/url_conversions.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::IsPlaceholderUrl;
using web::wk_navigation_util::kReferrerHeaderName;
using web::wk_navigation_util::IsRestoreSessionUrl;
@interface CRWWKNavigationHandler ()
// 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;
@end
@implementation CRWWKNavigationHandler {
// Used to poll for a SafeBrowsing warning being displayed. This is created in
// |decidePolicyForNavigationAction| and destroyed once any of the following
// happens: 1) a SafeBrowsing warning is detected; 2) any WKNavigationDelegate
// method is called; 3) |stopLoading| is called.
base::RepeatingTimer _safeBrowsingWarningDetectionTimer;
}
- (instancetype)init {
if (self = [super init]) {
_navigationStates = [[CRWWKNavigationStates alloc] init];
}
return self;
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView*)webView
decidePolicyForNavigationAction:(WKNavigationAction*)action
decisionHandler:
(void (^)(WKNavigationActionPolicy))decisionHandler {
[self didReceiveWKNavigationDelegateCallback];
self.webProcessCrashed = NO;
if ([self.delegate navigationHandlerWebViewBeingDestroyed:self]) {
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 (web::GetWebClient()->IsSlimNavigationManagerEnabled() &&
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 updatePendingNavigationInfoFromNavigationAction:action];
if (web::GetWebClient()->IsSlimNavigationManagerEnabled() &&
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 (IsPlaceholderUrl(requestURL)) {
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
ui::PageTransition transition =
[self.delegate navigationHandler:self
pageTransitionFromNavigationType:action.navigationType];
BOOL isMainFrameNavigationAction = [self isMainFrameNavigationAction:action];
if (isMainFrameNavigationAction) {
web::NavigationContextImpl* context =
[self contextForPendingMainFrameNavigationWithURL:requestURL];
if (context) {
DCHECK(!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;
}
}
}
if (web::GetWebClient()->IsSlimNavigationManagerEnabled()) {
// WKBasedNavigationManager doesn't use |loadCurrentURL| for reload or back/
// forward navigation. So this is the first point where a form repost would
// be detected. Display the confirmation dialog.
if ([action.request.HTTPMethod isEqual:@"POST"] &&
(action.navigationType == WKNavigationTypeFormResubmitted)) {
self.webStateImpl->ShowRepostFormWarningDialog(
base::BindOnce(^(bool shouldContinue) {
if (shouldContinue) {
decisionHandler(WKNavigationActionPolicyAllow);
} else {
decisionHandler(WKNavigationActionPolicyCancel);
if (action.targetFrame.mainFrame) {
[self.pendingNavigationInfo setCancelled:YES];
self.webStateImpl->SetIsLoading(false);
}
}
}));
return;
}
}
// Invalid URLs should not be loaded.
if (!requestURL.is_valid()) {
decisionHandler(WKNavigationActionPolicyCancel);
// 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) {
GURL aboutBlankURL(url::kAboutBlankURL);
web::NavigationManager::WebLoadParams loadParams(aboutBlankURL);
loadParams.referrer =
[self.delegate currentReferrerForNavigationHandler:self];
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.
BOOL allowLoad = YES;
if (web::GetWebClient()->IsAppSpecificURL(requestURL)) {
allowLoad = [self shouldAllowAppSpecificURLNavigationAction:action
transition:transition];
if (allowLoad && !self.webStateImpl->HasWebUI()) {
[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 (allowLoad) {
// If the URL doesn't look like one that can be shown as a web page, it may
// handled by the embedder. In that case, update the web controller to
// correctly reflect the current state.
if (!webControllerCanShow) {
if (!web::features::StorePendingItemInContext()) {
if ([self isMainFrameNavigationAction:action]) {
[self.delegate navigationHandlerStopLoading:self];
}
}
// Purge web view if last committed URL is different from the document
// URL. This can happen if external URL was added to the navigation stack
// and was loaded using Go Back or Go Forward navigation (in which case
// document URL will point to the previous page). If this is the first
// load for a NavigationManager, there will be no last committed item, so
// check here.
// TODO(crbug.com/850760): Check if this code is still needed. The current
// implementation doesn't put external apps URLs in the history, so they
// shouldn't be accessable by Go Back or Go Forward navigation.
web::NavigationItem* lastCommittedItem =
self.webStateImpl->GetNavigationManager()->GetLastCommittedItem();
if (lastCommittedItem) {
GURL lastCommittedURL = lastCommittedItem->GetURL();
if (lastCommittedURL !=
[self.delegate navigationHandlerDocumentURL:self]) {
[self.delegate navigationHandlerRequirePageReconstruction:self];
[self.delegate navigationHandler:self
setDocumentURL:lastCommittedURL
context:nullptr];
}
}
}
}
if (allowLoad) {
BOOL userInteractedWithRequestMainFrame =
self.userInteractionState->HasUserTappedRecently(webView) &&
net::GURLWithNSURL(action.request.mainDocumentURL) ==
self.userInteractionState->LastUserInteraction()->main_document_url;
web::WebStatePolicyDecider::RequestInfo requestInfo(
transition, isMainFrameNavigationAction,
userInteractedWithRequestMainFrame);
allowLoad =
self.webStateImpl->ShouldAllowRequest(action.request, requestInfo);
// The WebState may have been closed in the ShouldAllowRequest callback.
if ([self.delegate navigationHandlerWebViewBeingDestroyed:self]) {
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
if (!webControllerCanShow && web::features::StorePendingItemInContext()) {
allowLoad = NO;
}
if (allowLoad) {
if ([[action.request HTTPMethod] isEqualToString:@"POST"]) {
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) {
[self.pendingNavigationInfo setCancelled:YES];
// Discard the pending item to ensure that the current URL is not
// different from what is displayed on the view. Discard only happens
// if the last item was not a native view, to avoid ugly animation of
// inserting the webview.
[self discardNonCommittedItemsIfLastCommittedWasNotNativeView];
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.delegate navigationHandlerWebViewBeingDestroyed:self] &&
[self shouldClosePageOnNativeApplicationLoad]) {
// Loading was started for user initiated navigations and should be
// stopped because no other WKWebView callbacks are called.
// TODO(crbug.com/767092): Loading should not start until
// webView.loading is changed to YES.
self.webStateImpl->SetIsLoading(false);
self.webStateImpl->CloseWebState();
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
if (![self.delegate navigationHandlerWebViewBeingDestroyed:self]) {
// Loading was started for user initiated navigations and should be
// stopped because no other WKWebView callbacks are called.
// TODO(crbug.com/767092): Loading should not start until webView.loading
// is changed to YES.
self.webStateImpl->SetIsLoading(false);
}
}
// Only try to detect a SafeBrowsing warning if one isn't already displayed,
// since the detection logic won't be able to distinguish between the current
// warning and a warning for the page that's about to be loaded. Also, since
// the purpose of running this logic is to ensure that the right URL is
// displayed in the omnibox, don't try to detect a SafeBrowsing warning for
// iframe navigations, because the omnibox already shows the correct main
// frame URL in that case.
if (allowLoad && isMainFrameNavigationAction &&
!web::IsSafeBrowsingWarningDisplayedInWebView(webView)) {
__weak CRWWKNavigationHandler* weakSelf = self;
__weak WKWebView* weakWebView = webView;
const base::TimeDelta kDelayUntilSafeBrowsingWarningCheck =
base::TimeDelta::FromMilliseconds(20);
_safeBrowsingWarningDetectionTimer.Start(
FROM_HERE, kDelayUntilSafeBrowsingWarningCheck, base::BindRepeating(^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(weakWebView) strongWebView = weakWebView;
if (web::IsSafeBrowsingWarningDisplayedInWebView(strongWebView)) {
// Extract state from an existing navigation context if one exists.
// Create a new context rather than just re-using the existing one,
// since the existing context will continue to be used if the user
// decides to proceed to the unsafe page. In that case, WebKit
// continues the navigation with the same WKNavigation* that's
// associated with the existing context.
web::NavigationContextImpl* existingContext = [strongSelf
contextForPendingMainFrameNavigationWithURL:requestURL];
bool hasUserGesture =
existingContext ? existingContext->HasUserGesture() : false;
bool isRendererInitiated =
existingContext ? existingContext->IsRendererInitiated() : true;
std::unique_ptr<web::NavigationContextImpl> context =
web::NavigationContextImpl::CreateNavigationContext(
strongSelf.webStateImpl, requestURL, hasUserGesture,
transition, isRendererInitiated);
[strongSelf navigationManagerImpl] -> AddTransientItem(requestURL);
strongSelf.webStateImpl->OnNavigationStarted(context.get());
strongSelf.webStateImpl->OnNavigationFinished(context.get());
strongSelf->_safeBrowsingWarningDetectionTimer.Stop();
if (!existingContext) {
// If there's an existing context, observers will already be aware
// of a load in progress. Otherwise, observers need to be notified
// here, so that if the user decides to go back to the previous
// page (stopping the load), observers will be aware of a possible
// URL change and the URL displayed in the omnibox will get
// updated.
DCHECK(strongWebView.loading);
strongSelf.webStateImpl->SetIsLoading(true);
}
}
}));
}
if (!allowLoad) {
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 (IsPlaceholderUrl(responseURL)) {
handler(WKNavigationResponsePolicyAllow);
return;
}
scoped_refptr<net::HttpResponseHeaders> headers;
if ([WKResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
headers = net::CreateHeadersFromNSHTTPURLResponse(
static_cast<NSHTTPURLResponse*>(WKResponse.response));
// TODO(crbug.com/551677): remove |OnHttpResponseHeadersReceived| and attach
// headers to web::NavigationContext.
self.webStateImpl->OnHttpResponseHeadersReceived(headers.get(),
responseURL);
}
// The page will not be changed until this navigation is committed, so the
// retrieved state will be pending until |didCommitNavigation| callback.
[self updatePendingNavigationInfoFromNavigationResponse:WKResponse];
BOOL shouldRenderResponse = [self shouldRenderResponse:WKResponse];
if (!shouldRenderResponse) {
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 discardNonCommittedItemsIfLastCommittedWasNotNativeView];
if (!web::features::StorePendingItemInContext()) {
// Loading will be stopped in webView:didFinishNavigation: callback. This
// call is here to preserve the original behavior when pending item is not
// stored in NavigationContext.
self.webStateImpl->SetIsLoading(false);
}
} else {
shouldRenderResponse = self.webStateImpl->ShouldAllowResponse(
WKResponse.response, WKResponse.forMainFrame);
}
if (!shouldRenderResponse && WKResponse.canShowMIMEType &&
WKResponse.forMainFrame) {
self.pendingNavigationInfo.cancelled = YES;
}
if (web::GetWebClient()->IsSlimNavigationManagerEnabled() &&
!WKResponse.forMainFrame && !webView.loading) {
// This is the terminal callback for iframe navigation and there is no
// pending main frame navigation. Last chance to flip IsLoading to false.
self.webStateImpl->SetIsLoading(false);
}
handler(shouldRenderResponse ? WKNavigationResponsePolicyAllow
: WKNavigationResponsePolicyCancel);
}
- (void)webView:(WKWebView*)webView
didStartProvisionalNavigation:(WKNavigation*)navigation {
[self didReceiveWKNavigationDelegateCallback];
}
- (void)webView:(WKWebView*)webView
didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation {
[self didReceiveWKNavigationDelegateCallback];
}
- (void)webView:(WKWebView*)webView
didFailProvisionalNavigation:(WKNavigation*)navigation
withError:(NSError*)error {
[self didReceiveWKNavigationDelegateCallback];
}
- (void)webView:(WKWebView*)webView
didCommitNavigation:(WKNavigation*)navigation {
[self didReceiveWKNavigationDelegateCallback];
}
- (void)webView:(WKWebView*)webView
didFinishNavigation:(WKNavigation*)navigation {
[self didReceiveWKNavigationDelegateCallback];
}
- (void)webView:(WKWebView*)webView
didFailNavigation:(WKNavigation*)navigation
withError:(NSError*)error {
[self didReceiveWKNavigationDelegateCallback];
}
- (void)webView:(WKWebView*)webView
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
completionHandler:
(void (^)(NSURLSessionAuthChallengeDisposition,
NSURLCredential*))completionHandler {
[self didReceiveWKNavigationDelegateCallback];
}
- (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView {
[self didReceiveWKNavigationDelegateCallback];
}
#pragma mark - Private methods
- (web::NavigationManagerImpl*)navigationManagerImpl {
return &(self.webStateImpl->GetNavigationManagerImpl());
}
- (web::WebStateImpl*)webStateImpl {
return [self.delegate webStateImplForNavigationHandler:self];
}
- (web::UserInteractionState*)userInteractionState {
return [self.delegate userInteractionStateForNavigationHandler:self];
}
// This method should be called on receiving WKNavigationDelegate callbacks. It
// will log a metric if the callback occurs after the reciever has already been
// closed. It also stops the SafeBrowsing warning detection timer, since after
// this point it's too late for a SafeBrowsing warning to be displayed for the
// navigation for which the timer was started.
- (void)didReceiveWKNavigationDelegateCallback {
if ([self.delegate navigationHandlerWebViewBeingDestroyed:self]) {
UMA_HISTOGRAM_BOOLEAN("Renderer.WKWebViewCallbackAfterDestroy", true);
}
_safeBrowsingWarningDetectionTimer.Stop();
}
// 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)updatePendingNavigationInfoFromNavigationAction:
(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;
}
}
// 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 (!ui::PageTransitionIsNewNavigation(pageTransition)) {
// Allow reloads and 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.delegate navigationHandlerDocumentURL:self])) {
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]);
}
}
}
// Discards non committed items, only if the last committed URL was not loaded
// in native view. But if it was a native view, no discard will happen to avoid
// an ugly animation where the web view is inserted and quickly removed.
- (void)discardNonCommittedItemsIfLastCommittedWasNotNativeView {
GURL lastCommittedURL = self.webStateImpl->GetLastCommittedURL();
BOOL previousItemWasLoadedInNativeView =
[self.delegate navigationHandler:self
shouldLoadURLInNativeView:lastCommittedURL];
if (!previousItemWasLoadedInNativeView)
self.navigationManagerImpl->DiscardNonCommittedItems();
}
// 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, as occurs when an App Store link is
// opened from another application.
BOOL rendererInitiatedWithoutInteraction =
self.webStateImpl->HasOpener() &&
!self.userInteractionState
->UserInteractionRegisteredSinceWebViewCreated();
BOOL noNavigationItems = !(self.navigationManagerImpl->GetItemCount());
return rendererInitiatedWithoutInteraction || noNavigationItems;
}
// 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 {
if (response.isForMainFrame) {
if (!self.pendingNavigationInfo) {
self.pendingNavigationInfo = [[CRWPendingNavigationInfo alloc] init];
}
self.pendingNavigationInfo.MIMEType = response.response.MIMEType;
}
}
// 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->IsRendererInitiated()) {
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);
}
ui::PageTransition transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME;
if (WKResponse.forMainFrame) {
web::NavigationContextImpl* context =
[self contextForPendingMainFrameNavigationWithURL:responseURL];
context->SetIsDownload(true);
context->ReleaseItem();
// Navigation callbacks can only be called for the main frame.
self.webStateImpl->OnNavigationFinished(context);
transition = context->GetPageTransition();
bool transitionIsLink = ui::PageTransitionTypeIncludingQualifiersIs(
transition, ui::PAGE_TRANSITION_LINK);
if (transitionIsLink && !context->HasUserGesture()) {
// Link click is not possible without user gesture, so this transition
// was incorrectly classified and should be "client redirect" instead.
// TODO(crbug.com/549301): Remove this workaround when transition
// detection is fixed.
transition = ui::PAGE_TRANSITION_CLIENT_REDIRECT;
}
}
web::DownloadController::FromBrowserState(
self.webStateImpl->GetBrowserState())
->CreateDownloadTask(self.webStateImpl, [NSUUID UUID].UUIDString,
responseURL, contentDisposition, contentLength,
MIMEType, transition);
}
#pragma mark - Public methods
- (void)stopLoading {
self.pendingNavigationInfo.cancelled = YES;
_safeBrowsingWarningDetectionTimer.Stop();
}
- (base::RepeatingTimer*)safeBrowsingWarningDetectionTimer {
return &_safeBrowsingWarningDetectionTimer;
}
// 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;
}
@end