Prevent app list doodle from being pinch-to-zoomed.
[chromium-blink-merge.git] / ios / web / web_state / ui / crw_wk_web_view_web_controller.mm
blob54effb602f3920e2bca3b44cff0bf8af576017c7
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"
37 #endif
39 namespace {
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";
48 }  // namespace
50 @interface CRWWKWebViewWebController () <WKNavigationDelegate,
51                                          WKScriptMessageHandler,
52                                          WKUIDelegate> {
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.
63   GURL _documentURL;
65   // A set of script managers whose scripts have been injected into the current
66   // page.
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
72   // idempotent.
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
78   // committed.
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
101 // changed.
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
154 // popup.
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;
165 #endif
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;
177 #endif
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;
216 @end
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.
229   NOTIMPLEMENTED();
230   return NO;
233 - (void)setKeyboardDisplayRequiresUserAction:(BOOL)requiresUserAction {
234   NOTIMPLEMENTED();
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];
265 #pragma mark -
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
275 - (UIView*)webView {
276   return _wkWebView.get();
279 - (UIScrollView*)webScrollView {
280   return [_wkWebView scrollView];
283 - (BOOL)ignoreURLVerificationFailures {
284   return NO;
287 - (NSString*)title {
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 {
308   DCHECK(trustLevel);
309   *trustLevel = web::URLVerificationTrustLevel::kAbsolute;
310   return _documentURL;
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.
327   if (!_wkWebView) {
328     return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
329   }
331   if (!self.documentMIMEType) {
332     return web::WEB_VIEW_DOCUMENT_TYPE_UNKNOWN;
333   }
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;
339   }
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);
366   }
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 {
385   // Nothing to do.
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())
407     return;
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 {
427   return @{
428 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
429     @"estimatedProgress" : @"webViewEstimatedProgressDidChange",
430     @"hasOnlySecureContent" : @"webViewContentSecurityDidChange",
431 #endif
432     @"loading" : @"webViewLoadingStateDidChange",
433     @"title" : @"webViewTitleDidChange",
434     @"URL" : @"webViewURLDidChange",
435   };
438 - (NSString*)activityIndicatorGroupID {
439   return [NSString stringWithFormat:
440       @"WKWebViewWebController.NetworkActivityIndicatorKey.%@",
441           self.webStateImpl->GetRequestGroupID()];
444 #if !defined(ENABLE_CHROME_NET_STACK_FOR_WKWEBVIEW)
445 - (int)certGroupID {
446   DCHECK(self.webStateImpl);
447   // Request tracker IDs are used as certificate groups.
448   return self.webStateImpl->GetRequestTracker()->identifier();
450 #endif
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 {
459   if (!_wkWebView) {
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];
470   }
473 - (WKWebView*)createWebViewWithConfiguration:(WKWebViewConfiguration*)config {
474   return [web::CreateWKWebView(
475               CGRectZero,
476               config,
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];
493   }
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];
507   }
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:
516       return YES;
517     case WKNavigationTypeOther:
518       return [self userClickedRecently];
519     default:
520       return NO;
521   }
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 {
542   if (!webView)
543     return nil;
545   base::WeakNSObject<CRWWKWebViewWebController> weakSelf(self);
546   id crashHandler = ^{
547     [weakSelf webViewWebProcessDidCrash];
548   };
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];
557   }
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);
566   }];
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
582                     referrer:referrer
583                   windowName:kWindowName
584                 inBackground:NO];
585       DCHECK(!child || child.sessionController.openedByDOM);
586   };
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:)]) {
595     return;
596   }
598   base::WeakNSObject<CRWWKWebViewWebController> weakSelf(self);
599   dispatch_async(dispatch_get_main_queue(), ^{
600       [self queryPageReferrerPolicy:^(NSString* policy) {
601           [weakSelf didBlockPopupWithURL:popupURL
602                                sourceURL:sourceURL
603                           referrerPolicy:base::SysNSStringToUTF8(policy)];
604       }];
605   });
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
623                      recoverable:NO
624                         callback:nullptr];
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();
643   if (!item)
644     return;
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
680   // the next runloop.
681   if (!_changingHistoryState) {
682     [self registerLoadRequest:_documentURL];
683     [self didStartLoadingURL:_documentURL updateHistory:YES];
684     [self didFinishNavigation];
685   }
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];
696   return result;
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";
705   }
708 - (BOOL)respondToWKScriptMessage:(WKScriptMessage*)scriptMessage {
709   CHECK(scriptMessage.frameInfo.mainFrame);
710   int errorCode = 0;
711   std::string errorMessage;
712   scoped_ptr<base::Value> inputJSONData(
713       base::JSONReader::ReadAndReturnError(
714           base::SysNSStringToUTF8(scriptMessage.body),
715           false,
716           &errorCode,
717           &errorMessage));
718   if (errorCode) {
719     DLOG(WARNING) << "JSON parse error: %s" << errorMessage.c_str();
720     return NO;
721   }
722   base::DictionaryValue* message = nullptr;
723   if (!inputJSONData->GetAsDictionary(&message)) {
724     return NO;
725   }
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);
733     return NO;
734   }
735   base::DictionaryValue* command = nullptr;
736   if (!message->GetDictionary("crwCommand", &command)) {
737     return NO;
738   }
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])];
744   }
746   NOTREACHED();
747   return NO;
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:);
761   });
762   DCHECK(handlers);
763   auto iter = handlers->find(command);
764   return iter != handlers->end()
765              ? iter->second
766              : [super selectorToHandleJavaScriptCommand:command];
769 #pragma mark -
770 #pragma mark JavaScript message handlers
772 - (BOOL)handleWindowHistoryWillChangeStateMessage:
773     (base::DictionaryValue*)message
774                                           context:(NSDictionary*)context {
775   _changingHistoryState = YES;
776   return 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
792                                                   context:context];
795 #pragma mark -
796 #pragma mark KVO Observation
798 - (void)observeValueForKeyPath:(NSString*)keyPath
799                       ofObject:(id)object
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]];
815   }
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];
827   } else {
828     [self clearActivityIndicatorTasks];
829   }
832 - (void)webViewTitleDidChange {
833   if ([self.delegate respondsToSelector:
834           @selector(webController:titleDidChange:)]) {
835     DCHECK(self.title);
836     [self.delegate webController:self titleDidChange:self.title];
837   }
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";
845     return;
846   }
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
854   //    be reported.
855   // 3) When a navigation error occurs after provisional navigation starts,
856   //    the URL reverts to the previous URL without triggering a new navigation.
857   //
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.
862   //
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)
873       return;
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.
881                      if (!_wkWebView ||
882                          ![result isKindOfClass:[NSString class]]) {
883                        return;
884                      }
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];
890                      }
891                  }];
892   }
895 #pragma mark -
896 #pragma mark WKNavigationDelegate Methods
898 - (void)webView:(WKWebView *)webView
899     decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
900                     decisionHandler:
901         (void (^)(WKNavigationActionPolicy))decisionHandler {
902   if (self.isBeingDestroyed) {
903     decisionHandler(WKNavigationActionPolicyCancel);
904     return;
905   }
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
926                       decisionHandler:
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];
945   }
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];
967   else
968 #endif
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];
990 #endif
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
1012                     completionHandler:
1013         (void (^)(NSURLSessionAuthChallengeDisposition disposition,
1014                   NSURLCredential *credential))completionHandler {
1015   NOTIMPLEMENTED();
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)) :
1028                                [self currentURL];
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;
1035     return nil;
1036   }
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:
1055                                    requestURL:
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();
1064   }
1067 - (void)webView:(WKWebView*)webView
1068     runJavaScriptConfirmPanelWithMessage:(NSString*)message
1069                         initiatedByFrame:(WKFrameInfo*)frame
1070                        completionHandler:
1071         (void (^)(BOOL result))completionHandler {
1072   SEL confirmationSelector = @selector(webController:
1073                 runJavaScriptConfirmPanelWithMessage:
1074                                           requestURL:
1075                                    completionHandler:);
1076   if ([self.UIDelegate respondsToSelector:confirmationSelector]) {
1077     [self.UIDelegate webController:self
1078         runJavaScriptConfirmPanelWithMessage:message
1079                                   requestURL:
1080             net::GURLWithNSURL(frame.request.URL)
1081                            completionHandler:completionHandler];
1082   } else if (completionHandler) {
1083     completionHandler(NO);
1084   }
1087 - (void)webView:(WKWebView*)webView
1088     runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
1089                               defaultText:(NSString*)defaultText
1090                          initiatedByFrame:(WKFrameInfo*)frame
1091                         completionHandler:
1092         (void (^)(NSString *result))completionHandler {
1093   SEL textInputSelector = @selector(webController:
1094             runJavaScriptTextInputPanelWithPrompt:
1095                                   placeholderText:
1096                                        requestURL:
1097                                 completionHandler:);
1098   if ([self.UIDelegate respondsToSelector:textInputSelector]) {
1099     [self.UIDelegate webController:self
1100         runJavaScriptTextInputPanelWithPrompt:prompt
1101                               placeholderText:defaultText
1102                                    requestURL:
1103             net::GURLWithNSURL(frame.request.URL)
1104                             completionHandler:completionHandler];
1105   } else if (completionHandler) {
1106     completionHandler(nil);
1107   }
1110 @end