blob: f6ab7efa8c4f993d56b8321e5d24cf7621c97827 [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 <memory>
#include <utility>
#include "base/containers/mru_cache.h"
#include "base/ios/block_types.h"
#include "base/ios/ios_util.h"
#import "base/ios/ns_error_util.h"
#include "base/ios/weak_nsobject.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/json/string_escape.h"
#include "base/logging.h"
#include "base/mac/bind_objc_block.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/foundation_util.h"
#include "base/mac/objc_property_releaser.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/histogram.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/values.h"
#include "components/url_formatter/url_formatter.h"
#import "ios/net/http_response_headers_util.h"
#import "ios/net/nsurlrequest_util.h"
#include "ios/public/provider/web/web_ui_ios.h"
#import "ios/web/crw_network_activity_indicator_manager.h"
#import "ios/web/history_state_util.h"
#include "ios/web/interstitials/web_interstitial_impl.h"
#import "ios/web/navigation/crw_session_certificate_policy_manager.h"
#import "ios/web/navigation/crw_session_controller.h"
#import "ios/web/navigation/crw_session_entry.h"
#import "ios/web/navigation/navigation_item_impl.h"
#import "ios/web/navigation/navigation_manager_impl.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/net/request_group_util.h"
#include "ios/web/public/browser_state.h"
#include "ios/web/public/cert_store.h"
#include "ios/web/public/favicon_url.h"
#include "ios/web/public/navigation_item.h"
#import "ios/web/public/navigation_manager.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"
#include "ios/web/public/user_metrics.h"
#include "ios/web/public/web_client.h"
#include "ios/web/public/web_kit_constants.h"
#import "ios/web/public/web_state/context_menu_params.h"
#include "ios/web/public/web_state/credential.h"
#import "ios/web/public/web_state/crw_web_controller_observer.h"
#import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state/js/credential_util.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"
#include "ios/web/public/web_state/page_display_state.h"
#import "ios/web/public/web_state/ui/crw_content_view.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"
#include "ios/web/public/web_state/url_verification_constants.h"
#include "ios/web/public/web_state/web_state.h"
#include "ios/web/web_state/blocked_popup_info.h"
#import "ios/web/web_state/crw_pass_kit_downloader.h"
#import "ios/web/web_state/crw_web_view_proxy_impl.h"
#import "ios/web/web_state/error_translation_util.h"
#import "ios/web/web_state/js/crw_js_plugin_placeholder_manager.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/page_viewport_state.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_wk_script_message_router.h"
#import "ios/web/web_state/ui/wk_back_forward_list_item_holder.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
#import "ios/web/web_state/web_controller_observer_bridge.h"
#include "ios/web/web_state/web_state_facade_delegate.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 "net/base/mac/url_conversions.h"
#include "net/base/net_errors.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"
using base::UserMetricsAction;
using web::NavigationManager;
using web::NavigationManagerImpl;
using web::WebState;
using web::WebStateImpl;
namespace web {
NSString* const kContainerViewID = @"Container View";
const char* kWindowNameSeparator = "#";
NSString* const kUserIsInteractingKey = @"userIsInteracting";
NSString* const kOriginURLKey = @"originURL";
NSString* const kLogJavaScript = @"LogJavascript";
struct NewWindowInfo {
GURL url;
base::scoped_nsobject<NSString> window_name;
web::ReferrerPolicy referrer_policy;
bool user_is_interacting;
NewWindowInfo(GURL url,
NSString* window_name,
web::ReferrerPolicy referrer_policy,
bool user_is_interacting);
NewWindowInfo::NewWindowInfo(GURL target_url,
NSString* target_window_name,
web::ReferrerPolicy target_referrer_policy,
bool target_user_is_interacting)
: url(target_url),
window_name([target_window_name copy]),
user_is_interacting(target_user_is_interacting) {
NewWindowInfo::~NewWindowInfo() {
// 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 occured, measured in seconds since Jan 1 2001.
CFAbsoluteTime time;
// Values of the UMA |Web.URLVerificationFailure| histogram.
enum WebViewDocumentType {
// Generic contents (e.g. PDF documents).
// HTML contents.
// Unknown contents.
} // namespace web
namespace {
// Key of UMA IOSFix.ViewportZoomBugCount histogram.
const char kUMAViewportZoomBugCount[] = "Renderer.ViewportZoomBugCount";
// A tag for the web view, so that tests can identify it. This is used instead
// of exposing a getter (and deliberately not exposed in the header) to make it
// *very* clear that this is a hack which should only be used as a last resort.
const NSUInteger kWebViewTag = 0x3eb71e3;
// URL scheme for messages sent from javascript for asynchronous processing.
NSString* const kScriptMessageName = @"crwebinvoke";
// URL scheme for messages sent from javascript for immediate processing.
NSString* const kScriptImmediateName = @"crwebinvokeimmediate";
// Constants for storing the source of NSErrors received by WKWebViews:
// - Errors received by |-webView:didFailProvisionalNavigation:withError:| are
// recorded using WKWebViewErrorSource::PROVISIONAL_LOAD. These should be
// cancelled.
// - Errors received by |-webView:didFailNavigation:withError:| are recorded
// using WKWebViewsource::NAVIGATION. These errors should not be cancelled,
// as the WKWebView will automatically retry the load.
NSString* const kWKWebViewErrorSourceKey = @"ErrorSource";
typedef enum { NONE = 0, PROVISIONAL_LOAD, NAVIGATION } WKWebViewErrorSource;
// 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>
// 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;
// States for external URL requests. This enum is used in UMA and
// entries should not be re-ordered or deleted.
enum ExternalURLRequestStatus {
// Cancels touch events for the given gesture recognizer.
void CancelTouches(UIGestureRecognizer* gesture_recognizer) {
if (gesture_recognizer.enabled) {
gesture_recognizer.enabled = NO;
gesture_recognizer.enabled = YES;
// Utility function for getting the source of NSErrors received by WKWebViews.
WKWebViewErrorSource WKWebViewErrorSourceFromError(NSError* error) {
return static_cast<WKWebViewErrorSource>(
[error.userInfo[kWKWebViewErrorSourceKey] integerValue]);
// Utility function for converting the WKWebViewErrorSource to the NSError
// received by WKWebViews.
NSError* WKWebViewErrorWithSource(NSError* error, WKWebViewErrorSource source) {
base::scoped_nsobject<NSMutableDictionary> userInfo(
[error.userInfo mutableCopy]);
[userInfo setObject:@(source) forKey:kWKWebViewErrorSourceKey];
[NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
} // 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;
@implementation CRWWebControllerPendingNavigationInfo
@synthesize referrer = _referrer;
@synthesize MIMEType = _MIMEType;
@synthesize navigationType = _navigationType;
@synthesize HTTPMethod = _HTTPMethod;
@synthesize cancelled = _cancelled;
- (instancetype)init {
if ((self = [super init])) {
self, [CRWWebControllerPendingNavigationInfo class]);
_navigationType = WKNavigationTypeOther;
return self;
@interface CRWWebController ()<CRWNativeContentDelegate,
WKUIDelegate> {
base::WeakNSProtocol<id<CRWWebDelegate>> _delegate;
base::WeakNSProtocol<id<CRWWebUserInterfaceDelegate>> _UIDelegate;
base::WeakNSProtocol<id<CRWNativeContentProvider>> _nativeProvider;
base::WeakNSProtocol<id<CRWSwipeRecognizerProvider>> _swipeRecognizerProvider;
// The WKWebView managed by this instance.
base::scoped_nsobject<WKWebView> _webView;
// The CRWWebViewProxy is the wrapper to give components access to the
// web view in a controlled and limited way.
base::scoped_nsobject<CRWWebViewProxyImpl> _webViewProxy;
// 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.
base::scoped_nsobject<CRWWebControllerContainerView> _containerView;
// If |_contentView| contains a native view rather than a web view, this
// is its controller. If it's a web view, this is nil.
base::scoped_nsprotocol<id<CRWNativeContent>> _nativeController;
BOOL _isHalted; // YES if halted. Halting happens prior to destruction.
BOOL _isBeingDestroyed; // YES if in the process of closing.
// All CRWWebControllerObservers attached to the CRWWebController. A
// specially-constructed set is used that does not retain its elements.
base::scoped_nsobject<NSMutableSet> _observers;
// Each observer in |_observers| is associated with a
// WebControllerObserverBridge in order to listen from WebState callbacks.
// TODO(droger): Remove |_observerBridges| when all CRWWebControllerObservers
// are converted to WebStateObservers.
ScopedVector<web::WebControllerObserverBridge> _observerBridges;
// |windowId| that is saved when a page changes. Used to detect refreshes.
base::scoped_nsobject<NSString> _lastSeenWindowID;
// 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( Remove this in favor of just updating the
// navigation manager and treating that as authoritative.
GURL _documentURL;
// Last URL change reported to webWill/DidStartLoadingURL. Used to detect page
// location changes (client redirects) in practice.
GURL _lastRegisteredRequestURL;
// Last URL change reported to webDidStartLoadingURL. Used to detect page
// location changes in practice.
GURL _URLOnStartLoading;
// 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.
base::scoped_nsobject<NSMutableArray> _pendingLoadCompleteActions;
// UIGestureRecognizers to add to the web view.
base::scoped_nsobject<NSMutableArray> _gestureRecognizers;
// Toolbars to add to the web view.
base::scoped_nsobject<NSMutableArray> _webViewToolbars;
// Flag to say if browsing is enabled.
BOOL _webUsageEnabled;
// Content view was reset due to low memory. Use the placeholder overlay on
// next creation.
BOOL _usePlaceholderOverlay;
// The next time the view is requested, reload the page (using the placeholder
// overlay until it's loaded).
BOOL _requireReloadOnDisplay;
// Overlay view used instead of webView.
base::scoped_nsobject<UIImageView> _placeholderOverlayView;
// The touch tracking recognizer allowing us to decide if a navigation is
// started by the user.
base::scoped_nsobject<CRWTouchTrackingRecognizer> _touchTrackingRecognizer;
// Long press recognizer that allows showing context menus.
base::scoped_nsobject<UILongPressGestureRecognizer> _contextMenuRecognizer;
// DOM element information for the point where the user made the last touch.
// Can be null if has not been calculated yet. Precalculation is necessary
// because retreiving DOM element relies on async API so element info can not
// be built on demand. May contain the following keys: "href", "src", "title",
// "referrerPolicy". All values are strings. Used for showing context menus.
std::unique_ptr<base::DictionaryValue> _DOMElementForLastTouch;
// Whether a click is in progress.
BOOL _clickInProgress;
// Data on the recorded last user interaction.
std::unique_ptr<web::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;
// Show overlay view, don't reload web page.
BOOL _overlayPreviewMode;
// If |YES|, calls |setShouldSuppressDialogs:YES| when window id is injected
// into the web view.
BOOL _shouldSuppressDialogsOnWindowIDInjection;
// The URL of an expected future recreation of the |webView|. Valid
// only if the web view was discarded for non-user-visible reasons, such that
// if the next load request is for that URL, it should be treated as a
// reconstruction that should use cache aggressively.
GURL _expectedReconstructionURL;
// 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;
// YES if the web process backing _wkWebView is believed to currently be dead.
BOOL _webProcessIsDead;
std::unique_ptr<web::NewWindowInfo> _externalRequest;
// Object for loading POST requests with body.
base::scoped_nsobject<CRWJSPOSTRequestLoader> _POSTRequestLoader;
// WebStateImpl instance associated with this CRWWebController, web controller
// does not own this pointer.
WebStateImpl* _webStateImpl;
// A set of URLs opened in external applications; stored so that errors
// from the web view can be identified as resulting from these events.
base::scoped_nsobject<NSMutableSet> _openedApplicationURL;
// 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.
base::scoped_nsobject<NSMutableSet> _injectedScriptManagers;
// Script manager for setting the windowID.
base::scoped_nsobject<CRWJSWindowIdManager> _windowIDJSManager;
// The receiver of JavaScripts.
base::scoped_nsobject<CRWJSInjectionReceiver> _jsInjectionReceiver;
// Handles downloading PassKit data for WKWebView. Lazy initialized.
base::scoped_nsobject<CRWPassKitDownloader> _passKitDownloader;
// Referrer for the current page; does not include the fragment.
base::scoped_nsobject<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|.
// The WKNavigation for the most recent load request.
base::scoped_nsobject<WKNavigation> _latestWKNavigation;
// The WKNavigation captured when |stopLoading| was called. Used for reporting
// WebController.EmptyNavigationManagerCausedByStopLoading UMA metric which
// helps with diagnosing a navigation related crash (
base::WeakNSObject<WKNavigation> _stoppedWKNavigation;
// CRWWebUIManager object for loading WebUI pages.
base::scoped_nsobject<CRWWebUIManager> _webUIManager;
// Updates SSLStatus for current navigation item.
base::scoped_nsobject<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.
// 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(nonatomic, readonly) WKWebView* webView;
// The scroll view of |webView|.
@property(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(nonatomic, readwrite) id<CRWNativeContent> nativeController;
// Returns NavigationManager's session controller.
@property(nonatomic, readonly) CRWSessionController* sessionController;
// Activity indicator group ID for this web controller.
@property(nonatomic, readonly) NSString* activityIndicatorGroupID;
// Identifier used for storing and retrieving certificates.
@property(nonatomic, readonly) int certGroupID;
// 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(nonatomic, readonly) NSDictionary* WKWebViewObservers;
// Downloader for PassKit files. Lazy initialized.
@property(nonatomic, readonly) CRWPassKitDownloader* passKitDownloader;
// 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;
// Returns whether the desktop user agent should be used when setting the user
// agent.
@property(nonatomic, readonly) BOOL useDesktopUserAgent;
// Removes the container view from the hierarchy and resets the ivar.
- (void)resetContainerView;
// 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.
// TODO(stuartmorgan): The code conflates URL changes and document object
// changes; the two need to be separated and handled differently.
- (void)webPageChanged;
// 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, and informs the delegate.
- (void)didStartLoadingURL:(const GURL&)URL updateHistory:(BOOL)updateHistory;
// Returns YES if the URL looks like it is one CRWWebController can show.
+ (BOOL)webControllerCanShow:(const GURL&)url;
// Clears the currently-displayed transient content view.
- (void)clearTransientContentView;
// Returns a lazily created CRWTouchTrackingRecognizer.
- (CRWTouchTrackingRecognizer*)touchTrackingRecognizer;
// Adds an activity indicator tasks for this web controller.
- (void)addActivityIndicatorTask;
// Clears all activity indicator tasks for this web controller.
- (void)clearActivityIndicatorTasks;
// Shows placeholder overlay.
- (void)addPlaceholderOverlay;
// Removes placeholder overlay.
- (void)removePlaceholderOverlay;
// Returns the current entry from the underlying session controller.
// TODO(stuartmorgan): Audit all calls to these methods; these are just wrappers
// around the same logic as GetActiveEntry, so should probably not be used for
// the same reason that GetActiveEntry is deprecated. (E.g., page operations
// should generally be dealing with the last commited entry, not a pending
// entry).
- (CRWSessionEntry*)currentSessionEntry;
// Returns the navigation item for the current page.
- (web::NavigationItem*)currentNavItem;
// Returns the URL that the navigation system believes should be currently
// active.
// TODO(stuartmorgan):Remove this in favor of more specific getters.
- (const GURL&)currentNavigationURL;
// Returns the current transition type.
- (ui::PageTransition)currentTransition;
// Returns the referrer for current navigation item. May be empty.
- (web::Referrer)currentSessionEntryReferrer;
// The HTTP headers associated with the current navigation item. These are nil
// unless the request was a POST.
- (NSDictionary*)currentHTTPHeaders;
// 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*)createWebViewWithConfiguration:(WKWebViewConfiguration*)config;
// Sets the value of the webView property, and performs its basic setup.
- (void)setWebView:(WKWebView*)webView;
// Removes webView, optionally tracking the URL of the evicted
// page for later cache-based reconstruction.
- (void)removeWebViewAllowingCachedReconstruction:(BOOL)allowCache;
// Called when web view process has been terminated.
- (void)webViewWebProcessDidCrash;
// Returns the WKWebViewConfigurationProvider associated with the web
// controller's BrowserState.
- (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider;
// Returns the type of document object loaded in the web view.
- (web::WebViewDocumentType)webViewDocumentType;
// Converts MIME type string to WebViewDocumentType.
- (web::WebViewDocumentType)documentTypeFromMIMEType:(NSString*)MIMEType;
// Extracts Referer value from WKNavigationAction request header.
- (NSString*)refererFromNavigationAction:(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 the request into the |webView|.
- (void)loadRequest:(NSMutableURLRequest*)request;
// 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
- (void)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:
// 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:
// 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;
// Loads the current nativeController in a native view. If a web view is
// present, removes it and swaps in the native view in its place.
- (void)loadNativeViewWithSuccess:(BOOL)loadSuccess;
// YES if the navigation to |url| should be treated as a reload.
- (BOOL)shouldReload:(const GURL&)destinationURL
// Internal implementation of reload. Reloads without notifying the delegate.
// Most callers should use -reload instead.
- (void)reloadInternal;
// 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;
// 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;
// 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;
// Called after URL is finished loading and _loadPhase is set to PAGE_LOADED.
- (void)didFinishWithURL:(const GURL&)currentURL loadSuccess:(BOOL)loadSuccess;
// Informs the native controller if web usage is allowed or not.
- (void)setNativeControllerWebUsageEnabled:(BOOL)webUsageEnabled;
// Evaluates the supplied JavaScript in the web view. Calls |handler| with
// results of the evaluation (which may be nil if the implementing object has no
// way to run the evaluation or the evaluation returns a nil value) or an
// NSError if there is an error. The |handler| can be nil.
- (void)evaluateJavaScript:(NSString*)script
(void (^)(std::unique_ptr<base::Value>, NSError*))handler;
// 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;
// Registers load request with empty referrer and link or client redirect
// transition based on user interaction state.
- (void)registerLoadRequest:(const GURL&)URL;
// 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.
- (void)registerLoadRequest:(const GURL&)URL
referrer:(const web::Referrer&)referrer
// 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
// Injects JavaScript into the web view to update the URL to |URL|, to set
// window.history.state to |stateObject|, and to trigger a popstate() event.
// Upon the scripts completion, resets |urlOnStartLoading_| and
// |_lastRegisteredRequestURL| to |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 |-pageChanged| calls.
- (void)setPushedOrReplacedURL:(const GURL&)URL
- (BOOL)isLoaded;
// 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
// 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;
// Asynchronously fetches full width of the rendered web page.
- (void)fetchWebPageWidthWithCompletionHandler:(void (^)(CGFloat))handler;
// Asynchronously fetches information about DOM element for the given point (in
// UIView coordinates). |handler| can not be nil. See |_DOMElementForLastTouch|
// for element format description.
- (void)fetchDOMElementAtPoint:(CGPoint)point
(void (^)(std::unique_ptr<base::DictionaryValue>))handler;
// Extracts context menu information from the given DOM element.
- (web::ContextMenuParams)contextMenuParamsForElement:
// Sets the value of |_DOMElementForLastTouch|.
- (void)setDOMElementForLastTouch:
// Called when the window has determined there was a long-press and context menu
// must be shown.
- (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer;
// Cancels all touch events in the web view (long presses, tapping, scrolling).
- (void)cancelAllTouches;
// Returns the referrer for the current page.
- (web::Referrer)currentReferrer;
// Asynchronously returns the referrer policy for the current page.
- (void)queryPageReferrerPolicy:(void (^)(NSString*))responseHandler;
// Adds a new CRWSessionEntry 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 CRWSessionEntry/URL.
// TODO(stuartmorgan): Move the pushState/replaceState logic into
// NavigationManager.
- (void)pushStateWithPageURL:(const GURL&)pageURL
// Assigns the given URL and state object to the current CRWSessionEntry.
- (void)replaceStateWithPageURL:(const GURL&)pageUrl
// Sets _documentURL to newURL, and updates any relevant state information.
- (void)setDocumentURL:(const GURL&)newURL;
// 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 whether the given navigation is triggered by a user link click.
- (BOOL)isLinkNavigation:(WKNavigationType)navigationType;
// Inject windowID if not yet injected.
- (void)injectWindowID;
// Creates a new opened by DOM window and returns its autoreleased web
// controller.
- (CRWWebController*)createChildWebController;
// Returns YES if the given WKBackForwardListItem is valid to use for
// navigation.
- (BOOL)isBackForwardListItemValid:(WKBackForwardListItem*)item;
// Compares the two URLs being navigated between during a history navigation to
// determine if a # needs to be appended to the URL of |toItem| to trigger a
// hashchange event. If so, also saves the modified URL into |toItem|.
- (GURL)URLForHistoryNavigationFromItem:(web::NavigationItem*)fromItem
// 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;
// Tears down the old native controller, and then replaces it with the new one.
- (void)setNativeController:(id<CRWNativeContent>)nativeController;
// Returns whether |url| should be opened.
- (BOOL)shouldOpenURL:(const GURL&)url
mainDocumentURL:(const GURL&)mainDocumentURL
// Called when |URL| needs to be opened in a matching native app.
// Returns YES if the url was succesfully opened in the native app.
- (BOOL)urlTriggersNativeAppLaunch:(const GURL&)URL
sourceURL:(const GURL&)sourceURL
// Called when a JavaScript dialog, HTTP authentication dialog or
// call has been suppressed.
- (void)didSuppressDialog;
// Convenience method to inform CWRWebDelegate about a blocked popup.
- (void)didBlockPopupWithURL:(GURL)popupURL sourceURL:(GURL)sourceURL;
// Informs CWRWebDelegate that CRWWebController has detected and blocked a
// popup.
- (void)didBlockPopupWithURL:(GURL)popupURL
referrerPolicy:(const std::string&)referrerPolicyString;
// Returns YES if the navigation action is associated with a main frame request.
- (BOOL)isMainFrameNavigationAction:(WKNavigationAction*)action;
// Returns whether external URL navigation action should be opened.
- (BOOL)shouldOpenExternalURLForNavigationAction:(WKNavigationAction*)action;
// Called when a page updates its history stack using pushState or replaceState.
- (void)didUpdateHistoryStateWithPageURL:(const GURL&)url;
// Updates SSL status for the current navigation item based on the information
// provided by web view.
- (void)updateSSLStatusForCurrentNavigationItem;
// Called when SSL status has been updated for the current navigation item.
- (void)didUpdateSSLStatusForCurrentNavigationItem;
// Called when a load ends in an SSL error and certificate chain.
- (void)handleSSLCertError:(NSError*)error;
// Returns YES if the popup should be blocked, NO otherwise.
- (BOOL)shouldBlockPopupWithURL:(const GURL&)popupURL
sourceURL:(const GURL&)sourceURL;
// Tries to open a popup with the given new window information.
- (void)openPopupWithInfo:(const web::NewWindowInfo&)windowInfo;
// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to
// reply with NSURLSessionAuthChallengeDisposition and credentials.
- (void)processAuthChallenge:(NSURLAuthenticationChallenge*)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
// with NSURLSessionAuthChallengeDisposition and credentials.
- (void)handleHTTPAuthForChallenge:(NSURLAuthenticationChallenge*)challenge
(void (^)(NSURLSessionAuthChallengeDisposition,
// Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply
// with NSURLSessionAuthChallengeDisposition and credentials.
+ (void)processHTTPAuthForUser:(NSString*)user
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
// 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 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;
// 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 YES if there is currently a requested but uncommitted load for
// |targetURL|.
- (BOOL)isLoadRequestPendingForURL:(const GURL&)targetURL;
// 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;
// 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 'addPluginPlaceholders' message.
- (BOOL)handleAddPluginPlaceholdersMessage:(base::DictionaryValue*)message
// Handles 'chrome.send' message.
- (BOOL)handleChromeSendMessage:(base::DictionaryValue*)message
// Handles 'console' message.
- (BOOL)handleConsoleMessage:(base::DictionaryValue*)message
// Handles 'geolocationDialog.suppressed' message.
- (BOOL)handleGeolocationDialogSuppressedMessage:(base::DictionaryValue*)message
// Handles 'document.favicons' message.
- (BOOL)handleDocumentFaviconsMessage:(base::DictionaryValue*)message
// Handles 'document.submit' message.
- (BOOL)handleDocumentSubmitMessage:(base::DictionaryValue*)message
// Handles 'externalRequest' message.
- (BOOL)handleExternalRequestMessage:(base::DictionaryValue*)message
// Handles 'form.activity' message.
- (BOOL)handleFormActivityMessage:(base::DictionaryValue*)message
// Handles 'navigator.credentials.request' message.
- (BOOL)handleCredentialsRequestedMessage:(base::DictionaryValue*)message
// Handles 'navigator.credentials.notifySignedIn' message.
- (BOOL)handleSignedInMessage:(base::DictionaryValue*)message
// Handles 'navigator.credentials.notifySignedOut' message.
- (BOOL)handleSignedOutMessage:(base::DictionaryValue*)message
// Handles 'navigator.credentials.notifyFailedSignIn' message.
- (BOOL)handleSignInFailedMessage:(base::DictionaryValue*)message
// Handles 'resetExternalRequest' message.
- (BOOL)handleResetExternalRequestMessage:(base::DictionaryValue*)message
// Handles 'window.error' message.
- (BOOL)handleWindowErrorMessage:(base::DictionaryValue*)message
// Handles 'window.hashchange' message.
- (BOOL)handleWindowHashChangeMessage:(base::DictionaryValue*)message
// Handles 'window.history.back' message.
- (BOOL)handleWindowHistoryBackMessage:(base::DictionaryValue*)message
// Handles 'window.history.forward' message.
- (BOOL)handleWindowHistoryForwardMessage:(base::DictionaryValue*)message
// Handles 'window.history.go' message.
- (BOOL)handleWindowHistoryGoMessage:(base::DictionaryValue*)message
// Handles 'window.history.willChangeState' message.
- (BOOL)handleWindowHistoryWillChangeStateMessage:
// Handles 'window.history.didPushState' message.
- (BOOL)handleWindowHistoryDidPushStateMessage:(base::DictionaryValue*)message
// Handles 'window.history.didReplaceState' message.
- (BOOL)handleWindowHistoryDidReplaceStateMessage:
// Returns YES if the given |action| should be allowed to continue.
// If this returns NO, the load should be cancelled.
- (BOOL)shouldAllowLoadWithNavigationAction:(WKNavigationAction*)action;
// Called when a load ends in an error.
// TODO(stuartmorgan): Figure out if there's actually enough shared logic that
// this makes sense. At the very least remove inMainFrame since that only makes
// sense for UIWebView.
- (void)handleLoadError:(NSError*)error inMainFrame:(BOOL)inMainFrame;
// Handles cancelled load in WKWebView (error with NSURLErrorCancelled code).
- (void)handleCancelledError:(NSError*)error;
// Used to decide whether a load that generates errors with the
// NSURLErrorCancelled code should be cancelled.
- (BOOL)shouldCancelLoadForCancelledError:(NSError*)error;
// Sets up WebUI for URL.
- (void)createWebUIForURL:(const GURL&)URL;
// Clears WebUI, if one exists.
- (void)clearWebUI;
namespace {
NSString* const kReferrerHeaderName = @"Referer"; // [sic]
// The long press detection duration must be shorter than the UIWebView's
// long click gesture recognizer's minimum duration. That is 0.55s.
// If our detection duration is shorter, our gesture recognizer will fire
// first, and if it fails the long click gesture (processed simultaneously)
// still is able to complete.
const NSTimeInterval kLongPressDurationSeconds = 0.55 - 0.1;
const CGFloat kLongPressMoveDeltaPixels = 10.0;
// 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));
// Leave snapshot overlay up unless page loads.
const NSTimeInterval kSnapshotOverlayDelay = 1.5;
// Transition to fade snapshot overlay.
const NSTimeInterval kSnapshotOverlayTransition = 0.5;
} // namespace
@implementation CRWWebController
@synthesize webUsageEnabled = _webUsageEnabled;
@synthesize usePlaceholderOverlay = _usePlaceholderOverlay;
@synthesize loadPhase = _loadPhase;
@synthesize shouldSuppressDialogs = _shouldSuppressDialogs;
- (instancetype)initWithWebState:(WebStateImpl*)webState {
self = [super init];
if (self) {
_webStateImpl = webState;
// 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);
[[CRWJSInjectionReceiver alloc] initWithEvaluator:self]);
instanceOfClass:[CRWJSWindowIdManager class]] retain]);
[[CRWWebViewProxyImpl alloc] initWithWebController:self]);
[[_webViewProxy scrollViewProxy] addObserver:self];
_gestureRecognizers.reset([[NSMutableArray alloc] init]);
_webViewToolbars.reset([[NSMutableArray alloc] init]);
_pendingLoadCompleteActions.reset([[NSMutableArray alloc] init]);
web::BrowserState* browserState = _webStateImpl->GetBrowserState();
_certVerificationController.reset([[CRWCertVerificationController alloc]
new CertVerificationErrorsCacheType(kMaxCertErrorsCount));
[[NSNotificationCenter defaultCenter]
return self;
- (id<CRWNativeContentProvider>)nativeProvider {
return _nativeProvider.get();
- (void)setNativeProvider:(id<CRWNativeContentProvider>)nativeProvider {
- (id<CRWSwipeRecognizerProvider>)swipeRecognizerProvider {
return _swipeRecognizerProvider.get();
- (void)setSwipeRecognizerProvider:
(id<CRWSwipeRecognizerProvider>)swipeRecognizerProvider {
- (WebState*)webState {
return _webStateImpl;
- (WebStateImpl*)webStateImpl {
return _webStateImpl;
- (void)clearTransientContentView {
// Early return if there is no transient content view.
if (![_containerView transientContentView])
// Remove the transient content view from the hierarchy.
[_containerView clearTransientContentView];
// Notify the WebState so it can perform any required state cleanup.
if (_webStateImpl)
- (void)showTransientContentView:(CRWContentView*)contentView {
// TODO( Reenable DCHECK when |CRWWebControllerContainerView|
// is restructured so that subviews are not added during |layoutSubviews|.
// DCHECK([contentView.scrollView isDescendantOfView:contentView]);
[_containerView displayTransientContent:contentView];
- (id<CRWWebDelegate>)delegate {
return _delegate.get();
- (void)setDelegate:(id<CRWWebDelegate>)delegate {
if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) {
if ([_delegate respondsToSelector:@selector(webController:titleDidChange:)])
[self.nativeController setDelegate:self];
[self.nativeController setDelegate:nil];
- (id<CRWWebUserInterfaceDelegate>)UIDelegate {
return _UIDelegate.get();
- (void)setUIDelegate:(id<CRWWebUserInterfaceDelegate>)UIDelegate {
- (void)dealloc {
DCHECK([NSThread isMainThread]);
DCHECK(_isBeingDestroyed); // 'close' must have been called already.
_touchTrackingRecognizer.get().touchTrackingDelegate = nil;
[[_webViewProxy scrollViewProxy] removeObserver:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
- (BOOL)runUnloadListenerBeforeClosing {
// There's not much that can be done since there's limited access to WebKit.
// Always return that it's ok to close immediately.
return YES;
- (void)dismissKeyboard {
[_webView endEditing:YES];
if ([self.nativeController respondsToSelector:@selector(dismissKeyboard)])
[self.nativeController dismissKeyboard];
- (id<CRWNativeContent>)nativeController {
return [_containerView nativeController];
- (void)setNativeController:(id<CRWNativeContent>)nativeController {
// Check for pointer equality.
if (self.nativeController == nativeController)
// Unset the delegate on the previous instance.
if ([self.nativeController respondsToSelector:@selector(setDelegate:)])
[self.nativeController setDelegate:nil];
[_containerView displayNativeContent:nativeController];
[self setNativeControllerWebUsageEnabled:_webUsageEnabled];
- (NSString*)activityIndicatorGroupID {
return [NSString
- (int)certGroupID {
return self.webState->GetCertGroupId();
- (NSDictionary*)WKWebViewObservers {
return @{
@"certificateChain" : @"webViewSecurityFeaturesDidChange",
@"estimatedProgress" : @"webViewEstimatedProgressDidChange",
@"hasOnlySecureContent" : @"webViewSecurityFeaturesDidChange",
@"loading" : @"webViewLoadingStateDidChange",
@"title" : @"webViewTitleDidChange",
@"URL" : @"webViewURLDidChange",
// NativeControllerDelegate method, called to inform that title has changed.
- (void)nativeContent:(id)content titleDidChange:(NSString*)title {
// Responsiveness to delegate method was checked in setDelegate:.
[_delegate webController:self titleDidChange:title];
- (void)setNativeControllerWebUsageEnabled:(BOOL)webUsageEnabled {
if ([self.nativeController
respondsToSelector:@selector(setWebUsageEnabled:)]) {
[self.nativeController setWebUsageEnabled:webUsageEnabled];
- (void)setWebUsageEnabled:(BOOL)enabled {
if (_webUsageEnabled == enabled)
_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 {
[self clearTransientContentView];
[self removeWebViewAllowingCachedReconstruction:YES];
_touchTrackingRecognizer.get().touchTrackingDelegate = nil;
[self resetContainerView];
- (void)requirePageReconstruction {
[self removeWebViewAllowingCachedReconstruction:NO];
- (void)requirePageReload {
_requireReloadOnDisplay = YES;
- (void)resetContainerView {
[_containerView removeFromSuperview];
- (void)handleLowMemory {
[self removeWebViewAllowingCachedReconstruction:YES];
_touchTrackingRecognizer.get().touchTrackingDelegate = nil;
[self resetContainerView];
_usePlaceholderOverlay = YES;
- (void)reinitializeWebViewAndReload:(BOOL)reload {
if (_webView) {
[self removeWebViewAllowingCachedReconstruction:NO];
if (reload) {
[self loadCurrentURLInWebView];
} else {
// Clear the space for the web view to lazy load when needed.
_usePlaceholderOverlay = YES;
_touchTrackingRecognizer.get().touchTrackingDelegate = nil;
[self resetContainerView];
- (BOOL)isViewAlive {
return !_webProcessIsDead && [_containerView isViewAlive];
- (BOOL)contentIsHTML {
return [self webViewDocumentType] == web::WEB_VIEW_DOCUMENT_TYPE_HTML;
// Stop doing stuff, especially network stuff. Close the request tracker.
- (void)terminateNetworkActivity {
_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 {
[_certVerificationController shutDown];
self.nativeProvider = nil;
self.swipeRecognizerProvider = nil;
if ([self.nativeController respondsToSelector:@selector(close)])
[self.nativeController close];
base::scoped_nsobject<NSSet> observers([_observers copy]);
for (id it in observers.get()) {
if ([it respondsToSelector:@selector(webControllerWillClose:)])
[it webControllerWillClose:self];
if (!_isHalted) {
[self terminateNetworkActivity];
DCHECK(!_delegate); // Delegate should reset its association before closing.
// Mark the destruction sequence has started, in case someone else holds a
// strong reference and tries to continue using the tab.
_isBeingDestroyed = YES;
// Remove the web view now. Otherwise, delegate callbacks occur.
[self removeWebViewAllowingCachedReconstruction:NO];
_webStateImpl = nullptr;
- (void)checkLinkPresenceUnderGesture:(UIGestureRecognizer*)gestureRecognizer
completionHandler:(void (^)(BOOL))completionHandler {
CGPoint webViewPoint = [gestureRecognizer locationInView:_webView];
base::WeakNSObject<CRWWebController> weakSelf(self);
completionHandler:^(std::unique_ptr<base::DictionaryValue> element) {
std::string link;
BOOL hasLink =
element && element->GetString("href", &link) && link.size();
- (void)setDOMElementForLastTouch:
(std::unique_ptr<base::DictionaryValue>)element {
_DOMElementForLastTouch = std::move(element);
- (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer {
// We don't want ongoing notification that the long press is held.
if ([gestureRecognizer state] != UIGestureRecognizerStateBegan)
if (!_DOMElementForLastTouch || _DOMElementForLastTouch->empty())
web::ContextMenuParams params =
[self contextMenuParamsForElement:_DOMElementForLastTouch.get()];
params.view.reset([_webView retain]);
params.location = [gestureRecognizer locationInView:_webView];
if (self.webStateImpl->HandleContextMenu(params)) {
// Cancelling all touches has the intended side effect of suppressing the
// system's context menu.
[self cancelAllTouches];
- (void)cancelAllTouches {
// Disable web view scrolling.
// All user gestures are handled by a subview of web view scroll view
// (WKContentView).
for (UIView* subview in self.webScrollView.subviews) {
for (UIGestureRecognizer* recognizer in subview.gestureRecognizers) {
// Just disabling/enabling the gesture recognizers is not enough to suppress
// the click handlers on the JS side. This JS performs the function of
// suppressing these handlers on the JS side.
NSString* suppressNextClick = @"__gCrWeb.suppressNextClick()";
[self evaluateJavaScript:suppressNextClick stringResultHandler:nil];
// TODO(shreyasv): This code is shared with SnapshotManager. Remove this and add
// it as part of WebDelegate delegate API such that a default image is returned
// immediately.
+ (UIImage*)defaultSnapshotImage {
static UIImage* defaultImage = nil;
if (!defaultImage) {
CGRect frame = CGRectMake(0, 0, 2, 2);
[[UIColor whiteColor] setFill];
CGContextFillRect(UIGraphicsGetCurrentContext(), frame);
UIImage* result = UIGraphicsGetImageFromCurrentImageContext();
defaultImage =
[[result stretchableImageWithLeftCapWidth:1 topCapHeight:1] retain];
return defaultImage;
- (BOOL)canGoBack {
return _webStateImpl->GetNavigationManagerImpl().CanGoBack();
- (BOOL)canGoForward {
return _webStateImpl->GetNavigationManagerImpl().CanGoForward();
- (CGPoint)scrollPosition {
CGPoint position = CGPointMake(0.0, 0.0);
if (!self.webScrollView)
return position;
return self.webScrollView.contentOffset;
- (BOOL)atTop {
if (!_webView)
return YES;
UIScrollView* scrollView = self.webScrollView;
return scrollView.contentOffset.y ==;
- (void)setShouldSuppressDialogs:(BOOL)shouldSuppressDialogs {
_shouldSuppressDialogs = shouldSuppressDialogs;
if (_webView) {
NSString* const kSetSuppressDialogs = [NSString
[self evaluateJavaScript:kSetSuppressDialogs stringResultHandler:nil];
_shouldSuppressDialogsOnWindowIDInjection = NO;
} else {
_shouldSuppressDialogsOnWindowIDInjection = shouldSuppressDialogs;
- (GURL)currentURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
DCHECK(trustLevel) << "Verification of the trustLevel state is mandatory";
if (_webView) {
GURL url([self webURLWithTrustLevel:trustLevel]);
// Web views treat all about: URLs as the same origin, which makes it
// possible for pages to document.write into about:<foo> pages, where <foo>
// can be something misleading. Report any about: URL as about:blank to
// prevent that. See
if (url.scheme() == url::kAboutScheme)
return GURL(url::kAboutBlankURL);
return url;
// Any non-web URL source is trusted.
*trustLevel = web::URLVerificationTrustLevel::kAbsolute;
if (self.nativeController)
return [self.nativeController url];
return [self currentNavigationURL];
- (WKWebView*)webView {
return _webView.get();
- (UIScrollView*)webScrollView {
return [_webView scrollView];
- (GURL)currentURL {
web::URLVerificationTrustLevel trustLevel =
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();
NSString* previousURLString =
base::SysUTF8ToNSString([self currentNavigationURL].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.
return web::Referrer(GURL(base::SysNSStringToUTF8(referrerString)),
- (void)queryPageReferrerPolicy:(void (^)(NSString*))responseHandler {
[self evaluateJavaScript:@"__gCrWeb.getPageReferrerPolicy()"
stringResultHandler:^(NSString* referrer, NSError* error) {
DCHECK_NE(error.code, WKErrorJavaScriptExceptionOccurred);
responseHandler(!error ? referrer : nil);
- (void)pushStateWithPageURL:(const GURL&)pageURL
transition:(ui::PageTransition)transition {
[[self sessionController] pushNewEntryWithURL:pageURL
[self didUpdateHistoryStateWithPageURL:pageURL];
self.userInteractionRegistered = NO;
- (void)replaceStateWithPageURL:(const GURL&)pageUrl
stateObject:(NSString*)stateObject {
[[self sessionController] updateCurrentEntryWithURL:pageUrl
[self didUpdateHistoryStateWithPageURL:pageUrl];
- (void)setDocumentURL:(const GURL&)newURL {
if (newURL != _documentURL) {
_documentURL = newURL;
_interactionRegisteredSinceLastURLChange = NO;
- (BOOL)isCurrentNavigationItemPOST {
// |_pendingNavigationInfo| will be nil if the decidePolicy* delegate methods
// were not called.
NSString* HTTPMethod =
? [_pendingNavigationInfo HTTPMethod]
: [self currentBackForwardListItemHolder]->http_method();
return [HTTPMethod isEqual:@"POST"];
- (BOOL)isCurrentNavigationBackForward {
if (![self currentNavItem])
return NO;
WKNavigationType currentNavigationType =
[self currentBackForwardListItemHolder]->navigation_type();
return currentNavigationType == WKNavigationTypeBackForward;
- (BOOL)isLinkNavigation:(WKNavigationType)navigationType {
switch (navigationType) {
case WKNavigationTypeLinkActivated:
return YES;
case WKNavigationTypeOther:
return [self userClickedRecently];
return NO;
- (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;
- (GURL)URLForHistoryNavigationFromItem:(web::NavigationItem*)fromItem
toItem:(web::NavigationItem*)toItem {
// If navigating with native API, i.e. using a back forward list item,
// hashchange events will be triggered automatically, so no URL tampering is
// required.
web::WKBackForwardListItemHolder* holder =
if (holder->back_forward_list_item()) {
return toItem->GetURL();
const GURL& startURL = fromItem->GetURL();
const GURL& endURL = toItem->GetURL();
// Check the state of the fragments on both URLs (aka, is there a '#' in the
// url or not).
if (!startURL.has_ref() || endURL.has_ref()) {
return endURL;
// startURL contains a fragment and endURL doesn't. Remove the fragment from
// startURL and compare the resulting string to endURL. If they are equal, add
// # to endURL to cause a hashchange event.
GURL hashless = web::GURLByRemovingRefFromGURL(startURL);
if (hashless != endURL)
return endURL;
url::StringPieceReplacements<std::string> emptyRef;
GURL newEndURL = endURL.ReplaceComponents(emptyRef);
return newEndURL;
- (void)injectWindowID {
if (![_windowIDJSManager hasBeenInjected]) {
// Default value for shouldSuppressDialogs is NO, so updating them only
// when necessary is a good optimization.
if (_shouldSuppressDialogsOnWindowIDInjection) {
self.shouldSuppressDialogs = YES;
_shouldSuppressDialogsOnWindowIDInjection = NO;
[_windowIDJSManager inject];
DCHECK([_windowIDJSManager hasBeenInjected]);
// Set the specified recognizer to take priority over any recognizers in the
// view that have a description containing the specified text fragment.
+ (void)requireGestureRecognizerToFail:(UIGestureRecognizer*)recognizer
containingDescription:(NSString*)fragment {
for (UIGestureRecognizer* iRecognizer in [view gestureRecognizers]) {
if (iRecognizer != recognizer) {
NSString* description = [iRecognizer description];
if ([description rangeOfString:fragment].location != NSNotFound) {
[iRecognizer requireGestureRecognizerToFail:recognizer];
// requireGestureRecognizerToFail: doesn't retain the recognizer, so it
// is possible for |iRecognizer| to outlive |recognizer| and end up with
// a dangling pointer. Add a retaining associative reference to ensure
// that the lifetimes work out.
// Note that normally using the value as the key wouldn't make any
// sense, but here it's fine since nothing needs to look up the value.
objc_setAssociatedObject(view, recognizer, recognizer,
- (CRWWebController*)createChildWebController {
CRWWebController* result = [self.delegate webPageOrderedOpen];
DCHECK(!result || result.sessionController.openedByDOM);
return result;
- (BOOL)canUseViewForGeneratingOverlayPlaceholderView {
return _containerView != nil;
- (UIView*)view {
// Kick off the process of lazily creating the view and starting the load if
// necessary; this creates _containerView if it doesn't exist.
[self triggerPendingLoad];
return _containerView;
- (id<CRWWebViewProxy>)webViewProxy {
return _webViewProxy.get();
- (UIView*)viewForPrinting {
// TODO(ios): Printing is not supported for native
// controllers.
return _webView;
- (double)loadingProgress {
return [_webView estimatedProgress];
- (void)registerLoadRequest:(const GURL&)URL {
// 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 = ui::PAGE_TRANSITION_CLIENT_REDIRECT;
switch (navigationType) {
case WKNavigationTypeLinkActivated:
transition = ui::PAGE_TRANSITION_LINK;
case WKNavigationTypeFormSubmitted:
case WKNavigationTypeFormResubmitted:
case WKNavigationTypeBackForward:
case WKNavigationTypeReload:
transition = 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( See if this heuristic can be improved.
transition = _interactionRegisteredSinceLastURLChange
// The referrer is not known yet, and will be updated later.
const web::Referrer emptyReferrer;
[self registerLoadRequest:URL referrer:emptyReferrer transition:transition];
- (void)registerLoadRequest:(const GURL&)requestURL
referrer:(const web::Referrer&)referrer
transition:(ui::PageTransition)transition {
// Transfer time is registered so that further transitions within the time
// envelope are not also registered as links.
_lastTransferTimeInSeconds = CFAbsoluteTimeGetCurrent();
// 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;
_lastRegisteredRequestURL = requestURL;
if (!(transition & ui::PAGE_TRANSITION_IS_REDIRECT_MASK)) {
// Record state of outgoing page.
[self recordStateInHistory];
[_delegate webWillAddPendingURL:requestURL transition:transition];
// Add or update pending url.
// There is a crash being reported when accessing |_webStateImpl| in the
// condition below. This CHECK was added in order to ascertain whether this
// is caused by a null WebState. If the crash is still reported after the
// addition of this CHECK, it is likely that |_webStateImpl| has been
// corrupted.
// TODO( Remove this check once we know the cause of the
// crash.
if (_webStateImpl->GetNavigationManagerImpl().GetPendingItem()) {
// Update the existing pending entry.
[[self sessionController] updatePendingEntry:requestURL];
} else {
// A new session history entry needs to be created.
[[self sessionController] addPendingEntry:requestURL
[_delegate webDidAddPendingURL];
- (NSString*)javascriptToReplaceWebViewURL:(const GURL&)URL
stateObjectJSON:(NSString*)stateObject {
std::string outURL;
base::EscapeJSONString(URL.spec(), true, &outURL);
[NSString stringWithFormat:@"__gCrWeb.replaceWebViewURL(%@, %@);",
base::SysUTF8ToNSString(outURL), stateObject];
- (void)setPushedOrReplacedURL:(const GURL&)URL
stateObject:(NSString*)stateObject {
// TODO(stuartmorgan): Make CRWSessionController manage this internally (or
// remove it; it's not clear this matches other platforms' behavior).
NSString* replaceWebViewUrlJS =
[self javascriptToReplaceWebViewURL:URL stateObjectJSON:stateObject];
std::string outState;
base::EscapeJSONString(base::SysNSStringToUTF8(stateObject), true, &outState);
NSString* popstateJS =
[NSString stringWithFormat:@"__gCrWeb.dispatchPopstateEvent(%@);",
NSString* combinedJS =
[NSString stringWithFormat:@"%@%@", replaceWebViewUrlJS, popstateJS];
GURL urlCopy(URL);
base::WeakNSObject<CRWWebController> weakSelf(self);
[self evaluateJavaScript:combinedJS
stringResultHandler:^(NSString*, NSError*) {
if (!weakSelf || weakSelf.get()->_isBeingDestroyed)
base::scoped_nsobject<CRWWebController> strongSelf([weakSelf retain]);
strongSelf.get()->_URLOnStartLoading = urlCopy;
strongSelf.get()->_lastRegisteredRequestURL = urlCopy;
// Load the current URL in a web view, first ensuring the web view is visible.
- (void)loadCurrentURLInWebView {
// Clear the set of URLs opened in external applications.
_openedApplicationURL.reset([[NSMutableSet alloc] init]);
// Load the url. The UIWebView delegate callbacks take care of updating the
// session history and UI.
const GURL targetURL([self currentNavigationURL]);
if (!targetURL.is_valid()) {
[self didFinishWithURL:targetURL loadSuccess:NO];
// JavaScript should never be evaluated here. User-entered JS should be
// evaluated via stringByEvaluatingUserJavaScriptFromString.
[self ensureWebViewCreated];
[self loadRequestForCurrentNavigationItem];
- (void)updatePendingNavigationInfoFromNavigationAction:
(WKNavigationAction*)action {
if (action.targetFrame.mainFrame) {
[[CRWWebControllerPendingNavigationInfo alloc] init]);
setReferrer:[self refererFromNavigationAction:action]];
[_pendingNavigationInfo setNavigationType:action.navigationType];
[_pendingNavigationInfo setHTTPMethod:action.request.HTTPMethod];
- (void)updatePendingNavigationInfoFromNavigationResponse:
(WKNavigationResponse*)response {
if (response.isForMainFrame) {
if (!_pendingNavigationInfo) {
[[CRWWebControllerPendingNavigationInfo alloc] init]);
[_pendingNavigationInfo setMIMEType:response.response.MIMEType];
- (void)commitPendingNavigationInfo {
if ([_pendingNavigationInfo referrer]) {
_currentReferrerString.reset([[_pendingNavigationInfo referrer] copy]);
if ([_pendingNavigationInfo MIMEType]) {
base::SysNSStringToUTF8([_pendingNavigationInfo MIMEType]));
[self updateCurrentBackForwardListItemHolder];
- (NSMutableURLRequest*)requestForCurrentNavigationItem {
const GURL currentNavigationURL([self currentNavigationURL]);
NSMutableURLRequest* request = [NSMutableURLRequest
const web::Referrer referrer([self currentSessionEntryReferrer]);
if (referrer.url.is_valid()) {
std::string referrerValue =
web::ReferrerHeaderValueForNavigation(currentNavigationURL, referrer);
if (!referrerValue.empty()) {
[request setValue:base::SysUTF8ToNSString(referrerValue)
// 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]
return request;
- (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder {
web::NavigationItem* item = [self currentSessionEntry].navigationItemImpl;
web::WKBackForwardListItemHolder* 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)
web::WKBackForwardListItemHolder* holder =
[self currentBackForwardListItemHolder];
WKNavigationType navigationType =
_pendingNavigationInfo ? [_pendingNavigationInfo navigationType]
: WKNavigationTypeOther;
holder->set_back_forward_list_item([_webView backForwardList].currentItem);
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)loadNativeViewWithSuccess:(BOOL)loadSuccess {
const GURL currentURL([self currentURL]);
[self didStartLoadingURL:currentURL updateHistory:loadSuccess];
_loadPhase = web::PAGE_LOADED;
// Perform post-load-finished updates.
[self didFinishWithURL:currentURL loadSuccess:loadSuccess];
// Inform the embedder the title changed.
if ([_delegate respondsToSelector:@selector(webController:titleDidChange:)]) {
NSString* title = [self.nativeController title];
// If a title is present, notify the delegate.
if (title)
[_delegate webController:self titleDidChange:title];
// If the controller handles title change notification, route those to the
// delegate.
if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) {
[self.nativeController setDelegate:self];
- (void)loadErrorInNativeView:(NSError*)error {
[self removeWebViewAllowingCachedReconstruction:NO];
const GURL currentUrl = [self currentNavigationURL];
error = web::NetErrorFromError(error);
BOOL isPost = [self isCurrentNavigationItemPOST];
[self setNativeController:[_nativeProvider controllerForURL:currentUrl
[self loadNativeViewWithSuccess:NO];
// Load the current URL in a native controller, retrieved from the native
// provider. Call |loadNativeViewWithSuccess:YES| to load the native controller.
- (void)loadCurrentURLInNativeView {
// Free the web view.
[self removeWebViewAllowingCachedReconstruction:NO];
const GURL targetURL = [self currentNavigationURL];
const web::Referrer referrer;
// Unlike the WebView case, always create a new controller and view.
// TODO(pinkerton): What to do if this does return nil?
[self setNativeController:[_nativeProvider controllerForURL:targetURL]];
[self registerLoadRequest:targetURL
transition:[self currentTransition]];
[self loadNativeViewWithSuccess:YES];
- (void)loadWithParams:(const NavigationManager::WebLoadParams&)originalParams {
// Make a copy of |params|, as some of the delegate methods may modify it.
NavigationManager::WebLoadParams params(originalParams);
// Initiating a navigation from the UI, record the current page state before
// the new page loads. Don't record for back/forward, as the current entry
// has already been moved to the next entry in the history. Do, however,
// record it for general reload.
// TODO(jimblackler): consider a single unified call to record state whenever
// the page is about to be changed. This cannot currently be done after
// addPendingEntry is called.
[_delegate webWillInitiateLoadWithParams:params];
GURL navUrl = params.url;
ui::PageTransition transition = params.transition_type;
BOOL initialNavigation = NO;
BOOL forwardBack =
PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_RELOAD) &&
if (forwardBack) {
// Setting these for back/forward is not supported.
} else {
// Clear transient view before making any changes to history and navigation
// manager. TODO(stuartmorgan): Drive Transient Item clearing from
// navigation system, rather than from WebController.
[self clearTransientContentView];
// TODO(stuartmorgan): Why doesn't recordStateInHistory get called for
// forward/back transitions?
[self recordStateInHistory];
CRWSessionController* history =
if (!self.currentSessionEntry)
initialNavigation = YES;
[history addPendingEntry:navUrl
web::NavigationItemImpl* addedItem =
[self currentSessionEntry].navigationItemImpl;
if (params.extra_headers)
if (params.post_data) {
DCHECK([addedItem->GetHttpRequestHeaders() objectForKey:@"Content-Type"])
<< "Post data should have an associated content type";
[_delegate webDidUpdateSessionForLoadWithParams:params
[self loadCurrentURL];
- (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)
// 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];
// Remove the transient content view.
[self clearTransientContentView];
const GURL currentURL = [self currentNavigationURL];
// If it's a chrome URL, but not a native one, create the WebUI instance.
if (web::GetWebClient()->IsAppSpecificURL(currentURL) &&
![_nativeProvider hasControllerForURL:currentURL]) {
[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 {
[self loadCurrentURLInWebView];
// Once a URL has been loaded, any cached-based reconstruction state has
// either been handled or obsoleted.
_expectedReconstructionURL = GURL();
- (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
*trustLevel = web::URLVerificationTrustLevel::kAbsolute;
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) &&
- (void)triggerPendingLoad {
if (!_containerView) {
// Create the top-level parent view, which will contain the content (whether
// native or web). Note, this needs to be created with a non-zero size
// to allow for (native) subviews with autosize constraints to be correctly
// processed.
[[CRWWebControllerContainerView alloc] initWithDelegate:self]);
// Compute and set the frame of the containerView.
CGFloat statusBarHeight =
[[UIApplication sharedApplication] statusBarFrame].size.height;
CGRect containerViewFrame =
[UIApplication sharedApplication].keyWindow.bounds;
containerViewFrame.origin.y += statusBarHeight;
containerViewFrame.size.height -= statusBarHeight;
_containerView.get().frame = containerViewFrame;
[_containerView addGestureRecognizer:[self touchTrackingRecognizer]];
[_containerView setAccessibilityIdentifier:web::kContainerViewID];
// Is |currentUrl| a web scheme or native chrome scheme.
BOOL isChromeScheme =
web::GetWebClient()->IsAppSpecificURL([self currentNavigationURL]);
// Don't immediately load the web page if in overlay mode. Always load if
// native.
if (isChromeScheme || !_overlayPreviewMode) {
// TODO(jimblackler): 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];
// Display overlay view until current url has finished loading or delay and
// then transition away.
if ((_overlayPreviewMode || _usePlaceholderOverlay) && !isChromeScheme)
[self addPlaceholderOverlay];
// Don't reset the overlay flag if in preview mode.
if (!_overlayPreviewMode)
_usePlaceholderOverlay = NO;
} else if (_requireReloadOnDisplay && _webView) {
_requireReloadOnDisplay = NO;
[self addPlaceholderOverlay];
[self loadCurrentURL];
- (BOOL)shouldReload:(const GURL&)destinationURL
transition:(ui::PageTransition)transition {
// Do a reload if the user hits enter in the address bar or re-types a URL.
CRWSessionController* sessionController =
web::NavigationItem* item =
return (transition & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) && item &&
(destinationURL == item->GetURL() ||
destinationURL == [sessionController currentEntry].originalUrl);
// Reload either the web view or the native content depending on which is
// displayed.
- (void)reloadInternal {
// Clear last user interaction.
// TODO( Move to after the load commits, in the subclass
// implementation. This will be inaccurate if the reload fails or is
// cancelled.
if (_webView) {
web::NavigationItem* transientItem =
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;
[transientItem->GetHttpRequestHeaders() copy]);
[self loadWithParams:reloadParams];
} else {
// As with back and forward navigation, load the URL manually instead of
// using the web view's reload. This ensures state processing and delegate
// calls are consistent.
// TODO(eugenebut): revisit this for WKWebView.
[self loadCurrentURL];
} else {
[self.nativeController reload];
- (void)reload {
[_delegate webWillReload];
[self reloadInternal];
- (void)abortLoad {
[_webView stopLoading];
[_pendingNavigationInfo setCancelled:YES];
[self loadCancelled];
- (void)loadCancelled {
[_passKitDownloader cancelPendingDownload];
// Current load will not complete; this should be communicated upstream to the
// delegate.
switch (_loadPhase) {
// Load phase after abort is always PAGE_LOADED.
_loadPhase = web::PAGE_LOADED;
if (!_isHalted) {
[_delegate webCancelStartLoadingRequest];
case web::PAGE_LOADING:
// The previous load never fully completed before this page change. The
// loadPhase is changed to PAGE_LOADED to indicate the cycle is complete,
// and the delegate is called.
_loadPhase = web::PAGE_LOADED;
if (!_isHalted) {
// RequestTracker expects StartPageLoad to be followed by
// FinishPageLoad, passing the exact same URL.
_URLOnStartLoading, false);
[_delegate webLoadCancelled:_URLOnStartLoading];
case web::PAGE_LOADED:
- (void)prepareForGoBack {
// Make sure any transitions that may have occurred have been seen and acted
// on by the CRWWebController, so the history stack and state of the
// CRWWebController is 100% up to date before the stack navigation starts.
if (_webView) {
[self injectWindowID];
bool wasShowingInterstitial = _webStateImpl->IsShowingWebInterstitial();
// Before changing the current session history entry, record the tab state.
if (!wasShowingInterstitial) {
[self recordStateInHistory];
- (void)goBack {
[self goDelta:-1];
- (void)goForward {
[self goDelta:1];
- (void)goDelta:(int)delta {
if (delta == 0) {
[self reload];
// Abort if there is nothing next in the history.
// Note that it is NOT checked that the history depth is at least |delta|.
if ((delta < 0 && ![self canGoBack]) || (delta > 0 && ![self canGoForward])) {
if (delta < 0) {
[self prepareForGoBack];
} else {
// Before changing the current session history entry, record the tab state.
[self recordStateInHistory];
CRWSessionController* sessionController =
// fromEntry is retained because it has the potential to be released
// by goDelta: if it has not been committed.
base::scoped_nsobject<CRWSessionEntry> fromEntry(
[[sessionController currentEntry] retain]);
[sessionController goDelta:delta];
if (fromEntry) {
[self finishHistoryNavigationFromEntry:fromEntry];
- (BOOL)isLoaded {
return _loadPhase == web::PAGE_LOADED;
- (void)didFinishNavigation {
// 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)
[self loadCompleteWithSuccess:YES];
- (void)loadCompleteWithSuccess:(BOOL)loadSuccess {
[self removePlaceholderOverlay];
// 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)
const GURL currentURL([self currentURL]);
_loadPhase = web::PAGE_LOADED;
[self optOutScrollsToTopForSubviews];
// Ensure the URL is as expected (and already reported to the delegate).
// If |_lastRegisteredRequestURL| is invalid then |currentURL| will be
// "about:blank".
DCHECK((currentURL == _lastRegisteredRequestURL) ||
(!_lastRegisteredRequestURL.is_valid() &&
_documentURL.spec() == url::kAboutBlankURL))
<< std::endl
<< "currentURL = [" << currentURL << "]" << std::endl
<< "_lastRegisteredRequestURL = [" << _lastRegisteredRequestURL << "]";
// Perform post-load-finished updates.
[self didFinishWithURL:currentURL loadSuccess:loadSuccess];
// Execute the pending LoadCompleteActions.
for (ProceduralBlock action in _pendingLoadCompleteActions.get()) {
[_pendingLoadCompleteActions removeAllObjects];
- (void)didFinishWithURL:(const GURL&)currentURL loadSuccess:(BOOL)loadSuccess {
DCHECK(_loadPhase == web::PAGE_LOADED);
_webStateImpl->GetRequestTracker()->FinishPageLoad(currentURL, loadSuccess);
// Reset the navigation type to the default value.
// Note: it is possible that the web view has already started loading the
// next page when this is called. In that case the cache mode can leak to
// (some of) the requests of the next page. It's expected to be an edge case,
// but if it becomes a problem it should be possible to notice it afterwards
// and react to it (by warning the user or reloading the page for example).
[self restoreStateFromHistory];
_webStateImpl->OnPageLoaded(currentURL, loadSuccess);
// Inform the embedder the load completed.
[_delegate webDidFinishWithURL:currentURL loadSuccess:loadSuccess];
- (void)finishHistoryNavigationFromEntry:(CRWSessionEntry*)fromEntry {
[_delegate webWillFinishHistoryNavigationFromEntry:fromEntry];
// Only load the new URL if the current entry was not created by a JavaScript
// window.history.pushState() call from |fromEntry|.
BOOL shouldLoadURL =
web::NavigationItemImpl* currentItem =
GURL endURL = [self URLForHistoryNavigationFromItem:fromEntry.navigationItem
if (shouldLoadURL) {
ui::PageTransition transition = ui::PageTransitionFromInt(
NavigationManager::WebLoadParams params(endURL);
if (currentItem) {
params.referrer = currentItem->GetReferrer();
params.transition_type = transition;
[self loadWithParams:params];
// Set the serialized state if necessary. State must be set if the document
// objects are the same. This can happen if:
// - The navigation is a pushState (i.e., shouldLoadURL is NO).
// - The navigation is a hash change.
// TODO( This misses some edge cases (e.g., a mixed series
// of hash changes and push/replaceState calls will likely end up dispatching
// this in cases where it shouldn't.
if (!shouldLoadURL ||
(web::GURLByRemovingRefFromGURL(endURL) ==
web::GURLByRemovingRefFromGURL(fromEntry.navigationItem->GetURL()))) {
NSString* stateObject = currentItem->GetSerializedStateObject();
[self setPushedOrReplacedURL:currentItem->GetURL() stateObject:stateObject];
- (void)evaluateJavaScript:(NSString*)script
(void (^)(std::unique_ptr<base::Value>, NSError*))handler {
[self evaluateJavaScript:script
stringResultHandler:^(NSString* stringResult, NSError* error) {
DCHECK(stringResult || error);
if (handler) {
std::unique_ptr<base::Value> result(
handler(std::move(result), error);
- (void)addGestureRecognizerToWebView:(UIGestureRecognizer*)recognizer {
if ([_gestureRecognizers containsObject:recognizer])
[_webView addGestureRecognizer:recognizer];
[_gestureRecognizers addObject:recognizer];
- (void)removeGestureRecognizerFromWebView:(UIGestureRecognizer*)recognizer {
if (![_gestureRecognizers containsObject:recognizer])
[_webView removeGestureRecognizer:recognizer];
[_gestureRecognizers removeObject:recognizer];
- (void)addToolbarViewToWebView:(UIView*)toolbarView {
if ([_webViewToolbars containsObject:toolbarView])
[_webViewToolbars addObject:toolbarView];
if (_webView)
[_containerView addToolbar:toolbarView];
- (void)removeToolbarViewFromWebView:(UIView*)toolbarView {
if (![_webViewToolbars containsObject:toolbarView])
[_webViewToolbars removeObject:toolbarView];
if (_webView)
[_containerView removeToolbar:toolbarView];
- (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.sessionController.openedByDOM && !_userInteractedWithWebController;
BOOL noNavigationItems =
return rendererInitiatedWithoutInteraction || noNavigationItems;
- (BOOL)useDesktopUserAgent {
web::NavigationItem* item = [self currentNavItem];
return item && item->IsOverridingUserAgent();
- (CRWPassKitDownloader*)passKitDownloader {
if (_passKitDownloader) {
return _passKitDownloader.get();
base::WeakNSObject<CRWWebController> weakSelf(self);
web::PassKitCompletionHandler passKitCompletion = ^(NSData* data) {
base::scoped_nsobject<CRWWebController> strongSelf([weakSelf retain]);
if (!strongSelf) {
// Cancel load to update web state, since the PassKit download happens
// through a separate flow. This follows the same flow as when PassKit is
// downloaded through UIWebView.
[strongSelf loadCancelled];
SEL didLoadPassKitObject = @selector(webController:didLoadPassKitObject:);
id<CRWWebDelegate> delegate = [strongSelf delegate];
if ([delegate respondsToSelector:didLoadPassKitObject]) {
[delegate webController:strongSelf didLoadPassKitObject:data];
web::BrowserState* browserState = self.webStateImpl->GetBrowserState();
_passKitDownloader.reset([[CRWPassKitDownloader alloc]
return _passKitDownloader.get();
#pragma mark -
#pragma mark CRWWebControllerContainerViewDelegate
- (CRWWebViewProxyImpl*)contentViewProxyForContainerView:
(CRWWebControllerContainerView*)containerView {
return _webViewProxy.get();
- (CGFloat)headerHeightForContainerView:
(CRWWebControllerContainerView*)containerView {
return [self headerHeight];
#pragma mark -
#pragma mark CRWJSInjectionEvaluator Methods
- (void)evaluateJavaScript:(NSString*)script
stringResultHandler:(web::JavaScriptCompletion)handler {
NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script];
web::EvaluateJavaScript(_webView, safeScript, handler);
- (void)executeJavaScript:(NSString*)script
completionHandler:(web::JavaScriptResultBlock)completionHandler {
NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script];
web::ExecuteJavaScript(_webView, safeScript, completionHandler);
- (BOOL)scriptHasBeenInjectedForClass:(Class)JSInjectionManagerClass
presenceBeacon:(NSString*)beacon {
return [_injectedScriptManagers containsObject:JSInjectionManagerClass];
- (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass {
// Skip evaluation if there's no content (e.g., if what's being injected is
// an umbrella manager).
if ([script length]) {
// Every injection except windowID requires windowID check.
if (JSInjectionManagerClass != [CRWJSWindowIdManager class])
script = [self scriptByAddingWindowIDCheckForScript:script];
web::ExecuteJavaScript(_webView, script, nil);
[_injectedScriptManagers addObject:JSInjectionManagerClass];
#pragma mark -
- (void)evaluateUserJavaScript:(NSString*)script {
[self setUserInteractionRegistered:YES];
web::ExecuteJavaScript(_webView, script, nil);
- (BOOL)respondToMessage:(base::DictionaryValue*)message
originURL:(const GURL&)originURL {
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)) {
// Message was either unexpected or not correctly handled.
// Page is reset as a precaution.
DLOG(WARNING) << "Unexpected message received: " << command;
return NO;
return YES;
typedef BOOL (*HandlerType)(id, SEL, base::DictionaryValue*, NSDictionary*);
HandlerType handlerImplementation =
reinterpret_cast<HandlerType>([self methodForSelector:handler]);
NSMutableDictionary* context =
[NSMutableDictionary dictionaryWithObject:@(userIsInteracting)
NSURL* originNSURL = net::NSURLWithGURL(originURL);
if (originNSURL)
context[web::kOriginURLKey] = originNSURL;
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)["addPluginPlaceholders"] =
(*handlers)["chrome.send"] = @selector(handleChromeSendMessage:context:);
(*handlers)["console"] = @selector(handleConsoleMessage:context:);
(*handlers)["geolocationDialog.suppressed"] =
(*handlers)["document.favicons"] =
(*handlers)["document.retitled"] =
(*handlers)["document.submit"] =
(*handlers)["externalRequest"] =
(*handlers)["form.activity"] =
(*handlers)["navigator.credentials.request"] =
(*handlers)["navigator.credentials.notifySignedIn"] =
(*handlers)["navigator.credentials.notifySignedOut"] =
(*handlers)["navigator.credentials.notifyFailedSignIn"] =
(*handlers)["resetExternalRequest"] =
(*handlers)["window.error"] = @selector(handleWindowErrorMessage:context:);
(*handlers)["window.hashchange"] =
(*handlers)["window.history.back"] =
(*handlers)["window.history.willChangeState"] =
(*handlers)["window.history.didPushState"] =
(*handlers)["window.history.didReplaceState"] =
(*handlers)["window.history.forward"] =
(*handlers)["window.history.go"] =
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, [self windowId], script];
- (BOOL)respondToWKScriptMessage:(WKScriptMessage*)scriptMessage {
int errorCode = 0;
std::string errorMessage;
std::unique_ptr<base::Value> inputJSONData(
base::SysNSStringToUTF8(scriptMessage.body), false, &errorCode,
if (errorCode) {
DLOG(WARNING) << "JSON parse error: %s" << errorMessage.c_str();
return NO;
base::DictionaryValue* message = nullptr;
if (!inputJSONData->GetAsDictionary(&message)) {
return NO;
std::string windowID;
message->GetString("crwWindowId", &windowID);
// Check for correct windowID
if (![[self windowId] isEqualToString:base::SysUTF8ToNSString(windowID)]) {
DLOG(WARNING) << "Message from JS ignored due to non-matching windowID: " <<
[self windowId] << " != " << base::SysUTF8ToNSString(windowID);
return NO;
base::DictionaryValue* command = nullptr;
if (!message->GetDictionary("crwCommand", &command)) {
return NO;
if ([ isEqualToString:kScriptImmediateName] ||
[ isEqualToString:kScriptMessageName]) {
return [self respondToMessage:command
userIsInteracting:[self userIsInteracting]
originURL:net::GURLWithNSURL([_webView URL])];
return NO;
#pragma mark -
#pragma mark JavaScript message handlers
- (BOOL)handleAddPluginPlaceholdersMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
// Inject the script that adds the plugin placeholders.
instanceOfClass:[CRWJSPluginPlaceholderManager class]] inject];
return YES;
- (BOOL)handleChromeSendMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
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;
messageContent, *message, currentURL,
_webStateImpl->ProcessWebUIMessage(currentURL, messageContent,
return YES;
<< "chrome.send message not handled because WebUI was not found.";
return NO;
- (BOOL)handleConsoleMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
// Do not log if JS logging is off.
if (![[NSUserDefaults standardUserDefaults] boolForKey:web::kLogJavaScript]) {
return YES;
std::string method;
if (!message->GetString("method", &method)) {
DLOG(WARNING) << "JS message parameter not found: method";
return NO;
std::string consoleMessage;
if (!message->GetString("message", &consoleMessage)) {
DLOG(WARNING) << "JS message parameter not found: message";
return NO;
std::string origin;
if (!message->GetString("origin", &origin)) {
DLOG(WARNING) << "JS message parameter not found: origin";
return NO;
DVLOG(0) << origin << " [" << method << "] " << consoleMessage;
return YES;
- (BOOL)handleGeolocationDialogSuppressedMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
[self didSuppressDialog];
return YES;
- (BOOL)handleDocumentFaviconsMessage:(base::DictionaryValue*)message
context:(NSDictionary*)context {
base::ListValue* favicons = nullptr;
if (!message->GetList("favicons", &favicons)) {
DLOG(WARNING) << "JS message parameter not found: favicons";
return NO;
std::vector<web::FaviconURL> urls;
for (size_t fav_idx = 0; fav_idx != favicons->GetSize(); ++fav_idx) {
base::DictionaryValue* favicon = nullptr;
if (!favicons->GetDictionary(fav_idx, &favicon))
return NO;
std::string href;
std::string rel;
if (!favicon->GetString("href", &href)) {
DLOG(WARNING) << "JS message parameter not found: href";
return NO;
if (!favicon->GetString("rel", &rel)) {