blob: 490456f02c6b6d7f89cff8aede87c67b00984333 [file] [log] [blame]
// Copyright 2012 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/web_state/ui/crw_web_controller.h"
#import <WebKit/WebKit.h>
#import <objc/runtime.h>
#include <stddef.h>
#include <cmath>
#include <limits>
#include <memory>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/containers/mru_cache.h"
#include "base/feature_list.h"
#import "base/ios/block_types.h"
#include "base/ios/ios_util.h"
#import "base/ios/ns_error_util.h"
#include "base/json/string_escape.h"
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "crypto/symmetric_key.h"
#import "ios/net/http_response_headers_util.h"
#include "ios/web/history_state_util.h"
#import "ios/web/interstitials/web_interstitial_impl.h"
#import "ios/web/navigation/crw_navigation_item_holder.h"
#import "ios/web/navigation/crw_session_controller.h"
#include "ios/web/navigation/error_retry_state_machine.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/wk_navigation_util.h"
#include "ios/web/net/cert_host_pair.h"
#import "ios/web/net/crw_cert_verification_controller.h"
#import "ios/web/net/crw_ssl_status_updater.h"
#include "ios/web/public/browser_state.h"
#import "ios/web/public/download/download_controller.h"
#include "ios/web/public/favicon_url.h"
#include "ios/web/public/features.h"
#import "ios/web/public/java_script_dialog_presenter.h"
#import "ios/web/public/navigation_item.h"
#import "ios/web/public/navigation_manager.h"
#import "ios/web/public/origin_util.h"
#include "ios/web/public/referrer.h"
#include "ios/web/public/referrer_util.h"
#include "ios/web/public/ssl_status.h"
#import "ios/web/public/url_scheme_util.h"
#include "ios/web/public/url_util.h"
#import "ios/web/public/web_client.h"
#include "ios/web/public/web_kit_constants.h"
#import "ios/web/public/web_state/context_menu_params.h"
#import "ios/web/public/web_state/js/crw_js_injection_manager.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#import "ios/web/public/web_state/page_display_state.h"
#include "ios/web/public/web_state/session_certificate_policy_cache.h"
#import "ios/web/public/web_state/ui/crw_content_view.h"
#import "ios/web/public/web_state/ui/crw_context_menu_delegate.h"
#import "ios/web/public/web_state/ui/crw_native_content.h"
#import "ios/web/public/web_state/ui/crw_native_content_provider.h"
#import "ios/web/public/web_state/ui/crw_web_view_content_view.h"
#import "ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h"
#include "ios/web/public/web_state/url_verification_constants.h"
#include "ios/web/public/web_state/web_frame.h"
#include "ios/web/public/web_state/web_frame_util.h"
#import "ios/web/public/web_state/web_state.h"
#include "ios/web/public/web_state/web_state_interface_provider.h"
#import "ios/web/public/web_state/web_state_policy_decider.h"
#include "ios/web/public/webui/web_ui_ios.h"
#import "ios/web/web_state/error_translation_util.h"
#import "ios/web/web_state/js/crw_js_post_request_loader.h"
#import "ios/web/web_state/js/crw_js_window_id_manager.h"
#import "ios/web/web_state/navigation_context_impl.h"
#import "ios/web/web_state/page_viewport_state.h"
#import "ios/web/web_state/ui/crw_context_menu_controller.h"
#import "ios/web/web_state/ui/crw_swipe_recognizer_provider.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/ui/crw_web_controller_container_view.h"
#import "ios/web/web_state/ui/crw_web_view_navigation_proxy.h"
#import "ios/web/web_state/ui/crw_web_view_proxy_impl.h"
#import "ios/web/web_state/ui/crw_wk_navigation_states.h"
#import "ios/web/web_state/ui/crw_wk_script_message_router.h"
#import "ios/web/web_state/ui/favicon_util.h"
#import "ios/web/web_state/ui/wk_back_forward_list_item_holder.h"
#import "ios/web/web_state/ui/wk_navigation_action_util.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
#import "ios/web/web_state/web_frame_impl.h"
#import "ios/web/web_state/web_frames_manager_impl.h"
#import "ios/web/web_state/web_state_impl.h"
#import "ios/web/web_state/web_view_internal_creation_util.h"
#import "ios/web/web_state/wk_web_view_security_util.h"
#import "ios/web/webui/crw_web_ui_manager.h"
#import "ios/web/webui/mojo_facade.h"
#import "net/base/mac/url_conversions.h"
#include "net/base/net_errors.h"
#include "net/cert/x509_util_ios.h"
#include "net/ssl/ssl_info.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using web::NavigationManager;
using web::NavigationManagerImpl;
using web::WebState;
using web::WebStateImpl;
namespace {
using web::wk_navigation_util::IsPlaceholderUrl;
using web::wk_navigation_util::CreatePlaceholderUrlForUrl;
using web::wk_navigation_util::ExtractUrlFromPlaceholderUrl;
using web::wk_navigation_util::IsRestoreSessionUrl;
using web::wk_navigation_util::IsWKInternalUrl;
// Struct to capture data about a user interaction. Records the time of the
// interaction and the main document URL at that time.
struct UserInteractionEvent {
UserInteractionEvent(GURL url)
: main_document_url(url), time(CFAbsoluteTimeGetCurrent()) {}
// Main document URL at the time the interaction occurred.
GURL main_document_url;
// Time that the interaction occurred, measured in seconds since Jan 1 2001.
CFAbsoluteTime time;
};
// Keys for JavaScript command handlers context.
NSString* const kUserIsInteractingKey = @"userIsInteracting";
NSString* const kOriginURLKey = @"originURL";
NSString* const kIsMainFrame = @"isMainFrame";
// URL scheme for messages sent from javascript for asynchronous processing.
NSString* const kScriptMessageName = @"crwebinvoke";
// Message command sent when a frame becomes available.
NSString* const kFrameBecameAvailableMessageName = @"FrameBecameAvailable";
// Message command sent when a frame is unloading.
NSString* const kFrameBecameUnavailableMessageName = @"FrameBecameUnavailable";
// Values for the histogram that counts slow/fast back/forward navigations.
enum class BackForwardNavigationType {
// Fast back navigation through WKWebView back-forward list.
FAST_BACK = 0,
// Slow back navigation when back-forward list navigation is not possible.
SLOW_BACK = 1,
// Fast forward navigation through WKWebView back-forward list.
FAST_FORWARD = 2,
// Slow forward navigation when back-forward list navigation is not possible.
SLOW_FORWARD = 3,
BACK_FORWARD_NAVIGATION_TYPE_COUNT
};
// Represents cert verification error, which happened inside
// |webView:didReceiveAuthenticationChallenge:completionHandler:| and should
// be checked inside |webView:didFailProvisionalNavigation:withError:|.
struct CertVerificationError {
CertVerificationError(BOOL is_recoverable, net::CertStatus status)
: is_recoverable(is_recoverable), status(status) {}
BOOL is_recoverable;
net::CertStatus status;
};
// Type of Cache object for storing cert verification errors.
typedef base::MRUCache<web::CertHostPair, CertVerificationError>
CertVerificationErrorsCacheType;
// 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 CertVerificationErrorsCacheType::size_type kMaxCertErrorsCount = 100;
} // namespace
#pragma mark -
// A container object for any navigation information that is only available
// during pre-commit delegate callbacks, and thus must be held until the
// navigation commits and the informatino can be used.
@interface CRWWebControllerPendingNavigationInfo : NSObject {
}
// The referrer for the page.
@property(nonatomic, copy) NSString* referrer;
// The MIME type for the page.
@property(nonatomic, copy) NSString* MIMEType;
// The navigation type for the load.
@property(nonatomic, assign) WKNavigationType navigationType;
// HTTP request method for the load.
@property(nonatomic, copy) NSString* HTTPMethod;
// Whether the pending navigation has been directly cancelled before the
// navigation is committed.
// Cancelled navigations should be simply discarded without handling any
// specific error.
@property(nonatomic, assign) BOOL cancelled;
// Whether the navigation was initiated by a user gesture.
@property(nonatomic, assign) BOOL hasUserGesture;
@end
@implementation CRWWebControllerPendingNavigationInfo
@synthesize referrer = _referrer;
@synthesize MIMEType = _MIMEType;
@synthesize navigationType = _navigationType;
@synthesize HTTPMethod = _HTTPMethod;
@synthesize cancelled = _cancelled;
@synthesize hasUserGesture = _hasUserGesture;
- (instancetype)init {
if ((self = [super init])) {
_navigationType = WKNavigationTypeOther;
}
return self;
}
@end
@interface CRWWebController ()<CRWContextMenuDelegate,
CRWNativeContentDelegate,
CRWSSLStatusUpdaterDataSource,
CRWSSLStatusUpdaterDelegate,
CRWWebControllerContainerViewDelegate,
CRWWebViewScrollViewProxyObserver,
WKNavigationDelegate,
WKUIDelegate> {
// The WKWebView managed by this instance.
WKWebView* _webView;
// The view used to display content. Must outlive |_webViewProxy|. The
// container view should be accessed through this property rather than
// |self.view| from within this class, as |self.view| triggers creation while
// |self.containerView| will return nil if the view hasn't been instantiated.
CRWWebControllerContainerView* _containerView;
// YES if the current URL load was triggered in Web Controller. NO by default
// and after web usage was disabled. Used by |-loadCurrentURLIfNecessary| to
// prevent extra loads.
BOOL _currentURLLoadWasTrigerred;
// If |_contentView| contains a native view rather than a web view, this
// is its controller. If it's a web view, this is nil.
id<CRWNativeContent> _nativeController;
BOOL _isHalted; // YES if halted. Halting happens prior to destruction.
BOOL _isBeingDestroyed; // YES if in the process of closing.
// YES if a user interaction has been registered at any time once the page has
// loaded.
BOOL _userInteractionRegistered;
// YES if the user has interacted with the content area since the last URL
// change.
BOOL _interactionRegisteredSinceLastURLChange;
// The actual URL of the document object (i.e., the last committed URL).
// TODO(crbug.com/549616): Remove this in favor of just updating the
// navigation manager and treating that as authoritative.
GURL _documentURL;
// Page loading phase.
web::LoadPhase _loadPhase;
// The web::PageDisplayState recorded when the page starts loading.
web::PageDisplayState _displayStateOnStartLoading;
// Whether or not the page has zoomed since the current navigation has been
// committed, either by user interaction or via |-restoreStateFromHistory|.
BOOL _pageHasZoomed;
// Whether a PageDisplayState is currently being applied.
BOOL _applyingPageState;
// Actions to execute once the page load is complete.
NSMutableArray* _pendingLoadCompleteActions;
// UIGestureRecognizers to add to the web view.
NSMutableArray* _gestureRecognizers;
// Flag to say if browsing is enabled.
BOOL _webUsageEnabled;
// The touch tracking recognizer allowing us to decide if a navigation is
// started by the user.
CRWTouchTrackingRecognizer* _touchTrackingRecognizer;
// The controller that tracks long press and check context menu trigger.
CRWContextMenuController* _contextMenuController;
// Whether a click is in progress.
BOOL _clickInProgress;
// Data on the recorded last user interaction.
std::unique_ptr<UserInteractionEvent> _lastUserInteraction;
// YES if there has been user interaction with views owned by this controller.
BOOL _userInteractedWithWebController;
// The time of the last page transfer start, measured in seconds since Jan 1
// 2001.
CFAbsoluteTime _lastTransferTimeInSeconds;
// Default URL (about:blank).
GURL _defaultURL;
// Whether the web page is currently performing window.history.pushState or
// window.history.replaceState
// Set to YES on window.history.willChangeState message. To NO on
// window.history.didPushState or window.history.didReplaceState.
BOOL _changingHistoryState;
// Set to YES when a hashchange event is manually dispatched for same-document
// history navigations.
BOOL _dispatchingSameDocumentHashChangeEvent;
// Object for loading POST requests with body.
CRWJSPOSTRequestLoader* _POSTRequestLoader;
// WebStateImpl instance associated with this CRWWebController, web controller
// does not own this pointer.
WebStateImpl* _webStateImpl;
// A set of script managers whose scripts have been injected into the current
// page.
// TODO(stuartmorgan): Revisit this approach; it's intended only as a stopgap
// measure to make all the existing script managers work. Longer term, there
// should probably be a couple of points where managers can register to have
// things happen automatically based on page lifecycle, and if they don't want
// to use one of those fixed points, they should make their scripts internally
// idempotent.
NSMutableSet* _injectedScriptManagers;
// Script manager for setting the windowID.
CRWJSWindowIDManager* _windowIDJSManager;
// The receiver of JavaScripts.
CRWJSInjectionReceiver* _jsInjectionReceiver;
// Backs up property with the same name.
std::unique_ptr<web::MojoFacade> _mojoFacade;
// Referrer for the current page; does not include the fragment.
NSString* _currentReferrerString;
// Pending information for an in-progress page navigation. The lifetime of
// this object starts at |decidePolicyForNavigationAction| where the info is
// extracted from the request, and ends at either |didCommitNavigation| or
// |didFailProvisionalNavigation|.
CRWWebControllerPendingNavigationInfo* _pendingNavigationInfo;
// Holds all WKNavigation objects and their states which are currently in
// flight.
CRWWKNavigationStates* _navigationStates;
// The WKNavigation captured when |stopLoading| was called. Used for reporting
// WebController.EmptyNavigationManagerCausedByStopLoading UMA metric which
// helps with diagnosing a navigation related crash (crbug.com/565457).
__weak WKNavigation* _stoppedWKNavigation;
// 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) |abortLoad| is called.
base::RepeatingTimer _safeBrowsingWarningDetectionTimer;
// CRWWebUIManager object for loading WebUI pages.
CRWWebUIManager* _webUIManager;
// Updates SSLStatus for current navigation item.
CRWSSLStatusUpdater* _SSLStatusUpdater;
// Controller used for certs verification to help with blocking requests with
// bad SSL cert, presenting SSL interstitials and determining SSL status for
// Navigation Items.
CRWCertVerificationController* _certVerificationController;
// 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<CertVerificationErrorsCacheType> _certVerificationErrors;
}
// If |contentView_| contains a web view, this is the web view it contains.
// If not, it's nil.
@property(weak, nonatomic, readonly) WKWebView* webView;
// The scroll view of |webView|.
@property(weak, nonatomic, readonly) UIScrollView* webScrollView;
// The current page state of the web view. Writing to this property
// asynchronously applies the passed value to the current web view.
@property(nonatomic, readwrite) web::PageDisplayState pageDisplayState;
// The currently displayed native controller, if any.
@property(weak, nonatomic, readwrite) id<CRWNativeContent> nativeController;
// Returns NavigationManager's session controller.
@property(weak, nonatomic, readonly) CRWSessionController* sessionController;
// The associated NavigationManagerImpl.
@property(nonatomic, readonly) NavigationManagerImpl* navigationManagerImpl;
// Whether the associated WebState has an opener.
@property(nonatomic, readonly) BOOL hasOpener;
// Dictionary where keys are the names of WKWebView properties and values are
// selector names which should be called when a corresponding property has
// changed. e.g. @{ @"URL" : @"webViewURLDidChange" } means that
// -[self webViewURLDidChange] must be called every time when WKWebView.URL is
// changed.
@property(weak, nonatomic, readonly) NSDictionary* WKWebViewObservers;
// The web view's view of the current URL. During page transitions
// this may not be the same as the session history's view of the current URL.
// This method can change the state of the CRWWebController, as it will display
// an error if the returned URL is not reliable from a security point of view.
// Note that this method is expensive, so it should always be cached locally if
// it's needed multiple times in a method.
@property(nonatomic, readonly) GURL currentURL;
// Returns the referrer for the current page.
@property(nonatomic, readonly) web::Referrer currentReferrer;
// Returns YES if the user interacted with the page recently.
@property(nonatomic, readonly) BOOL userClickedRecently;
// User agent type of the transient item if any, the pending item if a
// navigation is in progress or the last committed item otherwise.
// Returns MOBILE, the default type, if navigation manager is nullptr or empty.
@property(nonatomic, readonly) web::UserAgentType userAgentType;
// Facade for Mojo API.
@property(nonatomic, readonly) web::MojoFacade* mojoFacade;
// TODO(crbug.com/692871): Remove these functions and replace with more
// appropriate NavigationItem getters.
// Returns the navigation item for the current page.
@property(nonatomic, readonly) web::NavigationItemImpl* currentNavItem;
// Returns the current transition type.
@property(nonatomic, readonly) ui::PageTransition currentTransition;
// Returns the referrer for current navigation item. May be empty.
@property(nonatomic, readonly) web::Referrer currentNavItemReferrer;
// The HTTP headers associated with the current navigation item. These are nil
// unless the request was a POST.
@property(weak, nonatomic, readonly) NSDictionary* currentHTTPHeaders;
// YES if a user interaction has been registered at any time since the page has
// loaded.
@property(nonatomic, readwrite) BOOL userInteractionRegistered;
// Called when the web page has changed document and/or URL, and so the page
// navigation should be reported to the delegate, and internal state updated to
// reflect the fact that the navigation has occurred. |context| contains
// information about the navigation that triggered the document/URL change.
// TODO(stuartmorgan): The code conflates URL changes and document object
// changes; the two need to be separated and handled differently.
- (void)webPageChangedWithContext:(const web::NavigationContextImpl*)context;
// Resets any state that is associated with a specific document object (e.g.,
// page interaction tracking).
- (void)resetDocumentSpecificState;
// Called when a page (native or web) has actually started loading (i.e., for
// a web page the document has actually changed), or after the load request has
// been registered for a non-document-changing URL change. Updates internal
// state not specific to web pages.
- (void)didStartLoading;
// Returns YES if the URL looks like it is one CRWWebController can show.
+ (BOOL)webControllerCanShow:(const GURL&)url;
// Returns a lazily created CRWTouchTrackingRecognizer.
- (CRWTouchTrackingRecognizer*)touchTrackingRecognizer;
// Creates a container view if it's not yet created.
- (void)ensureContainerViewCreated;
// Creates a web view if it's not yet created.
- (void)ensureWebViewCreated;
// Creates a web view with given |config|. No-op if web view is already created.
- (void)ensureWebViewCreatedWithConfiguration:(WKWebViewConfiguration*)config;
// Returns a new autoreleased web view created with given configuration.
- (WKWebView*)webViewWithConfiguration:(WKWebViewConfiguration*)config;
// Sets the value of the webView property, and performs its basic setup.
- (void)setWebView:(WKWebView*)webView;
// Wraps the web view in a CRWWebViewContentView and adds it to the container
// view.
- (void)displayWebView;
// Called when web view process has been terminated.
- (void)webViewWebProcessDidCrash;
// Returns the WKWebViewConfigurationProvider associated with the web
// controller's BrowserState.
- (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider;
// Extracts "Referer" [sic] value from WKNavigationAction request header.
- (NSString*)referrerFromNavigationAction:(WKNavigationAction*)action;
// Returns the current URL of the web view, and sets |trustLevel| accordingly
// based on the confidence in the verification.
- (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel;
// Returns |YES| if |url| should be loaded in a native view.
- (BOOL)shouldLoadURLInNativeView:(const GURL&)url;
// Loads POST request with body in |_wkWebView| by constructing an HTML page
// that executes the request through JavaScript and replaces document with the
// result.
// Note that this approach includes multiple body encodings and decodings, plus
// the data is passed to |_wkWebView| on main thread.
// This is necessary because WKWebView ignores POST request body.
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=145410
// TODO(crbug.com/740987): Remove |loadPOSTRequest:| workaround once iOS 10 is
// dropped.
- (WKNavigation*)loadPOSTRequest:(NSMutableURLRequest*)request;
// Loads the HTML into the page at the given URL.
- (void)loadHTML:(NSString*)html forURL:(const GURL&)url;
// 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;
// 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;
// Updates current state with any pending information. Should be called when a
// navigation is committed.
- (void)commitPendingNavigationInfo;
// Returns a NSMutableURLRequest that represents the current NavigationItem.
- (NSMutableURLRequest*)requestForCurrentNavigationItem;
// Returns the WKBackForwardListItemHolder for the current navigation item.
- (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder;
// Updates the WKBackForwardListItemHolder navigation item.
- (void)updateCurrentBackForwardListItemHolder;
// Presents native content using the native controller for |item| without
// notifying WebStateObservers. This method does not modify the underlying web
// view. It simply covers the web view with the native content.
// |-didLoadNativeContentForNavigationItem| must be called some time later
// to notify WebStateObservers.
- (void)presentNativeContentForNavigationItem:(web::NavigationItem*)item;
// Notifies WebStateObservers the completion of this navigation.
- (void)didLoadNativeContentForNavigationItem:(web::NavigationItemImpl*)item;
// Loads a blank page directly into WKWebView as a placeholder for a Native View
// or WebUI URL. This page has the URL about:blank?for=<encoded original URL>.
// If |originalContext| is provided, reuse it for the placeholder navigation
// instead of creating a new one. See "Handling App-specific URLs"
// section of go/bling-navigation-experiment for details.
- (web::NavigationContextImpl*)
loadPlaceholderInWebViewForURL:(const GURL&)originalURL
forContext:(std::unique_ptr<web::NavigationContextImpl>)
originalContext;
// Executes the command specified by the ErrorRetryStateMachine.
- (void)handleErrorRetryCommand:(web::ErrorRetryCommand)command
navigationItem:(web::NavigationItemImpl*)item
navigationContext:(web::NavigationContextImpl*)context
originalNavigation:(WKNavigation*)originalNavigation;
// Loads the error page.
- (void)loadErrorPageForNavigationItem:(web::NavigationItemImpl*)item
navigationContext:(web::NavigationContextImpl*)context;
// Aborts any load for both the web view and web controller.
- (void)abortLoad;
// Updates the internal state and informs the delegate that any outstanding load
// operations are cancelled.
- (void)loadCancelled;
// 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;
// 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;
// Updates URL for navigation context and navigation item.
- (void)didReceiveRedirectForNavigation:(web::NavigationContextImpl*)context
withURL:(const GURL&)URL;
// Called following navigation completion to generate final navigation lifecycle
// events. Navigation is considered complete when the document has finished
// loading, or when other page load mechanics are completed on a
// non-document-changing URL change.
- (void)didFinishNavigation:(web::NavigationContextImpl*)context;
// Update the appropriate parts of the model and broadcast to the embedder. This
// may be called multiple times and thus must be idempotent.
- (void)loadCompleteWithSuccess:(BOOL)loadSuccess
forContext:(web::NavigationContextImpl*)context;
// Called after URL is finished loading and _loadPhase is set to PAGE_LOADED.
// |context| contains information about the navigation associated with the URL.
// It is nil if currentURL is invalid.
- (void)didFinishWithURL:(const GURL&)currentURL
loadSuccess:(BOOL)loadSuccess
context:(nullable const web::NavigationContextImpl*)context;
// Navigates forwards or backwards by |delta| pages. No-op if delta is out of
// bounds. Reloads if delta is 0.
// TODO(crbug.com/661316): Move this method to NavigationManager.
- (void)rendererInitiatedGoDelta:(int)delta hasUserGesture:(BOOL)hasUserGesture;
// Informs the native controller if web usage is allowed or not.
- (void)setNativeControllerWebUsageEnabled:(BOOL)webUsageEnabled;
// Acts on a single message from the JS object, parsed from JSON into a
// DictionaryValue. Returns NO if the format for the message was invalid.
- (BOOL)respondToMessage:(base::DictionaryValue*)crwMessage
userIsInteracting:(BOOL)userIsInteracting
originURL:(const GURL&)originURL
isMainFrame:(BOOL)isMainFrame
senderFrame:(web::WebFrame*)senderFrame;
// Called when web controller receives a new message from the web page.
- (void)didReceiveScriptMessage:(WKScriptMessage*)message;
// Returns a new script which wraps |script| with windowID check so |script| is
// not evaluated on windowID mismatch.
- (NSString*)scriptByAddingWindowIDCheckForScript:(NSString*)script;
// Attempts to handle a script message. Returns YES on success, NO otherwise.
- (BOOL)respondToWKScriptMessage:(WKScriptMessage*)scriptMessage;
// Handles frame became available message.
- (void)frameBecameAvailableWithMessage:(WKScriptMessage*)message;
// Handles frame became unavailable message.
- (void)frameBecameUnavailableWithMessage:(WKScriptMessage*)message;
// Clears the frames list.
- (void)removeAllWebFrames;
// Registers load request with empty referrer and link or client redirect
// transition based on user interaction state. Returns navigation context for
// this request.
- (std::unique_ptr<web::NavigationContextImpl>)
registerLoadRequestForURL:(const GURL&)URL
sameDocumentNavigation:(BOOL)sameDocumentNavigation
hasUserGesture:(BOOL)hasUserGesture
placeholderNavigation:(BOOL)placeholderNavigation;
// Prepares web controller and delegates for anticipated page change.
// Allows several methods to invoke webWill/DidAddPendingURL on anticipated page
// change, using the same cached request and calculated transition types.
// Returns navigation context for this request.
- (std::unique_ptr<web::NavigationContextImpl>)
registerLoadRequestForURL:(const GURL&)URL
referrer:(const web::Referrer&)referrer
transition:(ui::PageTransition)transition
sameDocumentNavigation:(BOOL)sameDocumentNavigation
hasUserGesture:(BOOL)hasUserGesture
placeholderNavigation:(BOOL)placeholderNavigation;
// Maps WKNavigationType to ui::PageTransition.
- (ui::PageTransition)pageTransitionFromNavigationType:
(WKNavigationType)navigationType;
// Updates the HTML5 history state of the page using the current NavigationItem.
// For same-document navigations and navigations affected by
// window.history.[push/replace]State(), the URL and serialized state object
// will be updated to the current NavigationItem's values. A popState event
// will be triggered for all same-document navigations. Additionally, a
// hashchange event will be triggered for same-document navigations where the
// only difference between the current and previous URL is the fragment.
- (void)updateHTML5HistoryState;
// Generates the JavaScript string used to update the UIWebView's URL so that it
// matches the URL displayed in the omnibox and sets window.history.state to
// stateObject. Needed for history.pushState() and history.replaceState().
- (NSString*)javaScriptToReplaceWebViewURL:(const GURL&)URL
stateObjectJSON:(NSString*)stateObject;
// Generates the JavaScript string used to manually dispatch a popstate event,
// using |stateObjectJSON| as the event parameter.
- (NSString*)javaScriptToDispatchPopStateWithObject:(NSString*)stateObjectJSON;
// Generates the JavaScript string used to manually dispatch a hashchange event,
// using |oldURL| and |newURL| as the event parameters.
- (NSString*)javaScriptToDispatchHashChangeWithOldURL:(const GURL&)oldURL
newURL:(const GURL&)newURL;
// Injects JavaScript to update the URL and state object of the webview to the
// values found in the current NavigationItem. A hashchange event will be
// dispatched if |dispatchHashChange| is YES, and a popstate event will be
// dispatched if |sameDocument| is YES. Upon the script's completion, resets
// |urlOnStartLoading_| and |_lastRegisteredRequestURL| to the current
// NavigationItem's URL. This is necessary so that sites that depend on URL
// params/fragments continue to work correctly and that checks for the URL don't
// incorrectly trigger |-webPageChangedWithContext| calls.
- (void)injectHTML5HistoryScriptWithHashChange:(BOOL)dispatchHashChange
sameDocumentNavigation:(BOOL)sameDocumentNavigation;
// 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;
// Returns YES if the current live view is a web view with an image MIME type.
- (BOOL)contentIsImage;
// Restores the state for this page from session history.
- (void)restoreStateFromHistory;
// Extracts the current page's viewport tag information and calls |completion|.
// If the page has changed before the viewport tag is successfully extracted,
// |completion| is called with nullptr.
typedef void (^ViewportStateCompletion)(const web::PageViewportState*);
- (void)extractViewportTagWithCompletion:(ViewportStateCompletion)completion;
// Called by NSNotificationCenter upon orientation changes.
- (void)orientationDidChange;
// Queries the web view for the user-scalable meta tag and calls
// |-applyPageDisplayState:userScalable:| with the result.
- (void)applyPageDisplayState:(const web::PageDisplayState&)displayState;
// Restores state of the web view's scroll view from |scrollState|.
// |isUserScalable| represents the value of user-scalable meta tag.
- (void)applyPageDisplayState:(const web::PageDisplayState&)displayState
userScalable:(BOOL)isUserScalable;
// Calls the zoom-preparation UIScrollViewDelegate callbacks on the web view.
// This is called before |-applyWebViewScrollZoomScaleFromScrollState:|.
- (void)prepareToApplyWebViewScrollZoomScale;
// Calls the zoom-completion UIScrollViewDelegate callbacks on the web view.
// This is called after |-applyWebViewScrollZoomScaleFromScrollState:|.
- (void)finishApplyingWebViewScrollZoomScale;
// Sets zoom scale value for webview scroll view from |zoomState|.
- (void)applyWebViewScrollZoomScaleFromZoomState:
(const web::PageZoomState&)zoomState;
// Sets scroll offset value for webview scroll view from |scrollState|.
- (void)applyWebViewScrollOffsetFromScrollState:
(const web::PageScrollState&)scrollState;
// Returns the referrer for the current page.
- (web::Referrer)currentReferrer;
// Adds a new NavigationItem with the given URL and state object to the history
// stack. A state object is a serialized generic JavaScript object that contains
// details of the UI's state for a given NavigationItem/URL.
// TODO(stuartmorgan): Move the pushState/replaceState logic into
// NavigationManager.
- (void)pushStateWithPageURL:(const GURL&)pageURL
stateObject:(NSString*)stateObject
transition:(ui::PageTransition)transition
hasUserGesture:(BOOL)hasUserGesture;
// Assigns the given URL and state object to the current NavigationItem.
- (void)replaceStateWithPageURL:(const GURL&)pageUrl
stateObject:(NSString*)stateObject
hasUserGesture:(BOOL)hasUserGesture;
// Sets _documentURL to newURL, and updates any relevant state information.
- (void)setDocumentURL:(const GURL&)newURL
context:(web::NavigationContextImpl*)context;
// Sets last committed NavigationItem's title to the given |title|, which can
// not be nil.
- (void)setNavigationItemTitle:(NSString*)title;
// Returns YES if the current navigation item corresponds to a web page
// loaded by a POST request.
- (BOOL)isCurrentNavigationItemPOST;
// Returns YES if current navigation item is WKNavigationTypeBackForward.
- (BOOL)isCurrentNavigationBackForward;
// Returns YES if the given WKBackForwardListItem is valid to use for
// navigation.
- (BOOL)isBackForwardListItemValid:(WKBackForwardListItem*)item;
// Finds all the scrollviews in the view hierarchy and makes sure they do not
// interfere with scroll to top when tapping the statusbar.
- (void)optOutScrollsToTopForSubviews;
// Returns YES if the navigation action is associated with a main frame request.
- (BOOL)isMainFrameNavigationAction:(WKNavigationAction*)action;
// Updates SSL status for the current navigation item based on the information
// provided by web view.
- (void)updateSSLStatusForCurrentNavigationItem;
// Called when a load ends in an SSL error and certificate chain.
- (void)handleSSLCertError:(NSError*)error
forNavigation:(WKNavigation*)navigation;
// 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;
// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
// with NSURLSessionAuthChallengeDisposition and credentials.
- (void)handleHTTPAuthForChallenge:(NSURLAuthenticationChallenge*)challenge
completionHandler:
(void (^)(NSURLSessionAuthChallengeDisposition,
NSURLCredential*))completionHandler;
// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
// with NSURLSessionAuthChallengeDisposition and credentials.
+ (void)processHTTPAuthForUser:(NSString*)user
password:(NSString*)password
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
NSURLCredential*))completionHandler;
// Helper to respond to |webView:runJavaScript...| delegate methods.
// |completionHandler| must not be nil.
- (void)runJavaScriptDialogOfType:(web::JavaScriptDialogType)type
initiatedByFrame:(WKFrameInfo*)frame
message:(NSString*)message
defaultText:(NSString*)defaultText
completion:(void (^)(BOOL, NSString*))completionHandler;
// Called when WKWebView estimatedProgress has been changed.
- (void)webViewEstimatedProgressDidChange;
// Called when WKWebView certificateChain or hasOnlySecureContent property has
// changed.
- (void)webViewSecurityFeaturesDidChange;
// Called when WKWebView loading state has been changed.
- (void)webViewLoadingStateDidChange;
// Called when WKWebView title has been changed.
- (void)webViewTitleDidChange;
// Called when WKWebView canGoForward/canGoBack state has been changed.
- (void)webViewBackForwardStateDidChange;
// Called when WKWebView URL has been changed.
- (void)webViewURLDidChange;
// Returns YES if a KVO change to |newURL| could be a 'navigation' within the
// document (hash change, pushState/replaceState, etc.). This should only be
// used in the context of a URL KVO callback firing, and only if |isLoading| is
// YES for the web view (since if it's not, no guesswork is needed).
- (BOOL)isKVOChangePotentialSameDocumentNavigationToURL:(const GURL&)newURL;
// Returns YES if a SafeBrowsing warning is currently displayed within
// WKWebView.
- (BOOL)isSafeBrowsingWarningDisplayedInWebView;
// Called when a non-document-changing URL change occurs. Updates the
// _documentURL, and informs the superclass of the change.
- (void)URLDidChangeWithoutDocumentChange:(const GURL&)URL;
// Returns context for pending navigation that has |URL|. null if there is no
// matching pending navigation.
- (web::NavigationContextImpl*)contextForPendingMainFrameNavigationWithURL:
(const GURL&)URL;
// Loads request for the URL of the current navigation item. Subclasses may
// choose to build a new NSURLRequest and call |loadRequest| on the underlying
// web view, or use native web view navigation where possible (for example,
// going back and forward through the history stack).
- (void)loadRequestForCurrentNavigationItem;
// Reports Navigation.IOSWKWebViewSlowFastBackForward UMA. No-op if pending
// navigation is not back forward navigation.
- (void)reportBackForwardNavigationTypeForFastNavigation:(BOOL)isFast;
// Handlers for JavaScript messages. |message| contains a JavaScript command and
// data relevant to the message, and |context| contains contextual information
// about web view state needed for some handlers.
// Handles 'chrome.send' message.
- (BOOL)handleChromeSendMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'document.favicons' message.
- (BOOL)handleDocumentFaviconsMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.error' message.
- (BOOL)handleWindowErrorMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.hashchange' message.
- (BOOL)handleWindowHashChangeMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.history.back' message.
- (BOOL)handleWindowHistoryBackMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.history.forward' message.
- (BOOL)handleWindowHistoryForwardMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.history.go' message.
- (BOOL)handleWindowHistoryGoMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.history.willChangeState' message.
- (BOOL)handleWindowHistoryWillChangeStateMessage:
(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.history.didPushState' message.
- (BOOL)handleWindowHistoryDidPushStateMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'window.history.didReplaceState' message.
- (BOOL)handleWindowHistoryDidReplaceStateMessage:
(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Handles 'restoresession.error' message.
- (BOOL)handleRestoreSessionErrorMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context;
// Caches request POST data in the given session entry.
- (void)cachePOSTDataForRequest:(NSURLRequest*)request
inNavigationItem:(web::NavigationItemImpl*)item;
// 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;
// Called when a load ends in an error.
- (void)handleLoadError:(NSError*)error
forNavigation:(WKNavigation*)navigation
provisionalLoad:(BOOL)provisionalLoad;
// Handles cancelled load in WKWebView (error with NSURLErrorCancelled code).
- (void)handleCancelledError:(NSError*)error
forNavigation:(WKNavigation*)navigation
provisionalLoad:(BOOL)provisionalLoad;
// Used to decide whether a load that generates errors with the
// NSURLErrorCancelled code should be cancelled.
- (BOOL)shouldCancelLoadForCancelledError:(NSError*)error
provisionalLoad:(BOOL)provisionalLoad;
// Returns YES if response should be rendered in WKWebView.
- (BOOL)shouldRenderResponse:(WKNavigationResponse*)WKResponse;
// 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;
// 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)didReceiveWebViewNavigationDelegateCallback;
// Sets up WebUI for URL.
- (void)createWebUIForURL:(const GURL&)URL;
// Clears WebUI, if one exists.
- (void)clearWebUI;
@end
namespace {
NSString* const kReferrerHeaderName = @"Referer"; // [sic]
// The duration of the period following a screen touch during which the user is
// still considered to be interacting with the page.
const NSTimeInterval kMaximumDelayForUserInteractionInSeconds = 2;
// URLs that are fed into UIWebView as history push/replace get escaped,
// potentially changing their format. Code that attempts to determine whether a
// URL hasn't changed can be confused by those differences though, so method
// will round-trip a URL through the escaping process so that it can be adjusted
// pre-storing, to allow later comparisons to work as expected.
GURL URLEscapedForHistory(const GURL& url) {
// TODO(stuartmorgan): This is a very large hammer; see if limited unicode
// escaping would be sufficient.
return net::GURLWithNSURL(net::NSURLWithGURL(url));
}
} // namespace
@implementation CRWWebController
@synthesize webUsageEnabled = _webUsageEnabled;
@synthesize loadPhase = _loadPhase;
@synthesize webProcessCrashed = _webProcessCrashed;
@synthesize visible = _visible;
@synthesize nativeProvider = _nativeProvider;
@synthesize swipeRecognizerProvider = _swipeRecognizerProvider;
@synthesize webViewProxy = _webViewProxy;
@synthesize allowsBackForwardNavigationGestures =
_allowsBackForwardNavigationGestures;
- (instancetype)initWithWebState:(WebStateImpl*)webState {
self = [super init];
if (self) {
_webStateImpl = webState;
_webUsageEnabled = YES;
if (web::GetWebClient()->IsSlimNavigationManagerEnabled())
_allowsBackForwardNavigationGestures = YES;
DCHECK(_webStateImpl);
// Load phase when no WebView present is 'loaded' because this represents
// the idle state.
_loadPhase = web::PAGE_LOADED;
// Content area is lazily instantiated.
_defaultURL = GURL(url::kAboutBlankURL);
_jsInjectionReceiver =
[[CRWJSInjectionReceiver alloc] initWithEvaluator:self];
_webViewProxy = [[CRWWebViewProxyImpl alloc] initWithWebController:self];
[[_webViewProxy scrollViewProxy] addObserver:self];
_gestureRecognizers = [[NSMutableArray alloc] init];
_pendingLoadCompleteActions = [[NSMutableArray alloc] init];
web::BrowserState* browserState = _webStateImpl->GetBrowserState();
_certVerificationController = [[CRWCertVerificationController alloc]
initWithBrowserState:browserState];
_certVerificationErrors =
std::make_unique<CertVerificationErrorsCacheType>(kMaxCertErrorsCount);
_navigationStates = [[CRWWKNavigationStates alloc] init];
web::WebFramesManagerImpl::CreateForWebState(_webStateImpl);
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(orientationDidChange)
name:UIApplicationDidChangeStatusBarOrientationNotification
object:nil];
}
return self;
}
- (WebState*)webState {
return _webStateImpl;
}
- (WebStateImpl*)webStateImpl {
return _webStateImpl;
}
- (void)clearTransientContentView {
// Early return if there is no transient content view.
if (![_containerView transientContentView])
return;
// Remove the transient content view from the hierarchy.
[_containerView clearTransientContentView];
}
- (void)showTransientContentView:(CRWContentView*)contentView {
DCHECK(contentView);
DCHECK(contentView.scrollView);
// TODO(crbug.com/556848) Reenable DCHECK when |CRWWebControllerContainerView|
// is restructured so that subviews are not added during |layoutSubviews|.
// DCHECK([contentView.scrollView isDescendantOfView:contentView]);
[_containerView displayTransientContent:contentView];
}
- (void)dealloc {
DCHECK([NSThread isMainThread]);
DCHECK(_isBeingDestroyed); // 'close' must have been called already.
DCHECK(!_webView);
}
- (id<CRWNativeContent>)nativeController {
return [_containerView nativeController];
}
- (void)setNativeController:(id<CRWNativeContent>)nativeController {
// Check for pointer equality.
if (self.nativeController == nativeController)
return;
// Unset the delegate on the previous instance.
if ([self.nativeController respondsToSelector:@selector(setDelegate:)])
[self.nativeController setDelegate:nil];
[_containerView displayNativeContent:nativeController];
[self setNativeControllerWebUsageEnabled:_webUsageEnabled];
}
- (NSDictionary*)WKWebViewObservers {
return @{
@"serverTrust" : @"webViewSecurityFeaturesDidChange",
@"estimatedProgress" : @"webViewEstimatedProgressDidChange",
@"hasOnlySecureContent" : @"webViewSecurityFeaturesDidChange",
@"title" : @"webViewTitleDidChange",
@"loading" : @"webViewLoadingStateDidChange",
@"URL" : @"webViewURLDidChange",
@"canGoForward" : @"webViewBackForwardStateDidChange",
@"canGoBack" : @"webViewBackForwardStateDidChange"
};
}
// NativeControllerDelegate method, called to inform that title has changed.
- (void)nativeContent:(id)content titleDidChange:(NSString*)title {
[self setNavigationItemTitle:title];
}
- (void)setNativeControllerWebUsageEnabled:(BOOL)webUsageEnabled {
if ([self.nativeController
respondsToSelector:@selector(setWebUsageEnabled:)]) {
[self.nativeController setWebUsageEnabled:webUsageEnabled];
}
}
- (void)setWebUsageEnabled:(BOOL)enabled {
if (_webUsageEnabled == enabled)
return;
// WKWebView autoreleases its WKProcessPool on removal from superview.
// Deferring WKProcessPool deallocation may lead to issues with cookie
// clearing and and Browsing Data Partitioning implementation.
@autoreleasepool {
if (!enabled) {
[self removeWebView];
}
}
_webUsageEnabled = enabled;
// WKWebView autoreleases its WKProcessPool on removal from superview.
// Deferring WKProcessPool deallocation may lead to issues with cookie
// clearing and and Browsing Data Partitioning implementation.
@autoreleasepool {
[self setNativeControllerWebUsageEnabled:_webUsageEnabled];
if (enabled) {
// Don't create the web view; let it be lazy created as needed.
} else {
_webStateImpl->ClearTransientContent();
_touchTrackingRecognizer.touchTrackingDelegate = nil;
_touchTrackingRecognizer = nil;
_currentURLLoadWasTrigerred = NO;
}
}
}
- (void)requirePageReconstruction {
// TODO(crbug.com/736103): Removing web view will destroy session history for
// WKBasedNavigationManager.
if (!web::GetWebClient()->IsSlimNavigationManagerEnabled())
[self removeWebView];
}
- (BOOL)isViewAlive {
return !_webProcessCrashed && [_containerView isViewAlive];
}
- (BOOL)isSafeBrowsingWarningDisplayedInWebView {
// A SafeBrowsing warning is a UIScrollView that is inserted on top of
// WKWebView's scroll view. This method uses heuristics to detect this view.
// It may break in the future if WebKit's implementation of SafeBrowsing
// warnings changes.
UIView* containingView = _webView.scrollView.superview;
if (!containingView)
return NO;
UIView* topView = containingView.subviews.lastObject;
if (topView == _webView.scrollView)
return NO;
return
[topView isKindOfClass:[UIScrollView class]] &&
[NSStringFromClass([topView class]) containsString:@"Warning"] &&
topView.subviews.count > 0 &&
[topView.subviews[0].subviews.lastObject isKindOfClass:[UIButton class]];
}
- (BOOL)contentIsHTML {
if (!_webView)
return NO;
std::string MIMEType = self.webState->GetContentsMimeType();
return MIMEType == "text/html" || MIMEType == "application/xhtml+xml" ||
MIMEType == "application/xml";
}
// Stop doing stuff, especially network stuff. Close the request tracker.
- (void)terminateNetworkActivity {
DCHECK(!_isHalted);
_isHalted = YES;
// Cancel all outstanding perform requests, and clear anything already queued
// (since this may be called from within the handling loop) to prevent any
// asynchronous JavaScript invocation handling from continuing.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void)dismissModals {
if ([self.nativeController respondsToSelector:@selector(dismissModals)])
[self.nativeController dismissModals];
}
// Caller must reset the delegate before calling.
- (void)close {
_webStateImpl->CancelDialogs();
_SSLStatusUpdater = nil;
self.nativeProvider = nil;
self.swipeRecognizerProvider = nil;
if ([self.nativeController respondsToSelector:@selector(close)])
[self.nativeController close];
if (!_isHalted) {
[self terminateNetworkActivity];
}
// Mark the destruction sequence has started, in case someone else holds a
// strong reference and tries to continue using the tab.
DCHECK(!_isBeingDestroyed);
_isBeingDestroyed = YES;
// Remove the web view now. Otherwise, delegate callbacks occur.
[self removeWebView];
// Explicitly reset content to clean up views and avoid dangling KVO
// observers.
[_containerView resetContent];
_webStateImpl = nullptr;
DCHECK(!_webView);
// TODO(crbug.com/662860): Don't set the delegate to nil.
[_containerView setDelegate:nil];
if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) {
[self.nativeController setDelegate:nil];
}
_touchTrackingRecognizer.touchTrackingDelegate = nil;
[[_webViewProxy scrollViewProxy] removeObserver:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (CGPoint)scrollPosition {
return self.webScrollView.contentOffset;
}
- (GURL)currentURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
DCHECK(trustLevel) << "Verification of the trustLevel state is mandatory";
// The web view URL is the current URL only if it is neither a placeholder URL
// (used to hold WKBackForwardListItem for WebUI and Native Content views) nor
// a restore_session.html (used to replay session history in WKWebView).
// TODO(crbug.com/738020): Investigate if this method is still needed and if
// it can be implemented using NavigationManager API after removal of legacy
// navigation stack.
GURL webViewURL = net::GURLWithNSURL(_webView.URL);
if (_webView && !IsWKInternalUrl(webViewURL)) {
return [self webURLWithTrustLevel:trustLevel];
}
// Any non-web URL source is trusted.
*trustLevel = web::URLVerificationTrustLevel::kAbsolute;
if (self.nativeController) {
if ([self.nativeController respondsToSelector:@selector(virtualURL)]) {
return [self.nativeController virtualURL];
} else {
return [self.nativeController url];
}
}
web::NavigationItem* item =
self.navigationManagerImpl
->GetLastCommittedItemInCurrentOrRestoredSession();
return item ? item->GetVirtualURL() : GURL::EmptyGURL();
}
- (WKWebView*)webView {
return _webView;
}
- (UIScrollView*)webScrollView {
return [_webView scrollView];
}
- (GURL)currentURL {
web::URLVerificationTrustLevel trustLevel =
web::URLVerificationTrustLevel::kNone;
return [self currentURLWithTrustLevel:&trustLevel];
}
- (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(stuartmorgan): When possible, get the real referrer and policy in
// advance and use that instead. https://crbug.com/227769.
return web::Referrer(GURL(base::SysNSStringToUTF8(referrerString)),
web::ReferrerPolicyAlways);
}
- (void)pushStateWithPageURL:(const GURL&)pageURL
stateObject:(NSString*)stateObject
transition:(ui::PageTransition)transition
hasUserGesture:(BOOL)hasUserGesture {
std::unique_ptr<web::NavigationContextImpl> context =
web::NavigationContextImpl::CreateNavigationContext(
_webStateImpl, pageURL, hasUserGesture, transition,
/*is_renderer_initiated=*/true);
context->SetIsSameDocument(true);
_webStateImpl->OnNavigationStarted(context.get());
self.navigationManagerImpl->AddPushStateItemIfNecessary(pageURL, stateObject,
transition);
context->SetHasCommitted(true);
_webStateImpl->OnNavigationFinished(context.get());
self.userInteractionRegistered = NO;
}
- (void)replaceStateWithPageURL:(const GURL&)pageURL
stateObject:(NSString*)stateObject
hasUserGesture:(BOOL)hasUserGesture {
std::unique_ptr<web::NavigationContextImpl> context =
web::NavigationContextImpl::CreateNavigationContext(
_webStateImpl, pageURL, hasUserGesture,
ui::PageTransition::PAGE_TRANSITION_CLIENT_REDIRECT,
/*is_renderer_initiated=*/true);
context->SetIsSameDocument(true);
_webStateImpl->OnNavigationStarted(context.get());
self.navigationManagerImpl->UpdateCurrentItemForReplaceState(pageURL,
stateObject);
context->SetHasCommitted(true);
_webStateImpl->OnNavigationFinished(context.get());
}
- (void)setDocumentURL:(const GURL&)newURL
context:(web::NavigationContextImpl*)context {
if (newURL != _documentURL && newURL.is_valid()) {
_documentURL = newURL;
_interactionRegisteredSinceLastURLChange = NO;
}
if (web::GetWebClient()->IsSlimNavigationManagerEnabled() && context &&
!context->IsLoadingHtmlString() && !context->IsLoadingErrorPage() &&
!IsWKInternalUrl(newURL) && !newURL.SchemeIs(url::kAboutScheme) &&
_webView) {
GURL documentOrigin = newURL.GetOrigin();
web::NavigationItem* committedItem =
_webStateImpl->GetNavigationManager()->GetLastCommittedItem();
GURL committedOrigin =
committedItem ? committedItem->GetURL().GetOrigin() : GURL::EmptyGURL();
DCHECK_EQ(documentOrigin, committedOrigin)
<< "Old and new URL detection system have a mismatch";
ukm::SourceId sourceID = ukm::ConvertToSourceId(
context->GetNavigationId(), ukm::SourceIdType::NAVIGATION_ID);
if (sourceID != ukm::kInvalidSourceId) {
ukm::builders::IOS_URLMismatchInLegacyAndSlimNavigationManager(sourceID)
.SetHasMismatch(documentOrigin != committedOrigin)
.Record(ukm::UkmRecorder::Get());
}
}
}
- (void)setNavigationItemTitle:(NSString*)title {
DCHECK(title);
web::NavigationItem* item =
self.navigationManagerImpl->GetLastCommittedItem();
if (!item)
return;
item->SetTitle(base::SysNSStringToUTF16(title));
_webStateImpl->OnTitleChanged();
}
- (BOOL)isCurrentNavigationItemPOST {
// |_pendingNavigationInfo| will be nil if the decidePolicy* delegate methods
// were not called.
NSString* HTTPMethod =
_pendingNavigationInfo
? [_pendingNavigationInfo HTTPMethod]
: [self currentBackForwardListItemHolder]->http_method();
if ([HTTPMethod isEqual:@"POST"]) {
return YES;
}
if (!self.currentNavItem) {
return NO;
}
return self.currentNavItem->HasPostData();
}
- (BOOL)isCurrentNavigationBackForward {
if (!self.currentNavItem)
return NO;
WKNavigationType currentNavigationType =
[self currentBackForwardListItemHolder]->navigation_type();
return currentNavigationType == WKNavigationTypeBackForward;
}
- (BOOL)isBackForwardListItemValid:(WKBackForwardListItem*)item {
// The current back-forward list item MUST be in the WKWebView's back-forward
// list to be valid.
WKBackForwardList* list = [_webView backForwardList];
return list.currentItem == item ||
[list.forwardList indexOfObject:item] != NSNotFound ||
[list.backList indexOfObject:item] != NSNotFound;
}
- (UIView*)view {
[self ensureContainerViewCreated];
DCHECK(_containerView);
return _containerView;
}
- (id<CRWWebViewNavigationProxy>)webViewNavigationProxy {
return static_cast<id<CRWWebViewNavigationProxy>>(self.webView);
}
- (UIView*)viewForPrinting {
// Printing is not supported for native controllers.
return _webView;
}
- (double)loadingProgress {
return [_webView estimatedProgress];
}
- (std::unique_ptr<web::NavigationContextImpl>)
registerLoadRequestForURL:(const GURL&)URL
sameDocumentNavigation:(BOOL)sameDocumentNavigation
hasUserGesture:(BOOL)hasUserGesture
placeholderNavigation:(BOOL)placeholderNavigation {
// Get the navigation type from the last main frame load request, and try to
// map that to a PageTransition.
WKNavigationType navigationType =
_pendingNavigationInfo ? [_pendingNavigationInfo navigationType]
: WKNavigationTypeOther;
ui::PageTransition transition =
[self pageTransitionFromNavigationType:navigationType];
if (web::GetWebClient()->IsSlimNavigationManagerEnabled() &&
navigationType == WKNavigationTypeBackForward &&
_webView.backForwardList.currentItem) {
web::NavigationItem* currentItem = [[CRWNavigationItemHolder
holderForBackForwardListItem:_webView.backForwardList.currentItem]
navigationItem];
if (currentItem) {
transition = ui::PageTransitionFromInt(transition |
currentItem->GetTransitionType());
}
}
// The referrer is not known yet, and will be updated later.
const web::Referrer emptyReferrer;
std::unique_ptr<web::NavigationContextImpl> context =
[self registerLoadRequestForURL:URL
referrer:emptyReferrer
transition:transition
sameDocumentNavigation:sameDocumentNavigation
hasUserGesture:hasUserGesture
placeholderNavigation:placeholderNavigation];
context->SetWKNavigationType(navigationType);
return context;
}
- (std::unique_ptr<web::NavigationContextImpl>)
registerLoadRequestForURL:(const GURL&)requestURL
referrer:(const web::Referrer&)referrer
transition:(ui::PageTransition)transition
sameDocumentNavigation:(BOOL)sameDocumentNavigation
hasUserGesture:(BOOL)hasUserGesture
placeholderNavigation:(BOOL)placeholderNavigation {
// Transfer time is registered so that further transitions within the time
// envelope are not also registered as links.
_lastTransferTimeInSeconds = CFAbsoluteTimeGetCurrent();
// Add or update pending item before any WebStateObserver callbacks.
// See https://crbug.com/842151 for a scenario where this is important.
web::NavigationItem* item =
self.navigationManagerImpl->GetPendingItemInCurrentOrRestoredSession();
if (item) {
// Update the existing pending entry.
// Typically on PAGE_TRANSITION_CLIENT_REDIRECT.
// Don't update if request is a placeholder entry because the pending item
// should have the original target URL.
// Don't update if pending URL has a different origin, because client
// redirects can not change the origin. It is possible to have more than one
// pending navigations, so the redirect does not necesserily belong to the
// pending navigation item.
if (!placeholderNavigation &&
item->GetURL().GetOrigin() == requestURL.GetOrigin()) {
self.navigationManagerImpl->UpdatePendingItemUrl(requestURL);
}
} else {
self.navigationManagerImpl->AddPendingItem(
requestURL, referrer, transition,
web::NavigationInitiationType::RENDERER_INITIATED,
NavigationManager::UserAgentOverrideOption::INHERIT);
item =
self.navigationManagerImpl->GetPendingItemInCurrentOrRestoredSession();
}
bool redirect = transition & ui::PAGE_TRANSITION_IS_REDIRECT_MASK;
if (!redirect) {
// Before changing phases, the delegate should be informed that any existing
// request is being cancelled before completion.
[self loadCancelled];
DCHECK(_loadPhase == web::PAGE_LOADED);
}
_loadPhase = web::LOAD_REQUESTED;
// Record the state of outgoing web view. Do nothing if native controller
// exists, because in that case recordStateInHistory will record the state
// of incoming page as native controller is already inserted.
// TODO(crbug.com/811770) Don't record state under WKBasedNavigationManager
// because it may incorrectly clobber the incoming page if this is a
// back/forward navigation. WKWebView restores page scroll state for web view
// pages anyways so this only impacts user if WKWebView is deleted.
if (!redirect && !self.nativeController &&
!web::GetWebClient()->IsSlimNavigationManagerEnabled()) {
[self recordStateInHistory];
}
bool isRendererInitiated =
item ? (static_cast<web::NavigationItemImpl*>(item)
->NavigationInitiationType() ==
web::NavigationInitiationType::RENDERER_INITIATED)
: true;
std::unique_ptr<web::NavigationContextImpl> context =
web::NavigationContextImpl::CreateNavigationContext(
_webStateImpl, requestURL, hasUserGesture, transition,
isRendererInitiated);
context->SetPlaceholderNavigation(placeholderNavigation);
// TODO(crbug.com/676129): LegacyNavigationManagerImpl::AddPendingItem does
// not create a pending item in case of reload. Remove this workaround once
// the bug is fixed or WKBasedNavigationManager is fully adopted.
if (!item) {
DCHECK(!web::GetWebClient()->IsSlimNavigationManagerEnabled());
item = self.navigationManagerImpl->GetLastCommittedItem();
}
context->SetNavigationItemUniqueID(item->GetUniqueID());
context->SetIsPost([self isCurrentNavigationItemPOST]);
context->SetIsSameDocument(sameDocumentNavigation);
if (!IsWKInternalUrl(requestURL) && !placeholderNavigation) {
_webStateImpl->SetIsLoading(true);
// WKBasedNavigationManager triggers HTML load when placeholder navigation
// finishes.
if (!web::GetWebClient()->IsSlimNavigationManagerEnabled())
[_webUIManager loadWebUIForURL:requestURL];
}
return context;
}
- (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 _interactionRegisteredSinceLastURLChange
? ui::PAGE_TRANSITION_LINK
: ui::PAGE_TRANSITION_CLIENT_REDIRECT;
}
}
// TODO(crbug.com/788465): Verify that the history state management here are not
// needed for WKBasedNavigationManagerImpl and delete this method. The
// OnNavigationItemCommitted() call is likely the only thing that needs to be
// retained.
- (void)updateHTML5HistoryState {
web::NavigationItemImpl* currentItem = self.currentNavItem;
if (!currentItem)
return;
// Same-document navigations must trigger a popState event.
CRWSessionController* sessionController = self.sessionController;
BOOL sameDocumentNavigation = [sessionController
isSameDocumentNavigationBetweenItem:sessionController.currentItem
andItem:sessionController.previousItem];
// WKWebView doesn't send hashchange events for same-document non-BFLI
// navigations, so one must be dispatched manually for hash change same-
// document navigations.
const GURL URL = currentItem->GetURL();
web::NavigationItem* previousItem = self.sessionController.previousItem;
const GURL oldURL = previousItem ? previousItem->GetURL() : GURL();
BOOL shouldDispatchHashchange = sameDocumentNavigation && previousItem &&
(web::GURLByRemovingRefFromGURL(URL) ==
web::GURLByRemovingRefFromGURL(oldURL));
// The URL and state object must be set for same-document navigations and
// NavigationItems that were created or updated by calls to pushState() or
// replaceState().
BOOL shouldUpdateState = sameDocumentNavigation ||
currentItem->IsCreatedFromPushState() ||
currentItem->HasStateBeenReplaced();
if (!shouldUpdateState)
return;
// TODO(stuartmorgan): Make CRWSessionController manage this internally (or
// remove it; it's not clear this matches other platforms' behavior).
self.navigationManagerImpl->OnNavigationItemCommitted();
// Record that a same-document hashchange event will be fired. This flag will
// be reset when resonding to the hashchange message. Note that resetting the
// flag in the completion block below is too early, as that block is called
// before hashchange event listeners have a chance to fire.
_dispatchingSameDocumentHashChangeEvent = shouldDispatchHashchange;
// Inject the JavaScript to update the state on the browser side.
[self injectHTML5HistoryScriptWithHashChange:shouldDispatchHashchange
sameDocumentNavigation:sameDocumentNavigation];
}
- (NSString*)javaScriptToReplaceWebViewURL:(const GURL&)URL
stateObjectJSON:(NSString*)stateObject {
std::string outURL;
base::EscapeJSONString(URL.spec(), true, &outURL);
return
[NSString stringWithFormat:@"__gCrWeb.replaceWebViewURL(%@, %@);",
base::SysUTF8ToNSString(outURL), stateObject];
}
- (NSString*)javaScriptToDispatchPopStateWithObject:(NSString*)stateObjectJSON {
std::string outState;
base::EscapeJSONString(base::SysNSStringToUTF8(stateObjectJSON), true,
&outState);
return [NSString stringWithFormat:@"__gCrWeb.dispatchPopstateEvent(%@);",
base::SysUTF8ToNSString(outState)];
}
- (NSString*)javaScriptToDispatchHashChangeWithOldURL:(const GURL&)oldURL
newURL:(const GURL&)newURL {
return [NSString
stringWithFormat:@"__gCrWeb.dispatchHashchangeEvent(\'%s\', \'%s\');",
oldURL.spec().c_str(), newURL.spec().c_str()];
}
- (void)injectHTML5HistoryScriptWithHashChange:(BOOL)dispatchHashChange
sameDocumentNavigation:(BOOL)sameDocumentNavigation {
web::NavigationItemImpl* currentItem = self.currentNavItem;
if (!currentItem)
return;
const GURL URL = currentItem->GetURL();
NSString* stateObject = currentItem->GetSerializedStateObject();
NSMutableString* script = [NSMutableString
stringWithString:[self javaScriptToReplaceWebViewURL:URL
stateObjectJSON:stateObject]];
if (sameDocumentNavigation) {
[script
appendString:[self javaScriptToDispatchPopStateWithObject:stateObject]];
}
if (dispatchHashChange) {
web::NavigationItemImpl* previousItem = self.sessionController.previousItem;
const GURL oldURL = previousItem ? previousItem->GetURL() : GURL();
[script appendString:[self javaScriptToDispatchHashChangeWithOldURL:oldURL
newURL:URL]];
}
[self executeJavaScript:script completionHandler:nil];
}
// Load the current URL in a web view, first ensuring the web view is visible.
- (void)loadCurrentURLInWebView {
web::NavigationItem* item = self.currentNavItem;
GURL targetURL = item ? item->GetVirtualURL() : GURL::EmptyGURL();
// Load the url. The UIWebView delegate callbacks take care of updating the
// session history and UI.
if (!targetURL.is_valid()) {
[self didFinishWithURL:targetURL loadSuccess:NO context:nullptr];
_webStateImpl->SetIsLoading(false);
_webStateImpl->OnPageLoaded(targetURL, NO);
return;
}
// JavaScript should never be evaluated here. User-entered JS should be
// evaluated via stringByEvaluatingUserJavaScriptFromString.
DCHECK(!targetURL.SchemeIs(url::kJavaScriptScheme));
[self ensureWebViewCreated];
[self loadRequestForCurrentNavigationItem];
}
- (void)updatePendingNavigationInfoFromNavigationAction:
(WKNavigationAction*)action {
if (action.targetFrame.mainFrame) {
_pendingNavigationInfo =
[[CRWWebControllerPendingNavigationInfo alloc] init];
[_pendingNavigationInfo
setReferrer:[self referrerFromNavigationAction:action]];
[_pendingNavigationInfo setNavigationType:action.navigationType];
[_pendingNavigationInfo setHTTPMethod:action.request.HTTPMethod];
BOOL hasUserGesture = web::GetNavigationActionInitiationType(action) ==
web::NavigationActionInitiationType::kUserInitiated;
[_pendingNavigationInfo setHasUserGesture:hasUserGesture];
}
}
- (void)updatePendingNavigationInfoFromNavigationResponse:
(WKNavigationResponse*)response {
if (response.isForMainFrame) {
if (!_pendingNavigationInfo) {
_pendingNavigationInfo =
[[CRWWebControllerPendingNavigationInfo alloc] init];
}
[_pendingNavigationInfo setMIMEType:response.response.MIMEType];
}
}
- (void)commitPendingNavigationInfo {
if ([_pendingNavigationInfo referrer]) {
_currentReferrerString = [[_pendingNavigationInfo referrer] copy];
}
if ([_pendingNavigationInfo MIMEType]) {
self.webStateImpl->SetContentsMimeType(
base::SysNSStringToUTF8([_pendingNavigationInfo MIMEType]));
}
[self updateCurrentBackForwardListItemHolder];
_pendingNavigationInfo = nil;
}
- (NSMutableURLRequest*)requestForCurrentNavigationItem {
web::NavigationItem* item = self.currentNavItem;
const GURL currentNavigationURL = item ? item->GetURL() : GURL::EmptyGURL();
NSMutableURLRequest* request = [NSMutableURLRequest
requestWithURL:net::NSURLWithGURL(currentNavigationURL)];
const web::Referrer referrer(self.currentNavItemReferrer);
if (referrer.url.is_valid()) {
std::string referrerValue =
web::ReferrerHeaderValueForNavigation(currentNavigationURL, referrer);
if (!referrerValue.empty()) {
[request setValue:base::SysUTF8ToNSString(referrerValue)
forHTTPHeaderField:kReferrerHeaderName];
}
}
// If there are headers in the current session entry add them to |request|.
// Headers that would overwrite fields already present in |request| are
// skipped.
NSDictionary* headers = self.currentHTTPHeaders;
for (NSString* headerName in headers) {
if (![request valueForHTTPHeaderField:headerName]) {
[request setValue:[headers objectForKey:headerName]
forHTTPHeaderField:headerName];
}
}
return request;
}
- (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder {
web::NavigationItem* item = self.currentNavItem;
DCHECK(item);
web::WKBackForwardListItemHolder* holder =
web::WKBackForwardListItemHolder::FromNavigationItem(item);
DCHECK(holder);
return holder;
}
- (void)updateCurrentBackForwardListItemHolder {
// WebUI pages (which are loaded via loadHTMLString:baseURL:) have no entry
// in the back/forward list, so the current item will still be the previous
// page, and should not be associated.
if (_webUIManager)
return;
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 =
_pendingNavigationInfo ? [_pendingNavigationInfo navigationType]
: WKNavigationTypeOther;
holder->set_back_forward_list_item([_webView backForwardList].currentItem);
holder->set_navigation_type(navigationType);
holder->set_http_method([_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 ([_pendingNavigationInfo MIMEType])
holder->set_mime_type([_pendingNavigationInfo MIMEType]);
}
- (void)loadErrorPageForNavigationItem:(web::NavigationItemImpl*)item
navigationContext:(web::NavigationContextImpl*)context {
const GURL currentURL = item ? item->GetVirtualURL() : GURL::EmptyGURL();
NSError* error = context->GetError();
DCHECK(error);
DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID());
if (web::IsWKWebViewSSLCertError(error)) {
// This could happen only if certificate is absent or could not be parsed.
error = web::NetErrorFromError(error, net::ERR_SSL_SERVER_CERT_BAD_FORMAT);
#if defined(DEBUG)
net::SSLInfo info;
web::GetSSLInfoFromWKWebViewSSLCertError(error, &info);
CHECK(!error.cert);
#endif
} else {
error = web::NetErrorFromError(error);
}
NSString* errorHTML = nil;
web::GetWebClient()->PrepareErrorPage(
error, context->IsPost(),
_webStateImpl->GetBrowserState()->IsOffTheRecord(), &errorHTML);
WKNavigation* navigation =
[_webView loadHTMLString:errorHTML
baseURL:net::NSURLWithGURL(currentURL)];
auto loadHTMLContext = web::NavigationContextImpl::CreateNavigationContext(
_webStateImpl, currentURL,
/*has_user_gesture=*/false, ui::PAGE_TRANSITION_FIRST,
/*is_renderer_initiated=*/false);
loadHTMLContext->SetLoadingErrorPage(true);
loadHTMLContext->SetNavigationItemUniqueID(item->GetUniqueID());
[_navigationStates setContext:std::move(loadHTMLContext)
forNavigation:navigation];
[_navigationStates setState:web::WKNavigationState::REQUESTED
forNavigation:navigation];
// TODO(crbug.com/803503): only call these for placeholder navigation because
// they should have already been triggered during navigation commit for
// failures that happen after commit.
[self didStartLoading];
self.navigationManagerImpl->CommitPendingItem();
web::NavigationItem* lastCommittedItem =
self.navigationManagerImpl->GetLastCommittedItem();
[self setDocumentURL:lastCommittedItem->GetURL() context:context];
// If |context| is a placeholder navigation, this is the second part of the
// error page load for a provisional load failure. Rewrite the context URL to
// actual URL and trigger the deferred |OnNavigationFinished| callback. This
// is also needed if |context| is not yet committed, which can happen on a
// reload/back/forward load that failed in provisional navigation.
if (context->IsPlaceholderNavigation() || !context->HasCommitted()) {
context->SetUrl(item->GetURL());
context->SetPlaceholderNavigation(false);
context->SetHasCommitted(true);
_webStateImpl->OnNavigationFinished(context);
}
[self loadCompleteWithSuccess:NO forContext:context];
_webStateImpl->SetIsLoading(false);
_webStateImpl->OnPageLoaded(currentURL, NO);
}
// Loads the current URL in a native controller if using the legacy navigation
// stack. If the new navigation stack is used, start loading a placeholder
// into the web view, upon the completion of which the native controller will
// be triggered.
- (void)loadCurrentURLInNativeView {
if (!web::GetWebClient()->IsSlimNavigationManagerEnabled()) {
// Free the web view.
[self removeWebView];
[self presentNativeContentForNavigationItem:self.currentNavItem];
[self didLoadNativeContentForNavigationItem:self.currentNavItem];
} else {
// Just present the native view now. Leave the rest of native content load
// until the placeholder navigation finishes.
[self presentNativeContentForNavigationItem:self.currentNavItem];
web::NavigationContextImpl* context = [self
loadPlaceholderInWebViewForURL:self.currentNavItem->GetVirtualURL()
forContext:nullptr];
context->SetIsNativeContentPresented(true);
}
}
- (void)presentNativeContentForNavigationItem:(web::NavigationItem*)item {
const GURL targetURL = item ? item->GetURL() : GURL::EmptyGURL();
id<CRWNativeContent> nativeContent =
[_nativeProvider controllerForURL:targetURL webState:self.webState];
// Unlike the WebView case, always create a new controller and view.
// TODO(crbug.com/759178): What to do if this does return nil?
[self setNativeController:nativeContent];
if ([nativeContent respondsToSelector:@selector(virtualURL)]) {
item->SetVirtualURL([nativeContent virtualURL]);
}
NSString* title = [self.nativeController title];
if (title && item) {
base::string16 newTitle = base::SysNSStringToUTF16(title);
item->SetTitle(newTitle);
}
}
- (void)didLoadNativeContentForNavigationItem:(web::NavigationItemImpl*)item {
const GURL targetURL = item ? item->GetURL() : GURL::EmptyGURL();
const web::Referrer referrer;
std::unique_ptr<web::NavigationContextImpl> context =
[self registerLoadRequestForURL:targetURL
referrer:referrer
transition:self.currentTransition
sameDocumentNavigation:NO
hasUserGesture:YES
placeholderNavigation:NO];
_webStateImpl->OnNavigationStarted(context.get());
[self didStartLoading];
self.navigationManagerImpl->CommitPendingItem();
context->SetHasCommitted(true);
_webStateImpl->OnNavigationFinished(context.get());
if (item && web::GetWebClient()->IsAppSpecificURL(item->GetURL())) {
// Report the successful navigation to the ErrorRetryStateMachine.
item->error_retry_state_machine().SetNoNavigationError();
}
NSString* title = [self.nativeController title];
if (title) {
[self setNavigationItemTitle:title];
}
if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) {
[self.nativeController setDelegate:self];
}
_loadPhase = web::PAGE_LOADED;
[self didFinishWithURL:targetURL loadSuccess:YES context:context.get()];
}
- (web::NavigationContextImpl*)
loadPlaceholderInWebViewForURL:(const GURL&)originalURL
forContext:(std::unique_ptr<web::NavigationContextImpl>)
originalContext {
GURL placeholderURL = CreatePlaceholderUrlForUrl(originalURL);
[self ensureWebViewCreated];
NSURLRequest* request =
[NSURLRequest requestWithURL:net::NSURLWithGURL(placeholderURL)];
WKNavigation* navigation = [_webView loadRequest:request];
[_navigationStates setState:web::WKNavigationState::REQUESTED
forNavigation:navigation];
std::unique_ptr<web::NavigationContextImpl> navigationContext;
if (originalContext) {
navigationContext = std::move(originalContext);
navigationContext->SetPlaceholderNavigation(YES);
} else {
navigationContext = [self registerLoadRequestForURL:originalURL
sameDocumentNavigation:NO
hasUserGesture:NO
placeholderNavigation:YES];
}
[_navigationStates setContext:std::move(navigationContext)
forNavigation:navigation];
return [_navigationStates contextForNavigation:navigation];
}
- (void)handleErrorRetryCommand:(web::ErrorRetryCommand)command
navigationItem:(web::NavigationItemImpl*)item
navigationContext:(web::NavigationContextImpl*)context
originalNavigation:(WKNavigation*)originalNavigation {
if (command == web::ErrorRetryCommand::kDoNothing)
return;
DCHECK_EQ(item->GetUniqueID(), context->GetNavigationItemUniqueID());
switch (command) {
case web::ErrorRetryCommand::kLoadPlaceholder: {
// This case only happens when a new request failed in provisional
// navigation. Disassociate the navigation context from the original
// request and resuse it for the placeholder navigation.
std::unique_ptr<web::NavigationContextImpl> originalContext =
[_navigationStates removeNavigation:originalNavigation];
[self loadPlaceholderInWebViewForURL:item->GetURL()
forContext:std::move(originalContext)];
} break;
case web::ErrorRetryCommand::kLoadErrorView:
[self loadErrorPageForNavigationItem:item navigationContext:context];
break;
case web::ErrorRetryCommand::kReload:
[_webView reload];
break;
case web::ErrorRetryCommand::kRewriteWebViewURL: {
std::unique_ptr<web::NavigationContextImpl> navigationContext =
[self registerLoadRequestForURL:item->GetURL()
sameDocumentNavigation:NO
hasUserGesture:NO
placeholderNavigation:NO];
WKNavigation* navigation =
[_webView loadHTMLString:@""
baseURL:net::NSURLWithGURL(item->GetURL())];
navigationContext->SetError(context->GetError());
navigationContext->SetIsPost(context->IsPost());
[_navigationStates setContext:std::move(navigationContext)
forNavigation:navigation];
} break;
case web::ErrorRetryCommand::kDoNothing:
NOTREACHED();
}
}
- (void)loadCurrentURL {
// If the content view doesn't exist, the tab has either been evicted, or
// never displayed. Bail, and let the URL be loaded when the tab is shown.
if (!_containerView)
return;
// WKBasedNavigationManagerImpl needs WKWebView to load native views, but
// WKWebView cannot be created while web usage is disabled to avoid breaking
// clearing browser data. Bail now and let the URL be loaded when web
// usage is enabled again. This can happen when purging web pages when an
// interstitial is presented over a native view. See https://crbug.com/865985
// for details.
if (web::GetWebClient()->IsSlimNavigationManagerEnabled() &&
!_webUsageEnabled)
return;
_currentURLLoadWasTrigerred = YES;
// Reset current WebUI if one exists.
[self clearWebUI];
// Abort any outstanding page load. This ensures the delegate gets informed
// about the outgoing page, and further messages from the page are suppressed.
if (_loadPhase != web::PAGE_LOADED)
[self abortLoad];
DCHECK(!_isHalted);
_webStateImpl->ClearTransientContent();
web::NavigationItem* item = self.currentNavItem;
const GURL currentURL = item ? item->GetURL() : GURL::EmptyGURL();
const bool isCurrentURLAppSpecific =
web::GetWebClient()->IsAppSpecificURL(currentURL);
// If it's a chrome URL, but not a native one, create the WebUI instance.
if (isCurrentURLAppSpecific &&
![_nativeProvider hasControllerForURL:currentURL]) {
if (!(item->GetTransitionType() & ui::PAGE_TRANSITION_TYPED ||
item->GetTransitionType() & ui::PAGE_TRANSITION_AUTO_BOOKMARK) &&
self.hasOpener) {
// WebUI URLs can not be opened by DOM to prevent cross-site scripting as
// they have increased power. WebUI URLs may only be opened when the user
// types in the URL or use bookmarks.
self.navigationManagerImpl->DiscardNonCommittedItems();
return;
} else {
[self createWebUIForURL:currentURL];
}
}
// Loading a new url, must check here if it's a native chrome URL and
// replace the appropriate view if so, or transition back to a web view from
// a native view.
if ([self shouldLoadURLInNativeView:currentURL]) {
[self loadCurrentURLInNativeView];
} else if (web::GetWebClient()->IsSlimNavigationManagerEnabled() &&
isCurrentURLAppSpecific && _webStateImpl->HasWebUI()) {
[self loadPlaceholderInWebViewForURL:currentURL forContext:nullptr];
} else {
[self loadCurrentURLInWebView];
}
}
- (void)loadCurrentURLIfNecessary {
if (_webProcessCrashed) {
[self loadCurrentURL];
} else if (!_currentURLLoadWasTrigerred) {
[self ensureContainerViewCreated];
// This method reloads last committed item, so make than item also pending.
self.sessionController.pendingItemIndex =
self.sessionController.lastCommittedItemIndex;
// TODO(crbug.com/796608): end the practice of calling |loadCurrentURL|
// when it is possible there is no current URL. If the call performs
// necessary initialization, break that out.
[self loadCurrentURL];
}
}
- (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
DCHECK(trustLevel);
*trustLevel = web::URLVerificationTrustLevel::kAbsolute;
// Placeholder URL is an implementation detail. Don't expose it to users of
// web layer.
if (IsPlaceholderUrl(_documentURL))
return ExtractUrlFromPlaceholderUrl(_documentURL);
return _documentURL;
}
- (BOOL)shouldLoadURLInNativeView:(const GURL&)url {
// App-specific URLs that don't require WebUI are loaded in native views.
return web::GetWebClient()->IsAppSpecificURL(url) &&
!_webStateImpl->HasWebUI() &&
[_nativeProvider hasControllerForURL:url];
}
- (void)reloadWithRendererInitiatedNavigation:(BOOL)isRendererInitiated {
// Clear last user interaction.
// TODO(crbug.com/546337): Move to after the load commits, in the subclass
// implementation. This will be inaccurate if the reload fails or is
// cancelled.
_lastUserInteraction = nullptr;
base::RecordAction(base::UserMetricsAction("Reload"));
GURL URL = self.currentNavItem->GetURL();
if ([self shouldLoadURLInNativeView:URL]) {
std::unique_ptr<web::NavigationContextImpl> navigationContext = [self
registerLoadRequestForURL:URL
referrer:self.currentNavItemReferrer
transition:ui::PageTransition::PAGE_TRANSITION_RELOAD
sameDocumentNavigation:NO
hasUserGesture:YES
placeholderNavigation:NO];
navigationContext->SetIsRendererInitiated(isRendererInitiated);
_webStateImpl->OnNavigationStarted(navigationContext.get());
[self didStartLoading];
self.navigationManagerImpl->CommitPendingItem();
[self.nativeController reload];
navigationContext->SetHasCommitted(true);
_webStateImpl->OnNavigationFinished(navigationContext.get());
[self loadCompleteWithSuccess:YES forContext:nullptr];
} else {
web::NavigationItem* transientItem =
self.navigationManagerImpl->GetTransientItem();
if (transientItem) {
// If there's a transient item, a reload is considered a new navigation to
// the transient item's URL (as on other platforms).
NavigationManager::WebLoadParams reloadParams(transientItem->GetURL());
reloadParams.transition_type = ui::PAGE_TRANSITION_RELOAD;
reloadParams.extra_headers =
[transientItem->GetHttpRequestHeaders() copy];
self.webState->GetNavigationManager()->LoadURLWithParams(reloadParams);
} else {
self.currentNavItem->SetTransitionType(
ui::PageTransition::PAGE_TRANSITION_RELOAD);
if (web::GetWebClient()->IsSlimNavigationManagerEnabled() &&
!web::GetWebClient()->IsAppSpecificURL(
net::GURLWithNSURL(_webView.URL))) {
// New navigation manager can delegate directly to WKWebView to reload
// for non-app-specific URLs. The necessary navigation states will be
// updated in WKNavigationDelegate callbacks.
WKNavigation* navigation = [_webView reload];
[_navigationStates setState:web::WKNavigationState::REQUESTED
forNavigation:navigation];
std::unique_ptr<web::NavigationContextImpl> navigationContext = [self
registerLoadRequestForURL:URL
referrer:self.currentNavItemReferrer
transition:ui::PageTransition::PAGE_TRANSITION_RELOAD
sameDocumentNavigation:NO
hasUserGesture:YES
placeholderNavigation:NO];
[_navigationStates setContext:std::move(navigationContext)
forNavigation:navigation];
} else {
[self loadCurrentURL];
}
}
}
}
- (void)abortLoad {
[_webView stopLoading];
[_pendingNavigationInfo setCancelled:YES];
_certVerificationErrors->Clear();
[self loadCancelled];
_safeBrowsingWarningDetectionTimer.Stop();
}
- (void)loadCancelled {
// TODO(crbug.com/821995): Check if this function should be removed.
if (_loadPhase != web::PAGE_LOADED) {
_loadPhase = web::PAGE_LOADED;
if (!_isHalted) {
_webStateImpl->SetIsLoading(false);
}
}
}
- (BOOL)contentIsImage {
if (!_webView)
return NO;
const std::string image = "image";
std::string MIMEType = self.webState->GetContentsMimeType();
return MIMEType.compare(0, image.length(), image) == 0;
}
- (void)didReceiveRedirectForNavigation:(web::NavigationContextImpl*)context
withURL:(const GURL&)URL {
context->SetUrl(URL);
web::NavigationItemImpl* item = web::GetItemWithUniqueID(
self.navigationManagerImpl, context->GetNavigationItemUniqueID());
// 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 (!web::wk_navigation_util::IsWKInternalUrl(URL)) {
item->SetVirtualURL(URL);
item->SetURL(URL);
}
// Redirects (3xx response code), must change POST requests to GETs.
item->SetPostData(nil);
item->ResetHttpRequestHeaders();
}
_lastTransferTimeInSeconds = CFAbsoluteTimeGetCurrent();
}
- (void)didFinishNavigation:(web::NavigationContextImpl*)context {
// This can be called at multiple times after the document has loaded. Do
// nothing if the document has already loaded.
if (_loadPhase == web::PAGE_LOADED)
return;
BOOL success = !context || !context->GetError();
[self loadCompleteWithSuccess:success forContext:context];
}
- (void)loadCompleteWithSuccess:(BOOL)loadSuccess
forContext:(web::NavigationContextImpl*)context {
// The webView may have been torn down (or replaced by a native view). Be
// safe and do nothing if that's happened.
if (_loadPhase != web::PAGE_LOADING)
return;
const GURL currentURL([self currentURL]);
_loadPhase = web::PAGE_LOADED;
[self optOutScrollsToTopForSubviews];
// Perform post-load-finished updates.
[self didFinishWithURL:currentURL loadSuccess:loadSuccess context:context];
// Execute the pending LoadCompleteActions.
for (ProceduralBlock action in _pendingLoadCompleteActions) {
action();
}
[_pendingLoadCompleteActions removeAllObjects];
}
- (void)didFinishWithURL:(const GURL&)currentURL
loadSuccess:(BOOL)loadSuccess
context:(nullable const web::NavigationContextImpl*)context {
DCHECK(_loadPhase == web::PAGE_LOADED);
// Rather than creating a new WKBackForwardListItem when loading WebUI pages,
// WKWebView will cache the WebUI HTML in the previous WKBackForwardListItem
// since it's loaded via |-loadHTML:forURL:| instead of an NSURLRequest. As a
// result, the WebUI's HTML and URL will be loaded when navigating to that
// WKBackForwardListItem, causing a mismatch between the visible content and
// the visible URL (WebUI page will be visible, but URL will be the previous
// page's URL). To prevent this potential URL spoofing vulnerability, reset
// the previous NavigationItem's WKBackForwardListItem to force loading via
// NSURLRequest.
if (_webUIManager) {
web::NavigationItem* lastNavigationItem =
self.sessionController.previousItem;
if (lastNavigationItem) {
web::WKBackForwardListItemHolder* holder =
web::WKBackForwardListItemHolder::FromNavigationItem(
lastNavigationItem);
DCHECK(holder);
holder->set_back_forward_list_item(nil);
}
}
[self restoreStateFromHistory];
// Placeholder and restore session URLs are implementation details so should
// not notify WebStateObservers. If |context| is nullptr, don't skip
// placeholder URLs because this may be the only opportunity to update
// |isLoading| for native view reload.
if (context && context->IsPlaceholderNavigation())
return;
if (context && IsRestoreSessionUrl(context->GetUrl()))
return;
if (IsRestoreSessionUrl(net::GURLWithNSURL(_webView.URL)))
return;
if (context && context->IsLoadingErrorPage())
return;
if (!loadSuccess) {
// WebStateObserver callbacks will be called for load failure after
// loading placeholder URL.
return;
}
_webStateImpl->SetIsLoading(false);
_webStateImpl->OnPageLoaded(currentURL, YES);
}
- (void)rendererInitiatedGoDelta:(int)delta
hasUserGesture:(BOOL)hasUserGesture {
if (_isBeingDestroyed)
return;
if (delta == 0) {
[self reloadWithRendererInitiatedNavigation:YES];
return;
}
if (self.navigationManagerImpl->CanGoToOffset(delta)) {
int index = self.navigationManagerImpl->GetIndexForOffset(delta);
self.navigationManagerImpl->GoToIndex(
index, web::NavigationInitiationType::RENDERER_INITIATED,
/*has_user_gesture=*/hasUserGesture);
}
}
- (void)addGestureRecognizerToWebView:(UIGestureRecognizer*)recognizer {
if ([_gestureRecognizers containsObject:recognizer])
return;
[_webView addGestureRecognizer:recognizer];
[_gestureRecognizers addObject:recognizer];
}
- (void)removeGestureRecognizerFromWebView:(UIGestureRecognizer*)recognizer {
if (![_gestureRecognizers containsObject:recognizer])
return;
[_webView removeGestureRecognizer:recognizer];
[_gestureRecognizers removeObject:recognizer];
}
- (CRWJSInjectionReceiver*)jsInjectionReceiver {
return _jsInjectionReceiver;
}
- (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.hasOpener && !_userInteractedWithWebController;
BOOL noNavigationItems = !(self.navigationManagerImpl->GetItemCount());
return rendererInitiatedWithoutInteraction || noNavigationItems;
}
- (web::UserAgentType)userAgentType {
web::NavigationItem* item = self.currentNavItem;
return item ? item->GetUserAgentType() : web::UserAgentType::MOBILE;
}
- (web::MojoFacade*)mojoFacade {
if (!_mojoFacade) {
service_manager::mojom::InterfaceProvider* interfaceProvider =
_webStateImpl->GetWebStateInterfaceProvider();
_mojoFacade.reset(new web::MojoFacade(interfaceProvider, self));
}
return _mojoFacade.get();
}
- (void)updateDesktopUserAgentForItem:(web::NavigationItem*)item
previousUserAgentType:(web::UserAgentType)userAgentType {
if (!item)
return;
web::UserAgentType itemUserAgentType = item->GetUserAgentType();
if (itemUserAgentType == web::UserAgentType::NONE)
return;
if (itemUserAgentType != userAgentType)
[self requirePageReconstruction];
}
- (void)discardNonCommittedItemsIfLastCommittedWasNotNativeView {
GURL lastCommittedURL = self.webState->GetLastCommittedURL();
BOOL previousItemWasLoadedInNativeView =
[self shouldLoadURLInNativeView:lastCommittedURL];
if (!previousItemWasLoadedInNativeView)
self.navigationManagerImpl->DiscardNonCommittedItems();
}
- (void)takeSnapshotWithRect:(CGRect)rect
completion:(void (^)(UIImage*))completion {
if (@available(iOS 11, *)) {
if (_webView) {
WKSnapshotConfiguration* configuration =
[[WKSnapshotConfiguration alloc] init];
configuration.rect = [_webView convertRect:rect fromView:self.view];
__weak CRWWebController* weakSelf = self;
[_webView
takeSnapshotWithConfiguration:configuration
completionHandler:^(UIImage* snapshot, NSError* error) {
// Pass nil to the completion block if there is an error
// or if the web view has been removed before the
// snapshot is finished. |snapshot| can sometimes be
// corrupt if it's sent due to the WKWebView's
// deallocation, so callbacks received after
// |-removeWebView| are ignored to prevent crashing.
if (error || !weakSelf.webView) {
if (error) {
DLOG(ERROR) << "WKWebView snapshot error: "
<< error.description;
}
completion(nil);
} else {
completion(snapshot);
}
}];
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil);
});
}
#pragma mark -
#pragma mark CRWWebControllerContainerViewDelegate
- (CRWWebViewProxyImpl*)contentViewProxyForContainerView:
(CRWWebControllerContainerView*)containerView {
return _webViewProxy;
}
- (UIEdgeInsets)nativeContentInsetsForContainerView:
(CRWWebControllerContainerView*)containerView {
return [self.nativeProvider nativeContentInsetForWebState:self.webState];
}
#pragma mark -
#pragma mark CRWJSInjectionEvaluator Methods
- (void)executeJavaScript:(NSString*)script
completionHandler:(web::JavaScriptResultBlock)completionHandler {
NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script];
web::ExecuteJavaScript(_webView, safeScript, completionHandler);
}
- (BOOL)scriptHasBeenInjectedForClass:(Class)injectionManagerClass {
return [_injectedScriptManagers containsObject:injectionManagerClass];
}
- (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass {
DCHECK(script.length);
// Script execution is an asynchronous operation which may pass sensitive
// data to the page. executeJavaScript:completionHandler makes sure that
// receiver page did not change by checking its window id.
// |[_webView executeJavaScript:completionHandler:]| is not used here because
// it does not check that page is the same.
[self executeJavaScript:script completionHandler:nil];
[_injectedScriptManagers addObject:JSInjectionManagerClass];
}
#pragma mark -
- (void)executeUserJavaScript:(NSString*)script
completionHandler:(web::JavaScriptResultBlock)completion {
// For security reasons, executing JavaScript on pages with app-specific URLs
// is not allowed, because those pages may have elevated privileges.
GURL lastCommittedURL = self.webState->GetLastCommittedURL();
if (web::GetWebClient()->IsAppSpecificURL(lastCommittedURL)) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
NSError* error = [[NSError alloc]
initWithDomain:web::kJSEvaluationErrorDomain
code:web::JS_EVALUATION_ERROR_CODE_NO_WEB_VIEW
userInfo:nil];
completion(nil, error);
});
}
return;
}
[self touched:YES];
[self executeJavaScript:script completionHandler:completion];
}
- (BOOL)respondToMessage:(base::DictionaryValue*)message
userIsInteracting:(BOOL)userIsInteracting
originURL:(const GURL&)originURL
isMainFrame:(BOOL)isMainFrame
senderFrame:(web::WebFrame*)senderFrame {
std::string command;
if (!message->GetString("command", &command)) {
DLOG(WARNING) << "JS message parameter not found: command";
return NO;
}
SEL handler = [self selectorToHandleJavaScriptCommand:command];
if (!handler) {
if (self.webStateImpl->OnScriptCommandReceived(command, *message, originURL,
userIsInteracting,
isMainFrame, senderFrame)) {
return YES;
}
// Message was either unexpected or not correctly handled.
// Page is reset as a precaution.
DLOG(WARNING) << "Unexpected message received: " << command;
return NO;
}
typedef BOOL (*HandlerType)(id, SEL, base::DictionaryValue*, NSDictionary*);
HandlerType handlerImplementation =
reinterpret_cast<HandlerType>([self methodForSelector:handler]);
DCHECK(handlerImplementation);
NSMutableDictionary* context =
[NSMutableDictionary dictionaryWithObject:@(userIsInteracting)
forKey:kUserIsInteractingKey];
NSURL* originNSURL = net::NSURLWithGURL(originURL);
if (originNSURL)
context[kOriginURLKey] = originNSURL;
context[kIsMainFrame] = @(isMainFrame);
return handlerImplementation(self, handler, message, context);
}
- (SEL)selectorToHandleJavaScriptCommand:(const std::string&)command {
static std::map<std::string, SEL>* handlers = nullptr;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
handlers = new std::map<std::string, SEL>();
(*handlers)["chrome.send"] = @selector(handleChromeSendMessage:context:);
(*handlers)["document.favicons"] =
@selector(handleDocumentFaviconsMessage:context:);
(*handlers)["window.error"] = @selector(handleWindowErrorMessage:context:);
(*handlers)["window.hashchange"] =
@selector(handleWindowHashChangeMessage:context:);
(*handlers)["window.history.back"] =
@selector(handleWindowHistoryBackMessage:context:);
(*handlers)["window.history.willChangeState"] =
@selector(handleWindowHistoryWillChangeStateMessage:context:);
(*handlers)["window.history.didPushState"] =
@selector(handleWindowHistoryDidPushStateMessage:context:);
(*handlers)["window.history.didReplaceState"] =
@selector(handleWindowHistoryDidReplaceStateMessage:context:);
(*handlers)["window.history.forward"] =
@selector(handleWindowHistoryForwardMessage:context:);
(*handlers)["window.history.go"] =
@selector(handleWindowHistoryGoMessage:context:);
(*handlers)["restoresession.error"] =
@selector(handleRestoreSessionErrorMessage:context:);
});
DCHECK(handlers);
auto iter = handlers->find(command);
return iter != handlers->end() ? iter->second : nullptr;
}
- (void)didReceiveScriptMessage:(WKScriptMessage*)message {
// Broken out into separate method to catch errors.
if (![self respondToWKScriptMessage:message]) {
DLOG(WARNING) << "Message from JS not handled due to invalid format";
}
}
- (NSString*)scriptByAddingWindowIDCheckForScript:(NSString*)script {
NSString* kTemplate = @"if (__gCrWeb['windowId'] === '%@') { %@; }";
return [NSString
stringWithFormat:kTemplate, [_windowIDJSManager windowID], script];
}
- (BOOL)respondToWKScriptMessage:(WKScriptMessage*)scriptMessage {
if (![scriptMessage.name isEqualToString:kScriptMessageName]) {
return NO;
}
std::unique_ptr<base::Value> messageAsValue =
web::ValueResultFromWKResult(scriptMessage.body);
base::DictionaryValue* message = nullptr;
if (!messageAsValue || !messageAsValue->GetAsDictionary(&message)) {
return NO;
}
web::WebFrame* senderFrame = nullptr;
std::string frameID;
if (message->GetString("crwFrameId", &frameID)) {
senderFrame = web::GetWebFrameWithId([self webState], frameID);
}
if (base::FeatureList::IsEnabled(web::features::kWebFrameMessaging)) {
// Message must be associated with a current frame.
if (!senderFrame) {
return NO;
}
} else {
GURL messageFrameOrigin = web::GURLOriginWithWKSecurityOrigin(
scriptMessage.frameInfo.securityOrigin);
if (!scriptMessage.frameInfo.mainFrame &&
messageFrameOrigin.GetOrigin() != _documentURL.GetOrigin()) {
// Messages from cross-origin iframes are not currently supported.
// |scriptMessage.frameInfo.securityOrigin| returns opener's origin for
// about:blank pages, so it is important to allow all messages coming from
// the main frame, even if messageFrameOrigin and _documentURL have
// different origins.
return NO;
}
std::string windowID;
// If windowID exists, it must match the ID from the main frame.
if (message->GetString("crwWindowId", &windowID)) {
if (base::SysNSStringToUTF8([_windowIDJSManager windowID]) != windowID) {
DLOG(WARNING)
<< "Message from JS ignored due to non-matching windowID: "
<< base::SysNSStringToUTF8([_windowIDJSManager windowID])
<< " != " << windowID;
return NO;
}
}
}
base::DictionaryValue* command = nullptr;
if (!message->GetDictionary("crwCommand", &command)) {
return NO;
}
return [self respondToMessage:command
userIsInteracting:[self userIsInteracting]
originURL:net::GURLWithNSURL([_webView URL])
isMainFrame:scriptMessage.frameInfo.mainFrame
senderFrame:senderFrame];
}
#pragma mark -
#pragma mark Web frames management
- (void)frameBecameAvailableWithMessage:(WKScriptMessage*)message {
// Validate all expected message components because any frame could falsify
// this message.
// TODO(crbug.com/881816): Create a WebFrame even if key is empty.
if (_isBeingDestroyed || ![message.body isKindOfClass:[NSDictionary class]] ||
![message.body[@"crwFrameId"] isKindOfClass:[NSString class]]) {
// WebController is being destroyed or the message is invalid.
return;
}
std::string frameID = base::SysNSStringToUTF8(message.body[@"crwFrameId"]);
web::WebFramesManagerImpl* framesManager =
web::WebFramesManagerImpl::FromWebState([self webState]);
if (!framesManager->GetFrameWithId(frameID)) {
GURL messageFrameOrigin =
web::GURLOriginWithWKSecurityOrigin(message.frameInfo.securityOrigin);
std::unique_ptr<crypto::SymmetricKey> frameKey;
if ([message.body[@"crwFrameKey"] isKindOfClass:[NSString class]] &&
[message.body[@"crwFrameKey"] length] > 0) {
std::string decodedFrameKeyString;
std::string encodedFrameKeyString =
base::SysNSStringToUTF8(message.body[@"crwFrameKey"]);
base::Base64Decode(encodedFrameKeyString, &decodedFrameKeyString);
frameKey = crypto::SymmetricKey::Import(
crypto::SymmetricKey::Algorithm::AES, decodedFrameKeyString);
}
auto newFrame = std::make_unique<web::WebFrameImpl>(
frameID, message.frameInfo.mainFrame, messageFrameOrigin,
self.webState);
if (frameKey) {
newFrame->SetEncryptionKey(std::move(frameKey));
}
NSNumber* lastSentMessageID =
message.body[@"crwFrameLastReceivedMessageId"];
if ([lastSentMessageID isKindOfClass:[NSNumber class]]) {
int nextMessageID = std::max(0, lastSentMessageID.intValue + 1);
newFrame->SetNextMessageId(nextMessageID);
}
framesManager->AddFrame(std::move(newFrame));
_webStateImpl->OnWebFrameAvailable(framesManager->GetFrameWithId(frameID));
}
}
- (void)frameBecameUnavailableWithMessage:(WKScriptMessage*)message {
if (_isBeingDestroyed || ![message.body isKindOfClass:[NSString class]]) {
// WebController is being destroyed or message is invalid.
return;
}
std::string frameID = base::SysNSStringToUTF8(message.body);
web::WebFramesManagerImpl* framesManager =
web::WebFramesManagerImpl::FromWebState([self webState]);
if (framesManager->GetFrameWithId(frameID)) {
_webStateImpl->OnWebFrameUnavailable(
framesManager->GetFrameWithId(frameID));
framesManager->RemoveFrameWithId(frameID);
}
}
- (void)removeAllWebFrames {
web::WebFramesManagerImpl* framesManager =
web::WebFramesManagerImpl::FromWebState([self webState]);
for (auto* frame : framesManager->GetAllWebFrames()) {
_webStateImpl->OnWebFrameUnavailable(frame);
}
framesManager->RemoveAllWebFrames();
}
#pragma mark -
#pragma mark JavaScript message handlers
- (BOOL)handleChromeSendMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
// Chrome message are only handled if sent from the main frame.
if (![context[kIsMainFrame] boolValue])
return NO;
if (_webStateImpl->HasWebUI()) {
const GURL currentURL([self currentURL]);
if (web::GetWebClient()->IsAppSpecificURL(currentURL)) {
std::string messageContent;
base::ListValue* arguments = nullptr;
if (!message->GetString("message", &messageContent)) {
DLOG(WARNING) << "JS message parameter not found: message";
return NO;
}
if (!message->GetList("arguments", &arguments)) {
DLOG(WARNING) << "JS message parameter not found: arguments";
return NO;
}
// WebFrame messaging is not supported in WebUI (as window.isSecureContext
// is false. Pass nullptr as sender_frame.
_webStateImpl->OnScriptCommandReceived(
messageContent, *message, currentURL, context[kUserIsInteractingKey],
[context[kIsMainFrame] boolValue], nullptr);
_webStateImpl->ProcessWebUIMessage(currentURL, messageContent,
*arguments);
return YES;
}
}
DLOG(WARNING)
<< "chrome.send message not handled because WebUI was not found.";
return NO;
}
- (BOOL)handleDocumentFaviconsMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
if (![context[kIsMainFrame] boolValue])
return NO;
std::vector<web::FaviconURL> URLs;
GURL originGURL;
id origin = context[kOriginURLKey];
if (origin) {
NSURL* originNSURL = base::mac::ObjCCastStrict<NSURL>(origin);
originGURL = net::GURLWithNSURL(originNSURL);
}
if (!web::ExtractFaviconURL(message, originGURL, &URLs))
return NO;
if (!URLs.empty())
_webStateImpl->OnFaviconUrlUpdated(URLs);
return YES;
}
- (BOOL)handleWindowErrorMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
std::string errorMessage;
if (!message->GetString("message", &errorMessage)) {
DLOG(WARNING) << "JS message parameter not found: message";
return NO;
}