1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ios/web/web_state/ui/crw_wk_web_view_web_controller.h"
7 #import <WebKit/WebKit.h>
9 #include "base/ios/weak_nsobject.h"
10 #include "base/json/json_reader.h"
11 #import "base/mac/scoped_nsobject.h"
12 #include "base/macros.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "base/values.h"
15 #import "ios/web/crw_network_activity_indicator_manager.h"
16 #import "ios/web/navigation/crw_session_controller.h"
17 #include "ios/web/public/web_client.h"
18 #import "ios/web/public/web_state/js/crw_js_injection_manager.h"
19 #import "ios/web/ui_web_view_util.h"
20 #include "ios/web/web_state/blocked_popup_info.h"
21 #import "ios/web/web_state/js/crw_js_window_id_manager.h"
22 #import "ios/web/web_state/js/page_script_util.h"
23 #import "ios/web/web_state/ui/crw_web_controller+protected.h"
24 #import "ios/web/web_state/ui/crw_wk_web_view_crash_detector.h"
25 #import "ios/web/web_state/ui/web_view_js_utils.h"
26 #import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
27 #import "ios/web/web_state/web_state_impl.h"
28 #import "ios/web/web_state/web_view_creation_utils.h"
29 #import "net/base/mac/url_conversions.h"
31 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
32 #include "ios/web/public/cert_store.h"
33 #include "ios/web/public/navigation_item.h"
34 #include "ios/web/public/ssl_status.h"
35 #import "ios/web/web_state/wk_web_view_ssl_error_util.h"
36 #include "net/ssl/ssl_info.h"
40 // Extracts Referer value from WKNavigationAction request header.
41 NSString* GetRefererFromNavigationAction(WKNavigationAction* action) {
42 return [action.request valueForHTTPHeaderField:@"Referer"];
45 NSString* const kScriptMessageName = @"crwebinvoke";
46 NSString* const kScriptImmediateName = @"crwebinvokeimmediate";
50 @interface CRWWKWebViewWebController () <WKNavigationDelegate,
51 WKScriptMessageHandler,
53 // The WKWebView managed by this instance.
54 base::scoped_nsobject<WKWebView> _wkWebView;
56 // The Watch Dog that detects and reports WKWebView crashes.
57 base::scoped_nsobject<CRWWKWebViewCrashDetector> _crashDetector;
59 // The actual URL of the document object (i.e., the last committed URL).
60 // TODO(stuartmorgan): Remove this in favor of just updating the session
61 // controller and treating that as authoritative. For now, this allows sharing
62 // the flow that's currently in the superclass.
65 // A set of script managers whose scripts have been injected into the current
67 // TODO(stuartmorgan): Revisit this approach; it's intended only as a stopgap
68 // measure to make all the existing script managers work. Longer term, there
69 // should probably be a couple of points where managers can register to have
70 // things happen automatically based on page lifecycle, and if they don't want
71 // to use one of those fixed points, they should make their scripts internally
73 base::scoped_nsobject<NSMutableSet> _injectedScriptManagers;
75 // Referrer pending for the next navigated page. Lifetime of this value starts
76 // at |decidePolicyForNavigationAction| where the referrer is extracted from
77 // the request and ends at |didCommitNavigation| where the request is
79 base::scoped_nsobject<NSString> _pendingReferrerString;
81 // Referrer for the current page.
82 base::scoped_nsobject<NSString> _currentReferrerString;
84 // Backs the property of the same name.
85 base::scoped_nsobject<NSString> _documentMIMEType;
87 // Whether the web page is currently performing window.history.pushState or
88 // window.history.replaceState
89 // Set to YES on window.history.willChangeState message. To NO on
90 // window.history.didPushState or window.history.didReplaceState.
91 BOOL _changingHistoryState;
94 // Response's MIME type of the last known navigation.
95 @property(nonatomic, copy) NSString* documentMIMEType;
97 // Dictionary where keys are the names of WKWebView properties and values are
98 // selector names which should be called when a corresponding property has
99 // changed. e.g. @{ @"URL" : @"webViewURLDidChange" } means that
100 // -[self webViewURLDidChange] must be called every time when WKWebView.URL is
102 @property(nonatomic, readonly) NSDictionary* wkWebViewObservers;
104 // Activity indicator group ID for this web controller.
105 @property(nonatomic, readonly) NSString* activityIndicatorGroupID;
107 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
108 // Identifier used for storing and retrieving certificates.
109 @property(nonatomic, readonly) int certGroupID;
110 #endif // #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
112 // Returns the WKWebViewConfigurationProvider associated with the web
113 // controller's BrowserState.
114 - (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider;
116 // Creates a web view with given |config|. No-op if web view is already created.
117 - (void)ensureWebViewCreatedWithConfiguration:(WKWebViewConfiguration*)config;
119 // Returns a new autoreleased web view created with given configuration.
120 - (WKWebView*)createWebViewWithConfiguration:(WKWebViewConfiguration*)config;
122 // Sets the value of the webView property, and performs its basic setup.
123 - (void)setWebView:(WKWebView*)webView;
125 // Returns whether the given navigation is triggered by a user link click.
126 - (BOOL)isLinkNavigation:(WKNavigationType)navigationType;
128 // Sets value of the pendingReferrerString property.
129 - (void)setPendingReferrerString:(NSString*)referrer;
131 // Extracts the referrer value from WKNavigationAction and sets it as a pending.
132 // The referrer is known in |decidePolicyForNavigationAction| however it must
133 // be in a pending state until |didCommitNavigation| where it becames current.
134 - (void)updatePendingReferrerFromNavigationAction:(WKNavigationAction*)action;
136 // Replaces the current referrer with the pending one. Referrer becames current
137 // at |didCommitNavigation| callback.
138 - (void)commitPendingReferrerString;
140 // Discards the pending referrer.
141 - (void)discardPendingReferrerString;
143 // Returns a new CRWWKWebViewCrashDetector created with the given |webView| or
144 // nil if |webView| is nil. Callers are responsible for releasing the object.
145 - (CRWWKWebViewCrashDetector*)newCrashDetectorWithWebView:(WKWebView*)webView;
147 // Called when web view process has been terminated.
148 - (void)webViewWebProcessDidCrash;
150 // Asynchronously returns the referrer policy for the current page.
151 - (void)queryPageReferrerPolicy:(void(^)(NSString*))responseHandler;
153 // Informs CWRWebDelegate that CRWWebController has detected and blocked a
155 - (void)didBlockPopupWithURL:(GURL)popupURL
156 sourceURL:(GURL)sourceURL
157 referrerPolicy:(const std::string&)referrerPolicyString;
159 // Convenience method to inform CWRWebDelegate about a blocked popup.
160 - (void)didBlockPopupWithURL:(GURL)popupURL sourceURL:(GURL)sourceURL;
162 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
163 // Called when a load ends in an SSL error.
164 - (void)handleSSLError:(NSError*)error;
167 // Adds an activity indicator tasks for this web controller.
168 - (void)addActivityIndicatorTask;
170 // Clears all activity indicator tasks for this web controller.
171 - (void)clearActivityIndicatorTasks;
173 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
174 // Updates SSL status for the current navigation item based on the information
175 // provided by web view.
176 - (void)updateSSLStatusForCurrentNavigationItem;
179 // Registers load request with empty referrer and link or client redirect
180 // transition based on user interaction state.
181 - (void)registerLoadRequest:(const GURL&)url;
183 // Called when a non-document-changing URL change occurs. Updates the
184 // _documentURL, and informs the superclass of the change.
185 - (void)URLDidChangeWithoutDocumentChange:(const GURL&)URL;
187 // Returns new autoreleased instance of WKUserContentController which has
188 // early page script.
189 - (WKUserContentController*)createUserContentController;
191 // Attempts to handle a script message. Returns YES on success, NO otherwise.
192 - (BOOL)respondToWKScriptMessage:(WKScriptMessage*)scriptMessage;
194 // Handles 'window.history.willChangeState' message.
195 - (BOOL)handleWindowHistoryWillChangeStateMessage:
196 (base::DictionaryValue*)message
197 context:(NSDictionary*)context;
199 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
200 // Called when WKWebView estimatedProgress has been changed.
201 - (void)webViewEstimatedProgressDidChange;
203 // Called when WKWebView hasOnlySecureContent property has changed.
204 - (void)webViewContentSecurityDidChange;
205 #endif // !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
207 // Called when WKWebView loading state has been changed.
208 - (void)webViewLoadingStateDidChange;
210 // Called when WKWebView title has been changed.
211 - (void)webViewTitleDidChange;
213 // Called when WKWebView URL has been changed.
214 - (void)webViewURLDidChange;
218 @implementation CRWWKWebViewWebController
220 #pragma mark CRWWebController public methods
222 - (instancetype)initWithWebState:(scoped_ptr<web::WebStateImpl>)webState {
223 return [super initWithWebState:webState.Pass()];
226 - (BOOL)keyboardDisplayRequiresUserAction {
227 // TODO(stuartmorgan): Find out whether YES or NO is correct; see comment
228 // in protected header.
233 - (void)setKeyboardDisplayRequiresUserAction:(BOOL)requiresUserAction {
237 - (void)evaluateJavaScript:(NSString*)script
238 stringResultHandler:(web::JavaScriptCompletion)handler {
239 NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script];
240 web::EvaluateJavaScript(_wkWebView, safeScript, handler);
243 - (web::WebViewType)webViewType {
244 return web::WK_WEB_VIEW_TYPE;
247 - (void)evaluateUserJavaScript:(NSString*)script {
248 [self setUserInteractionRegistered:YES];
249 web::EvaluateJavaScript(_wkWebView, script, nil);
252 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
253 - (void)terminateNetworkActivity {
254 web::CertStore::GetInstance()->RemoveCertsForGroup(self.certGroupID);
255 [super terminateNetworkActivity];
257 #endif // #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
259 - (void)setPageDialogOpenPolicy:(web::PageDialogOpenPolicy)policy {
260 // TODO(eugenebut): implement dialogs/windows suppression using
261 // WKNavigationDelegate methods where possible.
262 [super setPageDialogOpenPolicy:policy];
266 #pragma mark Testing-Only Methods
268 - (void)injectWebView:(id)webView {
269 [super injectWebView:webView];
270 [self setWebView:webView];
273 #pragma mark - Protected property implementations
276 return _wkWebView.get();
279 - (UIScrollView*)webScrollView {
280 return [_wkWebView scrollView];
283 - (BOOL)ignoreURLVerificationFailures {
288 return [_wkWebView title];
291 - (NSString*)currentReferrerString {
292 return _currentReferrerString.get();
295 #pragma mark Protected method implementations
297 - (void)ensureWebViewCreated {
298 WKWebViewConfiguration* config =
299 [self webViewConfigurationProvider].GetWebViewConfiguration();
300 [self ensureWebViewCreatedWithConfiguration:config];
303 - (void)resetWebView {
304 [self setWebView:nil];
307 - (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
309 *trustLevel = web::URLVerificationTrustLevel::kAbsolute;
313 - (void)registerUserAgent {
314 // TODO(stuartmorgan): Rename this method, since it works for both.
315 web::BuildAndRegisterUserAgentForUIWebView(
316 self.webStateImpl->GetRequestGroupID(),
317 [self useDesktopUserAgent]);
320 // The core.js cannot pass messages back to obj-c if it is injected
321 // to |WEB_VIEW_DOCUMENT| because it does not support iframe creation used
322 // by core.js to communicate back. That functionality is only supported
323 // by |WEB_VIEW_HTML_DOCUMENT|. |WEB_VIEW_DOCUMENT| is used when displaying
324 // non-HTML contents (e.g. PDF documents).
325 - (web::WebViewDocumentType)webViewDocumentType {
326 // This happens during tests.
328 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
331 if (!self.documentMIMEType) {
332 return web::WEB_VIEW_DOCUMENT_TYPE_UNKNOWN;
335 if ([self.documentMIMEType isEqualToString:@"text/html"] ||
336 [self.documentMIMEType isEqualToString:@"application/xhtml+xml"] ||
337 [self.documentMIMEType isEqualToString:@"application/xml"]) {
338 return web::WEB_VIEW_DOCUMENT_TYPE_HTML;
341 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
344 - (void)loadWebRequest:(NSURLRequest*)request {
345 [_wkWebView loadRequest:request];
348 - (void)loadWebHTMLString:(NSString*)html forURL:(const GURL&)URL {
349 [_wkWebView loadHTMLString:html baseURL:net::NSURLWithGURL(URL)];
352 - (BOOL)scriptHasBeenInjectedForClass:(Class)jsInjectionManagerClass
353 presenceBeacon:(NSString*)beacon {
354 return [_injectedScriptManagers containsObject:jsInjectionManagerClass];
357 - (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass {
358 // Skip evaluation if there's no content (e.g., if what's being injected is
359 // an umbrella manager).
360 if ([script length]) {
361 [super injectScript:script forClass:JSInjectionManagerClass];
362 // Every injection except windowID requires windowID check.
363 if (JSInjectionManagerClass != [CRWJSWindowIdManager class])
364 script = [self scriptByAddingWindowIDCheckForScript:script];
365 web::EvaluateJavaScript(_wkWebView, script, nil);
367 [_injectedScriptManagers addObject:JSInjectionManagerClass];
370 - (void)willLoadCurrentURLInWebView {
371 // TODO(stuartmorgan): Get a WKWebView version of the request ID verification
372 // code working for debug builds.
376 - (void)setPageChangeProbability:(web::PageChangeProbability)probability {
377 // Nothing to do; no polling timer.
380 - (void)abortWebLoad {
381 [_wkWebView stopLoading];
384 - (void)resetLoadState {
388 - (void)setSuppressDialogsWithHelperScript:(NSString*)script {
389 [self evaluateJavaScript:script stringResultHandler:nil];
392 - (CGFloat)absoluteZoomScaleForScrollState:
393 (const web::PageScrollState&)scrollState {
394 return scrollState.zoom_scale();
397 - (void)applyWebViewScrollZoomScaleFromScrollState:
398 (const web::PageScrollState&)scrollState {
399 // After rendering a web page, WKWebView keeps the |minimumZoomScale| and
400 // |maximumZoomScale| properties of its scroll view constant while adjusting
401 // the |zoomScale| property accordingly. The maximum-scale or minimum-scale
402 // meta tags of a page may have changed since the state was recorded, so clamp
403 // the zoom scale to the current range if necessary.
404 DCHECK(scrollState.IsZoomScaleValid());
405 // Legacy-format scroll states cannot be applied to WKWebViews.
406 if (scrollState.IsZoomScaleLegacyFormat())
408 CGFloat zoomScale = scrollState.zoom_scale();
409 if (zoomScale < self.webScrollView.minimumZoomScale)
410 zoomScale = self.webScrollView.minimumZoomScale;
411 if (zoomScale > self.webScrollView.maximumZoomScale)
412 zoomScale = self.webScrollView.maximumZoomScale;
413 self.webScrollView.zoomScale = zoomScale;
416 #pragma mark Private methods
418 - (NSString*)documentMIMEType {
419 return _documentMIMEType.get();
422 - (void)setDocumentMIMEType:(NSString*)type {
423 _documentMIMEType.reset([type copy]);
426 - (NSDictionary*)wkWebViewObservers {
428 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
429 @"estimatedProgress" : @"webViewEstimatedProgressDidChange",
430 @"hasOnlySecureContent" : @"webViewContentSecurityDidChange",
432 @"loading" : @"webViewLoadingStateDidChange",
433 @"title" : @"webViewTitleDidChange",
434 @"URL" : @"webViewURLDidChange",
438 - (NSString*)activityIndicatorGroupID {
439 return [NSString stringWithFormat:
440 @"WKWebViewWebController.NetworkActivityIndicatorKey.%@",
441 self.webStateImpl->GetRequestGroupID()];
444 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
446 DCHECK(self.webStateImpl);
447 // Request tracker IDs are used as certificate groups.
448 return self.webStateImpl->GetRequestTracker()->identifier();
452 - (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider {
453 DCHECK(self.webStateImpl);
454 web::BrowserState* browserState = self.webStateImpl->GetBrowserState();
455 return web::WKWebViewConfigurationProvider::FromBrowserState(browserState);
458 - (void)ensureWebViewCreatedWithConfiguration:(WKWebViewConfiguration*)config {
460 // Use a separate userContentController for each web view.
461 // WKUserContentController does not allow adding multiple script message
462 // handlers for the same name, hence userContentController can't be shared
463 // between all web views.
464 config.userContentController = [self createUserContentController];
465 [self setWebView:[self createWebViewWithConfiguration:config]];
466 // Notify super class about created web view. -webViewDidChange is not
467 // called from -setWebView:scriptMessageRouter: as the latter used in unit
468 // tests with fake web view, which cannot be added to view hierarchy.
469 [self webViewDidChange];
473 - (WKWebView*)createWebViewWithConfiguration:(WKWebViewConfiguration*)config {
474 return [web::CreateWKWebView(
477 self.webStateImpl->GetRequestGroupID(),
478 [self useDesktopUserAgent]) autorelease];
481 - (void)setWebView:(WKWebView*)webView {
482 DCHECK_NE(_wkWebView.get(), webView);
484 // Unwind the old web view.
485 WKUserContentController* oldContentController =
486 [[_wkWebView configuration] userContentController];
487 [oldContentController removeScriptMessageHandlerForName:kScriptMessageName];
488 [oldContentController removeScriptMessageHandlerForName:kScriptImmediateName];
489 [_wkWebView setNavigationDelegate:nil];
490 [_wkWebView setUIDelegate:nil];
491 for (NSString* keyPath in self.wkWebViewObservers) {
492 [_wkWebView removeObserver:self forKeyPath:keyPath];
494 [self clearActivityIndicatorTasks];
496 _wkWebView.reset([webView retain]);
498 // Set up the new web view.
499 WKUserContentController* newContentController =
500 [[_wkWebView configuration] userContentController];
501 [newContentController addScriptMessageHandler:self name:kScriptMessageName];
502 [newContentController addScriptMessageHandler:self name:kScriptImmediateName];
503 [_wkWebView setNavigationDelegate:self];
504 [_wkWebView setUIDelegate:self];
505 for (NSString* keyPath in self.wkWebViewObservers) {
506 [_wkWebView addObserver:self forKeyPath:keyPath options:0 context:nullptr];
508 _injectedScriptManagers.reset([[NSMutableSet alloc] init]);
509 _crashDetector.reset([self newCrashDetectorWithWebView:_wkWebView]);
510 _documentURL = [self defaultURL];
513 - (BOOL)isLinkNavigation:(WKNavigationType)navigationType {
514 switch (navigationType) {
515 case WKNavigationTypeLinkActivated:
517 case WKNavigationTypeOther:
518 return [self userClickedRecently];
524 - (void)setPendingReferrerString:(NSString*)referrer {
525 _pendingReferrerString.reset([referrer copy]);
528 - (void)updatePendingReferrerFromNavigationAction:(WKNavigationAction*)action {
529 if (action.targetFrame.isMainFrame)
530 [self setPendingReferrerString:GetRefererFromNavigationAction(action)];
533 - (void)commitPendingReferrerString {
534 _currentReferrerString.reset(_pendingReferrerString.release());
537 - (void)discardPendingReferrerString {
538 _pendingReferrerString.reset();
541 - (CRWWKWebViewCrashDetector*)newCrashDetectorWithWebView:(WKWebView*)webView {
545 base::WeakNSObject<CRWWKWebViewWebController> weakSelf(self);
547 [weakSelf webViewWebProcessDidCrash];
549 return [[CRWWKWebViewCrashDetector alloc] initWithWebView:webView
550 crashHandler:crashHandler];
553 - (void)webViewWebProcessDidCrash {
554 if ([self.delegate respondsToSelector:
555 @selector(webControllerWebProcessDidCrash:)]) {
556 [self.delegate webControllerWebProcessDidCrash:self];
560 - (void)queryPageReferrerPolicy:(void(^)(NSString*))responseHandler {
561 DCHECK(responseHandler);
562 [self evaluateJavaScript:@"__gCrWeb.getPageReferrerPolicy()"
563 stringResultHandler:^(NSString* referrer, NSError* error) {
564 DCHECK_NE(error.code, WKErrorJavaScriptExceptionOccurred);
565 responseHandler(!error ? referrer : nil);
569 - (void)didBlockPopupWithURL:(GURL)popupURL
570 sourceURL:(GURL)sourceURL
571 referrerPolicy:(const std::string&)referrerPolicyString {
572 web::ReferrerPolicy referrerPolicy =
573 [self referrerPolicyFromString:referrerPolicyString];
574 web::Referrer referrer(sourceURL, referrerPolicy);
575 NSString* const kWindowName = @""; // obsoleted
576 base::WeakNSObject<CRWWKWebViewWebController> weakSelf(self);
577 void(^showPopupHandler)() = ^{
578 // On Desktop cross-window comunication is not supported for unblocked
579 // popups; so it's ok to create a new independent page.
580 CRWWebController* child = [[weakSelf delegate]
581 webPageOrderedOpen:popupURL
583 windowName:kWindowName
585 DCHECK(!child || child.sessionController.openedByDOM);
588 web::BlockedPopupInfo info(popupURL, referrer, kWindowName, showPopupHandler);
589 [self.delegate webController:self didBlockPopup:info];
592 - (void)didBlockPopupWithURL:(GURL)popupURL sourceURL:(GURL)sourceURL {
593 if (![self.delegate respondsToSelector:
594 @selector(webController:didBlockPopup:)]) {
598 base::WeakNSObject<CRWWKWebViewWebController> weakSelf(self);
599 dispatch_async(dispatch_get_main_queue(), ^{
600 [self queryPageReferrerPolicy:^(NSString* policy) {
601 [weakSelf didBlockPopupWithURL:popupURL
603 referrerPolicy:base::SysNSStringToUTF8(policy)];
608 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
609 - (void)handleSSLError:(NSError*)error {
610 DCHECK(web::IsWKWebViewSSLError(error));
612 net::SSLInfo sslInfo;
613 web::GetSSLInfoFromWKWebViewSSLError(error, &sslInfo);
615 web::SSLStatus sslStatus;
616 sslStatus.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN;
617 sslStatus.cert_status = sslInfo.cert_status;
618 sslStatus.cert_id = web::CertStore::GetInstance()->StoreCert(
619 sslInfo.cert.get(), self.certGroupID);
621 [self.delegate presentSSLError:sslInfo
622 forSSLStatus:sslStatus
626 #endif // #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
628 - (void)addActivityIndicatorTask {
629 [[CRWNetworkActivityIndicatorManager sharedInstance]
630 startNetworkTaskForGroup:[self activityIndicatorGroupID]];
633 - (void)clearActivityIndicatorTasks {
634 [[CRWNetworkActivityIndicatorManager sharedInstance]
635 clearNetworkTasksForGroup:[self activityIndicatorGroupID]];
638 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
639 - (void)updateSSLStatusForCurrentNavigationItem {
640 DCHECK(self.webStateImpl);
641 web::NavigationItem* item =
642 self.webStateImpl->GetNavigationManagerImpl().GetLastCommittedItem();
646 // WKWebView will not load unauthenticated content.
647 item->GetSSL().security_style = item->GetURL().SchemeIs(url::kHttpsScheme) ?
648 web::SECURITY_STYLE_AUTHENTICATED : web::SECURITY_STYLE_UNAUTHENTICATED;
649 int contentStatus = [_wkWebView hasOnlySecureContent] ?
650 web::SSLStatus::NORMAL_CONTENT :
651 web::SSLStatus::DISPLAYED_INSECURE_CONTENT;
652 item->GetSSL().content_status = contentStatus;
653 [self didUpdateSSLStatusForCurrentNavigationItem];
655 #endif // !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
657 - (void)registerLoadRequest:(const GURL&)url {
658 // If load request is registered via WKWebViewWebController, assume transition
659 // is link or client redirect as other transitions will already be registered
660 // by web controller or delegates.
661 // TODO(stuartmorgan): Remove guesswork and replace with information from
662 // decidePolicyForNavigationAction:.
663 ui::PageTransition transition = self.userInteractionRegistered
664 ? ui::PAGE_TRANSITION_LINK
665 : ui::PAGE_TRANSITION_CLIENT_REDIRECT;
666 // The referrer is not known yet, and will be updated later.
667 const web::Referrer emptyReferrer;
668 [self registerLoadRequest:url referrer:emptyReferrer transition:transition];
671 - (void)URLDidChangeWithoutDocumentChange:(const GURL&)newURL {
672 DCHECK(newURL == net::GURLWithNSURL([_wkWebView URL]));
673 _documentURL = newURL;
674 // If called during window.history.pushState or window.history.replaceState
675 // JavaScript evaluation, only update the document URL. This callback does not
676 // have any information about the state object and cannot create (or edit) the
677 // navigation entry for this page change. Web controller will sync with
678 // history changes when a window.history.didPushState or
679 // window.history.didReplaceState message is received, which should happen in
681 if (!_changingHistoryState) {
682 [self registerLoadRequest:_documentURL];
683 [self didStartLoadingURL:_documentURL updateHistory:YES];
684 [self didFinishNavigation];
688 - (WKUserContentController*)createUserContentController {
689 WKUserContentController* result =
690 [[[WKUserContentController alloc] init] autorelease];
691 base::scoped_nsobject<WKUserScript> script([[WKUserScript alloc]
692 initWithSource:web::GetEarlyPageScript(web::WK_WEB_VIEW_TYPE)
693 injectionTime:WKUserScriptInjectionTimeAtDocumentStart
694 forMainFrameOnly:YES]);
695 [result addUserScript:script];
699 - (void)userContentController:(WKUserContentController*)userContentController
700 didReceiveScriptMessage:(WKScriptMessage*)message {
701 // Broken out into separate method to catch errors.
702 // TODO(jyquinn): Evaluate whether this is necessary for WKWebView.
703 if (![self respondToWKScriptMessage:message]) {
704 DLOG(WARNING) << "Message from JS not handled due to invalid format";
708 - (BOOL)respondToWKScriptMessage:(WKScriptMessage*)scriptMessage {
709 CHECK(scriptMessage.frameInfo.mainFrame);
711 std::string errorMessage;
712 scoped_ptr<base::Value> inputJSONData(
713 base::JSONReader::ReadAndReturnError(
714 base::SysNSStringToUTF8(scriptMessage.body),
719 DLOG(WARNING) << "JSON parse error: %s" << errorMessage.c_str();
722 base::DictionaryValue* message = nullptr;
723 if (!inputJSONData->GetAsDictionary(&message)) {
726 std::string windowID;
727 message->GetString("crwWindowId", &windowID);
728 // Check for correct windowID
729 if (![[self windowId] isEqualToString:base::SysUTF8ToNSString(windowID)]) {
730 DLOG(WARNING) << "Message from JS ignored due to non-matching windowID: "
731 << [self windowId] << " != "
732 << base::SysUTF8ToNSString(windowID);
735 base::DictionaryValue* command = nullptr;
736 if (!message->GetDictionary("crwCommand", &command)) {
739 if ([scriptMessage.name isEqualToString:kScriptImmediateName] ||
740 [scriptMessage.name isEqualToString:kScriptMessageName]) {
741 return [self respondToMessage:command
742 userIsInteracting:[self userIsInteracting]
743 originURL:net::GURLWithNSURL([_wkWebView URL])];
750 - (SEL)selectorToHandleJavaScriptCommand:(const std::string&)command {
751 static std::map<std::string, SEL>* handlers = nullptr;
752 static dispatch_once_t onceToken;
753 dispatch_once(&onceToken, ^{
754 handlers = new std::map<std::string, SEL>();
755 (*handlers)["window.history.didPushState"] =
756 @selector(handleWindowHistoryDidPushStateMessage:context:);
757 (*handlers)["window.history.didReplaceState"] =
758 @selector(handleWindowHistoryDidReplaceStateMessage:context:);
759 (*handlers)["window.history.willChangeState"] =
760 @selector(handleWindowHistoryWillChangeStateMessage:context:);
763 auto iter = handlers->find(command);
764 return iter != handlers->end()
766 : [super selectorToHandleJavaScriptCommand:command];
770 #pragma mark JavaScript message handlers
772 - (BOOL)handleWindowHistoryWillChangeStateMessage:
773 (base::DictionaryValue*)message
774 context:(NSDictionary*)context {
775 _changingHistoryState = YES;
779 - (BOOL)handleWindowHistoryDidPushStateMessage:(base::DictionaryValue*)message
780 context:(NSDictionary*)context {
781 DCHECK(_changingHistoryState);
782 _changingHistoryState = NO;
783 return [super handleWindowHistoryDidPushStateMessage:message context:context];
786 - (BOOL)handleWindowHistoryDidReplaceStateMessage:
787 (base::DictionaryValue*)message
788 context:(NSDictionary*)context {
789 DCHECK(_changingHistoryState);
790 _changingHistoryState = NO;
791 return [super handleWindowHistoryDidReplaceStateMessage:message
796 #pragma mark KVO Observation
798 - (void)observeValueForKeyPath:(NSString*)keyPath
800 change:(NSDictionary*)change
801 context:(void*)context {
802 NSString* dispatcherSelectorName = self.wkWebViewObservers[keyPath];
803 DCHECK(dispatcherSelectorName);
804 if (dispatcherSelectorName)
805 [self performSelector:NSSelectorFromString(dispatcherSelectorName)];
808 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
809 // TODO(eugenebut): use WKWebView progress even if Chrome net stack is enabled.
810 - (void)webViewEstimatedProgressDidChange {
811 if ([self.delegate respondsToSelector:
812 @selector(webController:didUpdateProgress:)]) {
813 [self.delegate webController:self
814 didUpdateProgress:[_wkWebView estimatedProgress]];
818 - (void)webViewContentSecurityDidChange {
819 [self updateSSLStatusForCurrentNavigationItem];
822 #endif // !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
824 - (void)webViewLoadingStateDidChange {
825 if ([_wkWebView isLoading]) {
826 [self addActivityIndicatorTask];
828 [self clearActivityIndicatorTasks];
832 - (void)webViewTitleDidChange {
833 if ([self.delegate respondsToSelector:
834 @selector(webController:titleDidChange:)]) {
836 [self.delegate webController:self titleDidChange:self.title];
840 - (void)webViewURLDidChange {
841 // TODO(stuartmorgan): Determine if there are any cases where this still
842 // happens, and if so whether anything should be done when it does.
843 if (![_wkWebView URL]) {
844 DVLOG(1) << "Received nil URL callback";
847 GURL url(net::GURLWithNSURL([_wkWebView URL]));
848 // URL changes happen at three points:
849 // 1) When a load starts; at this point, the load is provisional, and
850 // it should be ignored until it's committed, since the document/window
851 // objects haven't changed yet.
852 // 2) When a non-document-changing URL change happens (hash change,
853 // history.pushState, etc.). This URL change happens instantly, so should
855 // 3) When a navigation error occurs after provisional navigation starts,
856 // the URL reverts to the previous URL without triggering a new navigation.
858 // If |isLoading| is NO, then it must be case 2 or 3. If the last committed
859 // URL (_documentURL) matches the current URL, assume that it is a revert from
860 // navigation failure and do nothing. If the URL does not match, assume it is
861 // a non-document-changing URL change, and handle accordingly.
863 // If |isLoading| is YES, then it could either be case 1, or it could be
864 // case 2 on a page that hasn't finished loading yet. If the domain of the
865 // new URL matches the last committed URL, then check window.location.href,
866 // and if it matches, trust it. The domain check ensures that if a site
867 // somehow corrupts window.location.href it can't do a redirect to a
868 // slow-loading target page while it is still loading to spoof the domain.
869 // On a document-changing URL change, the window.location.href will match the
870 // previous URL at this stage, not the web view's current URL.
871 if (![_wkWebView isLoading]) {
872 if (_documentURL == url)
874 [self URLDidChangeWithoutDocumentChange:url];
875 } else if (!_documentURL.host().empty() &&
876 _documentURL.host() == url.host()) {
877 [_wkWebView evaluateJavaScript:@"window.location.href"
878 completionHandler:^(id result, NSError* error) {
879 // If the web view has gone away, or the location
880 // couldn't be retrieved, abort.
882 ![result isKindOfClass:[NSString class]]) {
885 GURL jsURL([result UTF8String]);
886 // Make sure that the URL is as expected, and re-check
887 // the host to prevent race conditions.
888 if (jsURL == url && _documentURL.host() == url.host()) {
889 [self URLDidChangeWithoutDocumentChange:url];
896 #pragma mark WKNavigationDelegate Methods
898 - (void)webView:(WKWebView *)webView
899 decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
901 (void (^)(WKNavigationActionPolicy))decisionHandler {
902 if (self.isBeingDestroyed) {
903 decisionHandler(WKNavigationActionPolicyCancel);
907 NSURLRequest* request = navigationAction.request;
908 GURL url = net::GURLWithNSURL(request.URL);
910 // The page will not be changed until this navigation is commited, so the
911 // retrieved referrer will be pending until |didCommitNavigation| callback.
912 [self updatePendingReferrerFromNavigationAction:navigationAction];
914 if (navigationAction.sourceFrame.isMainFrame)
915 self.documentMIMEType = nil;
916 BOOL isLinkClick = [self isLinkNavigation:navigationAction.navigationType];
917 WKNavigationActionPolicy policy =
918 [self shouldAllowLoadWithRequest:request
919 isLinkClick:isLinkClick] ?
920 WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel;
921 decisionHandler(policy);
924 - (void)webView:(WKWebView *)webView
925 decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
927 (void (^)(WKNavigationResponsePolicy))handler {
928 if (navigationResponse.isForMainFrame)
929 self.documentMIMEType = navigationResponse.response.MIMEType;
930 handler(navigationResponse.canShowMIMEType ? WKNavigationResponsePolicyAllow :
931 WKNavigationResponsePolicyCancel);
934 // TODO(stuartmorgan): Move all the guesswork around these states out of the
935 // superclass, and wire these up to the remaining methods.
936 - (void)webView:(WKWebView *)webView
937 didStartProvisionalNavigation:(WKNavigation *)navigation {
938 GURL webViewURL = net::GURLWithNSURL(webView.URL);
939 // If this navigation has not yet been registered, do so. loadPhase check is
940 // necessary because lastRegisteredRequestURL may be the same as the
941 // webViewURL on a new tab created by window.open (default is about::blank).
942 if (self.lastRegisteredRequestURL != webViewURL ||
943 self.loadPhase != web::LOAD_REQUESTED) {
944 [self registerLoadRequest:webViewURL];
946 // Ensure the URL is registered and loadPhase is as expected.
947 DCHECK(self.lastRegisteredRequestURL == webViewURL);
948 DCHECK(self.loadPhase == web::LOAD_REQUESTED);
951 - (void)webView:(WKWebView *)webView
952 didReceiveServerRedirectForProvisionalNavigation:
953 (WKNavigation *)navigation {
954 [self registerLoadRequest:net::GURLWithNSURL(webView.URL)
955 referrer:[self currentReferrer]
956 transition:ui::PAGE_TRANSITION_SERVER_REDIRECT];
959 - (void)webView:(WKWebView *)webView
960 didFailProvisionalNavigation:(WKNavigation *)navigation
961 withError:(NSError *)error {
962 [self discardPendingReferrerString];
964 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
965 if (web::IsWKWebViewSSLError(error))
966 [self handleSSLError:error];
969 [self handleLoadError:error inMainFrame:YES];
972 - (void)webView:(WKWebView *)webView
973 didCommitNavigation:(WKNavigation *)navigation {
974 DCHECK_EQ(_wkWebView, webView);
975 // This point should closely approximate the document object change, so reset
976 // the list of injected scripts to those that are automatically injected.
977 _injectedScriptManagers.reset([[NSMutableSet alloc] init]);
978 [self injectWindowID];
980 // The page has changed; commit the pending referrer.
981 [self commitPendingReferrerString];
983 // This is the point where the document's URL has actually changed.
984 _documentURL = net::GURLWithNSURL([_wkWebView URL]);
985 DCHECK(_documentURL == self.lastRegisteredRequestURL);
986 [self webPageChanged];
988 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
989 [self updateSSLStatusForCurrentNavigationItem];
993 - (void)webView:(WKWebView *)webView
994 didFinishNavigation:(WKNavigation *)navigation {
995 DCHECK(!self.isHalted);
996 // Trigger JavaScript driven post-document-load-completion tasks.
997 // TODO(jyquinn): Investigate using WKUserScriptInjectionTimeAtDocumentEnd to
998 // inject this material at the appropriate time rather than invoking here.
999 web::EvaluateJavaScript(webView,
1000 @"__gCrWeb.didFinishNavigation()", nil);
1001 [self didFinishNavigation];
1004 - (void)webView:(WKWebView *)webView
1005 didFailNavigation:(WKNavigation *)navigation
1006 withError:(NSError *)error {
1007 [self handleLoadError:error inMainFrame:YES];
1010 - (void)webView:(WKWebView *)webView
1011 didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
1013 (void (^)(NSURLSessionAuthChallengeDisposition disposition,
1014 NSURLCredential *credential))completionHandler {
1016 completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
1019 #pragma mark WKUIDelegate Methods
1021 - (WKWebView*)webView:(WKWebView*)webView
1022 createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration
1023 forNavigationAction:(WKNavigationAction*)navigationAction
1024 windowFeatures:(WKWindowFeatures*)windowFeatures {
1025 GURL requestURL = net::GURLWithNSURL(navigationAction.request.URL);
1026 NSString* referer = GetRefererFromNavigationAction(navigationAction);
1027 GURL referrerURL = referer ? GURL(base::SysNSStringToUTF8(referer)) :
1030 if (![self userIsInteracting] &&
1031 [self shouldBlockPopupWithURL:requestURL sourceURL:referrerURL]) {
1032 [self didBlockPopupWithURL:requestURL sourceURL:referrerURL];
1033 // Desktop Chrome does not return a window for the blocked popups;
1034 // follow the same approach by returning nil;
1038 id child = [self createChildWebControllerWithReferrerURL:referrerURL];
1039 // WKWebView requires WKUIDelegate to return a child view created with
1040 // exactly the same |configuration| object (exception is raised if config is
1041 // different). |configuration| param and config returned by
1042 // WKWebViewConfigurationProvider are different objects because WKWebView
1043 // makes a shallow copy of the config inside init, so every WKWebView
1044 // owns a separate shallow copy of WKWebViewConfiguration.
1045 [child ensureWebViewCreatedWithConfiguration:configuration];
1046 return [child webView];
1049 - (void)webView:(WKWebView*)webView
1050 runJavaScriptAlertPanelWithMessage:(NSString*)message
1051 initiatedByFrame:(WKFrameInfo*)frame
1052 completionHandler:(void(^)())completionHandler {
1053 SEL alertSelector = @selector(webController:
1054 runJavaScriptAlertPanelWithMessage:
1056 completionHandler:);
1057 if ([self.UIDelegate respondsToSelector:alertSelector]) {
1058 [self.UIDelegate webController:self
1059 runJavaScriptAlertPanelWithMessage:message
1060 requestURL:net::GURLWithNSURL(frame.request.URL)
1061 completionHandler:completionHandler];
1062 } else if (completionHandler) {
1063 completionHandler();
1067 - (void)webView:(WKWebView*)webView
1068 runJavaScriptConfirmPanelWithMessage:(NSString*)message
1069 initiatedByFrame:(WKFrameInfo*)frame
1071 (void (^)(BOOL result))completionHandler {
1072 SEL confirmationSelector = @selector(webController:
1073 runJavaScriptConfirmPanelWithMessage:
1075 completionHandler:);
1076 if ([self.UIDelegate respondsToSelector:confirmationSelector]) {
1077 [self.UIDelegate webController:self
1078 runJavaScriptConfirmPanelWithMessage:message
1080 net::GURLWithNSURL(frame.request.URL)
1081 completionHandler:completionHandler];
1082 } else if (completionHandler) {
1083 completionHandler(NO);
1087 - (void)webView:(WKWebView*)webView
1088 runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
1089 defaultText:(NSString*)defaultText
1090 initiatedByFrame:(WKFrameInfo*)frame
1092 (void (^)(NSString *result))completionHandler {
1093 SEL textInputSelector = @selector(webController:
1094 runJavaScriptTextInputPanelWithPrompt:
1097 completionHandler:);
1098 if ([self.UIDelegate respondsToSelector:textInputSelector]) {
1099 [self.UIDelegate webController:self
1100 runJavaScriptTextInputPanelWithPrompt:prompt
1101 placeholderText:defaultText
1103 net::GURLWithNSURL(frame.request.URL)
1104 completionHandler:completionHandler];
1105 } else if (completionHandler) {
1106 completionHandler(nil);