| // 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 <vector> |
| |
| #include "base/callback.h" |
| #include "base/containers/mru_cache.h" |
| #import "base/ios/block_types.h" |
| #include "base/ios/ios_util.h" |
| #import "base/ios/ns_error_util.h" |
| #import "base/ios/weak_nsobject.h" |
| #include "base/json/string_escape.h" |
| #include "base/logging.h" |
| #import "base/mac/bind_objc_block.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/foundation_util.h" |
| #import "base/mac/objc_property_releaser.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #import "base/mac/scoped_nsobject.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #import "ios/net/http_response_headers_util.h" |
| #import "ios/net/nsurlrequest_util.h" |
| #include "ios/web/history_state_util.h" |
| #import "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/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/public/browser_state.h" |
| #include "ios/web/public/favicon_url.h" |
| #import "ios/web/public/java_script_dialog_presenter.h" |
| #import "ios/web/public/navigation_item.h" |
| #import "ios/web/public/navigation_manager.h" |
| #import "ios/web/public/origin_util.h" |
| #include "ios/web/public/referrer.h" |
| #include "ios/web/public/referrer_util.h" |
| #include "ios/web/public/ssl_status.h" |
| #import "ios/web/public/url_scheme_util.h" |
| #include "ios/web/public/url_util.h" |
| #import "ios/web/public/web_client.h" |
| #include "ios/web/public/web_kit_constants.h" |
| #import "ios/web/public/web_state/context_menu_params.h" |
| #include "ios/web/public/web_state/credential.h" |
| #import "ios/web/public/web_state/crw_web_controller_observer.h" |
| #include "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" |
| #import "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_context_menu_delegate.h" |
| #import "ios/web/public/web_state/ui/crw_native_content.h" |
| #import "ios/web/public/web_state/ui/crw_native_content_provider.h" |
| #import "ios/web/public/web_state/ui/crw_web_view_content_view.h" |
| #import "ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h" |
| #include "ios/web/public/web_state/url_verification_constants.h" |
| #import "ios/web/public/web_state/web_state.h" |
| #include "ios/web/public/webui/web_ui_ios.h" |
| #import "ios/web/web_state/crw_pass_kit_downloader.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_context_menu_controller.h" |
| #import "ios/web/web_state/ui/crw_swipe_recognizer_provider.h" |
| #import "ios/web/web_state/ui/crw_web_controller.h" |
| #import "ios/web/web_state/ui/crw_web_controller_container_view.h" |
| #import "ios/web/web_state/ui/crw_web_view_proxy_impl.h" |
| #import "ios/web/web_state/ui/crw_wk_navigation_states.h" |
| #import "ios/web/web_state/ui/crw_wk_script_message_router.h" |
| #import "ios/web/web_state/ui/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 "ios/web/webui/mojo_facade.h" |
| #import "net/base/mac/url_conversions.h" |
| #include "net/base/net_errors.h" |
| #include "net/ssl/ssl_info.h" |
| #include "services/service_manager/public/cpp/interface_registry.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 { |
| |
| // 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; |
| }; |
| |
| // Keys for JavaScript command handlers context. |
| NSString* const kUserIsInteractingKey = @"userIsInteracting"; |
| NSString* const kOriginURLKey = @"originURL"; |
| |
| // Standard User Defaults key for "Log JS" debug setting. |
| NSString* const kLogJavaScript = @"LogJavascript"; |
| |
| // Key of UMA IOSFix.ViewportZoomBugCount histogram. |
| const char kUMAViewportZoomBugCount[] = "Renderer.ViewportZoomBugCount"; |
| |
| // URL scheme for messages sent from javascript for asynchronous processing. |
| NSString* const kScriptMessageName = @"crwebinvoke"; |
| |
| // 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> |
| CertVerificationErrorsCacheType; |
| |
| // Maximum number of errors to store in cert verification errors cache. |
| // Cache holds errors only for pending navigations, so the actual number of |
| // stored errors is not expected to be high. |
| const CertVerificationErrorsCacheType::size_type kMaxCertErrorsCount = 100; |
| |
| // States for external URL requests. This enum is used in UMA and |
| // entries should not be re-ordered or deleted. |
| enum ExternalURLRequestStatus { |
| MAIN_FRAME_ALLOWED = 0, |
| SUBFRAME_ALLOWED, |
| SUBFRAME_BLOCKED, |
| NUM_EXTERNAL_URL_REQUEST_STATUS |
| }; |
| |
| // Utility function for getting the source of NSErrors received by WKWebViews. |
| WKWebViewErrorSource WKWebViewErrorSourceFromError(NSError* error) { |
| DCHECK(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) { |
| DCHECK(error); |
| base::scoped_nsobject<NSMutableDictionary> userInfo( |
| [error.userInfo mutableCopy]); |
| [userInfo setObject:@(source) forKey:kWKWebViewErrorSourceKey]; |
| return |
| [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 { |
| base::mac::ObjCPropertyReleaser |
| _propertyReleaser_CRWWebControllerPendingNavigationInfo; |
| } |
| // 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; |
| @end |
| |
| @implementation CRWWebControllerPendingNavigationInfo |
| @synthesize referrer = _referrer; |
| @synthesize MIMEType = _MIMEType; |
| @synthesize navigationType = _navigationType; |
| @synthesize HTTPMethod = _HTTPMethod; |
| @synthesize cancelled = _cancelled; |
| |
| - (instancetype)init { |
| if ((self = [super init])) { |
| _propertyReleaser_CRWWebControllerPendingNavigationInfo.Init( |
| self, [CRWWebControllerPendingNavigationInfo class]); |
| _navigationType = WKNavigationTypeOther; |
| } |
| return self; |
| } |
| @end |
| |
| @interface CRWWebController ()<CRWContextMenuDelegate, |
| CRWNativeContentDelegate, |
| CRWSSLStatusUpdaterDataSource, |
| CRWSSLStatusUpdaterDelegate, |
| CRWWebControllerContainerViewDelegate, |
| CRWWebViewScrollViewProxyObserver, |
| WKNavigationDelegate, |
| WKUIDelegate> { |
| base::WeakNSProtocol<id<CRWWebDelegate>> _delegate; |
| 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. |
| std::vector<std::unique_ptr<web::WebControllerObserverBridge>> |
| _observerBridges; |
| // YES if a user interaction has been registered at any time once the page has |
| // loaded. |
| BOOL _userInteractionRegistered; |
| // YES if the user has interacted with the content area since the last URL |
| // change. |
| BOOL _interactionRegisteredSinceLastURLChange; |
| // The actual URL of the document object (i.e., the last committed URL). |
| // TODO(crbug.com/549616): Remove this in favor of just updating the |
| // navigation manager and treating that as authoritative. |
| GURL _documentURL; |
| // 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; |
| // The controller that tracks long press and check context menu trigger. |
| base::scoped_nsobject<CRWContextMenuController> _contextMenuController; |
| // Whether a click is in progress. |
| BOOL _clickInProgress; |
| // Data on the recorded last user interaction. |
| std::unique_ptr<UserInteractionEvent> _lastUserInteraction; |
| // YES if there has been user interaction with views owned by this controller. |
| BOOL _userInteractedWithWebController; |
| // The time of the last page transfer start, measured in seconds since Jan 1 |
| // 2001. |
| CFAbsoluteTime _lastTransferTimeInSeconds; |
| // Default URL (about:blank). |
| GURL _defaultURL; |
| // 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; |
| // Set to YES when a hashchange event is manually dispatched for same-document |
| // history navigations. |
| BOOL _dispatchingSameDocumentHashChangeEvent; |
| // YES if the web process backing _wkWebView is believed to currently be dead. |
| BOOL _webProcessIsDead; |
| |
| // 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; |
| |
| // Backs up property with the same name. |
| std::unique_ptr<web::MojoFacade> _mojoFacade; |
| |
| // 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|. |
| base::scoped_nsobject<CRWWebControllerPendingNavigationInfo> |
| _pendingNavigationInfo; |
| |
| // Holds all WKNavigation objects and their states which are currently in |
| // flight. |
| base::scoped_nsobject<CRWWKNavigationStates> _navigationStates; |
| |
| // The WKNavigation captured when |stopLoading| was called. Used for reporting |
| // WebController.EmptyNavigationManagerCausedByStopLoading UMA metric which |
| // helps with diagnosing a navigation related crash (crbug.com/565457). |
| 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. |
| base::scoped_nsobject<CRWCertVerificationController> |
| _certVerificationController; |
| |
| // CertVerification errors which happened inside |
| // |webView:didReceiveAuthenticationChallenge:completionHandler:|. |
| // Key is leaf-cert/host pair. This storage is used to carry calculated |
| // cert status from |didReceiveAuthenticationChallenge:| to |
| // |didFailProvisionalNavigation:| delegate method. |
| std::unique_ptr<CertVerificationErrorsCacheType> _certVerificationErrors; |
| } |
| |
| // If |contentView_| contains a web view, this is the web view it contains. |
| // If not, it's nil. |
| @property(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; |
| // The associated NavigationManagerImpl. |
| @property(nonatomic, readonly) NavigationManagerImpl* navigationManagerImpl; |
| // Whether the associated WebState has an opener. |
| @property(nonatomic, readonly) BOOL hasOpener; |
| // Dictionary where keys are the names of WKWebView properties and values are |
| // selector names which should be called when a corresponding property has |
| // changed. e.g. @{ @"URL" : @"webViewURLDidChange" } means that |
| // -[self webViewURLDidChange] must be called every time when WKWebView.URL is |
| // changed. |
| @property(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; |
| |
| // Whether or not desktop user agent is used for the currentItem. |
| @property(nonatomic, readonly) BOOL usesDesktopUserAgent; |
| |
| // Facade for Mojo API. |
| @property(nonatomic, readonly) web::MojoFacade* mojoFacade; |
| |
| // TODO(crbug.com/692871): Remove these functions and replace with more |
| // appropriate NavigationItem getters. |
| // Returns the navigation item for the current page. |
| @property(nonatomic, readonly) web::NavigationItemImpl* currentNavItem; |
| // Returns the current transition type. |
| @property(nonatomic, readonly) ui::PageTransition currentTransition; |
| // Returns the referrer for current navigation item. May be empty. |
| @property(nonatomic, readonly) web::Referrer currentNavItemReferrer; |
| // The HTTP headers associated with the current navigation item. These are nil |
| // unless the request was a POST. |
| @property(nonatomic, readonly) NSDictionary* currentHTTPHeaders; |
| |
| // Requires page reconstruction if |item| has a non-NONE UserAgentType and it |
| // differs from that of |fromItem|. |
| - (void)updateDesktopUserAgentForItem:(web::NavigationItem*)item |
| previousUserAgentType:(web::UserAgentType)userAgentType; |
| |
| // 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. |
| - (void)didStartLoadingURL:(const GURL&)URL; |
| // 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; |
| // Shows placeholder overlay. |
| - (void)addPlaceholderOverlay; |
| // Removes placeholder overlay. |
| - (void)removePlaceholderOverlay; |
| |
| // Creates a web view if it's not yet created. |
| - (void)ensureWebViewCreated; |
| // Creates a web view with given |config|. No-op if web view is already created. |
| - (void)ensureWebViewCreatedWithConfiguration:(WKWebViewConfiguration*)config; |
| // Returns a new autoreleased web view created with given configuration. |
| - (WKWebView*)webViewWithConfiguration:(WKWebViewConfiguration*)config; |
| // Sets the value of the webView property, and performs its basic setup. |
| - (void)setWebView:(WKWebView*)webView; |
| // 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; |
| // Extracts "Referer" [sic] value from WKNavigationAction request header. |
| - (NSString*)referrerFromNavigationAction:(WKNavigationAction*)action; |
| |
| // Returns the current URL of the web view, and sets |trustLevel| accordingly |
| // based on the confidence in the verification. |
| - (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel; |
| // Returns |YES| if |url| should be loaded in a native view. |
| - (BOOL)shouldLoadURLInNativeView:(const GURL&)url; |
| // Loads 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 https://bugs.webkit.org/show_bug.cgi?id=145410 |
| - (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: |
| (WKNavigationAction*)action; |
| // Extracts navigation info from WKNavigationResponse and sets it as a pending. |
| // Some pieces of navigation information are only known in |
| // |decidePolicyForNavigationResponse|, but must be in a pending state until |
| // |didCommitNavigation| where it becames current. |
| - (void)updatePendingNavigationInfoFromNavigationResponse: |
| (WKNavigationResponse*)response; |
| // Updates current state with any pending information. Should be called when a |
| // navigation is committed. |
| - (void)commitPendingNavigationInfo; |
| // Returns a NSMutableURLRequest that represents the current NavigationItem. |
| - (NSMutableURLRequest*)requestForCurrentNavigationItem; |
| // Returns the WKBackForwardListItemHolder for the current navigation item. |
| - (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder; |
| // Updates the WKBackForwardListItemHolder navigation item. |
| - (void)updateCurrentBackForwardListItemHolder; |
| |
| // 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 |
| transition:(ui::PageTransition)transition; |
| // 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; |
| // Navigates forwards or backwards by |delta| pages. No-op if delta is out of |
| // bounds. Reloads if delta is 0. |
| // TODO(crbug.com/661316): Move this method to NavigationManager. |
| - (void)goDelta:(int)delta; |
| // Informs the native controller if web usage is allowed or not. |
| - (void)setNativeControllerWebUsageEnabled:(BOOL)webUsageEnabled; |
| // 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 |
| transition:(ui::PageTransition)transition; |
| // Updates the HTML5 history state of the page using the current NavigationItem. |
| // For same-document navigations and navigations affected by |
| // window.history.[push/replace]State(), the URL and serialized state object |
| // will be updated to the current NavigationItem's values. A popState event |
| // will be triggered for all same-document navigations. Additionaly, a |
| // hashchange event will be triggered for same-document navigations where the |
| // only difference between the current and previous URL is the fragment. |
| - (void)updateHTML5HistoryState; |
| // Generates the JavaScript string used to update the UIWebView's URL so that it |
| // matches the URL displayed in the omnibox and sets window.history.state to |
| // stateObject. Needed for history.pushState() and history.replaceState(). |
| - (NSString*)javaScriptToReplaceWebViewURL:(const GURL&)URL |
| stateObjectJSON:(NSString*)stateObject; |
| // Generates the JavaScript string used to manually dispatch a popstate event, |
| // using |stateObjectJSON| as the event parameter. |
| - (NSString*)javaScriptToDispatchPopStateWithObject:(NSString*)stateObjectJSON; |
| // Generates the JavaScript string used to manually dispatch a hashchange event, |
| // using |oldURL| and |newURL| as the event parameters. |
| - (NSString*)javaScriptToDispatchHashChangeWithOldURL:(const GURL&)oldURL |
| newURL:(const GURL&)newURL; |
| // Injects JavaScript to update the URL and state object of the webview to the |
| // values found in the current NavigationItem. A hashchange event will be |
| // dispatched if |dispatchHashChange| is YES, and a popstate event will be |
| // dispatched if |sameDocument| is YES. Upon the script's completion, resets |
| // |urlOnStartLoading_| and |_lastRegisteredRequestURL| to the current |
| // NavigationItem's URL. This is necessary so that sites that depend on URL |
| // params/fragments continue to work correctly and that checks for the URL don't |
| // incorrectly trigger |-webPageChanged| calls. |
| - (void)injectHTML5HistoryScriptWithHashChange:(BOOL)dispatchHashChange |
| sameDocumentNavigation:(BOOL)sameDocumentNavigation; |
| |
| - (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 |
| userScalable:(BOOL)isUserScalable; |
| // Calls the zoom-preparation UIScrollViewDelegate callbacks on the web view. |
| // This is called before |-applyWebViewScrollZoomScaleFromScrollState:|. |
| - (void)prepareToApplyWebViewScrollZoomScale; |
| // Calls the zoom-completion UIScrollViewDelegate callbacks on the web view. |
| // This is called after |-applyWebViewScrollZoomScaleFromScrollState:|. |
| - (void)finishApplyingWebViewScrollZoomScale; |
| // Sets zoom scale value for webview scroll view from |zoomState|. |
| - (void)applyWebViewScrollZoomScaleFromZoomState: |
| (const web::PageZoomState&)zoomState; |
| // Sets scroll offset value for webview scroll view from |scrollState|. |
| - (void)applyWebViewScrollOffsetFromScrollState: |
| (const web::PageScrollState&)scrollState; |
| // Returns the referrer for the current page. |
| - (web::Referrer)currentReferrer; |
| // Adds a new NavigationItem with the given URL and state object to the history |
| // stack. A state object is a serialized generic JavaScript object that contains |
| // details of the UI's state for a given NavigationItem/URL. |
| // TODO(stuartmorgan): Move the pushState/replaceState logic into |
| // NavigationManager. |
| - (void)pushStateWithPageURL:(const GURL&)pageURL |
| stateObject:(NSString*)stateObject |
| transition:(ui::PageTransition)transition; |
| // Assigns the given URL and state object to the current NavigationItem. |
| - (void)replaceStateWithPageURL:(const GURL&)pageUrl |
| stateObject:(NSString*)stateObject; |
| // Sets _documentURL to newURL, and updates any relevant state information. |
| - (void)setDocumentURL:(const GURL&)newURL; |
| // Sets last committed NavigationItem's title to the given |title|, which can |
| // not be nil. |
| - (void)setNavigationItemTitle:(NSString*)title; |
| // Returns YES if the current navigation item corresponds to a web page |
| // loaded by a POST request. |
| - (BOOL)isCurrentNavigationItemPOST; |
| // Returns YES if current navigation item is WKNavigationTypeBackForward. |
| - (BOOL)isCurrentNavigationBackForward; |
| // Returns whether the given navigation is triggered by a user link click. |
| - (BOOL)isLinkNavigation:(WKNavigationType)navigationType; |
| |
| // Inject windowID if not yet injected. |
| - (void)injectWindowID; |
| |
| // Returns YES if the given WKBackForwardListItem is valid to use for |
| // navigation. |
| - (BOOL)isBackForwardListItemValid:(WKBackForwardListItem*)item; |
| // Finds all the scrollviews in the view hierarchy and makes sure they do not |
| // interfere with scroll to top when tapping the statusbar. |
| - (void)optOutScrollsToTopForSubviews; |
| // 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 |
| linkClicked:(BOOL)linkClicked; |
| // 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 |
| linkActivatedNavigation:(BOOL)linkActivatedNavigation; |
| // 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; |
| // Updates SSL status for the current navigation item based on the information |
| // provided by web view. |
| - (void)updateSSLStatusForCurrentNavigationItem; |
| // Called when a load ends in an SSL error and certificate chain. |
| - (void)handleSSLCertError:(NSError*)error; |
| |
| // Used in webView:didReceiveAuthenticationChallenge:completionHandler: to |
| // reply with NSURLSessionAuthChallengeDisposition and credentials. |
| - (void)processAuthChallenge:(NSURLAuthenticationChallenge*)challenge |
| forCertAcceptPolicy:(web::CertAcceptPolicy)policy |
| certStatus:(net::CertStatus)certStatus |
| completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler; |
| // Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply |
| // with NSURLSessionAuthChallengeDisposition and credentials. |
| - (void)handleHTTPAuthForChallenge:(NSURLAuthenticationChallenge*)challenge |
| completionHandler: |
| (void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler; |
| // Used in webView:didReceiveAuthenticationChallenge:completionHandler: to reply |
| // with NSURLSessionAuthChallengeDisposition and credentials. |
| + (void)processHTTPAuthForUser:(NSString*)user |
| password:(NSString*)password |
| completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler; |
| |
| // Helper to respond to |webView:runJavaScript...| delegate methods. |
| // |completionHandler| must not be nil. |
| - (void)runJavaScriptDialogOfType:(web::JavaScriptDialogType)type |
| initiatedByFrame:(WKFrameInfo*)frame |
| message:(NSString*)message |
| defaultText:(NSString*)defaultText |
| completion:(void (^)(BOOL, NSString*))completionHandler; |
| |
| // Called when WKWebView estimatedProgress has been changed. |
| - (void)webViewEstimatedProgressDidChange; |
| // Called when WKWebView certificateChain or hasOnlySecureContent property has |
| // changed. |
| - (void)webViewSecurityFeaturesDidChange; |
| // Called when WKWebView loading state has been changed. |
| - (void)webViewLoadingStateDidChange; |
| // Called when WKWebView title has been changed. |
| - (void)webViewTitleDidChange; |
| // Called when WKWebView 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 |
| context:(NSDictionary*)context; |
| // Handles 'chrome.send' message. |
| - (BOOL)handleChromeSendMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'console' message. |
| - (BOOL)handleConsoleMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'geolocationDialog.suppressed' message. |
| - (BOOL)handleGeolocationDialogSuppressedMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'document.favicons' message. |
| - (BOOL)handleDocumentFaviconsMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'document.submit' message. |
| - (BOOL)handleDocumentSubmitMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'form.activity' message. |
| - (BOOL)handleFormActivityMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'navigator.credentials.request' message. |
| - (BOOL)handleCredentialsRequestedMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'navigator.credentials.notifySignedIn' message. |
| - (BOOL)handleSignedInMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'navigator.credentials.notifySignedOut' message. |
| - (BOOL)handleSignedOutMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'navigator.credentials.notifyFailedSignIn' message. |
| - (BOOL)handleSignInFailedMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'window.error' message. |
| - (BOOL)handleWindowErrorMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'window.hashchange' message. |
| - (BOOL)handleWindowHashChangeMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'window.history.back' message. |
| - (BOOL)handleWindowHistoryBackMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'window.history.forward' message. |
| - (BOOL)handleWindowHistoryForwardMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'window.history.go' message. |
| - (BOOL)handleWindowHistoryGoMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'window.history.willChangeState' message. |
| - (BOOL)handleWindowHistoryWillChangeStateMessage: |
| (base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| // Handles 'window.history.didPushState' message. |
| - (BOOL)handleWindowHistoryDidPushStateMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| |
| // Handles 'window.history.didReplaceState' message. |
| - (BOOL)handleWindowHistoryDidReplaceStateMessage: |
| (base::DictionaryValue*)message |
| context:(NSDictionary*)context; |
| |
| // 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; |
| |
| @end |
| |
| namespace { |
| |
| NSString* const kReferrerHeaderName = @"Referer"; // [sic] |
| |
| // The duration of the period following a screen touch during which the user is |
| // still considered to be interacting with the page. |
| const NSTimeInterval kMaximumDelayForUserInteractionInSeconds = 2; |
| |
| // URLs that are fed into UIWebView as history push/replace get escaped, |
| // potentially changing their format. Code that attempts to determine whether a |
| // URL hasn't changed can be confused by those differences though, so method |
| // will round-trip a URL through the escaping process so that it can be adjusted |
| // pre-storing, to allow later comparisons to work as expected. |
| GURL URLEscapedForHistory(const GURL& url) { |
| // TODO(stuartmorgan): This is a very large hammer; see if limited unicode |
| // escaping would be sufficient. |
| return net::GURLWithNSURL(net::NSURLWithGURL(url)); |
| } |
| |
| // 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; |
| DCHECK(_webStateImpl); |
| // Load phase when no WebView present is 'loaded' because this represents |
| // the idle state. |
| _loadPhase = web::PAGE_LOADED; |
| // Content area is lazily instantiated. |
| _defaultURL = GURL(url::kAboutBlankURL); |
| _jsInjectionReceiver.reset( |
| [[CRWJSInjectionReceiver alloc] initWithEvaluator:self]); |
| _webViewProxy.reset( |
| [[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] |
| initWithBrowserState:browserState]); |
| _certVerificationErrors.reset( |
| new CertVerificationErrorsCacheType(kMaxCertErrorsCount)); |
| _navigationStates.reset([[CRWWKNavigationStates alloc] init]); |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(orientationDidChange) |
| name:UIApplicationDidChangeStatusBarOrientationNotification |
| object:nil]; |
| } |
| return self; |
| } |
| |
| - (id<CRWNativeContentProvider>)nativeProvider { |
| return _nativeProvider.get(); |
| } |
| |
| - (void)setNativeProvider:(id<CRWNativeContentProvider>)nativeProvider { |
| _nativeProvider.reset(nativeProvider); |
| } |
| |
| - (id<CRWSwipeRecognizerProvider>)swipeRecognizerProvider { |
| return _swipeRecognizerProvider.get(); |
| } |
| |
| - (void)setSwipeRecognizerProvider: |
| (id<CRWSwipeRecognizerProvider>)swipeRecognizerProvider { |
| _swipeRecognizerProvider.reset(swipeRecognizerProvider); |
| } |
| |
| - (WebState*)webState { |
| return _webStateImpl; |
| } |
| |
| - (WebStateImpl*)webStateImpl { |
| return _webStateImpl; |
| } |
| |
| - (void)clearTransientContentView { |
| // Early return if there is no transient content view. |
| if (![_containerView transientContentView]) |
| return; |
| |
| // Remove the transient content view from the hierarchy. |
| [_containerView clearTransientContentView]; |
| |
| // Notify the WebState so it can perform any required state cleanup. |
| if (_webStateImpl) |
| _webStateImpl->ClearTransientContentView(); |
| } |
| |
| - (void)showTransientContentView:(CRWContentView*)contentView { |
| DCHECK(contentView); |
| DCHECK(contentView.scrollView); |
| // TODO(crbug.com/556848) Reenable DCHECK when |CRWWebControllerContainerView| |
| // is restructured so that subviews are not added during |layoutSubviews|. |
| // DCHECK([contentView.scrollView isDescendantOfView:contentView]); |
| [_containerView displayTransientContent:contentView]; |
| } |
| |
| - (id<CRWWebDelegate>)delegate { |
| return _delegate.get(); |
| } |
| |
| - (void)setDelegate:(id<CRWWebDelegate>)delegate { |
| _delegate.reset(delegate); |
| if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) |
| [self.nativeController setDelegate:self]; |
| } |
| |
| - (void)dealloc { |
| DCHECK([NSThread isMainThread]); |
| DCHECK(_isBeingDestroyed); // 'close' must have been called already. |
| DCHECK(!_webView); |
| // TODO(crbug.com/662860): Don't set the delegate to nil. |
| [_containerView setDelegate:nil]; |
| if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) { |
| [self.nativeController setDelegate:nil]; |
| } |
| _touchTrackingRecognizer.get().touchTrackingDelegate = nil; |
| [[_webViewProxy scrollViewProxy] removeObserver:self]; |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| [super dealloc]; |
| } |
| |
| - (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) |
| return; |
| |
| // Unset the delegate on the previous instance. |
| if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) |
| [self.nativeController setDelegate:nil]; |
| |
| [_containerView displayNativeContent:nativeController]; |
| [self setNativeControllerWebUsageEnabled:_webUsageEnabled]; |
| } |
| |
| - (NSDictionary*)WKWebViewObservers { |
| NSMutableDictionary* result = [NSMutableDictionary dictionary]; |
| if (base::ios::IsRunningOnIOS10OrLater()) { |
| result[@"serverTrust"] = @"webViewSecurityFeaturesDidChange"; |
| } else { |
| result[@"certificateChain"] = @"webViewSecurityFeaturesDidChange"; |
| } |
| |
| [result addEntriesFromDictionary:@{ |
| @"estimatedProgress" : @"webViewEstimatedProgressDidChange", |
| @"hasOnlySecureContent" : @"webViewSecurityFeaturesDidChange", |
| @"loading" : @"webViewLoadingStateDidChange", |
| @"title" : @"webViewTitleDidChange", |
| @"URL" : @"webViewURLDidChange", |
| }]; |
| |
| return result; |
| } |
| |
| // NativeControllerDelegate method, called to inform that title has changed. |
| - (void)nativeContent:(id)content titleDidChange:(NSString*)title { |
| [self setNavigationItemTitle:title]; |
| } |
| |
| - (void)setNativeControllerWebUsageEnabled:(BOOL)webUsageEnabled { |
| if ([self.nativeController |
| respondsToSelector:@selector(setWebUsageEnabled:)]) { |
| [self.nativeController setWebUsageEnabled:webUsageEnabled]; |
| } |
| } |
| |
| - (void)setWebUsageEnabled:(BOOL)enabled { |
| if (_webUsageEnabled == enabled) |
| return; |
| _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; |
| _touchTrackingRecognizer.reset(); |
| [self resetContainerView]; |
| } |
| } |
| } |
| |
| - (void)requirePageReconstruction { |
| [self removeWebViewAllowingCachedReconstruction:NO]; |
| } |
| |
| - (void)requirePageReload { |
| _requireReloadOnDisplay = YES; |
| } |
| |
| - (void)resetContainerView { |
| [_containerView removeFromSuperview]; |
| _containerView.reset(); |
| } |
| |
| - (void)handleLowMemory { |
| [self removeWebViewAllowingCachedReconstruction:YES]; |
| _touchTrackingRecognizer.get().touchTrackingDelegate = nil; |
| _touchTrackingRecognizer.reset(); |
| [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; |
| _touchTrackingRecognizer.reset(); |
| [self resetContainerView]; |
| } |
| } |
| } |
| |
| - (BOOL)isViewAlive { |
| return !_webProcessIsDead && [_containerView isViewAlive]; |
| } |
| |
| - (BOOL)contentIsHTML { |
| if (!_webView) |
| return NO; |
| |
| std::string MIMEType = self.webState->GetContentsMimeType(); |
| return MIMEType == "text/html" || MIMEType == "application/xhtml+xml" || |
| MIMEType == "application/xml"; |
| } |
| |
| // Stop doing stuff, especially network stuff. Close the request tracker. |
| - (void)terminateNetworkActivity { |
| DCHECK(!_isHalted); |
| _isHalted = YES; |
| |
| // Cancel all outstanding perform requests, and clear anything already queued |
| // (since this may be called from within the handling loop) to prevent any |
| // asynchronous JavaScript invocation handling from continuing. |
| [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| } |
| |
| - (void)dismissModals { |
| if ([self.nativeController respondsToSelector:@selector(dismissModals)]) |
| [self.nativeController dismissModals]; |
| } |
| |
| // Caller must reset the delegate before calling. |
| - (void)close { |
| _webStateImpl->CancelDialogs(); |
| |
| _SSLStatusUpdater.reset(); |
| |
| 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(!_isBeingDestroyed); |
| 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; |
| } |
| |
| // 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); |
| UIGraphicsBeginImageContext(frame.size); |
| [[UIColor whiteColor] setFill]; |
| CGContextFillRect(UIGraphicsGetCurrentContext(), frame); |
| |
| UIImage* result = UIGraphicsGetImageFromCurrentImageContext(); |
| UIGraphicsEndImageContext(); |
| |
| defaultImage = |
| [[result stretchableImageWithLeftCapWidth:1 topCapHeight:1] retain]; |
| } |
| return defaultImage; |
| } |
| |
| - (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 == -scrollView.contentInset.top; |
| } |
| |
| - (void)setShouldSuppressDialogs:(BOOL)shouldSuppressDialogs { |
| _shouldSuppressDialogs = shouldSuppressDialogs; |
| if (_webView) { |
| NSString* const kSetSuppressDialogs = [NSString |
| stringWithFormat:@"__gCrWeb.setSuppressGeolocationDialogs(%d);", |
| shouldSuppressDialogs]; |
| [self executeJavaScript:kSetSuppressDialogs completionHandler: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 crbug.com/326118 |
| 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) { |
| if ([self.nativeController respondsToSelector:@selector(virtualURL)]) { |
| return [self.nativeController virtualURL]; |
| } else { |
| return [self.nativeController url]; |
| } |
| } |
| web::NavigationItem* item = self.currentNavItem; |
| return item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| } |
| |
| - (WKWebView*)webView { |
| return _webView.get(); |
| } |
| |
| - (UIScrollView*)webScrollView { |
| return [_webView scrollView]; |
| } |
| |
| - (GURL)currentURL { |
| web::URLVerificationTrustLevel trustLevel = |
| web::URLVerificationTrustLevel::kNone; |
| return [self currentURLWithTrustLevel:&trustLevel]; |
| } |
| |
| - (web::Referrer)currentReferrer { |
| // Referrer string doesn't include the fragment, so in cases where the |
| // previous URL is equal to the current referrer plus the fragment the |
| // previous URL is returned as current referrer. |
| NSString* referrerString = _currentReferrerString; |
| |
| // In case of an error evaluating the JavaScript simply return empty string. |
| if ([referrerString length] == 0) |
| return web::Referrer(); |
| |
| web::NavigationItem* item = self.currentNavItem; |
| GURL navigationURL = item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| NSString* previousURLString = base::SysUTF8ToNSString(navigationURL.spec()); |
| // Check if the referrer is equal to the previous URL minus the hash symbol. |
| // L'#' is used to convert the char '#' to a unichar. |
| if ([previousURLString length] > [referrerString length] && |
| [previousURLString hasPrefix:referrerString] && |
| [previousURLString characterAtIndex:[referrerString length]] == L'#') { |
| referrerString = previousURLString; |
| } |
| // Since referrer is being extracted from the destination page, the correct |
| // policy from the origin has *already* been applied. Since the extracted URL |
| // is the post-policy value, and the source policy is no longer available, |
| // the policy is set to Always so that whatever WebKit decided to send will be |
| // re-sent when replaying the entry. |
| // TODO(stuartmorgan): When possible, get the real referrer and policy in |
| // advance and use that instead. https://crbug.com/227769. |
| return web::Referrer(GURL(base::SysNSStringToUTF8(referrerString)), |
| web::ReferrerPolicyAlways); |
| } |
| |
| - (void)pushStateWithPageURL:(const GURL&)pageURL |
| stateObject:(NSString*)stateObject |
| transition:(ui::PageTransition)transition { |
| [[self sessionController] pushNewItemWithURL:pageURL |
| stateObject:stateObject |
| transition:transition]; |
| _webStateImpl->OnSameDocumentNavigation(pageURL); |
| self.userInteractionRegistered = NO; |
| } |
| |
| - (void)replaceStateWithPageURL:(const GURL&)pageURL |
| stateObject:(NSString*)stateObject { |
| [[self sessionController] updateCurrentItemWithURL:pageURL |
| stateObject:stateObject]; |
| _webStateImpl->OnSameDocumentNavigation(pageURL); |
| } |
| |
| - (void)setDocumentURL:(const GURL&)newURL { |
| if (newURL != _documentURL && newURL.is_valid()) { |
| _documentURL = newURL; |
| _interactionRegisteredSinceLastURLChange = NO; |
| } |
| } |
| |
| - (void)setNavigationItemTitle:(NSString*)title { |
| DCHECK(title); |
| web::NavigationItem* item = |
| self.navigationManagerImpl->GetLastCommittedItem(); |
| if (!item) |
| return; |
| |
| base::string16 newTitle = base::SysNSStringToUTF16(title); |
| if (item->GetTitle() == newTitle) |
| return; |
| |
| item->SetTitle(newTitle); |
| // TODO(crbug.com/546218): See if this can be removed; it's not clear that |
| // other platforms send this (tab sync triggers need to be compared against |
| // upstream). |
| self.navigationManagerImpl->OnNavigationItemChanged(); |
| _webStateImpl->OnTitleChanged(); |
| } |
| |
| - (BOOL)isCurrentNavigationItemPOST { |
| // |_pendingNavigationInfo| will be nil if the decidePolicy* delegate methods |
| // were not called. |
| NSString* HTTPMethod = |
| _pendingNavigationInfo |
| ? [_pendingNavigationInfo HTTPMethod] |
| : [self currentBackForwardListItemHolder]->http_method(); |
| 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: |
| // Sometimes link navigation is not detected by navigation type, so |
| // check last user interaction with the page in the recent time. |
| return [self userClickedRecently]; |
| default: |
| 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; |
| } |
| |
| - (void)injectWindowID { |
| // 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]; |
| } |
| |
| - (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]; |
| DCHECK(_containerView); |
| return _containerView; |
| } |
| |
| - (id<CRWWebViewProxy>)webViewProxy { |
| return _webViewProxy.get(); |
| } |
| |
| - (UIView*)viewForPrinting { |
| // 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; |
| break; |
| case WKNavigationTypeFormSubmitted: |
| case WKNavigationTypeFormResubmitted: |
| transition = ui::PAGE_TRANSITION_FORM_SUBMIT; |
| break; |
| case WKNavigationTypeBackForward: |
| transition = ui::PAGE_TRANSITION_FORWARD_BACK; |
| break; |
| case WKNavigationTypeReload: |
| transition = ui::PAGE_TRANSITION_RELOAD; |
| break; |
| case WKNavigationTypeOther: |
| // The "Other" type covers a variety of very different cases, which may |
| // or may not be the result of user actions. For now, guess based on |
| // whether there's been an interaction since the last URL change. |
| // TODO(crbug.com/549301): See if this heuristic can be improved. |
| transition = _interactionRegisteredSinceLastURLChange |
| ? ui::PAGE_TRANSITION_LINK |
| : ui::PAGE_TRANSITION_CLIENT_REDIRECT; |
| break; |
| } |
| // 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(); |
| if (!(transition & ui::PAGE_TRANSITION_IS_REDIRECT_MASK)) { |
| // 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. |
| if (self.navigationManagerImpl->GetPendingItem()) { |
| // Update the existing pending entry. |
| // Typically on PAGE_TRANSITION_CLIENT_REDIRECT. |
| [[self sessionController] updatePendingItem:requestURL]; |
| } else { |
| // A new session history entry needs to be created. |
| self.navigationManagerImpl->AddPendingItem( |
| requestURL, referrer, transition, |
| web::NavigationInitiationType::RENDERER_INITIATED); |
| } |
| _webStateImpl->SetIsLoading(true); |
| _webStateImpl->OnProvisionalNavigationStarted(requestURL); |
| } |
| |
| - (void)updateHTML5HistoryState { |
| web::NavigationItemImpl* currentItem = self.currentNavItem; |
| if (!currentItem) |
| return; |
| |
| // Same-document navigations must trigger a popState event. |
| CRWSessionController* sessionController = self.sessionController; |
| BOOL sameDocumentNavigation = [sessionController |
| isSameDocumentNavigationBetweenItem:sessionController.currentItem |
| andItem:sessionController.previousItem]; |
| // WKWebView doesn't send hashchange events for same-document non-BFLI |
| // navigations, so one must be dispatched manually for hash change same- |
| // document navigations. |
| const GURL URL = currentItem->GetURL(); |
| web::NavigationItem* previousItem = self.sessionController.previousItem; |
| const GURL oldURL = previousItem ? previousItem->GetURL() : GURL(); |
| BOOL shouldDispatchHashchange = sameDocumentNavigation && previousItem && |
| (web::GURLByRemovingRefFromGURL(URL) == |
| web::GURLByRemovingRefFromGURL(oldURL)); |
| // The URL and state object must be set for same-document navigations and |
| // NavigationItems that were created or updated by calls to pushState() or |
| // replaceState(). |
| BOOL shouldUpdateState = sameDocumentNavigation || |
| currentItem->IsCreatedFromPushState() || |
| currentItem->HasStateBeenReplaced(); |
| if (!shouldUpdateState) |
| return; |
| |
| // TODO(stuartmorgan): Make CRWSessionController manage this internally (or |
| // remove it; it's not clear this matches other platforms' behavior). |
| self.navigationManagerImpl->OnNavigationItemCommitted(); |
| // Record that a same-document hashchange event will be fired. This flag will |
| // be reset when resonding to the hashchange message. Note that resetting the |
| // flag in the completion block below is too early, as that block is called |
| // before hashchange event listeners have a chance to fire. |
| _dispatchingSameDocumentHashChangeEvent = shouldDispatchHashchange; |
| // Inject the JavaScript to update the state on the browser side. |
| [self injectHTML5HistoryScriptWithHashChange:shouldDispatchHashchange |
| sameDocumentNavigation:sameDocumentNavigation]; |
| } |
| |
| - (NSString*)javaScriptToReplaceWebViewURL:(const GURL&)URL |
| stateObjectJSON:(NSString*)stateObject { |
| std::string outURL; |
| base::EscapeJSONString(URL.spec(), true, &outURL); |
| return |
| [NSString stringWithFormat:@"__gCrWeb.replaceWebViewURL(%@, %@);", |
| base::SysUTF8ToNSString(outURL), stateObject]; |
| } |
| |
| - (NSString*)javaScriptToDispatchPopStateWithObject:(NSString*)stateObjectJSON { |
| std::string outState; |
| base::EscapeJSONString(base::SysNSStringToUTF8(stateObjectJSON), true, |
| &outState); |
| return [NSString stringWithFormat:@"__gCrWeb.dispatchPopstateEvent(%@);", |
| base::SysUTF8ToNSString(outState)]; |
| } |
| |
| - (NSString*)javaScriptToDispatchHashChangeWithOldURL:(const GURL&)oldURL |
| newURL:(const GURL&)newURL { |
| return [NSString |
| stringWithFormat:@"__gCrWeb.dispatchHashchangeEvent(\'%s\', \'%s\');", |
| oldURL.spec().c_str(), newURL.spec().c_str()]; |
| } |
| |
| - (void)injectHTML5HistoryScriptWithHashChange:(BOOL)dispatchHashChange |
| sameDocumentNavigation:(BOOL)sameDocumentNavigation { |
| web::NavigationItemImpl* currentItem = self.currentNavItem; |
| if (!currentItem) |
| return; |
| |
| const GURL URL = currentItem->GetURL(); |
| NSString* stateObject = currentItem->GetSerializedStateObject(); |
| NSMutableString* script = [NSMutableString |
| stringWithString:[self javaScriptToReplaceWebViewURL:URL |
| stateObjectJSON:stateObject]]; |
| if (sameDocumentNavigation) { |
| [script |
| appendString:[self javaScriptToDispatchPopStateWithObject:stateObject]]; |
| } |
| if (dispatchHashChange) { |
| web::NavigationItemImpl* previousItem = self.sessionController.previousItem; |
| const GURL oldURL = previousItem ? previousItem->GetURL() : GURL(); |
| [script appendString:[self javaScriptToDispatchHashChangeWithOldURL:oldURL |
| newURL:URL]]; |
| } |
| base::WeakNSObject<CRWWebController> weakSelf(self); |
| [self executeJavaScript:script |
| completionHandler:^(id, NSError*) { |
| if (!weakSelf || weakSelf.get()->_isBeingDestroyed) |
| return; |
| base::scoped_nsobject<CRWWebController> strongSelf([weakSelf retain]); |
| strongSelf.get()->_URLOnStartLoading = URL; |
| strongSelf.get()->_lastRegisteredRequestURL = URL; |
| }]; |
| } |
| |
| // 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]); |
| |
| web::NavigationItem* item = self.currentNavItem; |
| GURL targetURL = item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| // Load the url. The UIWebView delegate callbacks take care of updating the |
| // session history and UI. |
| if (!targetURL.is_valid()) { |
| [self didFinishWithURL:targetURL loadSuccess:NO]; |
| return; |
| } |
| |
| // JavaScript should never be evaluated here. User-entered JS should be |
| // evaluated via stringByEvaluatingUserJavaScriptFromString. |
| DCHECK(!targetURL.SchemeIs(url::kJavaScriptScheme)); |
| |
| [self ensureWebViewCreated]; |
| |
| [self loadRequestForCurrentNavigationItem]; |
| } |
| |
| - (void)updatePendingNavigationInfoFromNavigationAction: |
| (WKNavigationAction*)action { |
| if (action.targetFrame.mainFrame) { |
| _pendingNavigationInfo.reset( |
| [[CRWWebControllerPendingNavigationInfo alloc] init]); |
| [_pendingNavigationInfo |
| setReferrer:[self referrerFromNavigationAction:action]]; |
| [_pendingNavigationInfo setNavigationType:action.navigationType]; |
| [_pendingNavigationInfo setHTTPMethod:action.request.HTTPMethod]; |
| } |
| } |
| |
| - (void)updatePendingNavigationInfoFromNavigationResponse: |
| (WKNavigationResponse*)response { |
| if (response.isForMainFrame) { |
| if (!_pendingNavigationInfo) { |
| _pendingNavigationInfo.reset( |
| [[CRWWebControllerPendingNavigationInfo alloc] init]); |
| } |
| [_pendingNavigationInfo setMIMEType:response.response.MIMEType]; |
| } |
| } |
| |
| - (void)commitPendingNavigationInfo { |
| if ([_pendingNavigationInfo referrer]) { |
| _currentReferrerString.reset([[_pendingNavigationInfo referrer] copy]); |
| } |
| if ([_pendingNavigationInfo MIMEType]) { |
| self.webStateImpl->SetContentsMimeType( |
| base::SysNSStringToUTF8([_pendingNavigationInfo MIMEType])); |
| } |
| [self updateCurrentBackForwardListItemHolder]; |
| |
| _pendingNavigationInfo.reset(); |
| } |
| |
| - (NSMutableURLRequest*)requestForCurrentNavigationItem { |
| web::NavigationItem* item = self.currentNavItem; |
| const GURL currentNavigationURL = |
| item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| NSMutableURLRequest* request = [NSMutableURLRequest |
| requestWithURL:net::NSURLWithGURL(currentNavigationURL)]; |
| const web::Referrer referrer(self.currentNavItemReferrer); |
| if (referrer.url.is_valid()) { |
| std::string referrerValue = |
| web::ReferrerHeaderValueForNavigation(currentNavigationURL, referrer); |
| if (!referrerValue.empty()) { |
| [request setValue:base::SysUTF8ToNSString(referrerValue) |
| forHTTPHeaderField:kReferrerHeaderName]; |
| } |
| } |
| |
| // If there are headers in the current session entry add them to |request|. |
| // Headers that would overwrite fields already present in |request| are |
| // skipped. |
| NSDictionary* headers = self.currentHTTPHeaders; |
| for (NSString* headerName in headers) { |
| if (![request valueForHTTPHeaderField:headerName]) { |
| [request setValue:[headers objectForKey:headerName] |
| forHTTPHeaderField:headerName]; |
| } |
| } |
| |
| return request; |
| } |
| |
| - (web::WKBackForwardListItemHolder*)currentBackForwardListItemHolder { |
| web::NavigationItem* item = self.currentNavItem; |
| DCHECK(item); |
| web::WKBackForwardListItemHolder* holder = |
| web::WKBackForwardListItemHolder::FromNavigationItem(item); |
| DCHECK(holder); |
| return holder; |
| } |
| |
| - (void)updateCurrentBackForwardListItemHolder { |
| // WebUI pages (which are loaded via loadHTMLString:baseURL:) have no entry |
| // in the back/forward list, so the current item will still be the previous |
| // page, and should not be associated. |
| if (_webUIManager) |
| return; |
| |
| web::WKBackForwardListItemHolder* holder = |
| [self currentBackForwardListItemHolder]; |
| |
| WKNavigationType navigationType = |
| _pendingNavigationInfo ? [_pendingNavigationInfo navigationType] |
| : WKNavigationTypeOther; |
| holder->set_back_forward_list_item([_webView backForwardList].currentItem); |
| holder->set_navigation_type(navigationType); |
| holder->set_http_method([_pendingNavigationInfo HTTPMethod]); |
| |
| // Only update the MIME type in the holder if there was MIME type information |
| // as part of this pending load. It will be nil when doing a fast |
| // back/forward navigation, for instance, because the callback that would |
| // populate it is not called in that flow. |
| if ([_pendingNavigationInfo MIMEType]) |
| holder->set_mime_type([_pendingNavigationInfo MIMEType]); |
| } |
| |
| - (void)loadNativeViewWithSuccess:(BOOL)loadSuccess { |
| const GURL currentURL([self currentURL]); |
| [self didStartLoadingURL:currentURL]; |
| _loadPhase = web::PAGE_LOADED; |
| if (loadSuccess) { |
| _webStateImpl->OnNavigationCommitted(currentURL); |
| } else { |
| _webStateImpl->OnErrorPageNavigation(currentURL); |
| } |
| |
| // Perform post-load-finished updates. |
| [self didFinishWithURL:currentURL loadSuccess:loadSuccess]; |
| |
| NSString* title = [self.nativeController title]; |
| if (title) { |
| [self setNavigationItemTitle:title]; |
| } |
| |
| if ([self.nativeController respondsToSelector:@selector(setDelegate:)]) { |
| [self.nativeController setDelegate:self]; |
| } |
| } |
| |
| - (void)loadErrorInNativeView:(NSError*)error { |
| [self removeWebViewAllowingCachedReconstruction:NO]; |
| web::NavigationItem* item = self.currentNavItem; |
| const GURL currentURL = item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| |
| if (web::IsWKWebViewSSLCertError(error)) { |
| // This could happen only if certificate is absent or could not be parsed. |
| error = web::NetErrorFromError(error, net::ERR_SSL_SERVER_CERT_BAD_FORMAT); |
| #if defined(DEBUG) |
| net::SSLInfo info; |
| web::GetSSLInfoFromWKWebViewSSLCertError(error, &info); |
| CHECK(!error.cert); |
| #endif |
| } else { |
| error = web::NetErrorFromError(error); |
| } |
| |
| BOOL isPost = [self isCurrentNavigationItemPOST]; |
| [self setNativeController:[_nativeProvider controllerForURL:currentURL |
| withError:error |
| isPost:isPost]]; |
| [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]; |
| |
| web::NavigationItem* item = self.currentNavItem; |
| const GURL targetURL = item ? item->GetURL() : GURL::EmptyGURL(); |
| const web::Referrer referrer; |
| id<CRWNativeContent> nativeContent = |
| [_nativeProvider controllerForURL:targetURL webState:self.webState]; |
| // Unlike the WebView case, always create a new controller and view. |
| // TODO(pinkerton): What to do if this does return nil? |
| [self setNativeController:nativeContent]; |
| if ([nativeContent respondsToSelector:@selector(virtualURL)]) { |
| item->SetVirtualURL([nativeContent virtualURL]); |
| } |
| |
| [self registerLoadRequest:targetURL |
| referrer:referrer |
| 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. |
| |
| [_delegate webWillInitiateLoadWithParams:params]; |
| |
| GURL navUrl = params.url; |
| ui::PageTransition transition = params.transition_type; |
| DCHECK(!(transition & ui::PAGE_TRANSITION_FORWARD_BACK)); |
| // This method is allowed to handle reload only for transient items, which |
| // is essentially loading the same URL again. |
| DCHECK(!(transition & ui::PAGE_TRANSITION_RELOAD) || |
| self.navigationManagerImpl->GetTransientItem()); |
| |
| BOOL initialNavigation = NO; |
| // 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]; |
| |
| [self recordStateInHistory]; |
| |
| if (!self.currentNavItem) |
| initialNavigation = YES; |
| |
| web::NavigationInitiationType navigationInitiationType = |
| params.is_renderer_initiated |
| ? web::NavigationInitiationType::RENDERER_INITIATED |
| : web::NavigationInitiationType::USER_INITIATED; |
| self.navigationManagerImpl->AddPendingItem( |
| navUrl, params.referrer, transition, navigationInitiationType); |
| |
| web::NavigationItemImpl* addedItem = self.currentNavItem; |
| DCHECK(addedItem); |
| if (params.extra_headers) |
| addedItem->AddHttpRequestHeaders(params.extra_headers); |
| if (params.post_data) { |
| DCHECK([addedItem->GetHttpRequestHeaders() objectForKey:@"Content-Type"]) |
| << "Post data should have an associated content type"; |
| addedItem->SetPostData(params.post_data); |
| addedItem->SetShouldSkipRepostFormConfirmation(true); |
| } |
| |
| [_delegate webDidUpdateSessionForLoadWithParams:params |
| wasInitialNavigation:initialNavigation]; |
| |
| [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) |
| return; |
| |
| // Reset current WebUI if one exists. |
| [self clearWebUI]; |
| |
| // Abort any outstanding page load. This ensures the delegate gets informed |
| // about the outgoing page, and further messages from the page are suppressed. |
| if (_loadPhase != web::PAGE_LOADED) |
| [self abortLoad]; |
| |
| DCHECK(!_isHalted); |
| // Remove the transient content view. |
| [self clearTransientContentView]; |
| |
| web::NavigationItem* item = self.currentNavItem; |
| const GURL currentURL = item ? item->GetURL() : GURL::EmptyGURL(); |
| // If it's a chrome URL, but not a native one, create the WebUI instance. |
| if (web::GetWebClient()->IsAppSpecificURL(currentURL) && |
| ![_nativeProvider hasControllerForURL:currentURL]) { |
| if (!(item->GetTransitionType() & ui::PAGE_TRANSITION_TYPED || |
| item->GetTransitionType() & ui::PAGE_TRANSITION_AUTO_BOOKMARK) && |
| self.hasOpener) { |
| // WebUI URLs can not be opened by DOM to prevent cross-site scripting as |
| // they have increased power. WebUI URLs may only be opened when the user |
| // types in the URL or use bookmarks. |
| [[self sessionController] discardNonCommittedItems]; |
| return; |
| } else { |
| [self createWebUIForURL:currentURL]; |
| } |
| } |
| |
| // Loading a new url, must check here if it's a native chrome URL and |
| // replace the appropriate view if so, or transition back to a web view from |
| // a native view. |
| if ([self shouldLoadURLInNativeView:currentURL]) { |
| [self loadCurrentURLInNativeView]; |
| } else { |
| [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 { |
| DCHECK(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) && |
| !_webStateImpl->HasWebUI(); |
| } |
| |
| - (void)triggerPendingLoad { |
| if (!_containerView) { |
| DCHECK(!_isBeingDestroyed); |
| // 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. |
| _containerView.reset( |
| [[CRWWebControllerContainerView alloc] initWithDelegate:self]); |
| |
| // This will be resized later, but matching the final frame will minimize |
| // re-rendering. Use the screen size because the application's key window |
| // may still be nil. |
| // TODO(crbug.com/688259): Stop subtracting status bar height. |
| CGFloat statusBarHeight = |
| [[UIApplication sharedApplication] statusBarFrame].size.height; |
| CGRect containerViewFrame = [UIScreen mainScreen].bounds; |
| containerViewFrame.origin.y += statusBarHeight; |
| containerViewFrame.size.height -= statusBarHeight; |
| _containerView.get().frame = containerViewFrame; |
| DCHECK(!CGRectIsEmpty(_containerView.get().frame)); |
| |
| // TODO(crbug.com/691116): Remove this workaround once tests are no longer |
| // dependent upon this accessibility ID. |
| if (!base::ios::IsRunningOnIOS10OrLater()) |
| [_containerView setAccessibilityIdentifier:@"Container View"]; |
| |
| [_containerView addGestureRecognizer:[self touchTrackingRecognizer]]; |
| // Is |currentUrl| a web scheme or native chrome scheme. |
| web::NavigationItem* item = self.currentNavItem; |
| const GURL currentNavigationURL = |
| item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| BOOL isChromeScheme = |
| web::GetWebClient()->IsAppSpecificURL(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. |
| web::NavigationItem* item = self.navigationManagerImpl->GetVisibleItem(); |
| return (transition & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) && item && |
| (destinationURL == item->GetURL() || |
| destinationURL == item->GetOriginalRequestURL()); |
| } |
| |
| // Reload either the web view or the native content depending on which is |
| // displayed. |
| - (void)reloadInternal { |
| // Clear last user interaction. |
| // TODO(crbug.com/546337): Move to after the load commits, in the subclass |
| // implementation. This will be inaccurate if the reload fails or is |
| // cancelled. |
| _lastUserInteraction.reset(); |
| base::RecordAction(UserMetricsAction("Reload")); |
| if ([self shouldLoadURLInNativeView:self.currentNavItem->GetURL()]) { |
| [self.nativeController reload]; |
| } else { |
| web::NavigationItem* transientItem = |
| self.navigationManagerImpl->GetTransientItem(); |
| if (transientItem) { |
| // If there's a transient item, a reload is considered a new navigation to |
| // the transient item's URL (as on other platforms). |
| NavigationManager::WebLoadParams reloadParams(transientItem->GetURL()); |
| reloadParams.transition_type = ui::PAGE_TRANSITION_RELOAD; |
| reloadParams.extra_headers.reset( |
| [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]; |
| } |
| } |
| } |
| |
| - (void)reload { |
| [_delegate webWillReload]; |
| [self reloadInternal]; |
| } |
| |
| - (void)abortLoad { |
| [_webView stopLoading]; |
| [_pendingNavigationInfo setCancelled:YES]; |
| _certVerificationErrors->Clear(); |
| [self loadCancelled]; |
| } |
| |
| - (void)loadCancelled { |
| [_passKitDownloader cancelPendingDownload]; |
| if (_loadPhase != web::PAGE_LOADED) { |
| _loadPhase = web::PAGE_LOADED; |
| if (!_isHalted) { |
| _webStateImpl->SetIsLoading(false); |
| } |
| } |
| } |
| |
| - (void)goToItemAtIndex:(int)index { |
| CRWSessionController* sessionController = self.sessionController; |
| const web::ScopedNavigationItemImplList& items = sessionController.items; |
| if (index < 0 || index >= static_cast<int>(items.size())) { |
| NOTREACHED(); |
| return; |
| } |
| |
| if (!_webStateImpl->IsShowingWebInterstitial()) |
| [self recordStateInHistory]; |
| |
| [self clearTransientContentView]; |
| |
| // Update the user agent before attempting the navigation. |
| web::NavigationItem* toItem = items[index].get(); |
| web::NavigationItem* previousItem = sessionController.currentItem; |
| web::UserAgentType previousUserAgentType = |
| previousItem ? previousItem->GetUserAgentType() |
| : web::UserAgentType::NONE; |
| [self updateDesktopUserAgentForItem:toItem |
| previousUserAgentType:previousUserAgentType]; |
| |
| BOOL sameDocumentNavigation = |
| [sessionController isSameDocumentNavigationBetweenItem:previousItem |
| andItem:toItem]; |
| if (sameDocumentNavigation) { |
| [sessionController goToItemAtIndex:index]; |
| [self updateHTML5HistoryState]; |
| } else { |
| [sessionController discardNonCommittedItems]; |
| [sessionController setPendingItemIndex:index]; |
| |
| web::NavigationItemImpl* pendingItem = sessionController.pendingItem; |
| pendingItem->SetTransitionType(ui::PageTransitionFromInt( |
| pendingItem->GetTransitionType() | ui::PAGE_TRANSITION_FORWARD_BACK)); |
| |
| [self loadCurrentURL]; |
| } |
| } |
| |
| - (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) |
| return; |
| [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) |
| return; |
| |
| DCHECK(_webView); |
| |
| 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()) { |
| action(); |
| } |
| [_pendingLoadCompleteActions removeAllObjects]; |
| } |
| |
| - (void)didFinishWithURL:(const GURL&)currentURL loadSuccess:(BOOL)loadSuccess { |
| DCHECK(_loadPhase == web::PAGE_LOADED); |
| // Rather than creating a new WKBackForwardListItem when loading WebUI pages, |
| // WKWebView will cache the WebUI HTML in the previous WKBackForwardListItem |
| // since it's loaded via |-loadHTML:forURL:| instead of an NSURLRequest. As a |
| // result, the WebUI's HTML and URL will be loaded when navigating to that |
| // WKBackForwardListItem, causing a mismatch between the visible content and |
| // the visible URL (WebUI page will be visible, but URL will be the previous |
| // page's URL). To prevent this potential URL spoofing vulnerability, reset |
| // the previous NavigationItem's WKBackForwardListItem to force loading via |
| // NSURLRequest. |
| if (_webUIManager) { |
| web::NavigationItem* lastNavigationItem = |
| self.sessionController.previousItem; |
| if (lastNavigationItem) { |
| web::WKBackForwardListItemHolder* holder = |
| web::WKBackForwardListItemHolder::FromNavigationItem( |
| lastNavigationItem); |
| DCHECK(holder); |
| holder->set_back_forward_list_item(nil); |
| } |
| } |
| |
| [self restoreStateFromHistory]; |
| _webStateImpl->SetIsLoading(false); |
| _webStateImpl->OnPageLoaded(currentURL, loadSuccess); |
| } |
| |
| - (void)goDelta:(int)delta { |
| if (_isBeingDestroyed) |
| return; |
| |
| if (delta == 0) { |
| [self reload]; |
| return; |
| } |
| |
| if (self.navigationManagerImpl->CanGoToOffset(delta)) { |
| NSInteger index = self.navigationManagerImpl->GetIndexForOffset(delta); |
| [self goToItemAtIndex:index]; |
| } |
| } |
| |
| - (void)addGestureRecognizerToWebView:(UIGestureRecognizer*)recognizer { |
| if ([_gestureRecognizers containsObject:recognizer]) |
| return; |
| |
| [_webView addGestureRecognizer:recognizer]; |
| [_gestureRecognizers addObject:recognizer]; |
| } |
| |
| - (void)removeGestureRecognizerFromWebView:(UIGestureRecognizer*)recognizer { |
| if (![_gestureRecognizers containsObject:recognizer]) |
| return; |
| |
| [_webView removeGestureRecognizer:recognizer]; |
| [_gestureRecognizers removeObject:recognizer]; |
| } |
| |
| - (void)addToolbarViewToWebView:(UIView*)toolbarView { |
| DCHECK(toolbarView); |
| if ([_webViewToolbars containsObject:toolbarView]) |
| return; |
| [_webViewToolbars addObject:toolbarView]; |
| if (_webView) |
| [_containerView addToolbar:toolbarView]; |
| } |
| |
| - (void)removeToolbarViewFromWebView:(UIView*)toolbarView { |
| if (![_webViewToolbars containsObject:toolbarView]) |
| return; |
| [_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.hasOpener && !_userInteractedWithWebController; |
| BOOL noNavigationItems = !(self.navigationManagerImpl->GetItemCount()); |
| return rendererInitiatedWithoutInteraction || noNavigationItems; |
| } |
| |
| - (BOOL)usesDesktopUserAgent { |
| web::NavigationItem* item = self.currentNavItem; |
| return item && item->GetUserAgentType() == web::UserAgentType::DESKTOP; |
| } |
| |
| - (web::MojoFacade*)mojoFacade { |
| if (!_mojoFacade) { |
| service_manager::mojom::InterfaceProvider* interfaceProvider = |
| _webStateImpl->GetMojoInterfaceRegistry(); |
| _mojoFacade.reset(new web::MojoFacade(interfaceProvider, self)); |
| } |
| return _mojoFacade.get(); |
| } |
| |
| - (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) { |
| return; |
| } |
| // 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] |
| initWithContextGetter:browserState->GetRequestContext() |
| completionHandler:passKitCompletion]); |
| return _passKitDownloader.get(); |
| } |
| |
| - (void)updateDesktopUserAgentForItem:(web::NavigationItem*)item |
| previousUserAgentType:(web::UserAgentType)userAgentType { |
| if (!item) |
| return; |
| web::UserAgentType itemUserAgentType = item->GetUserAgentType(); |
| if (itemUserAgentType == web::UserAgentType::NONE) |
| return; |
| if (itemUserAgentType != userAgentType) |
| [self requirePageReconstruction]; |
| } |
| |
| #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)executeJavaScript:(NSString*)script |
| completionHandler:(web::JavaScriptResultBlock)completionHandler { |
| NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script]; |
| web::ExecuteJavaScript(_webView, safeScript, completionHandler); |
| } |
| |
| - (BOOL)scriptHasBeenInjectedForClass:(Class)injectionManagerClass { |
| return [_injectedScriptManagers containsObject:injectionManagerClass]; |
| } |
| |
| - (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass { |
| DCHECK(script.length); |
| // Script execution is an asynchronous operation which may pass sensitive |
| // data to the page. executeJavaScript:completionHandler makes sure that |
| // receiver page did not change by checking its window id. |
| // |[_webView executeJavaScript:completionHandler:]| is not used here because |
| // it does not check that page is the same. |
| [self executeJavaScript:script completionHandler:nil]; |
| [_injectedScriptManagers addObject:JSInjectionManagerClass]; |
| } |
| |
| #pragma mark - |
| |
| - (void)executeUserJavaScript:(NSString*)script |
| completionHandler:(web::JavaScriptResultBlock)completion { |
| // For security reasons, executing JavaScript on pages with app-specific URLs |
| // is not allowed, because those pages may have elevated privileges. |
| GURL lastCommittedURL = self.webState->GetLastCommittedURL(); |
| if (web::GetWebClient()->IsAppSpecificURL(lastCommittedURL)) { |
| if (completion) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| base::scoped_nsobject<NSError> error([[NSError alloc] |
| initWithDomain:web::kJSEvaluationErrorDomain |
| code:web::JS_EVALUATION_ERROR_CODE_NO_WEB_VIEW |
| userInfo:nil]); |
| completion(nil, error); |
| }); |
| } |
| return; |
| } |
| |
| [self setUserInteractionRegistered:YES]; |
| [self executeJavaScript:script completionHandler:completion]; |
| } |
| |
| - (BOOL)respondToMessage:(base::DictionaryValue*)message |
| userIsInteracting:(BOOL)userIsInteracting |
| 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]); |
| DCHECK(handlerImplementation); |
| NSMutableDictionary* context = |
| [NSMutableDictionary dictionaryWithObject:@(userIsInteracting) |
| forKey:kUserIsInteractingKey]; |
| NSURL* originNSURL = net::NSURLWithGURL(originURL); |
| if (originNSURL) |
| context[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"] = |
| @selector(handleAddPluginPlaceholdersMessage:context:); |
| (*handlers)["chrome.send"] = @selector(handleChromeSendMessage:context:); |
| (*handlers)["console"] = @selector(handleConsoleMessage:context:); |
| (*handlers)["geolocationDialog.suppressed"] = |
| @selector(handleGeolocationDialogSuppressedMessage:context:); |
| (*handlers)["document.favicons"] = |
| @selector(handleDocumentFaviconsMessage:context:); |
| (*handlers)["document.submit"] = |
| @selector(handleDocumentSubmitMessage:context:); |
| (*handlers)["form.activity"] = |
| @selector(handleFormActivityMessage:context:); |
| (*handlers)["navigator.credentials.request"] = |
| @selector(handleCredentialsRequestedMessage:context:); |
| (*handlers)["navigator.credentials.notifySignedIn"] = |
| @selector(handleSignedInMessage:context:); |
| (*handlers)["navigator.credentials.notifySignedOut"] = |
| @selector(handleSignedOutMessage:context:); |
| (*handlers)["navigator.credentials.notifyFailedSignIn"] = |
| @selector(handleSignInFailedMessage:context:); |
| (*handlers)["window.error"] = @selector(handleWindowErrorMessage:context:); |
| (*handlers)["window.hashchange"] = |
| @selector(handleWindowHashChangeMessage:context:); |
| (*handlers)["window.history.back"] = |
| @selector(handleWindowHistoryBackMessage:context:); |
| (*handlers)["window.history.willChangeState"] = |
| @selector(handleWindowHistoryWillChangeStateMessage:context:); |
| (*handlers)["window.history.didPushState"] = |
| @selector(handleWindowHistoryDidPushStateMessage:context:); |
| (*handlers)["window.history.didReplaceState"] = |
| @selector(handleWindowHistoryDidReplaceStateMessage:context:); |
| (*handlers)["window.history.forward"] = |
| @selector(handleWindowHistoryForwardMessage:context:); |
| (*handlers)["window.history.go"] = |
| @selector(handleWindowHistoryGoMessage:context:); |
| }); |
| DCHECK(handlers); |
| auto iter = handlers->find(command); |
| return iter != handlers->end() ? iter->second : nullptr; |
| } |
| |
| - (void)didReceiveScriptMessage:(WKScriptMessage*)message { |
| // Broken out into separate method to catch errors. |
| if (![self respondToWKScriptMessage:message]) { |
| DLOG(WARNING) << "Message from JS not handled due to invalid format"; |
| } |
| } |
| |
| - (NSString*)scriptByAddingWindowIDCheckForScript:(NSString*)script { |
| NSString* kTemplate = @"if (__gCrWeb['windowId'] === '%@') { %@; }"; |
| return [NSString |
| stringWithFormat:kTemplate, [_windowIDJSManager windowID], script]; |
| } |
| |
| - (BOOL)respondToWKScriptMessage:(WKScriptMessage*)scriptMessage { |
| if (!scriptMessage.frameInfo.mainFrame) { |
| // Messages from iframes are not currently supported. |
| return NO; |
| } |
| |
| std::unique_ptr<base::Value> messageAsValue = |
| web::ValueResultFromWKResult(scriptMessage.body); |
| base::DictionaryValue* message = nullptr; |
| if (!messageAsValue || !messageAsValue->GetAsDictionary(&message)) { |
| return NO; |
| } |
| std::string windowID; |
| message->GetString("crwWindowId", &windowID); |
| // Check for correct windowID |
| if (base::SysNSStringToUTF8([_windowIDJSManager windowID]) != windowID) { |
| DLOG(WARNING) << "Message from JS ignored due to non-matching windowID: " << |
| [_windowIDJSManager windowID] |
| << " != " << base::SysUTF8ToNSString(windowID); |
| return NO; |
| } |
| base::DictionaryValue* command = nullptr; |
| if (!message->GetDictionary("crwCommand", &command)) { |
| return NO; |
| } |
| if ([scriptMessage.name isEqualToString:kScriptMessageName]) { |
| return [self respondToMessage:command |
| userIsInteracting:[self userIsInteracting] |
| originURL:net::GURLWithNSURL([_webView URL])]; |
| } |
| |
| NOTREACHED(); |
| 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. |
| [[_jsInjectionReceiver |
| 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; |
| } |
| _webStateImpl->OnScriptCommandReceived( |
| messageContent, *message, currentURL, context[kUserIsInteractingKey]); |
| _webStateImpl->ProcessWebUIMessage(currentURL, messageContent, |
| *arguments); |
| return YES; |
| } |
| } |
| |
| DLOG(WARNING) |
| << "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: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 { |
| _webStateImpl->OnDialogSuppressed(); |
| 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)) { |
| DLOG(WARNING) << "JS message parameter not found: rel"; |
| return NO; |
| } |
| web::FaviconURL::IconType icon_type = web::FaviconURL::FAVICON; |
| if (rel == "apple-touch-icon") |
| icon_type = web::FaviconURL::TOUCH_ICON; |
| else if (rel == "apple-touch-icon-precomposed") |
| icon_type = web::FaviconURL::TOUCH_PRECOMPOSED_ICON; |
| urls.push_back( |
| web::FaviconURL(GURL(href), icon_type, std::vector<gfx::Size>())); |
| } |
| if (!urls.empty()) |
| _webStateImpl->OnFaviconUrlUpdated(urls); |
| return YES; |
| } |
| |
| - (BOOL)handleDocumentSubmitMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| std::string href; |
| if (!message->GetString("href", &href)) { |
| DLOG(WARNING) << "JS message parameter not found: href"; |
| return NO; |
| } |
| const GURL targetURL(href); |
| const GURL currentURL([self currentURL]); |
| bool targetsFrame = false; |
| message->GetBoolean("targetsFrame", &targetsFrame); |
| if (!targetsFrame && web::UrlHasWebScheme(targetURL)) { |
| // The referrer is not known yet, and will be updated later. |
| const web::Referrer emptyReferrer; |
| [self registerLoadRequest:targetURL |
| referrer:emptyReferrer |
| transition:ui::PAGE_TRANSITION_FORM_SUBMIT]; |
| } |
| std::string formName; |
| message->GetString("formName", &formName); |
| base::scoped_nsobject<NSSet> observers([_observers copy]); |
| // We decide the form is user-submitted if the user has interacted with |
| // the main page (using logic from the popup blocker), or if the keyboard |
| // is visible. |
| BOOL submittedByUser = [context[kUserIsInteractingKey] boolValue] || |
| [_webViewProxy keyboardAccessory]; |
| _webStateImpl->OnDocumentSubmitted(formName, submittedByUser); |
| return YES; |
| } |
| |
| - (BOOL)handleFormActivityMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| std::string formName; |
| std::string fieldName; |
| std::string type; |
| std::string value; |
| bool inputMissing = false; |
| if (!message->GetString("formName", &formName) || |
| !message->GetString("fieldName", &fieldName) || |
| !message->GetString("type", &type) || |
| !message->GetString("value", &value)) { |
| inputMissing = true; |
| } |
| |
| _webStateImpl->OnFormActivityRegistered(formName, fieldName, type, value, |
| inputMissing); |
| return YES; |
| } |
| |
| - (BOOL)handleCredentialsRequestedMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| double request_id = -1; |
| if (!message->GetDouble("requestId", &request_id)) { |
| DLOG(WARNING) << "JS message parameter not found: requestId"; |
| return NO; |
| } |
| bool unmediated = false; |
| if (!message->GetBoolean("unmediated", &unmediated)) { |
| DLOG(WARNING) << "JS message parameter not found: unmediated"; |
| return NO; |
| } |
| base::ListValue* federations_value = nullptr; |
| if (!message->GetList("federations", &federations_value)) { |
| DLOG(WARNING) << "JS message parameter not found: federations"; |
| return NO; |
| } |
| std::vector<std::string> federations; |
| for (const auto& federation_value : *federations_value) { |
| std::string federation; |
| if (!federation_value->GetAsString(&federation)) { |
| DLOG(WARNING) << "JS message parameter 'federations' contains wrong type"; |
| return NO; |
| } |
| federations.push_back(federation); |
| } |
| DCHECK(context[kUserIsInteractingKey]); |
| _webStateImpl->OnCredentialsRequested( |
| static_cast<int>(request_id), net::GURLWithNSURL(context[kOriginURLKey]), |
| unmediated, federations, [context[kUserIsInteractingKey] boolValue]); |
| return YES; |
| } |
| |
| - (BOOL)handleSignedInMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| double request_id = -1; |
| if (!message->GetDouble("requestId", &request_id)) { |
| DLOG(WARNING) << "JS message parameter not found: requestId"; |
| return NO; |
| } |
| base::DictionaryValue* credential_data = nullptr; |
| web::Credential credential; |
| if (message->GetDictionary("credential", &credential_data)) { |
| if (!web::DictionaryValueToCredential(*credential_data, &credential)) { |
| DLOG(WARNING) << "JS message parameter 'credential' is invalid"; |
| return NO; |
| } |
| _webStateImpl->OnSignedIn(static_cast<int>(request_id), |
| net::GURLWithNSURL(context[kOriginURLKey]), |
| credential); |
| } else { |
| _webStateImpl->OnSignedIn(static_cast<int>(request_id), |
| net::GURLWithNSURL(context[kOriginURLKey])); |
| } |
| return YES; |
| } |
| |
| - (BOOL)handleSignedOutMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| double request_id = -1; |
| if (!message->GetDouble("requestId", &request_id)) { |
| DLOG(WARNING) << "JS message parameter not found: requestId"; |
| return NO; |
| } |
| _webStateImpl->OnSignedOut(static_cast<int>(request_id), |
| net::GURLWithNSURL(context[kOriginURLKey])); |
| return YES; |
| } |
| |
| - (BOOL)handleSignInFailedMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| double request_id = -1; |
| if (!message->GetDouble("requestId", &request_id)) { |
| DLOG(WARNING) << "JS message parameter not found: requestId"; |
| return NO; |
| } |
| base::DictionaryValue* credential_data = nullptr; |
| web::Credential credential; |
| if (message->GetDictionary("credential", &credential_data)) { |
| if (!web::DictionaryValueToCredential(*credential_data, &credential)) { |
| DLOG(WARNING) << "JS message parameter 'credential' is invalid"; |
| return NO; |
| } |
| _webStateImpl->OnSignInFailed(static_cast<int>(request_id), |
| net::GURLWithNSURL(context[kOriginURLKey]), |
| credential); |
| } else { |
| _webStateImpl->OnSignInFailed(static_cast<int>(request_id), |
| net::GURLWithNSURL(context[kOriginURLKey])); |
| } |
| return YES; |
| } |
| |
| - (BOOL)handleWindowErrorMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| std::string errorMessage; |
| if (!message->GetString("message", &errorMessage)) { |
| DLOG(WARNING) << "JS message parameter not found: message"; |
| return NO; |
| } |
| DLOG(ERROR) << "JavaScript error: " << errorMessage |
| << " URL:" << [self currentURL].spec(); |
| return YES; |
| } |
| |
| - (BOOL)handleWindowHashChangeMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| // Because hash changes don't trigger |-didFinishNavigation|, fetch favicons |
| // for the new page manually. |
| [self executeJavaScript:@"__gCrWeb.sendFaviconsToHost();" |
| completionHandler:nil]; |
| |
| // Record that the current NavigationItem was created by a hash change, but |
| // ignore hashchange events that are manually dispatched for same-document |
| // navigations. |
| if (_dispatchingSameDocumentHashChangeEvent) { |
| _dispatchingSameDocumentHashChangeEvent = NO; |
| } else { |
| web::NavigationItemImpl* item = self.currentNavItem; |
| DCHECK(item); |
| item->SetIsCreatedFromHashChange(true); |
| } |
| |
| return YES; |
| } |
| |
| - (BOOL)handleWindowHistoryBackMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| [self goDelta:-1]; |
| return YES; |
| } |
| |
| - (BOOL)handleWindowHistoryForwardMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| [self goDelta:1]; |
| return YES; |
| } |
| |
| - (BOOL)handleWindowHistoryGoMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| double delta = 0; |
| if (message->GetDouble("value", &delta)) { |
| [self goDelta:static_cast<int>(delta)]; |
| return YES; |
| } |
| return NO; |
| } |
| |
| - (BOOL)handleWindowHistoryWillChangeStateMessage:(base::DictionaryValue*)unused |
| context:(NSDictionary*)unusedContext { |
| _changingHistoryState = YES; |
| return YES; |
| } |
| |
| - (BOOL)handleWindowHistoryDidPushStateMessage:(base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| DCHECK(_changingHistoryState); |
| _changingHistoryState = NO; |
| |
| // If there is a pending entry, a new navigation has been registered but |
| // hasn't begun loading. Since the pushState message is coming from the |
| // previous page, ignore it and allow the previously registered navigation to |
| // continue. This can ocur if a pushState is issued from an anchor tag |
| // onClick event, as the click would have already been registered. |
| if ([self sessionController].pendingItem) |
| return NO; |
| |
| std::string pageURL; |
| std::string baseURL; |
| if (!message->GetString("pageUrl", &pageURL) || |
| !message->GetString("baseUrl", &baseURL)) { |
| DLOG(WARNING) << "JS message parameter not found: pageUrl or baseUrl"; |
| return NO; |
| } |
| GURL pushURL = web::history_state_util::GetHistoryStateChangeUrl( |
| [self currentURL], GURL(baseURL), pageURL); |
| // UIWebView seems to choke on unicode characters that haven't been |
| // escaped; escape the URL now so the expected load URL is correct. |
| pushURL = URLEscapedForHistory(pushURL); |
| if (!pushURL.is_valid()) |
| return YES; |
| web::NavigationItem* navItem = self.currentNavItem; |
| // PushState happened before first navigation entry or called when the |
| // navigation entry does not contain a valid URL. |
| if (!navItem || !navItem->GetURL().is_valid()) |
| return YES; |
| if (!web::history_state_util::IsHistoryStateChangeValid( |
| self.currentNavItem->GetURL(), pushURL)) { |
| // If the current session entry URL origin still doesn't match pushURL's |
| // origin, ignore the pushState. This can happen if a new URL is loaded |
| // just before the pushState. |
| return YES; |
| } |
| std::string stateObjectJSON; |
| if (!message->GetString("stateObject", &stateObjectJSON)) { |
| DLOG(WARNING) << "JS message parameter not found: stateObject"; |
| return NO; |
| } |
| NSString* stateObject = base::SysUTF8ToNSString(stateObjectJSON); |
| _URLOnStartLoading = pushURL; |
| _lastRegisteredRequestURL = pushURL; |
| |
| // If the user interacted with the page, categorize it as a link navigation. |
| // If not, categorize it is a client redirect as it occurred without user |
| // input and should not be added to the history stack. |
| // TODO(crbug.com/549301): Improve transition detection. |
| ui::PageTransition transition = self.userInteractionRegistered |
| ? ui::PAGE_TRANSITION_LINK |
| : ui::PAGE_TRANSITION_CLIENT_REDIRECT; |
| [self pushStateWithPageURL:pushURL |
| stateObject:stateObject |
| transition:transition]; |
| |
| NSString* replaceWebViewJS = |
| [self javaScriptToReplaceWebViewURL:pushURL stateObjectJSON:stateObject]; |
| base::WeakNSObject<CRWWebController> weakSelf(self); |
| [self executeJavaScript:replaceWebViewJS completionHandler:^(id, NSError*) { |
| if (!weakSelf || weakSelf.get()->_isBeingDestroyed) |
| return; |
| base::scoped_nsobject<CRWWebController> strongSelf([weakSelf retain]); |
| [strongSelf optOutScrollsToTopForSubviews]; |
| [strongSelf didFinishNavigation]; |
| }]; |
| return YES; |
| } |
| |
| - (BOOL)handleWindowHistoryDidReplaceStateMessage: |
| (base::DictionaryValue*)message |
| context:(NSDictionary*)context { |
| DCHECK(_changingHistoryState); |
| _changingHistoryState = NO; |
| |
| std::string pageURL; |
| std::string baseURL; |
| if (!message->GetString("pageUrl", &pageURL) || |
| !message->GetString("baseUrl", &baseURL)) { |
| DLOG(WARNING) << "JS message parameter not found: pageUrl or baseUrl"; |
| return NO; |
| } |
| GURL replaceURL = web::history_state_util::GetHistoryStateChangeUrl( |
| [self currentURL], GURL(baseURL), pageURL); |
| // UIWebView seems to choke on unicode characters that haven't been |
| // escaped; escape the URL now so the expected load URL is correct. |
| replaceURL = URLEscapedForHistory(replaceURL); |
| if (!replaceURL.is_valid()) |
| return YES; |
| |
| web::NavigationItem* navItem = self.currentNavItem; |
| // ReplaceState happened before first navigation entry or called right |
| // after window.open when the url is empty/not valid. |
| if (!navItem || (self.navigationManagerImpl->GetItemCount() <= 1 && |
| navItem->GetURL().is_empty())) |
| return YES; |
| if (!web::history_state_util::IsHistoryStateChangeValid( |
| self.currentNavItem->GetURL(), replaceURL)) { |
| // If the current session entry URL origin still doesn't match |
| // replaceURL's origin, ignore the replaceState. This can happen if a |
| // new URL is loaded just before the replaceState. |
| return YES; |
| } |
| std::string stateObjectJSON; |
| if (!message->GetString("stateObject", &stateObjectJSON)) { |
| DLOG(WARNING) << "JS message parameter not found: stateObject"; |
| return NO; |
| } |
| NSString* stateObject = base::SysUTF8ToNSString(stateObjectJSON); |
| _URLOnStartLoading = replaceURL; |
| _lastRegisteredRequestURL = replaceURL; |
| [self replaceStateWithPageURL:replaceURL stateObject:stateObject]; |
| NSString* replaceStateJS = [self javaScriptToReplaceWebViewURL:replaceURL |
| stateObjectJSON:stateObject]; |
| base::WeakNSObject<CRWWebController> weakSelf(self); |
| [self executeJavaScript:replaceStateJS completionHandler:^(id, NSError*) { |
| if (!weakSelf || weakSelf.get()->_isBeingDestroyed) |
| return; |
| base::scoped_nsobject<CRWWebController> strongSelf([weakSelf retain]); |
| [strongSelf didFinishNavigation]; |
| }]; |
| return YES; |
| } |
| |
| #pragma mark - |
| |
| - (BOOL)wantsKeyboardShield { |
| if ([self.nativeController |
| respondsToSelector:@selector(wantsKeyboardShield)]) { |
| return [self.nativeController wantsKeyboardShield]; |
| } |
| return YES; |
| } |
| |
| - (BOOL)wantsLocationBarHintText { |
| if ([self.nativeController |
| respondsToSelector:@selector(wantsLocationBarHintText)]) { |
| return [self.nativeController wantsLocationBarHintText]; |
| } |
| return YES; |
| } |
| |
| // TODO(stuartmorgan): This method conflates document changes and URL changes; |
| // we should be distinguishing better, and be clear about the expected |
| // WebDelegate and WCO callbacks in each case. |
| - (void)webPageChanged { |
| DCHECK(_loadPhase == web::LOAD_REQUESTED); |
| |
| const GURL currentURL([self currentURL]); |
| web::Referrer referrer = [self currentReferrer]; |
| // If no referrer was known in advance, record it now. (If there was one, |
| // keep it since it will have a more accurate URL and policy than what can |
| // be extracted from the landing page.) |
| web::NavigationItem* currentItem = self.currentNavItem; |
| if (!currentItem->GetReferrer().url.is_valid()) { |
| currentItem->SetReferrer(referrer); |
| } |
| |
| // TODO(stuartmorgan): This shouldn't be called for hash state or |
| // push/replaceState. |
| [self resetDocumentSpecificState]; |
| |
| [self didStartLoadingURL:currentURL]; |
| } |
| |
| - (void)resetDocumentSpecificState { |
| _lastUserInteraction.reset(); |
| _clickInProgress = NO; |
| } |
| |
| - (void)didStartLoadingURL:(const GURL&)URL { |
| _loadPhase = web::PAGE_LOADING; |
| _URLOnStartLoading = URL; |
| _displayStateOnStartLoading = self.pageDisplayState; |
| |
| self.userInteractionRegistered = NO; |
| _pageHasZoomed = NO; |
| |
| [[self sessionController] commitPendingItem]; |
| } |
| |
| - (void)wasShown { |
| if ([self.nativeController respondsToSelector:@selector(wasShown)]) { |
| [self.nativeController wasShown]; |
| } |
| } |
| |
| - (void)wasHidden { |
| if (_isHalted) |
| return; |
| if ([self.nativeController respondsToSelector:@selector(wasHidden)]) { |
| [self.nativeController wasHidden]; |
| } |
| } |
| |
| + (BOOL)webControllerCanShow:(const GURL&)url { |
| return web::UrlHasWebScheme(url) || |
| web::GetWebClient()->IsAppSpecificURL(url) || |
| url.SchemeIs(url::kFileScheme) || url.SchemeIs(url::kAboutScheme); |
| } |
| |
| - (void)setUserInteractionRegistered:(BOOL)flag { |
| _userInteractionRegistered = flag; |
| if (flag) |
| _interactionRegisteredSinceLastURLChange = YES; |
| } |
| |
| - (BOOL)userInteractionRegistered { |
| return _userInteractionRegistered; |
| } |
| |
| - (void)cachePOSTDataForRequest:(NSURLRequest*)request |
| inNavigationItem:(web::NavigationItemImpl*)item { |
| NSUInteger maxPOSTDataSizeInBytes = 4096; |
| NSString* cookieHeaderName = @"cookie"; |
| |
| DCHECK(item); |
| const bool shouldUpdateEntry = |
| ui::PageTransitionCoreTypeIs(item->GetTransitionType(), |
| ui::PAGE_TRANSITION_FORM_SUBMIT) && |
| ![request HTTPBodyStream] && // Don't cache streams. |
| !item->HasPostData() && |
| item->GetURL() == net::GURLWithNSURL([request URL]); |
| const bool belowSizeCap = |
| [[request HTTPBody] length] < maxPOSTDataSizeInBytes; |
| DLOG_IF(WARNING, shouldUpdateEntry && !belowSizeCap) |
| << "Data in POST request exceeds the size cap (" << maxPOSTDataSizeInBytes |
| << " bytes), and will not be cached."; |
| |
| if (shouldUpdateEntry && belowSizeCap) { |
| item->SetPostData([request HTTPBody]); |
| item->ResetHttpRequestHeaders(); |
| item->AddHttpRequestHeaders([request allHTTPHeaderFields]); |
| // Don't cache the "Cookie" header. |
| // According to NSURLRequest documentation, |-valueForHTTPHeaderField:| is |
| // case insensitive, so it's enough to test the lower case only. |
| if ([request valueForHTTPHeaderField:cookieHeaderName]) { |
| // Case insensitive search in |headers|. |
| NSSet* cookieKeys = [item->GetHttpRequestHeaders() |
| keysOfEntriesPassingTest:^(id key, id obj, BOOL* stop) { |
| NSString* header = (NSString*)key; |
| const BOOL found = |
| [header caseInsensitiveCompare:cookieHeaderName] == |
| NSOrderedSame; |
| *stop = found; |
| return found; |
| }]; |
| DCHECK_EQ(1u, [cookieKeys count]); |
| item->RemoveHttpRequestHeaderForKey([cookieKeys anyObject]); |
| } |
| } |
| } |
| |
| // TODO(stuartmorgan): This is mostly logic from the original UIWebView delegate |
| // method, which provides less information than the WKWebView version. Audit |
| // this for things that should be handled in the subclass instead. |
| - (BOOL)shouldAllowLoadWithNavigationAction:(WKNavigationAction*)action { |
| NSURLRequest* request = action.request; |
| GURL requestURL = net::GURLWithNSURL(request.URL); |
| |
| // External application launcher needs |isNavigationTypeLinkActivated| to |
| // decide if the user intended to open the application by clicking on a link. |
| BOOL isNavigationTypeLinkActivated = |
| action.navigationType == WKNavigationTypeLinkActivated; |
| |
| // Checks if the link navigation leads to a launch of an external app. |
| // TODO(crbug.com/704417): External apps will not be launched from clicking |
| // a Bookmarked URL or a Recently Closed URL. |
| // TODO(crbug.com/607780): Revise the logic of allowing external app launch |
| // and move it to externalAppLauncher. |
| BOOL isOpenInNewTabNavigation = !(self.navigationManagerImpl->GetItemCount()); |
| BOOL isPossibleLinkClick = [self isLinkNavigation:action.navigationType]; |
| if (isPossibleLinkClick || isOpenInNewTabNavigation) { |
| web::NavigationItem* item = self.currentNavItem; |
| const GURL currentNavigationURL = |
| item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| // Check If the URL is handled by a native app. |
| if ([self urlTriggersNativeAppLaunch:requestURL |
| sourceURL:currentNavigationURL |
| linkActivatedNavigation:isNavigationTypeLinkActivated]) { |
| // External app has been launched successfully. Stop the current page |
| // load operation (e.g. notifying all observers) and record the URL so |
| // that errors reported following the 'NO' reply can be safely ignored. |
| if ([self shouldClosePageOnNativeApplicationLoad]) |
| _webStateImpl->CloseWebState(); |
| [self stopLoading]; |
| [_openedApplicationURL addObject:request.URL]; |
| return NO; |
| } |
| } |
| |
| // The WebDelegate may instruct the CRWWebController to stop loading, and |
| // instead instruct the next page to be loaded in an animation. |
| GURL mainDocumentURL = net::GURLWithNSURL(request.mainDocumentURL); |
| DCHECK(_webView); |
| if (![self shouldOpenURL:requestURL |
| mainDocumentURL:mainDocumentURL |
| linkClicked:isPossibleLinkClick]) { |
| return NO; |
| } |
| |
| // If the URL doesn't look like one we can show, try to open the link with an |
| // external application. |
| // TODO(droger): Check transition type before opening an external |
| // application? For example, only allow it for TYPED and LINK transitions. |
| if (![CRWWebController webControllerCanShow:requestURL]) { |
| if (![self shouldOpenExternalURLForNavigationAction:action]) { |
| return NO; |
| } |
| |
| // Stop load if navigation is believed to be happening on the main frame. |
| if ([self isMainFrameNavigationAction:action]) |
| [self stopLoading]; |
| |
| // Purge web view if last committed URL is different from the document URL. |
| // This can happen if external URL was added to the navigation stack and was |
| // loaded using Go Back or Go Forward navigation (in which case document URL |
| // will point to the previous page). If this is the first load for a |
| // NavigationManager, there will be no last committed item, so check here. |
| web::NavigationItem* lastCommittedItem = |
| self.webState->GetNavigationManager()->GetLastCommittedItem(); |
| if (lastCommittedItem) { |
| GURL lastCommittedURL = lastCommittedItem->GetURL(); |
| if (lastCommittedURL != _documentURL) { |
| [self requirePageReconstruction]; |
| [self setDocumentURL:lastCommittedURL]; |
| } |
| } |
| |
| if ([_delegate openExternalURL:requestURL |
| linkClicked:isNavigationTypeLinkActivated]) { |
| // Record the URL so that errors reported following the 'NO' reply can be |
| // safely ignored. |
| [_openedApplicationURL addObject:request.URL]; |
| if ([self shouldClosePageOnNativeApplicationLoad]) |
| _webStateImpl->CloseWebState(); |
| } |
| return NO; |
| } |
| |
| if ([[request HTTPMethod] isEqualToString:@"POST"]) { |
| web::NavigationItemImpl* item = self.currentNavItem; |
| // TODO(crbug.com/570699): Remove this check once it's no longer possible to |
| // have no current entries. |
| if (item) |
| [self cachePOSTDataForRequest:request inNavigationItem:item]; |
| } |
| |
| return YES; |
| } |
| |
| - (void)handleLoadError:(NSError*)error inMainFrame:(BOOL)inMainFrame { |
| NSString* MIMEType = [_pendingNavigationInfo MIMEType]; |
| if ([_passKitDownloader isMIMETypePassKitType:MIMEType]) |
| return; |
| if ([error code] == NSURLErrorUnsupportedURL) |
| return; |
| // In cases where a Plug-in handles the load do not take any further action. |
| if ([error.domain isEqual:base::SysUTF8ToNSString(web::kWebKitErrorDomain)] && |
| (error.code == web::kWebKitErrorPlugInLoadFailed || |
| error.code == web::kWebKitErrorCannotShowUrl)) |
| return; |
| |
| // Continue processing only if the error is on the main request or is the |
| // result of a user interaction. |
| NSDictionary* userInfo = [error userInfo]; |
| // |userinfo| contains the request creation date as a NSDate. |
| NSTimeInterval requestCreationDate = |
| [[userInfo objectForKey:@"CreationDate"] timeIntervalSinceReferenceDate]; |
| bool userInteracted = false; |
| if (requestCreationDate != 0.0 && _lastUserInteraction) { |
| NSTimeInterval timeSinceInteraction = |
| requestCreationDate - _lastUserInteraction->time; |
| // The error is considered to be the result of a user interaction if any |
| // interaction happened just before the request was made. |
| // TODO(droger): If the user interacted with the page after the request was |
| // made (i.e. creationTimeSinceLastInteraction < 0), then |
| // |_lastUserInteraction| has been overridden. The current behavior is to |
| // discard the interstitial in that case. A better decision could be made if |
| // we had a history of all the user interactions instead of just the last |
| // one. |
| userInteracted = |
| timeSinceInteraction < kMaximumDelayForUserInteractionInSeconds && |
| _lastUserInteraction->time > _lastTransferTimeInSeconds && |
| timeSinceInteraction >= 0.0; |
| } else { |
| // If the error does not have timing information, check if the user |
| // interacted with the page recently. |
| userInteracted = [self userIsInteracting]; |
| } |
| if (!inMainFrame && !userInteracted) |
| return; |
| |
| // Reset SSL status to default, unless the load was cancelled (manually or by |
| // back-forward navigation). |
| web::NavigationManager* navManager = self.webState->GetNavigationManager(); |
| if (navManager->GetLastCommittedItem() && [error code] != NSURLErrorCancelled) |
| navManager->GetLastCommittedItem()->GetSSL() = web::SSLStatus(); |
| |
| NSURL* errorURL = [NSURL |
| URLWithString:[userInfo objectForKey:NSURLErrorFailingURLStringErrorKey]]; |
| const GURL errorGURL = net::GURLWithNSURL(errorURL); |
| |
| // Handles Frame Load Interrupted errors from WebView. |
| if ([error.domain isEqual:base::SysUTF8ToNSString(web::kWebKitErrorDomain)] && |
| error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange) { |
| // See if the delegate wants to handle this case. |
| if (errorGURL.is_valid() && |
| [_delegate |
| respondsToSelector:@selector( |
| controllerForUnhandledContentAtURL:)]) { |
| id<CRWNativeContent> controller = |
| [_delegate controllerForUnhandledContentAtURL:errorGURL]; |
| if (controller) { |
| [self loadCompleteWithSuccess:NO]; |
| [self removeWebViewAllowingCachedReconstruction:NO]; |
| [self setNativeController:controller]; |
| [self loadNativeViewWithSuccess:YES]; |
| return; |
| } |
| } |
| |
| // Ignore errors that originate from URLs that are opened in external apps. |
| if ([_openedApplicationURL containsObject:errorURL]) |
| return; |
| // Certain frame errors don't have URL information for some reason; for |
| // those cases (so far the only known case is plugin content loaded directly |
| // in a frame) just ignore the error. See crbug.com/414295 |
| if (!errorURL) { |
| DCHECK(!inMainFrame); |
| return; |
| } |
| // The wrapper error uses the URL of the error and not the requested URL |
| // (which can be different in case of a redirect) to match desktop Chrome |
| // behavior. |
| NSError* wrapperError = [NSError |
| errorWithDomain:[error domain] |
| code:[error code] |
| userInfo:@{ |
| NSURLErrorFailingURLStringErrorKey : [errorURL absoluteString], |
| NSUnderlyingErrorKey : error |
| }]; |
| [self loadCompleteWithSuccess:NO]; |
| [self loadErrorInNativeView:wrapperError]; |
| return; |
| } |
| |
| if ([error code] == NSURLErrorCancelled) { |
| [self handleCancelledError:error]; |
| // NSURLErrorCancelled errors that aren't handled by aborting the load will |
| // automatically be retried by the web view, so early return in this case. |
| return; |
| } |
| |
| [self loadCompleteWithSuccess:NO]; |
| [self loadErrorInNativeView:error]; |
| } |
| |
| - (void)handleCancelledError:(NSError*)error { |
| if ([self shouldCancelLoadForCancelledError:error]) { |
| [self loadCancelled]; |
| [[self sessionController] discardNonCommittedItems]; |
| } |
| } |
| |
| - (BOOL)shouldCancelLoadForCancelledError:(NSError*)error { |
| DCHECK_EQ(error.code, NSURLErrorCancelled); |
| // Do not cancel the load if it is for an app specific URL, as such errors |
| // are produced during the app specific URL load process. |
| const GURL errorURL = |
| net::GURLWithNSURL(error.userInfo[NSURLErrorFailingURLErrorKey]); |
| if (web::GetWebClient()->IsAppSpecificURL(errorURL)) |
| return NO; |
| |
| // Don't cancel NSURLErrorCancelled errors originating from navigation |
| // as the WKWebView will automatically retry these loads. |
| WKWebViewErrorSource source = WKWebViewErrorSourceFromError(error); |
| return source != NAVIGATION; |
| } |
| |
| #pragma mark - |
| #pragma mark WebUI |
| |
| - (void)createWebUIForURL:(const GURL&)URL { |
| _webStateImpl->CreateWebUI(URL); |
| _webUIManager.reset( |
| [[CRWWebUIManager alloc] initWithWebState:self.webStateImpl]); |
| } |
| |
| - (void)clearWebUI { |
| _webStateImpl->ClearWebUI(); |
| _webUIManager.reset(); |
| } |
| |
| #pragma mark - |
| #pragma mark Auth Challenge |
| |
| - (void)processAuthChallenge:(NSURLAuthenticationChallenge*)challenge |
| forCertAcceptPolicy:(web::CertAcceptPolicy)policy |
| certStatus:(net::CertStatus)certStatus |
| completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| SecTrustRef trust = challenge.protectionSpace.serverTrust; |
| if (policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER) { |
| // Cert is invalid, but user agreed to proceed, override default behavior. |
| completionHandler(NSURLSessionAuthChallengeUseCredential, |
| [NSURLCredential credentialForTrust:trust]); |
| return; |
| } |
| |
| if (policy != web::CERT_ACCEPT_POLICY_ALLOW && |
| SecTrustGetCertificateCount(trust)) { |
| // The cert is invalid and the user has not agreed to proceed. Cache the |
| // cert verification result in |_certVerificationErrors|, so that it can |
| // later be reused inside |didFailProvisionalNavigation:|. |
| // The leaf cert is used as the key, because the chain provided by |
| // |didFailProvisionalNavigation:| will differ (it is the server-supplied |
| // chain), thus if intermediates were considered, the keys would mismatch. |
| scoped_refptr<net::X509Certificate> leafCert = |
| net::X509Certificate::CreateFromHandle( |
| SecTrustGetCertificateAtIndex(trust, 0), |
| net::X509Certificate::OSCertHandles()); |
| if (leafCert) { |
| BOOL is_recoverable = |
| policy == web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_UNDECIDED_BY_USER; |
| std::string host = |
| base::SysNSStringToUTF8(challenge.protectionSpace.host); |
| _certVerificationErrors->Put( |
| web::CertHostPair(leafCert, host), |
| CertVerificationError(is_recoverable, certStatus)); |
| } |
| } |
| completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| } |
| |
| - (void)handleHTTPAuthForChallenge:(NSURLAuthenticationChallenge*)challenge |
| completionHandler: |
| (void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| NSURLProtectionSpace* space = challenge.protectionSpace; |
| DCHECK( |
| [space.authenticationMethod isEqual:NSURLAuthenticationMethodHTTPBasic] || |
| [space.authenticationMethod isEqual:NSURLAuthenticationMethodNTLM] || |
| [space.authenticationMethod isEqual:NSURLAuthenticationMethodHTTPDigest]); |
| |
| if (self.shouldSuppressDialogs) { |
| // TODO(crbug.com/702381): Web Controller should not assume that embedder |
| // handles HTTP Authentication by showing the dialog. |
| _webStateImpl->OnDialogSuppressed(); |
| completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| return; |
| } |
| |
| _webStateImpl->OnAuthRequired( |
| space, challenge.proposedCredential, |
| base::BindBlock(^(NSString* user, NSString* password) { |
| [CRWWebController processHTTPAuthForUser:user |
| password:password |
| completionHandler:completionHandler]; |
| })); |
| } |
| |
| + (void)processHTTPAuthForUser:(NSString*)user |
| password:(NSString*)password |
| completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| DCHECK_EQ(user == nil, password == nil); |
| if (!user || !password) { |
| // Embedder cancelled authentication. |
| completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| return; |
| } |
| completionHandler( |
| NSURLSessionAuthChallengeUseCredential, |
| [NSURLCredential |
| credentialWithUser:user |
| password:password |
| persistence:NSURLCredentialPersistenceForSession]); |
| } |
| |
| #pragma mark - |
| #pragma mark JavaScript Dialog |
| |
| - (void)runJavaScriptDialogOfType:(web::JavaScriptDialogType)type |
| initiatedByFrame:(WKFrameInfo*)frame |
| message:(NSString*)message |
| defaultText:(NSString*)defaultText |
| completion:(void (^)(BOOL, NSString*))completionHandler { |
| DCHECK(completionHandler); |
| if (self.shouldSuppressDialogs) { |
| _webStateImpl->OnDialogSuppressed(); |
| completionHandler(NO, nil); |
| return; |
| } |
| |
| self.webStateImpl->RunJavaScriptDialog( |
| net::GURLWithNSURL(frame.request.URL), type, message, defaultText, |
| base::BindBlock(^(bool success, NSString* input) { |
| completionHandler(success, input); |
| })); |
| } |
| |
| #pragma mark - |
| #pragma mark TouchTracking |
| |
| - (void)touched:(BOOL)touched { |
| _clickInProgress = touched; |
| if (touched) { |
| self.userInteractionRegistered = YES; |
| _userInteractedWithWebController = YES; |
| if (_isBeingDestroyed) |
| return; |
| const web::NavigationManagerImpl& navigationManager = |
| self.webStateImpl->GetNavigationManagerImpl(); |
| GURL mainDocumentURL = |
| navigationManager.GetItemCount() |
| ? navigationManager.GetLastCommittedItem()->GetURL() |
| : [self currentURL]; |
| _lastUserInteraction.reset(new UserInteractionEvent(mainDocumentURL)); |
| } |
| } |
| |
| - (CRWTouchTrackingRecognizer*)touchTrackingRecognizer { |
| if (!_touchTrackingRecognizer) { |
| _touchTrackingRecognizer.reset( |
| [[CRWTouchTrackingRecognizer alloc] initWithDelegate:self]); |
| } |
| return _touchTrackingRecognizer.get(); |
| } |
| |
| - (BOOL)userIsInteracting { |
| // If page transfer started after last click, user is deemed to be no longer |
| // interacting. |
| if (!_lastUserInteraction || |
| _lastTransferTimeInSeconds > _lastUserInteraction->time) { |
| return NO; |
| } |
| return [self userClickedRecently]; |
| } |
| |
| - (BOOL)userClickedRecently { |
| // Scrolling generates a pair of touch on/off event which causes |
| // _lastUserInteraction to register that there was user interaction. |
| // Checks for scrolling first to override time-based click heuristics. |
| BOOL scrolling = [[self webScrollView] isDragging] || |
| [[self webScrollView] isDecelerating]; |
| if (scrolling) |
| return NO; |
| if (!_lastUserInteraction) |
| return NO; |
| return _clickInProgress || |
| ((CFAbsoluteTimeGetCurrent() - _lastUserInteraction->time) < |
| kMaximumDelayForUserInteractionInSeconds); |
| } |
| |
| #pragma mark Placeholder Overlay Methods |
| |
| - (void)addPlaceholderOverlay { |
| if (!_overlayPreviewMode) { |
| // Create |kSnapshotOverlayDelay| second timer to remove image with |
| // transition. |
| [self performSelector:@selector(removePlaceholderOverlay) |
| withObject:nil |
| afterDelay:kSnapshotOverlayDelay]; |
| } |
| |
| // Add overlay image. |
| _placeholderOverlayView.reset([[UIImageView alloc] init]); |
| CGRect frame = [self visibleFrame]; |
| [_placeholderOverlayView setFrame:frame]; |
| [_placeholderOverlayView |
| setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| UIViewAutoresizingFlexibleHeight]; |
| [_placeholderOverlayView setContentMode:UIViewContentModeScaleAspectFill]; |
| [_containerView addSubview:_placeholderOverlayView]; |
| |
| id callback = ^(UIImage* image) { |
| [_placeholderOverlayView setImage:image]; |
| }; |
| [_delegate webController:self retrievePlaceholderOverlayImage:callback]; |
| |
| if (!_placeholderOverlayView.get().image) { |
| // TODO(shreyasv): This is just a blank white image. Consider adding an API |
| // so that the delegate can return something immediately for the default |
| // overlay image. |
| _placeholderOverlayView.get().image = [[self class] defaultSnapshotImage]; |
| } |
| } |
| |
| - (void)removePlaceholderOverlay { |
| if (!_placeholderOverlayView || _overlayPreviewMode) |
| return; |
| |
| [NSObject |
| cancelPreviousPerformRequestsWithTarget:self |
| selector:@selector( |
| removePlaceholderOverlay) |
| object:nil]; |
| // Remove overlay with transition. |
| [UIView animateWithDuration:kSnapshotOverlayTransition |
| animations:^{ |
| [_placeholderOverlayView setAlpha:0.0f]; |
| } |
| completion:^(BOOL finished) { |
| [_placeholderOverlayView removeFromSuperview]; |
| _placeholderOverlayView.reset(); |
| }]; |
| } |
| |
| - (void)setOverlayPreviewMode:(BOOL)overlayPreviewMode { |
| _overlayPreviewMode = overlayPreviewMode; |
| |
| // If we were showing the preview, remove it. |
| if (!_overlayPreviewMode && _placeholderOverlayView) { |
| [self resetContainerView]; |
| // Reset |_placeholderOverlayView| directly instead of calling |
| // -removePlaceholderOverlay, which removes |_placeholderOverlayView| in an |
| // animation. |
| [_placeholderOverlayView removeFromSuperview]; |
| _placeholderOverlayView.reset(); |
| // There are cases when resetting the contentView, above, may happen after |
| // the web view has been created. Re-add it here, rather than |
| // relying on a subsequent call to loadCurrentURLInWebView. |
| if (_webView) { |
| [[self view] addSubview:_webView]; |
| } |
| } |
| } |
| |
| #pragma mark - |
| #pragma mark Session Information |
| |
| - (CRWSessionController*)sessionController { |
| NavigationManagerImpl* navigationManager = self.navigationManagerImpl; |
| return navigationManager ? navigationManager->GetSessionController() : nil; |
| } |
| |
| - (NavigationManagerImpl*)navigationManagerImpl { |
| return _webStateImpl ? &(_webStateImpl->GetNavigationManagerImpl()) : nil; |
| } |
| |
| - (BOOL)hasOpener { |
| return _webStateImpl ? _webStateImpl->HasOpener() : NO; |
| } |
| |
| - (web::NavigationItemImpl*)currentNavItem { |
| // This goes through the legacy Session* interface rather than Navigation* |
| // because it is itself a legacy method that should not exist, and this |
| // avoids needing to add a GetActiveItem to NavigationManager. If/when this |
| // method chain becomes a blocker to eliminating SessionController, the logic |
| // can be moved here, using public NavigationManager getters. That's not |
| // done now in order to avoid code duplication. |
| return [[self sessionController] currentItem]; |
| } |
| |
| - (ui::PageTransition)currentTransition { |
| if (self.currentNavItem) |
| return self.currentNavItem->GetTransitionType(); |
| else |
| return ui::PageTransitionFromInt(0); |
| } |
| |
| - (web::Referrer)currentNavItemReferrer { |
| web::NavigationItem* currentItem = self.currentNavItem; |
| return currentItem ? currentItem->GetReferrer() : web::Referrer(); |
| } |
| |
| - (NSDictionary*)currentHTTPHeaders { |
| web::NavigationItem* currentItem = self.currentNavItem; |
| return currentItem ? currentItem->GetHttpRequestHeaders() : nil; |
| } |
| |
| #pragma mark - |
| #pragma mark CRWWebViewScrollViewProxyObserver |
| |
| - (void)webViewScrollViewDidZoom: |
| (CRWWebViewScrollViewProxy*)webViewScrollViewProxy { |
| _pageHasZoomed = YES; |
| |
| base::WeakNSObject<UIScrollView> weakScrollView(self.webScrollView); |
| [self extractViewportTagWithCompletion:^( |
| const web::PageViewportState* viewportState) { |
| if (!weakScrollView) |
| return; |
| base::scoped_nsobject<UIScrollView> scrollView([weakScrollView retain]); |
| if (viewportState && !viewportState->viewport_tag_present() && |
| [scrollView minimumZoomScale] == [scrollView maximumZoomScale] && |
| [scrollView zoomScale] > 1.0) { |
| UMA_HISTOGRAM_BOOLEAN(kUMAViewportZoomBugCount, true); |
| } |
| }]; |
| } |
| |
| - (void)webViewScrollViewDidResetContentSize: |
| (CRWWebViewScrollViewProxy*)webViewScrollViewProxy { |
| web::NavigationItem* currentItem = self.currentNavItem; |
| if (webViewScrollViewProxy.isZooming || _applyingPageState || !currentItem) |
| return; |
| CGSize contentSize = webViewScrollViewProxy.contentSize; |
| if (contentSize.width < CGRectGetWidth(webViewScrollViewProxy.frame)) { |
| // The renderer incorrectly resized the content area. Resetting the scroll |
| // view's zoom scale will force a re-rendering. rdar://23963992 |
| _applyingPageState = YES; |
| web::PageZoomState zoomState = |
| currentItem->GetPageDisplayState().zoom_state(); |
| if (!zoomState.IsValid()) |
| zoomState = web::PageZoomState(1.0, 1.0, 1.0); |
| [self applyWebViewScrollZoomScaleFromZoomState:zoomState]; |
| _applyingPageState = NO; |
| } |
| } |
| |
| #pragma mark - |
| #pragma mark CRWWebViewScrollViewProxyObserver |
| |
| // Under WKWebView, JavaScript can execute asynchronously. User can start |
| // scrolling and calls to window.scrollTo executed during scrolling will be |
| // treated as "during user interaction" and can cause app to go fullscreen. |
| // This is a workaround to use this webViewScrollViewIsDragging flag to ignore |
| // window.scrollTo while user is scrolling. See crbug.com/554257 |
| - (void)webViewScrollViewWillBeginDragging: |
| (CRWWebViewScrollViewProxy*)webViewScrollViewProxy { |
| [self executeJavaScript:@"__gCrWeb.setWebViewScrollViewIsDragging(true)" |
| completionHandler:nil]; |
| } |
| |
| - (void)webViewScrollViewDidEndDragging: |
| (CRWWebViewScrollViewProxy*)webViewScrollViewProxy |
| willDecelerate:(BOOL)decelerate { |
| [self executeJavaScript:@"__gCrWeb.setWebViewScrollViewIsDragging(false)" |
| completionHandler:nil]; |
| } |
| |
| #pragma mark - |
| #pragma mark Page State |
| |
| - (void)recordStateInHistory { |
| // Only record the state if: |
| // - the current NavigationItem's URL matches the current URL, and |
| // - the user has interacted with the page. |
| web::NavigationItem* item = self.currentNavItem; |
| if (item && item->GetURL() == [self currentURL] && |
| self.userInteractionRegistered) { |
| item->SetPageDisplayState(self.pageDisplayState); |
| } |
| } |
| |
| - (void)restoreStateFromHistory { |
| web::NavigationItem* item = self.currentNavItem; |
| if (item) |
| self.pageDisplayState = item->GetPageDisplayState(); |
| } |
| |
| - (web::PageDisplayState)pageDisplayState { |
| web::PageDisplayState displayState; |
| if (_webView) { |
| CGPoint scrollOffset = [self scrollPosition]; |
| displayState.scroll_state().set_offset_x(std::floor(scrollOffset.x)); |
| displayState.scroll_state().set_offset_y(std::floor(scrollOffset.y)); |
| UIScrollView* scrollView = self.webScrollView; |
| displayState.zoom_state().set_minimum_zoom_scale( |
| scrollView.minimumZoomScale); |
| displayState.zoom_state().set_maximum_zoom_scale( |
| scrollView.maximumZoomScale); |
| displayState.zoom_state().set_zoom_scale(scrollView.zoomScale); |
| } else { |
| // TODO(crbug.com/546146): Handle native views. |
| } |
| return displayState; |
| } |
| |
| - (void)setPageDisplayState:(web::PageDisplayState)displayState { |
| if (!displayState.IsValid()) |
| return; |
| if (_webView) { |
| // Page state is restored after a page load completes. If the user has |
| // scrolled or changed the zoom scale while the page is still loading, don't |
| // restore any state since it will confuse the user. |
| web::PageDisplayState currentPageDisplayState = self.pageDisplayState; |
| if (currentPageDisplayState.scroll_state().offset_x() == |
| _displayStateOnStartLoading.scroll_state().offset_x() && |
| currentPageDisplayState.scroll_state().offset_y() == |
| _displayStateOnStartLoading.scroll_state().offset_y() && |
| !_pageHasZoomed) { |
| [self applyPageDisplayState:displayState]; |
| } |
| } |
| } |
| |
| - (void)extractViewportTagWithCompletion:(ViewportStateCompletion)completion { |
| DCHECK(completion); |
| web::NavigationItem* currentItem = self.currentNavItem; |
| if (!currentItem) { |
| completion(nullptr); |
| return; |
| } |
| NSString* const kViewportContentQuery = |
| @"var viewport = document.querySelector('meta[name=\"viewport\"]');" |
| "viewport ? viewport.content : '';"; |
| base::WeakNSObject<CRWWebController> weakSelf(self); |
| int itemID = currentItem->GetUniqueID(); |
| [self executeJavaScript:kViewportContentQuery |
| completionHandler:^(id viewportContent, NSError*) { |
| web::NavigationItem* item = [weakSelf currentNavItem]; |
| if (item && item->GetUniqueID() == itemID) { |
| web::PageViewportState viewportState( |
| base::mac::ObjCCast<NSString>(viewportContent)); |
| completion(&viewportState); |
| } else { |
| completion(nullptr); |
| } |
| }]; |
| } |
| |
| - (void)orientationDidChange { |
| // When rotating, the available zoom scale range may change, zoomScale's |
| // percentage into this range should remain constant. However, there are |
| // two known bugs with respect to adjusting the zoomScale on rotation: |
| // - WKWebView sometimes erroneously resets the scroll view's zoom scale to |
| // an incorrect value ( rdar://20100815 ). |
| // - After zooming occurs in a UIWebView that's displaying a page with a hard- |
| // coded viewport width, the zoom will not be updated upon rotation |
| // ( crbug.com/485055 ). |
| if (!_webView) |
| return; |
| web::NavigationItem* currentItem = self.currentNavItem; |
| if (!currentItem) |
| return; |
| web::PageDisplayState displayState = currentItem->GetPageDisplayState(); |
| if (!displayState.IsValid()) |
| return; |
| CGFloat zoomPercentage = (displayState.zoom_state().zoom_scale() - |
| displayState.zoom_state().minimum_zoom_scale()) / |
| displayState.zoom_state().GetMinMaxZoomDifference(); |
| displayState.zoom_state().set_minimum_zoom_scale( |
| self.webScrollView.minimumZoomScale); |
| displayState.zoom_state().set_maximum_zoom_scale( |
| self.webScrollView.maximumZoomScale); |
| displayState.zoom_state().set_zoom_scale( |
| displayState.zoom_state().minimum_zoom_scale() + |
| zoomPercentage * displayState.zoom_state().GetMinMaxZoomDifference()); |
| currentItem->SetPageDisplayState(displayState); |
| [self applyPageDisplayState:currentItem->GetPageDisplayState()]; |
| } |
| |
| - (void)applyPageDisplayState:(const web::PageDisplayState&)displayState { |
| if (!displayState.IsValid()) |
| return; |
| base::WeakNSObject<CRWWebController> weakSelf(self); |
| web::PageDisplayState displayStateCopy = displayState; |
| [self extractViewportTagWithCompletion:^( |
| const web::PageViewportState* viewportState) { |
| if (viewportState) { |
| [weakSelf applyPageDisplayState:displayStateCopy |
| userScalable:viewportState->user_scalable()]; |
| } |
| }]; |
| } |
| |
| - (void)applyPageDisplayState:(const web::PageDisplayState&)displayState |
| userScalable:(BOOL)isUserScalable { |
| // Early return if |scrollState| doesn't match the current NavigationItem. |
| // This can sometimes occur in tests, as navigation occurs programmatically |
| // and |-applyPageScrollState:| is asynchronous. |
| web::NavigationItem* currentItem = self.currentNavItem; |
| if (currentItem && currentItem->GetPageDisplayState() != displayState) |
| return; |
| DCHECK(displayState.IsValid()); |
| _applyingPageState = YES; |
| if (isUserScalable) { |
| [self prepareToApplyWebViewScrollZoomScale]; |
| [self applyWebViewScrollZoomScaleFromZoomState:displayState.zoom_state()]; |
| [self finishApplyingWebViewScrollZoomScale]; |
| } |
| [self applyWebViewScrollOffsetFromScrollState:displayState.scroll_state()]; |
| _applyingPageState = NO; |
| } |
| |
| - (void)prepareToApplyWebViewScrollZoomScale { |
| id webView = _webView; |
| if (![webView respondsToSelector:@selector(viewForZoomingInScrollView:)]) { |
| return; |
| } |
| |
| UIView* contentView = [webView viewForZoomingInScrollView:self.webScrollView]; |
| |
| if ([webView |
| respondsToSelector:@selector(scrollViewWillBeginZooming:withView:)]) { |
| [webView scrollViewWillBeginZooming:self.webScrollView |
| withView:contentView]; |
| } |
| } |
| |
| - (void)finishApplyingWebViewScrollZoomScale { |
| id webView = _webView; |
| if ([webView respondsToSelector:@selector(scrollViewDidEndZooming: |
| withView: |
| atScale:)] && |
| [webView respondsToSelector:@selector(viewForZoomingInScrollView:)]) { |
| // This correctly sets the content's frame in the scroll view to |
| // fit the web page and upscales the content so that it isn't |
| // blurry. |
| UIView* contentView = |
| [webView viewForZoomingInScrollView:self.webScrollView]; |
| [webView scrollViewDidEndZooming:self.webScrollView |
| withView:contentView |
| atScale:self.webScrollView.zoomScale]; |
| } |
| } |
| |
| - (void)applyWebViewScrollZoomScaleFromZoomState: |
| (const web::PageZoomState&)zoomState { |
| // After rendering a web page, WKWebView keeps the |minimumZoomScale| and |
| // |maximumZoomScale| properties of its scroll view constant while adjusting |
| // the |zoomScale| property accordingly. The maximum-scale or minimum-scale |
| // meta tags of a page may have changed since the state was recorded, so clamp |
| // the zoom scale to the current range if necessary. |
| DCHECK(zoomState.IsValid()); |
| CGFloat zoomScale = zoomState.zoom_scale(); |
| if (zoomScale < self.webScrollView.minimumZoomScale) |
| zoomScale = self.webScrollView.minimumZoomScale; |
| if (zoomScale > self.webScrollView.maximumZoomScale) |
| zoomScale = self.webScrollView.maximumZoomScale; |
| self.webScrollView.zoomScale = zoomScale; |
| } |
| |
| - (void)applyWebViewScrollOffsetFromScrollState: |
| (const web::PageScrollState&)scrollState { |
| DCHECK(scrollState.IsValid()); |
| CGPoint scrollOffset = |
| CGPointMake(scrollState.offset_x(), scrollState.offset_y()); |
| if (_loadPhase == web::PAGE_LOADED) { |
| // If the page is loaded, update the scroll immediately. |
| [self.webScrollView setContentOffset:scrollOffset]; |
| } else { |
| // If the page isn't loaded, store the action to update the scroll |
| // when the page finishes loading. |
| base::WeakNSObject<UIScrollView> weakScrollView(self.webScrollView); |
| base::scoped_nsprotocol<ProceduralBlock> action([^{ |
| [weakScrollView setContentOffset:scrollOffset]; |
| } copy]); |
| [_pendingLoadCompleteActions addObject:action]; |
| } |
| } |
| |
| #pragma mark - |
| #pragma mark Fullscreen |
| |
| - (CGRect)visibleFrame { |
| CGRect frame = [_containerView bounds]; |
| CGFloat headerHeight = [self headerHeight]; |
| frame.origin.y = headerHeight; |
| frame.size.height -= headerHeight; |
| return frame; |
| } |
| |
| - (void)optOutScrollsToTopForSubviews { |
| NSMutableArray* stack = |
| [NSMutableArray arrayWithArray:[self.webScrollView subviews]]; |
| while (stack.count) { |
| UIView* current = [stack lastObject]; |
| [stack removeLastObject]; |
| [stack addObjectsFromArray:[current subviews]]; |
| if ([current isKindOfClass:[UIScrollView class]]) |
| static_cast<UIScrollView*>(current).scrollsToTop = NO; |
| } |
| } |
| |
| #pragma mark - |
| #pragma mark WebDelegate Calls |
| |
| - (BOOL)shouldOpenURL:(const GURL&)url |
| mainDocumentURL:(const GURL&)mainDocumentURL |
| linkClicked:(BOOL)linkClicked { |
| if (![_delegate respondsToSelector:@selector(webController: |
| shouldOpenURL: |
| mainDocumentURL: |
| linkClicked:)]) { |
| return YES; |
| } |
| return [_delegate webController:self |
| shouldOpenURL:url |
| mainDocumentURL:mainDocumentURL |
| linkClicked:linkClicked]; |
| } |
| |
| - (BOOL)isMainFrameNavigationAction:(WKNavigationAction*)action { |
| if (action.targetFrame) { |
| return action.targetFrame.mainFrame; |
| } |
| // According to WKNavigationAction documentation, in the case of a new window |
| // navigation, target frame will be nil. In this case check if the |
| // |sourceFrame| is the mainFrame. |
| return action.sourceFrame.mainFrame; |
| } |
| |
| - (BOOL)shouldOpenExternalURLForNavigationAction:(WKNavigationAction*)action { |
| ExternalURLRequestStatus requestStatus = NUM_EXTERNAL_URL_REQUEST_STATUS; |
| if ([self isMainFrameNavigationAction:action]) { |
| requestStatus = MAIN_FRAME_ALLOWED; |
| } else { |
| // If the request's main document URL differs from that at the time of the |
| // last user interaction, then the page has changed since the user last |
| // interacted. |
| BOOL userInteractedWithRequestMainFrame = |
| [self userClickedRecently] && |
| net::GURLWithNSURL(action.request.mainDocumentURL) == |
| _lastUserInteraction->main_document_url; |
| // Prevent subframe requests from opening an external URL if the user has |
| // not interacted with the request's main frame. |
| requestStatus = userInteractedWithRequestMainFrame ? SUBFRAME_ALLOWED |
| : SUBFRAME_BLOCKED; |
| } |
| DCHECK_NE(requestStatus, NUM_EXTERNAL_URL_REQUEST_STATUS); |
| UMA_HISTOGRAM_ENUMERATION("WebController.ExternalURLRequestBlocking", |
| requestStatus, NUM_EXTERNAL_URL_REQUEST_STATUS); |
| if (requestStatus == SUBFRAME_BLOCKED) { |
| return NO; |
| } |
| |
| GURL requestURL = net::GURLWithNSURL(action.request.URL); |
| return [_delegate respondsToSelector:@selector(webController: |
| shouldOpenExternalURL:)] && |
| [_delegate webController:self shouldOpenExternalURL:requestURL]; |
| } |
| |
| - (BOOL)urlTriggersNativeAppLaunch:(const GURL&)URL |
| sourceURL:(const GURL&)sourceURL |
| linkActivatedNavigation:(BOOL)linkActivatedNavigation { |
| if (![_delegate respondsToSelector:@selector(urlTriggersNativeAppLaunch: |
| sourceURL: |
| linkClicked:)]) { |
| return NO; |
| } |
| return [_delegate urlTriggersNativeAppLaunch:URL |
| sourceURL:sourceURL |
| linkClicked:linkActivatedNavigation]; |
| } |
| |
| - (CGFloat)headerHeight { |
| if (![_delegate respondsToSelector:@selector(headerHeightForWebController:)]) |
| return 0.0f; |
| return [_delegate headerHeightForWebController:self]; |
| } |
| |
| - (void)updateSSLStatusForCurrentNavigationItem { |
| if (_isBeingDestroyed) { |
| return; |
| } |
| |
| web::NavigationManager* navManager = self.webState->GetNavigationManager(); |
| web::NavigationItem* currentNavItem = navManager->GetLastCommittedItem(); |
| if (!currentNavItem) { |
| return; |
| } |
| |
| if (!_SSLStatusUpdater) { |
| _SSLStatusUpdater.reset([[CRWSSLStatusUpdater alloc] |
| initWithDataSource:self |
| navigationManager:navManager]); |
| [_SSLStatusUpdater setDelegate:self]; |
| } |
| NSString* host = base::SysUTF8ToNSString(_documentURL.host()); |
| BOOL hasOnlySecureContent = [_webView hasOnlySecureContent]; |
| base::ScopedCFTypeRef<SecTrustRef> trust; |
| if (base::ios::IsRunningOnIOS10OrLater()) { |
| trust.reset([_webView serverTrust], base::scoped_policy::RETAIN); |
| } else { |
| trust = web::CreateServerTrustFromChain([_webView certificateChain], host); |
| } |
| |
| [_SSLStatusUpdater updateSSLStatusForNavigationItem:currentNavItem |
| withCertHost:host |
| trust:std::move(trust) |
| hasOnlySecureContent:hasOnlySecureContent]; |
| } |
| |
| - (void)didShowPasswordInputOnHTTP { |
| DCHECK(!web::IsOriginSecure(self.webState->GetLastCommittedURL())); |
| web::NavigationItem* item = |
| _webStateImpl->GetNavigationManager()->GetLastCommittedItem(); |
| item->GetSSL().content_status |= |
| web::SSLStatus::DISPLAYED_PASSWORD_FIELD_ON_HTTP; |
| _webStateImpl->OnVisibleSecurityStateChange(); |
| } |
| |
| - (void)didShowCreditCardInputOnHTTP { |
| DCHECK(!web::IsOriginSecure(self.webState->GetLastCommittedURL())); |
| web::NavigationItem* item = |
| _webStateImpl->GetNavigationManager()->GetLastCommittedItem(); |
| item->GetSSL().content_status |= |
| web::SSLStatus::DISPLAYED_CREDIT_CARD_FIELD_ON_HTTP; |
| _webStateImpl->OnVisibleSecurityStateChange(); |
| } |
| |
| - (void)handleSSLCertError:(NSError*)error { |
| CHECK(web::IsWKWebViewSSLCertError(error)); |
| |
| net::SSLInfo info; |
| web::GetSSLInfoFromWKWebViewSSLCertError(error, &info); |
| |
| if (!info.cert) { |
| // |info.cert| can be null if certChain in NSError is empty or can not be |
| // parsed, in this case do not ask delegate if error should be allowed, it |
| // should not be. |
| [self handleLoadError:error inMainFrame:YES]; |
| return; |
| } |
| |
| // Retrieve verification results from _certVerificationErrors cache to avoid |
| // unnecessary recalculations. Verification results are cached for the leaf |
| // cert, because the cert chain in |didReceiveAuthenticationChallenge:| is |
| // the OS constructed chain, while |chain| is the chain from the server. |
| NSArray* chain = error.userInfo[web::kNSErrorPeerCertificateChainKey]; |
| NSURL* requestURL = error.userInfo[web::kNSErrorFailingURLKey]; |
| NSString* host = [requestURL host]; |
| scoped_refptr<net::X509Certificate> leafCert; |
| BOOL recoverable = NO; |
| if (chain.count && host.length) { |
| // The complete cert chain may not be available, so the leaf cert is used |
| // as a key to retrieve _certVerificationErrors, as well as for storing the |
| // cert decision. |
| leafCert = web::CreateCertFromChain(@[ chain.firstObject ]); |
| if (leafCert) { |
| auto error = _certVerificationErrors->Get( |
| {leafCert, base::SysNSStringToUTF8(host)}); |
| bool cacheHit = error != _certVerificationErrors->end(); |
| if (cacheHit) { |
| recoverable = error->second.is_recoverable; |
| info.cert_status = error->second.status; |
| } |
| UMA_HISTOGRAM_BOOLEAN("WebController.CertVerificationErrorsCacheHit", |
| cacheHit); |
| } |
| } |
| |
| // Ask web client if this cert error should be allowed. |
| web::GetWebClient()->AllowCertificateError( |
| _webStateImpl, net::MapCertStatusToNetError(info.cert_status), info, |
| net::GURLWithNSURL(requestURL), recoverable, |
| base::BindBlock(^(bool proceed) { |
| if (proceed) { |
| DCHECK(recoverable); |
| [_certVerificationController allowCert:leafCert |
| forHost:host |
| status:info.cert_status]; |
| [self.sessionController.sessionCertificatePolicyManager |
| registerAllowedCertificate:leafCert |
| forHost:base::SysNSStringToUTF8(host) |
| status:info.cert_status]; |
| [self loadCurrentURL]; |
| } |
| })); |
| |
| _webStateImpl->OnVisibleSecurityStateChange(); |
| [self loadCancelled]; |
| } |
| |
| - (void)ensureWebViewCreated { |
| WKWebViewConfiguration* config = |
| [self webViewConfigurationProvider].GetWebViewConfiguration(); |
| [self ensureWebViewCreatedWithConfiguration:config]; |
| } |
| |
| - (void)ensureWebViewCreatedWithConfiguration:(WKWebViewConfiguration*)config { |
| if (!_webView) { |
| [self setWebView:[self webViewWithConfiguration:config]]; |
| // The following is not called in -setWebView: as the latter used in unit |
| // tests with fake web view, which cannot be added to view hierarchy. |
| CHECK(_webUsageEnabled) << "Tried to create a web view while suspended!"; |
| |
| DCHECK(_webView); |
| |
| [_webView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| UIViewAutoresizingFlexibleHeight]; |
| [_webView setBackgroundColor:[UIColor colorWithWhite:0.2 alpha:1.0]]; |
| |
| // Create a dependency between the |webView| pan gesture and BVC side swipe |
| // gestures. Note: This needs to be added before the longPress recognizers |
| // below, or the longPress appears to deadlock the remaining recognizers, |
| // thereby breaking scroll. |
| NSSet* recognizers = [_swipeRecognizerProvider swipeRecognizers]; |
| for (UISwipeGestureRecognizer* swipeRecognizer in recognizers) { |
| [self.webScrollView.panGestureRecognizer |
| requireGestureRecognizerToFail:swipeRecognizer]; |
| } |
| |
| _contextMenuController.reset([[CRWContextMenuController alloc] |
| initWithWebView:_webView |
| injectionEvaluator:self |
| delegate:self]); |
| |
| // Add all additional gesture recognizers to the web view. |
| for (UIGestureRecognizer* recognizer in _gestureRecognizers.get()) { |
| [_webView addGestureRecognizer:recognizer]; |
| } |
| |
| _URLOnStartLoading = _defaultURL; |
| |
| // Add the web toolbars. |
| [_containerView addToolbars:_webViewToolbars]; |
| |
| base::scoped_nsobject<CRWWebViewContentView> webViewContentView( |
| [[CRWWebViewContentView alloc] initWithWebView:_webView |
| scrollView:self.webScrollView]); |
| [_containerView displayWebViewContentView:webViewContentView]; |
| } |
| } |
| |
| - (WKWebView*)webViewWithConfiguration:(WKWebViewConfiguration*)config { |
| // Do not attach the context menu controller immediately as the JavaScript |
| // delegate must be specified. |
| return web::BuildWKWebView(CGRectZero, config, |
| self.webStateImpl->GetBrowserState(), |
| self.usesDesktopUserAgent); |
| } |
| |
| - (void)setWebView:(WKWebView*)webView { |
| DCHECK_NE(_webView.get(), webView); |
| |
| // Unwind the old web view. |
| // TODO(eugenebut): Remove CRWWKScriptMessageRouter once crbug.com/543374 is |
| // fixed. |
| CRWWKScriptMessageRouter* messageRouter = |
| [self webViewConfigurationProvider].GetScriptMessageRouter(); |
| if (_webView) { |
| [messageRouter removeAllScriptMessageHandlersForWebView:_webView]; |
| } |
| [_webView setNavigationDelegate:nil]; |
| [_webView setUIDelegate:nil]; |
| for (NSString* keyPath in self.WKWebViewObservers) { |
| [_webView removeObserver:self forKeyPath:keyPath]; |
| } |
| |
| _webView.reset([webView retain]); |
| |
| // Set up the new web view. |
| if (webView) { |
| base::WeakNSObject<CRWWebController> weakSelf(self); |
| [messageRouter setScriptMessageHandler:^(WKScriptMessage* message) { |
| [weakSelf didReceiveScriptMessage:message]; |
| } |
| name:kScriptMessageName |
| webView:webView]; |
| _windowIDJSManager.reset( |
| [[CRWJSWindowIDManager alloc] initWithWebView:webView]); |
| } else { |
| _windowIDJSManager.reset(); |
| } |
| [_webView setNavigationDelegate:self]; |
| [_webView setUIDelegate:self]; |
| for (NSString* keyPath in self.WKWebViewObservers) { |
| [_webView addObserver:self forKeyPath:keyPath options:0 context:nullptr]; |
| } |
| _injectedScriptManagers.reset([[NSMutableSet alloc] init]); |
| [self setDocumentURL:_defaultURL]; |
| } |
| |
| - (void)removeWebViewAllowingCachedReconstruction:(BOOL)allowCache { |
| if (!_webView) |
| return; |
| |
| _webStateImpl->CancelDialogs(); |
| |
| web::NavigationItem* item = self.currentNavItem; |
| if (allowCache && item) { |
| _expectedReconstructionURL = item->GetVirtualURL(); |
| } else { |
| _expectedReconstructionURL = GURL::EmptyGURL(); |
| } |
| |
| [self abortLoad]; |
| [_webView removeFromSuperview]; |
| [_containerView resetContent]; |
| [self setWebView:nil]; |
| } |
| |
| - (void)webViewWebProcessDidCrash { |
| _webProcessIsDead = YES; |
| _webStateImpl->CancelDialogs(); |
| _webStateImpl->OnRenderProcessGone(); |
| } |
| |
| - (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider { |
| web::BrowserState* browserState = self.webStateImpl->GetBrowserState(); |
| return web::WKWebViewConfigurationProvider::FromBrowserState(browserState); |
| } |
| |
| - (void)loadRequest:(NSMutableURLRequest*)request { |
| [_navigationStates setState:web::WKNavigationState::REQUESTED |
| forNavigation:[_webView loadRequest:request]]; |
| } |
| |
| - (void)loadPOSTRequest:(NSMutableURLRequest*)request { |
| if (!_POSTRequestLoader) { |
| _POSTRequestLoader.reset([[CRWJSPOSTRequestLoader alloc] init]); |
| } |
| |
| CRWWKScriptMessageRouter* messageRouter = |
| [self webViewConfigurationProvider].GetScriptMessageRouter(); |
| |
| [_POSTRequestLoader loadPOSTRequest:request |
| inWebView:_webView |
| messageRouter:messageRouter |
| completionHandler:^(NSError* loadError) { |
| if (loadError) |
| [self handleLoadError:loadError inMainFrame:YES]; |
| else |
| self.webStateImpl->SetContentsMimeType("text/html"); |
| }]; |
| } |
| |
| - (void)loadHTML:(NSString*)HTML forURL:(const GURL&)URL { |
| // Remove the transient content view. |
| [self clearTransientContentView]; |
| |
| _loadPhase = web::LOAD_REQUESTED; |
| |
| // Web View should not be created for App Specific URLs. |
| if (!web::GetWebClient()->IsAppSpecificURL(URL)) { |
| [self ensureWebViewCreated]; |
| DCHECK(_webView) << "_webView null while trying to load HTML"; |
| } |
| WKNavigation* navigation = |
| [_webView loadHTMLString:HTML baseURL:net::NSURLWithGURL(URL)]; |
| [_navigationStates setState:web::WKNavigationState::REQUESTED |
| forNavigation:navigation]; |
| } |
| |
| - (void)loadHTML:(NSString*)HTML forAppSpecificURL:(const GURL&)URL { |
| CHECK(web::GetWebClient()->IsAppSpecificURL(URL)); |
| [self loadHTML:HTML forURL:URL]; |
| } |
| |
| - (void)loadHTMLForCurrentURL:(NSString*)HTML { |
| [self loadHTML:HTML forURL:self.currentURL]; |
| } |
| |
| - (void)stopLoading { |
| _stoppedWKNavigation.reset([_navigationStates lastAddedNavigation]); |
| |
| base::RecordAction(UserMetricsAction("Stop")); |
| // Discard the pending and transient entried before notifying the tab model |
| // observers of the change via |-abortLoad|. |
| [[self sessionController] discardNonCommittedItems]; |
| [self abortLoad]; |
| web::NavigationItem* item = self.currentNavItem; |
| GURL navigationURL = item ? item->GetVirtualURL() : GURL::EmptyGURL(); |
| // If discarding the non-committed entries results in an app-specific URL, |
| // reload it in its native view. |
| if (!self.nativeController && |
| [self shouldLoadURLInNativeView:navigationURL]) { |
| [self loadCurrentURLInNativeView]; |
| } |
| } |
| |
| #pragma mark - |
| #pragma mark WKUIDelegate Methods |
| |
| - (WKWebView*)webView:(WKWebView*)webView |
| createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration |
| forNavigationAction:(WKNavigationAction*)action |
| windowFeatures:(WKWindowFeatures*)windowFeatures { |
| if (self.shouldSuppressDialogs) { |
| _webStateImpl->OnDialogSuppressed(); |
| return nil; |
| } |
| |
| // Do not create windows for non-empty invalid URLs. |
| GURL requestURL = net::GURLWithNSURL(action.request.URL); |
| if (!requestURL.is_empty() && !requestURL.is_valid()) { |
| DLOG(WARNING) << "Unable to open a window with invalid URL: " |
| << requestURL.spec(); |
| return nil; |
| } |
| |
| NSString* referrer = [self referrerFromNavigationAction:action]; |
| GURL openerURL = |
| referrer.length ? GURL(base::SysNSStringToUTF8(referrer)) : _documentURL; |
| |
| WebState* childWebState = _webStateImpl->CreateNewWebState( |
| requestURL, openerURL, [self userIsInteracting]); |
| if (!childWebState) |
| return nil; |
| |
| CRWWebController* childWebController = |
| static_cast<WebStateImpl*>(childWebState)->GetWebController(); |
| |
| DCHECK(!childWebController || childWebController.hasOpener); |
| |
| // WKWebView requires WKUIDelegate to return a child view created with |
| // exactly the same |configuration| object (exception is raised if config is |
| // different). |configuration| param and config returned by |
| // WKWebViewConfigurationProvider are different objects because WKWebView |
| // makes a shallow copy of the config inside init, so every WKWebView |
| // owns a separate shallow copy of WKWebViewConfiguration. |
| [childWebController ensureWebViewCreatedWithConfiguration:configuration]; |
| return childWebController.webView; |
| } |
| |
| - (void)webViewDidClose:(WKWebView*)webView { |
| if (self.hasOpener) |
| _webStateImpl->CloseWebState(); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| runJavaScriptAlertPanelWithMessage:(NSString*)message |
| initiatedByFrame:(WKFrameInfo*)frame |
| completionHandler:(void (^)())completionHandler { |
| [self runJavaScriptDialogOfType:web::JAVASCRIPT_DIALOG_TYPE_ALERT |
| initiatedByFrame:frame |
| message:message |
| defaultText:nil |
| completion:^(BOOL, NSString*) { |
| completionHandler(); |
| }]; |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| runJavaScriptConfirmPanelWithMessage:(NSString*)message |
| initiatedByFrame:(WKFrameInfo*)frame |
| completionHandler: |
| (void (^)(BOOL result))completionHandler { |
| [self runJavaScriptDialogOfType:web::JAVASCRIPT_DIALOG_TYPE_CONFIRM |
| initiatedByFrame:frame |
| message:message |
| defaultText:nil |
| completion:^(BOOL success, NSString*) { |
| if (completionHandler) { |
| completionHandler(success); |
| } |
| }]; |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt |
| defaultText:(NSString*)defaultText |
| initiatedByFrame:(WKFrameInfo*)frame |
| completionHandler: |
| (void (^)(NSString* result))completionHandler { |
| GURL origin(web::GURLOriginWithWKSecurityOrigin(frame.securityOrigin)); |
| if (web::GetWebClient()->IsAppSpecificURL(origin) && _webUIManager) { |
| std::string mojoResponse = |
| self.mojoFacade->HandleMojoMessage(base::SysNSStringToUTF8(prompt)); |
| completionHandler(base::SysUTF8ToNSString(mojoResponse)); |
| return; |
| } |
| |
| [self runJavaScriptDialogOfType:web::JAVASCRIPT_DIALOG_TYPE_PROMPT |
| initiatedByFrame:frame |
| message:prompt |
| defaultText:defaultText |
| completion:^(BOOL, NSString* input) { |
| if (completionHandler) { |
| completionHandler(input); |
| } |
| }]; |
| } |
| |
| #pragma mark - |
| #pragma mark WKNavigationDelegate Methods |
| |
| - (void)webView:(WKWebView*)webView |
| decidePolicyForNavigationAction:(WKNavigationAction*)action |
| decisionHandler: |
| (void (^)(WKNavigationActionPolicy))decisionHandler { |
| _webProcessIsDead = NO; |
| if (_isBeingDestroyed) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| return; |
| } |
| |
| // The page will not be changed until this navigation is commited, so the |
| // retrieved state will be pending until |didCommitNavigation| callback. |
| [self updatePendingNavigationInfoFromNavigationAction:action]; |
| |
| // Invalid URLs should not be loaded. However, simply doing nothing upon |
| // tapping a link or button is a jarring user experience. Instead, cancel |
| // the invalid navigation and load about:blank if navigation was requested for |
| // the main frame. |
| GURL requestURL = net::GURLWithNSURL(action.request.URL); |
| if (!requestURL.is_valid()) { |
| decisionHandler(WKNavigationActionPolicyCancel); |
| if (action.targetFrame.mainFrame) { |
| GURL aboutBlankURL(url::kAboutBlankURL); |
| web::NavigationManager::WebLoadParams loadParams(aboutBlankURL); |
| loadParams.referrer = [self currentReferrer]; |
| self.webState->GetNavigationManager()->LoadURLWithParams(loadParams); |
| } |
| return; |
| } |
| |
| BOOL allowLoad = [self shouldAllowLoadWithNavigationAction:action]; |
| |
| if (allowLoad) { |
| allowLoad = self.webStateImpl->ShouldAllowRequest(action.request); |
| if (!allowLoad && action.targetFrame.mainFrame) { |
| [_pendingNavigationInfo setCancelled:YES]; |
| } |
| } |
| |
| decisionHandler(allowLoad ? WKNavigationActionPolicyAllow |
| : WKNavigationActionPolicyCancel); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| decidePolicyForNavigationResponse:(WKNavigationResponse*)navigationResponse |
| decisionHandler: |
| (void (^)(WKNavigationResponsePolicy))handler { |
| if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { |
| // Create HTTP headers from the response. |
| // TODO(crbug.com/546157): Due to the limited interface of |
| // NSHTTPURLResponse, some data in the HttpResponseHeaders generated here is |
| // inexact. Once UIWebView is no longer supported, update WebState's |
| // implementation so that the Content-Language and the MIME type can be set |
| // without using this imperfect conversion. |
| scoped_refptr<net::HttpResponseHeaders> HTTPHeaders = |
| net::CreateHeadersFromNSHTTPURLResponse( |
| static_cast<NSHTTPURLResponse*>(navigationResponse.response)); |
| self.webStateImpl->OnHttpResponseHeadersReceived( |
| HTTPHeaders.get(), net::GURLWithNSURL(navigationResponse.response.URL)); |
| } |
| |
| // The page will not be changed until this navigation is commited, so the |
| // retrieved state will be pending until |didCommitNavigation| callback. |
| [self updatePendingNavigationInfoFromNavigationResponse:navigationResponse]; |
| |
| BOOL allowNavigation = navigationResponse.canShowMIMEType; |
| if (allowNavigation) { |
| allowNavigation = |
| self.webStateImpl->ShouldAllowResponse(navigationResponse.response); |
| if (!allowNavigation && navigationResponse.isForMainFrame) { |
| [_pendingNavigationInfo setCancelled:YES]; |
| } |
| } |
| if ([self.passKitDownloader |
| isMIMETypePassKitType:[_pendingNavigationInfo MIMEType]]) { |
| GURL URL = net::GURLWithNSURL(navigationResponse.response.URL); |
| [self.passKitDownloader downloadPassKitFileWithURL:URL]; |
| allowNavigation = NO; |
| |
| // Discard the pending PassKit entry to ensure that the current URL is not |
| // different from what is displayed on the view. If there is no previous |
| // committed URL, which can happen when a link is opened in a new tab via a |
| // context menu or window.open, the pending entry should not be |
| // discarded so that the NavigationManager is never empty. Also, URLs loaded |
| // in a native view should be excluded to avoid an ugly animation where the |
| // web view is inserted and quickly removed. |
| GURL lastCommittedURL = self.webState->GetLastCommittedURL(); |
| BOOL isFirstLoad = lastCommittedURL.is_empty(); |
| BOOL previousItemWasLoadedInNativeView = |
| [self shouldLoadURLInNativeView:lastCommittedURL]; |
| if (!isFirstLoad && !previousItemWasLoadedInNativeView) |
| [self.sessionController discardNonCommittedItems]; |
| } |
| |
| handler(allowNavigation ? WKNavigationResponsePolicyAllow |
| : WKNavigationResponsePolicyCancel); |
| } |
| |
| // TODO(stuartmorgan): Move all the guesswork around these states out of the |
| // superclass, and wire these up to the remaining methods. |
| - (void)webView:(WKWebView*)webView |
| didStartProvisionalNavigation:(WKNavigation*)navigation { |
| [_navigationStates setState:web::WKNavigationState::STARTED |
| forNavigation:navigation]; |
| |
| GURL webViewURL = net::GURLWithNSURL(webView.URL); |
| if (webViewURL.is_empty()) { |
| // May happen on iOS9, however in didCommitNavigation: callback the URL |
| // will be "about:blank". |
| webViewURL = GURL(url::kAboutBlankURL); |
| } |
| |
| // Intercept renderer-initiated navigations. If this navigation has not yet |
| // been registered, do so. loadPhase check is necessary because |
| // lastRegisteredRequestURL may be the same as the webViewURL on a new tab |
| // created by window.open (default is about::blank). |
| if (_lastRegisteredRequestURL != webViewURL || |
| self.loadPhase != web::LOAD_REQUESTED) { |
| // Reset current WebUI if one exists. |
| [self clearWebUI]; |
| // Restart app specific URL loads to properly capture state. |
| // TODO(crbug.com/546347): Extract necessary tasks for app specific URL |
| // navigation rather than restarting the load. |
| if (web::GetWebClient()->IsAppSpecificURL(webViewURL)) { |
| // Renderer-initiated loads of WebUI can be done only from other WebUI |
| // pages. WebUI pages may have increased power and using the same web |
| // process (which may potentially be controller by an attacker) is |
| // dangerous. |
| if (web::GetWebClient()->IsAppSpecificURL(_documentURL)) { |
| [self abortLoad]; |
| NavigationManager::WebLoadParams params(webViewURL); |
| [self loadWithParams:params]; |
| } |
| return; |
| } else { |
| [self registerLoadRequest:webViewURL]; |
| } |
| } |
| |
| // Ensure the URL is registered and loadPhase is as expected. |
| DCHECK(_lastRegisteredRequestURL == webViewURL); |
| DCHECK(self.loadPhase == web::LOAD_REQUESTED); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation { |
| [_navigationStates setState:web::WKNavigationState::REDIRECTED |
| forNavigation:navigation]; |
| |
| [self registerLoadRequest:net::GURLWithNSURL(webView.URL) |
| referrer:[self currentReferrer] |
| transition:ui::PAGE_TRANSITION_SERVER_REDIRECT]; |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didFailProvisionalNavigation:(WKNavigation*)navigation |
| withError:(NSError*)error { |
| [_navigationStates setState:web::WKNavigationState::PROVISIONALY_FAILED |
| forNavigation:navigation]; |
| |
| // Ignore provisional navigation failure if a new navigation has been started, |
| // for example, if a page is reloaded after the start of the provisional |
| // load but before the load has been committed. |
| if (![[_navigationStates lastAddedNavigation] isEqual:navigation]) { |
| return; |
| } |
| |
| // TODO(crbug.com/570699): Remove this workaround once |stopLoading| does not |
| // discard pending navigation items. |
| if ((!self.webStateImpl || |
| !self.webStateImpl->GetNavigationManagerImpl().GetVisibleItem()) && |
| [error.domain isEqual:base::SysUTF8ToNSString(web::kWebKitErrorDomain)] && |
| error.code == web::kWebKitErrorFrameLoadInterruptedByPolicyChange) { |
| // App is going to crash in this state (crbug.com/565457). Crash will occur |
| // on dereferencing visible navigation item, which is null. This scenario is |
| // possible after pending load was stopped for a child window. Early return |
| // to prevent the crash and report UMA metric to check if crash happening |
| // because the load was stopped. |
| UMA_HISTOGRAM_BOOLEAN( |
| "WebController.EmptyNavigationManagerCausedByStopLoading", |
| [_stoppedWKNavigation isEqual:navigation]); |
| return; |
| } |
| |
| // Handle load cancellation for directly cancelled navigations without |
| // handling their potential errors. Otherwise, handle the error. |
| if ([_pendingNavigationInfo cancelled]) { |
| [self loadCancelled]; |
| } else { |
| error = WKWebViewErrorWithSource(error, PROVISIONAL_LOAD); |
| |
| if (web::IsWKWebViewSSLCertError(error)) |
| [self handleSSLCertError:error]; |
| else |
| [self handleLoadError:error inMainFrame:YES]; |
| } |
| |
| // This must be reset at the end, since code above may need information about |
| // the pending load. |
| _pendingNavigationInfo.reset(); |
| _certVerificationErrors->Clear(); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didCommitNavigation:(WKNavigation*)navigation { |
| [_navigationStates setState:web::WKNavigationState::COMMITTED |
| forNavigation:navigation]; |
| |
| DCHECK_EQ(_webView, webView); |
| _certVerificationErrors->Clear(); |
| |
| // This is the point where the document's URL has actually changed, and |
| // pending navigation information should be applied to state information. |
| [self setDocumentURL:net::GURLWithNSURL([_webView URL])]; |
| |
| if (!_lastRegisteredRequestURL.is_valid() && |
| _documentURL != _lastRegisteredRequestURL) { |
| // if |_lastRegisteredRequestURL| is an invalid URL, then |_documentURL| |
| // will be "about:blank". |
| [[self sessionController] updatePendingItem:_documentURL]; |
| } |
| DCHECK(_documentURL == _lastRegisteredRequestURL || |
| (!_lastRegisteredRequestURL.is_valid() && |
| _documentURL.spec() == url::kAboutBlankURL)); |
| |
| self.webStateImpl->UpdateHttpResponseHeaders(_documentURL); |
| [self commitPendingNavigationInfo]; |
| if ([self currentBackForwardListItemHolder]->navigation_type() == |
| WKNavigationTypeBackForward) { |
| // A fast back/forward won't call decidePolicyForNavigationResponse, so |
| // the MIME type needs to be updated explicitly. |
| NSString* storedMIMEType = |
| [self currentBackForwardListItemHolder]->mime_type(); |
| if (storedMIMEType) { |
| self.webStateImpl->SetContentsMimeType( |
| base::SysNSStringToUTF8(storedMIMEType)); |
| } |
| } |
| |
| // This point should closely approximate the document object change, so reset |
| // the list of injected scripts to those that are automatically injected. |
| _injectedScriptManagers.reset([[NSMutableSet alloc] init]); |
| if ([self contentIsHTML] || self.webState->GetContentsMimeType().empty()) { |
| // In unit tests MIME type will be empty, because loadHTML:forURL: does not |
| // notify web view delegate about received response, so web controller does |
| // not get a chance to properly update MIME type. |
| [self injectWindowID]; |
| } |
| |
| [self webPageChanged]; |
| self.webStateImpl->OnNavigationCommitted(_documentURL); |
| |
| [self updateSSLStatusForCurrentNavigationItem]; |
| |
| // Attempt to update the HTML5 history state. |
| [self updateHTML5HistoryState]; |
| |
| // This is the point where pending entry has been committed, and navigation |
| // item title should be updated. |
| [self setNavigationItemTitle:[_webView title]]; |
| |
| // Report cases where SSL cert is missing for a secure connection. |
| if (_documentURL.SchemeIsCryptographic()) { |
| scoped_refptr<net::X509Certificate> cert = |
| web::CreateCertFromChain([_webView certificateChain]); |
| UMA_HISTOGRAM_BOOLEAN("WebController.WKWebViewHasCertForSecureConnection", |
| static_cast<bool>(cert)); |
| } |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didFinishNavigation:(WKNavigation*)navigation { |
| [_navigationStates setState:web::WKNavigationState::FINISHED |
| forNavigation:navigation]; |
| |
| DCHECK(!_isHalted); |
| // Trigger JavaScript driven post-document-load-completion tasks. |
| // TODO(crbug.com/546350): Investigate using |
| // WKUserScriptInjectionTimeAtDocumentEnd to inject this material at the |
| // appropriate time rather than invoking here. |
| web::ExecuteJavaScript(webView, @"__gCrWeb.didFinishNavigation()", nil); |
| [self didFinishNavigation]; |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didFailNavigation:(WKNavigation*)navigation |
| withError:(NSError*)error { |
| [_navigationStates setState:web::WKNavigationState::FAILED |
| forNavigation:navigation]; |
| |
| [self handleLoadError:WKWebViewErrorWithSource(error, NAVIGATION) |
| inMainFrame:YES]; |
| _certVerificationErrors->Clear(); |
| } |
| |
| - (void)webView:(WKWebView*)webView |
| didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge |
| completionHandler: |
| (void (^)(NSURLSessionAuthChallengeDisposition, |
| NSURLCredential*))completionHandler { |
| NSString* authMethod = challenge.protectionSpace.authenticationMethod; |
| if ([authMethod isEqual:NSURLAuthenticationMethodHTTPBasic] || |
| [authMethod isEqual:NSURLAuthenticationMethodNTLM] || |
| [authMethod isEqual:NSURLAuthenticationMethodHTTPDigest]) { |
| [self handleHTTPAuthForChallenge:challenge |
| completionHandler:completionHandler]; |
| return; |
| } |
| |
| if (![authMethod isEqual:NSURLAuthenticationMethodServerTrust]) { |
| completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| return; |
| } |
| |
| SecTrustRef trust = challenge.protectionSpace.serverTrust; |
| base::ScopedCFTypeRef<SecTrustRef> scopedTrust(trust, |
| base::scoped_policy::RETAIN); |
| base::WeakNSObject<CRWWebController> weakSelf(self); |
| [_certVerificationController |
| decideLoadPolicyForTrust:scopedTrust |
| host:challenge.protectionSpace.host |
| completionHandler:^(web::CertAcceptPolicy policy, |
| net::CertStatus status) { |
| base::scoped_nsobject<CRWWebController> strongSelf( |
| [weakSelf retain]); |
| if (!strongSelf) { |
| completionHandler( |
| NSURLSessionAuthChallengeRejectProtectionSpace, nil); |
| return; |
| } |
| [strongSelf processAuthChallenge:challenge |
| forCertAcceptPolicy:policy |
| certStatus:status |
| completionHandler:completionHandler]; |
| }]; |
| } |
| |
| - (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView { |
| _certVerificationErrors->Clear(); |
| [self webViewWebProcessDidCrash]; |
| } |
| |
| #pragma mark - |
| #pragma mark CRWSSLStatusUpdater DataSource/Delegate Methods |
| |
| - (void)SSLStatusUpdater:(CRWSSLStatusUpdater*)SSLStatusUpdater |
| querySSLStatusForTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust |
| host:(NSString*)host |
| completionHandler:(StatusQueryHandler)completionHandler { |
| [_certVerificationController querySSLStatusForTrust:std::move(trust) |
| host:host |
| completionHandler:completionHandler]; |
| } |
| |
| - (void)SSLStatusUpdater:(CRWSSLStatusUpdater*)SSLStatusUpdater |
| didChangeSSLStatusForNavigationItem:(web::NavigationItem*)navigationItem { |
| web::NavigationItem* lastCommittedNavigationItem = |
| _webStateImpl->GetNavigationManager()->GetLastCommittedItem(); |
| if (navigationItem == lastCommittedNavigationItem) { |
| _webStateImpl->OnVisibleSecurityStateChange(); |
| } |
| } |
| |
| #pragma mark - |
| #pragma mark CRWWebContextMenuControllerDelegate methods |
| |
| - (BOOL)webView:(WKWebView*)webView |
| handleContextMenu:(const web::ContextMenuParams&)params { |
| DCHECK(webView == _webView); |
| if (_isBeingDestroyed) { |
| return NO; |
| } |
| return self.webStateImpl->HandleContextMenu(params); |
| } |
| |
| #pragma mark - |
| #pragma mark CRWNativeContentDelegate methods |
| |
| - (BOOL)nativeContent:(id)content |
| handleContextMenu:(const web::ContextMenuParams&)params { |
| if (_isBeingDestroyed) { |
| return NO; |
| } |
| return self.webStateImpl->HandleContextMenu(params); |
| } |
| |
| #pragma mark - |
| #pragma mark KVO Observation |
| |
| - (void)observeValueForKeyPath:(NSString*)keyPath |
| ofObject:(id)object |
| change:(NSDictionary*)change |
| context:(void*)context { |
| NSString* dispatcherSelectorName = self.WKWebViewObservers[keyPath]; |
| DCHECK(dispatcherSelectorName); |
| if (dispatcherSelectorName) |
| [self performSelector:NSSelectorFromString(dispatcherSelectorName)]; |
| } |
| |
| - (void)webViewEstimatedProgressDidChange { |
| if (!_isBeingDestroyed) { |
| self.webStateImpl->SendChangeLoadProgress([_webView estimatedProgress]); |
| } |
| } |
| |
| - (void)webViewSecurityFeaturesDidChange { |
| if (self.loadPhase == web::LOAD_REQUESTED) { |
| // Do not update SSL Status for pending load. It will be updated in |
| // |webView:didCommitNavigation:| callback. |
| return; |
| } |
| [self updateSSLStatusForCurrentNavigationItem]; |
| } |
| |
| - (void)webViewLoadingStateDidChange { |
| if ([_webView isLoading] || ![self isCurrentNavigationBackForward]) { |
| return; |
| } |
| |
| GURL webViewURL = net::GURLWithNSURL([_webView URL]); |
| |
| // For failed navigations, WKWebView will sometimes revert to the previous URL |
| // before committing the current navigation or resetting the web view's |
| // |isLoading| property to NO. If this is the first navigation for the web |
| // view, this will result in an empty URL. |
| BOOL navigationWasCommitted = _loadPhase != web::LOAD_REQUESTED; |
| if (!navigationWasCommitted && |
| (webViewURL.is_empty() || webViewURL == _documentURL)) { |
| return; |
| } |
| |
| if (!navigationWasCommitted && ![_pendingNavigationInfo cancelled]) { |
| // A fast back/forward within the same origin does not call |
| // |didCommitNavigation:|, so signal page change explicitly. |
| DCHECK_EQ(_documentURL.GetOrigin(), webViewURL.GetOrigin()); |
| BOOL isSameDocumentNavigation = |
| [self isKVOChangePotentialSameDocumentNavigationToURL:webViewURL]; |
| [self setDocumentURL:webViewURL]; |
| [self webPageChanged]; |
| if (isSameDocumentNavigation) { |
| _webStateImpl->OnSameDocumentNavigation(webViewURL); |
| } else { |
| _webStateImpl->OnNavigationCommitted(webViewURL); |
| } |
| } |
| |
| [self updateSSLStatusForCurrentNavigationItem]; |
| |
| // Fast back forward navigation may not call |didFinishNavigation:|, so |
| // signal did finish navigation explicitly. |
| if (_lastRegisteredRequestURL == _documentURL) { |
| [self didFinishNavigation]; |
| } |
| } |
| |
| - (void)webViewTitleDidChange { |
| // WKWebView's title becomes empty when the web process dies; ignore that |
| // update. |
| if (_webProcessIsDead) { |
| DCHECK_EQ([_webView title].length, 0U); |
| return; |
| } |
| |
| bool hasPendingNavigation = web::WKNavigationState::COMMITTED <= |
| [_navigationStates lastAddedNavigationState]; |
| if (hasPendingNavigation) { |
| // Do not update the title if there is a navigation in progress because |
| // there is no way to tell if KVO change fired for new or previous page. |
| [self setNavigationItemTitle:[_webView title]]; |
| } |
| } |
| |
| - (void)webViewURLDidChange { |
| // TODO(stuartmorgan): Determine if there are any cases where this still |
| // happens, and if so whether anything should be done when it does. |
| if (![_webView URL]) { |
| DVLOG(1) << "Received nil URL callback"; |
| return; |
| } |
| GURL URL(net::GURLWithNSURL([_webView URL])); |
| // URL changes happen at three points: |
| // 1) When a load starts; at this point, the load is provisional, and |
| // it should be ignored until it's committed, since the document/window |
| // objects haven't changed yet. |
| // 2) When a non-document-changing URL change happens (hash change, |
| // history.pushState, etc.). This URL change happens instantly, so should |
| // be reported. |
| // 3) When a navigation error occurs after provisional navigation starts, |
| // the URL reverts to the previous URL without triggering a new navigation. |
| // |
| // If |isLoading| is NO, then it must be case 2 or 3. If the last committed |
| // URL (_documentURL) matches the current URL, assume that it is a revert from |
| // navigation failure and do nothing. If the URL does not match, assume it is |
| // a non-document-changing URL change, and handle accordingly. |
| // |
| // If |isLoading| is YES, then it could either be case 1, or it could be case |
| // 2 on a page that hasn't finished loading yet. If it's possible that it |
| // could be a same-page navigation (in which case there may not be any other |
| // callback about the URL having changed), then check the actual page URL via |
| // JavaScript. If the origin of the new URL matches the last committed URL, |
| // then check window.location.href, and if it matches, trust it. The origin |
| // check ensures that if a site somehow corrupts window.location.href it can't |
| // do a redirect to a slow-loading target page while it is still loading to |
| // spoof the origin. On a document-changing URL change, the |
| // window.location.href will match the previous URL at this stage, not the web |
| // view's current URL. |
| if (![_webView isLoading]) { |
| if (_documentURL == URL) |
| return; |
| [self URLDidChangeWithoutDocumentChange:URL]; |
| } else if ([self isKVOChangePotentialSameDocumentNavigationToURL:URL]) { |
| [_webView |
| evaluateJavaScript:@"window.location.href" |
| completionHandler:^(id result, NSError* error) { |
| // If the web view has gone away, or the location |
| // couldn't be retrieved, abort. |
| if (!_webView || ![result isKindOfClass:[NSString class]]) { |
| return; |
| } |
| GURL JSURL([result UTF8String]); |
| // Check that window.location matches the new URL. If |
| // it does not, this is a document-changing URL change as |
| // the window location would not have changed to the new |
| // URL when the script was called. |
| BOOL windowLocationMatchesNewURL = JSURL == URL; |
| // Re-check origin in case navigaton has occured since |
| // start of JavaScript evaluation. |
| BOOL newURLOriginMatchesDocumentURLOrigin = |
| _documentURL.GetOrigin() == URL.GetOrigin(); |
| // Check that the web view URL still matches the new URL. |
| // TODO(crbug.com/563568): webViewURLMatchesNewURL check |
| // may drop same document URL changes if pending URL |
| // change occurs immediately after. Revisit heuristics to |
| // prevent this. |
| BOOL webViewURLMatchesNewURL = |
| net::GURLWithNSURL([_webView URL]) == URL; |
| // Check that the new URL is different from the current |
| // document URL. If not, URL change should not be reported. |
| BOOL URLDidChangeFromDocumentURL = URL != _documentURL; |
| if (windowLocationMatchesNewURL && |
| newURLOriginMatchesDocumentURLOrigin && |
| webViewURLMatchesNewURL && URLDidChangeFromDocumentURL) { |
| [self URLDidChangeWithoutDocumentChange:URL]; |
| } |
| }]; |
| } |
| } |
| |
| - (BOOL)isKVOChangePotentialSameDocumentNavigationToURL:(const GURL&)newURL { |
| // If the origin changes, it can't be same-document. |
| if (_documentURL.GetOrigin().is_empty() || |
| _documentURL.GetOrigin() != newURL.GetOrigin()) { |
| return NO; |
| } |
| if (self.loadPhase == web::LOAD_REQUESTED) { |
| // Normally LOAD_REQUESTED indicates that this is a regular, pending |
| // navigation, but it can also happen during a fast-back navigation across |
| // a hash change, so that case is potentially a same-document navigation. |
| return web::GURLByRemovingRefFromGURL(newURL) == |
| web::GURLByRemovingRefFromGURL(_documentURL); |
| } |
| // If it passes all the checks above, it might be (but there's no guarantee |
| // that it is). |
| return YES; |
| } |
| |
| - (void)URLDidChangeWithoutDocumentChange:(const GURL&)newURL { |
| DCHECK(newURL == net::GURLWithNSURL([_webView URL])); |
| DCHECK_EQ(_documentURL.host(), newURL.host()); |
| DCHECK(_documentURL != newURL); |
| |
| // If called during window.history.pushState or window.history.replaceState |
| // JavaScript evaluation, only update the document URL. This callback does not |
| // have any information about the state object and cannot create (or edit) the |
| // navigation entry for this page change. Web controller will sync with |
| // history changes when a window.history.didPushState or |
| // window.history.didReplaceState message is received, which should happen in |
| // the next runloop. |
| // |
| // Otherwise, simulate the whole delegate flow for a load (since the |
| // superclass currently doesn't have a clean separation between URL changes |
| // and document changes). Note that the order of these calls is important: |
| // registering a load request logically comes before updating the document |
| // URL, but also must come first since it uses state that is reset on URL |
| // changes. |
| if (!_changingHistoryState) { |
| // If this wasn't a previously-expected load (e.g., certain back/forward |
| // navigations), register the load request. |
| if (![self isLoadRequestPendingForURL:newURL]) |
| [self registerLoadRequest:newURL]; |
| } |
| |
| [self setDocumentURL:newURL]; |
| |
| if (!_changingHistoryState) { |
| [self didStartLoadingURL:_documentURL]; |
| _webStateImpl->OnSameDocumentNavigation(newURL); |
| [self updateSSLStatusForCurrentNavigationItem]; |
| [self didFinishNavigation]; |
| } |
| } |
| |
| - (BOOL)isLoadRequestPendingForURL:(const GURL&)targetURL { |
| if (self.loadPhase != web::LOAD_REQUESTED) |
| return NO; |
| |
| web::NavigationItem* pendingItem = |
| self.webState->GetNavigationManager()->GetPendingItem(); |
| return pendingItem && pendingItem->GetURL() == targetURL; |
| } |
| |
| - (void)loadRequestForCurrentNavigationItem { |
| DCHECK(_webView && !self.nativeController); |
| DCHECK(self.currentNavItem); |
| // If a load is kicked off on a WKWebView with a frame whose size is {0, 0} or |
| // that has a negative dimension for a size, rendering issues occur that |
| // manifest in erroneous scrolling and tap handling (crbug.com/574996, |
| // crbug.com/577793). |
| DCHECK_GT(CGRectGetWidth([_webView frame]), 0.0); |
| DCHECK_GT(CGRectGetHeight([_webView frame]), 0.0); |
| |
| web::WKBackForwardListItemHolder* holder = |
| [self currentBackForwardListItemHolder]; |
| BOOL repostedForm = |
| [holder->http_method() isEqual:@"POST"] && |
| (holder->navigation_type() == WKNavigationTypeFormResubmitted || |
| holder->navigation_type() == WKNavigationTypeFormSubmitted); |
| web::NavigationItemImpl* currentItem = self.currentNavItem; |
| NSData* POSTData = currentItem->GetPostData(); |
| NSMutableURLRequest* request = [self requestForCurrentNavigationItem]; |
| |
| // If the request has POST data and is not a repost form, configure and |
| // run the POST request. |
| if (POSTData.length && !repostedForm) { |
| [request setHTTPMethod:@"POST"]; |
| [request setHTTPBody:POSTData]; |
| [request setAllHTTPHeaderFields:self.currentHTTPHeaders]; |
| GURL navigationURL = |
| currentItem ? currentItem->GetURL() : GURL::EmptyGURL(); |
| [self registerLoadRequest:navigationURL |
| referrer:self.currentNavItemReferrer |
| transition:self.currentTransition]; |
| [self loadPOSTRequest:request]; |
| return; |
| } |
| |
| ProceduralBlock defaultNavigationBlock = ^{ |
| web::NavigationItem* item = self.currentNavItem; |
| GURL navigationURL = item ? item->GetURL() : GURL::EmptyGURL(); |
| [self registerLoadRequest:navigationURL |
| referrer:self.currentNavItemReferrer |
| transition:self.currentTransition]; |
| [self loadRequest:request]; |
| }; |
| |
| // When navigating via WKBackForwardListItem to pages created or updated by |
| // calls to pushState() and replaceState(), sometimes core.js is not injected |
| // correctly. This means that calling window.history navigation functions |
| // will invoke WKWebView's non-overridden implementations, causing a mismatch |
| // between the WKBackForwardList and NavigationManager. |
| // TODO(crbug.com/659816): Figure out how to prevent core.js injection flake. |
| if (currentItem->HasStateBeenReplaced() || |
| currentItem->IsCreatedFromPushState()) { |
| defaultNavigationBlock(); |
| return; |
| } |
| |
| // If there is no corresponding WKBackForwardListItem, or the item is not in |
| // the current WKWebView's back-forward list, navigating using WKWebView API |
| // is not possible. In this case, fall back to the default navigation |
| // mechanism. |
| if (!holder->back_forward_list_item() || |
| ![self isBackForwardListItemValid:holder->back_forward_list_item()]) { |
| defaultNavigationBlock(); |
| return; |
| } |
| |
| ProceduralBlock webViewNavigationBlock = ^{ |
| // If the current navigation URL is the same as the URL of the visible |
| // page, that means the user requested a reload. |goToBackForwardListItem| |
| // will be a no-op when it is passed the current back forward list item, |
| // so |reload| must be explicitly called. |
| web::NavigationItem* item = self.currentNavItem; |
| GURL navigationURL = item ? item->GetURL() : GURL::EmptyGURL(); |
| [self registerLoadRequest:navigationURL |
| referrer:self.currentNavItemReferrer |
| transition:self.currentTransition]; |
| if (navigationURL == net::GURLWithNSURL([_webView URL])) { |
| [_navigationStates setState:web::WKNavigationState::REQUESTED |
| forNavigation:[_webView reload]]; |
| } else { |
| // |didCommitNavigation:| may not be called for fast navigation, so update |
| // the navigation type now as it is already known. |
| holder->set_navigation_type(WKNavigationTypeBackForward); |
| WKNavigation* navigation = |
| [_webView goToBackForwardListItem:holder->back_forward_list_item()]; |
| [_navigationStates setState:web::WKNavigationState::REQUESTED |
| forNavigation:navigation]; |
| } |
| }; |
| |
| // If the request is not a form submission or resubmission, or the user |
| // doesn't need to confirm the load, then continue right away. |
| |
| if (!repostedForm || currentItem->ShouldSkipRepostFormConfirmation()) { |
| webViewNavigationBlock(); |
| return; |
| } |
| |
| // If the request is form submission or resubmission, then prompt the |
| // user before proceeding. |
| DCHECK(repostedForm); |
| _webStateImpl->ShowRepostFormWarningDialog( |
| base::BindBlock(^(bool shouldContinue) { |
| if (shouldContinue) |
| webViewNavigationBlock(); |
| else |
| defaultNavigationBlock(); |
| })); |
| } |
| |
| #pragma mark - |
| #pragma mark Testing-Only Methods |
| |
| - (void)injectWebViewContentView:(CRWWebViewContentView*)webViewContentView { |
| [self removeWebViewAllowingCachedReconstruction:NO]; |
| |
| _lastRegisteredRequestURL = _defaultURL; |
| [_containerView displayWebViewContentView:webViewContentView]; |
| [self setWebView:static_cast<WKWebView*>(webViewContentView.webView)]; |
| } |
| |
| - (void)resetInjectedWebViewContentView { |
| [self setWebView:nil]; |
| [self resetContainerView]; |
| } |
| |
| - (void)addObserver:(id<CRWWebControllerObserver>)observer { |
| DCHECK(observer); |
| if (!_observers) { |
| // We don't want our observer set to block dealloc on the observers. For the |
| // observer container, make an object compatible with NSMutableSet that does |
| // not perform retain or release on the contained objects (weak references). |
| CFSetCallBacks callbacks = |
| {0, NULL, NULL, CFCopyDescription, CFEqual, CFHash}; |
| _observers.reset(base::mac::CFToNSCast( |
| CFSetCreateMutable(kCFAllocatorDefault, 1, &callbacks))); |
| } |
| DCHECK(![_observers containsObject:observer]); |
| [_observers addObject:observer]; |
| _observerBridges.push_back(base::MakeUnique<web::WebControllerObserverBridge>( |
| observer, self.webStateImpl, self)); |
| |
| if ([observer respondsToSelector:@selector(setWebViewProxy:controller:)]) |
| [observer setWebViewProxy:_webViewProxy controller:self]; |
| } |
| |
| - (void)removeObserver:(id<CRWWebControllerObserver>)observer { |
| DCHECK([_observers containsObject:observer]); |
| [_observers removeObject:observer]; |
| // Remove the associated WebControllerObserverBridge. |
| auto it = std::find_if( |
| _observerBridges.begin(), _observerBridges.end(), |
| [observer]( |
| const std::unique_ptr<web::WebControllerObserverBridge>& bridge) { |
| return bridge->web_controller_observer() == observer; |
| }); |
| DCHECK(it != _observerBridges.end()); |
| _observerBridges.erase(it); |
| } |
| |
| - (NSUInteger)observerCount { |
| DCHECK_EQ(_observerBridges.size(), [_observers count]); |
| return [_observers count]; |
| } |
| |
| - (void)simulateLoadRequestWithURL:(const GURL&)URL { |
| _lastRegisteredRequestURL = URL; |
| _loadPhase = web::LOAD_REQUESTED; |
| } |
| |
| - (NSString*)referrerFromNavigationAction:(WKNavigationAction*)action { |
| return [action.request valueForHTTPHeaderField:kReferrerHeaderName]; |
| } |
| |
| @end |