Prevent app list doodle from being pinch-to-zoomed.
[chromium-blink-merge.git] / ios / web / web_state / ui / crw_ui_web_view_web_controller.mm
blob36a9d8355b95e293957d35ffb6490f85408fc391
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_ui_web_view_web_controller.h"
7 #import "base/ios/weak_nsobject.h"
8 #include "base/json/json_reader.h"
9 #include "base/json/string_escape.h"
10 #include "base/mac/bind_objc_block.h"
11 #import "base/mac/scoped_nsobject.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/metrics/field_trial.h"
14 #include "base/metrics/histogram.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/sys_string_conversions.h"
17 #include "base/timer/timer.h"
18 #include "base/values.h"
19 #import "ios/net/nsurlrequest_util.h"
20 #import "ios/web/navigation/crw_session_controller.h"
21 #import "ios/web/navigation/crw_session_entry.h"
22 #import "ios/web/net/crw_url_verifying_protocol_handler.h"
23 #include "ios/web/net/request_group_util.h"
24 #include "ios/web/public/url_scheme_util.h"
25 #include "ios/web/public/web_client.h"
26 #import "ios/web/ui_web_view_util.h"
27 #import "ios/web/web_state/js/crw_js_invoke_parameter_queue.h"
28 #import "ios/web/web_state/ui/crw_context_menu_provider.h"
29 #import "ios/web/web_state/ui/crw_web_controller+protected.h"
30 #import "ios/web/web_state/ui/crw_web_controller.h"
31 #import "ios/web/web_state/ui/web_view_js_utils.h"
32 #import "ios/web/web_state/web_state_impl.h"
33 #import "ios/web/web_state/web_view_creation_utils.h"
34 #import "net/base/mac/url_conversions.h"
35 #include "url/url_constants.h"
37 namespace web {
39 // The following continuous check timer frequency constants are externally
40 // available for the purpose of performance tests.
41 // Frequency for the continuous checks when a reset in the page object is
42 // anticipated shortly. In milliseconds.
43 const int64 kContinuousCheckIntervalMSHigh = 100;
45 // The maximum duration that the CRWWebController can run in high-frequency
46 // check mode before being changed back to the low frequency.
47 const int64 kContinuousCheckHighFrequencyMSMaxDuration = 5000;
49 // Frequency for the continuous checks when a reset in the page object is not
50 // anticipated; checks are only made as a precaution.
51 // The URL could be out of date for this many milliseconds, so this should not
52 // be increased without careful consideration.
53 const int64 kContinuousCheckIntervalMSLow = 3000;
55 }  // namespace web
57 @interface CRWUIWebViewWebController ()<UIWebViewDelegate> {
58   // The UIWebView managed by this instance.
59   base::scoped_nsobject<UIWebView> _uiWebView;
61   // Whether caching of the current URL is enabled or not.
62   BOOL _urlCachingEnabled;
64   // Temporarily cached current URL. Only valid/set while urlCachingEnabled
65   // is YES.
66   // TODO(stuartmorgan): Change this to a struct so code using it is more
67   // readable.
68   std::pair<GURL, web::URLVerificationTrustLevel> _cachedURL;
70   // The last time a URL with absolute trust level was computed.
71   // When an untrusted URL is retrieved from the
72   // |CRWURLVerifyingProtocolHandler|, if the last trusted URL is within
73   // |kContinuousCheckIntervalMSLow|, the trustLevel is upgraded to Mixed.
74   // The reason is that it is sometimes temporarily impossible to do a
75   // AsyncXMLHttpRequest on a web page. When this happen, it is not possible
76   // to check for the validity of the current URL. Because the checker is
77   // only checking every |kContinuousCheckIntervalMSLow| anyway, waiting this
78   // amount of time before triggering an interstitial does not weaken the
79   // security of the browser.
80   base::TimeTicks _lastCorrectURLTime;
82   // Each new UIWebView starts in a state where:
83   // - window.location.href is equal to about:blank
84   // - Ajax requests seem to be impossible
85   // Because Ajax requests are used to determine is a URL is verified, this
86   // means it is impossible to do this check until the UIWebView is in a more
87   // sane state. This variable tracks whether verifying the URL is currently
88   // impossible. It starts at YES when a new UIWebView is created,
89   // and will change to NO, as soon as either window.location.href is not
90   // about:blank anymore, or an URL verification request succeeds. This means
91   // that a malicious site that is able to change the value of
92   // window.location.href and that is loaded as the first request will be able
93   // to change its URL to about:blank. As this is not an interesting URL, it is
94   // considered acceptable.
95   BOOL _spoofableRequest;
97   // Timer used to make continuous checks on the UIWebView.  Timer is
98   // running only while |webView| is non-nil.
99   scoped_ptr<base::Timer> _continuousCheckTimer;
100   // Timer to lower the check frequency automatically.
101   scoped_ptr<base::Timer> _lowerFrequencyTimer;
103   // Counts of calls to |-webViewDidStartLoad:| and |-webViewDidFinishLoad|.
104   // When |_loadCount| is equal to |_unloadCount|, the page is no longer
105   // loading. Used as a fallback to determine when the page is done loading in
106   // case document.readyState isn't sufficient.
107   // When |_loadCount| is 1, the main page is loading (as opposed to a
108   // sub-resource).
109   int _loadCount;
110   int _unloadCount;
112   // Backs the property of the same name.
113   BOOL _inJavaScriptContext;
115   // Backs the property of the same name.
116   base::scoped_nsobject<CRWJSInvokeParameterQueue> _jsInvokeParameterQueue;
118   // Blocks message queue processing (for testing).
119   BOOL _jsMessageQueueThrottled;
121   // YES if a video is playing in fullscreen.
122   BOOL _inFullscreenVideo;
124   // Backs the property of the same name.
125   id<CRWRecurringTaskDelegate>_recurringTaskDelegate;
128 // Whether or not URL caching is enabled. Between enabling and disabling
129 // caching, calls to webURLWithTrustLevel: after the first may return the same
130 // answer without re-checking the URL.
131 @property(nonatomic, setter=setURLCachingEnabled:) BOOL urlCachingEnabled;
133 // Returns whether the current page is the web views initial (default) page.
134 - (BOOL)isDefaultPage;
136 // Returns whether the given navigation is triggered by a user link click.
137 - (BOOL)isLinkNavigation:(UIWebViewNavigationType)navigationType;
139 // Starts (at the given interval) the timer that drives runRecurringTasks to see
140 // whether or not the page has changed. This is used to work around the fact
141 // that UIWebView callbacks are not sufficiently reliable to catch every page
142 // change.
143 - (void)setContinuousCheckTimerInterval:(const base::TimeDelta&)interval;
145 // Evaluates the supplied JavaScript and returns the result. Will return nil
146 // if it is unable to evaluate the JavaScript.
147 - (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script;
149 // Evaluates the user-entered JavaScript in the WebView and returns the result.
150 // Will return nil if the web view is currently not available.
151 - (NSString*)stringByEvaluatingUserJavaScriptFromString:(NSString*)script;
153 // Checks if the document is loaded, and if so triggers any necessary logic.
154 - (void)checkDocumentLoaded;
156 // Returns a new autoreleased UIWebView.
157 - (UIWebView*)createWebView;
159 // Sets value to web view property.
160 - (void)setWebView:(UIWebView*)webView;
162 // Simulates the events generated by core.js during document loading process.
163 // Used for non-HTML documents (e.g. PDF) that do not support data flow from
164 // JavaScript to obj-c via iframe injection.
165 - (void)generateMissingDocumentLifecycleEvents;
167 // Makes a best-effort attempt to retroactively construct a load request for an
168 // observed-but-unexpected navigation. Should be called any time a page
169 // change is detected as having happened without the current internal state
170 // indicating it was expected.
171 - (void)generateMissingLoadRequestWithURL:(const GURL&)currentURL
172                                  referrer:(const web::Referrer&)referrer;
174 // Returns a child scripting CRWWebController with the given window name.
175 - (id<CRWWebControllerScripting>)scriptingInterfaceForWindowNamed:
176     (NSString*)name;
178 // Called when UIMoviePlayerControllerDidEnterFullscreenNotification is posted.
179 - (void)moviePlayerDidEnterFullscreen:(NSNotification*)notification;
181 // Called when UIMoviePlayerControllerDidExitFullscreenNotification is posted.
182 - (void)moviePlayerDidExitFullscreen:(NSNotification*)notification;
184 // Exits fullscreen mode for any playing videos.
185 - (void)exitFullscreenVideo;
187 // Handles presentation of the web document. Checks and handles URL changes,
188 // page refreshes, and title changes.
189 - (void)documentPresent;
191 // Handles queued JS to ObjC messages.
192 // All commands are passed via JSON strings, including parameters.
193 - (void)respondToJSInvoke;
195 // Pauses (|throttle|=YES) or resumes (|throttle|=NO) crwebinvoke message
196 // processing.
197 - (void)setJsMessageQueueThrottled:(BOOL)throttle;
199 // Removes document load commands from the queue. Otherwise they could be
200 // handled after a new page load has begun, which would cause an unwanted
201 // redirect.
202 - (void)removeDocumentLoadCommandsFromQueue;
204 // Returns YES if the given URL has a scheme associated with JS->native calls.
205 - (BOOL)urlSchemeIsWebInvoke:(const GURL&)url;
207 // Returns window name given a message and its context.
208 - (NSString*)windowNameFromMessage:(base::DictionaryValue*)message
209                            context:(NSDictionary*)context;
211 // Handles 'anchor.click' message.
212 - (BOOL)handleAnchorClickMessage:(base::DictionaryValue*)message
213                          context:(NSDictionary*)context;
214 // Handles 'document.loaded' message.
215 - (BOOL)handleDocumentLoadedMessage:(base::DictionaryValue*)message
216                             context:(NSDictionary*)context;
217 // Handles 'document.present' message.
218 - (BOOL)handleDocumentPresentMessage:(base::DictionaryValue*)message
219                              context:(NSDictionary*)context;
220 // Handles 'document.retitled' message.
221 - (BOOL)handleDocumentRetitledMessage:(base::DictionaryValue*)message
222                               context:(NSDictionary*)context;
223 // Handles 'window.close' message.
224 - (BOOL)handleWindowCloseMessage:(base::DictionaryValue*)message
225                          context:(NSDictionary*)context;
226 // Handles 'window.document.write' message.
227 - (BOOL)handleWindowDocumentWriteMessage:(base::DictionaryValue*)message
228                                  context:(NSDictionary*)context;
229 // Handles 'window.location' message.
230 - (BOOL)handleWindowLocationMessage:(base::DictionaryValue*)message
231                             context:(NSDictionary*)context;
232 // Handles 'window.open' message.
233 - (BOOL)handleWindowOpenMessage:(base::DictionaryValue*)message
234                         context:(NSDictionary*)context;
235 // Handles 'window.stop' message.
236 - (BOOL)handleWindowStopMessage:(base::DictionaryValue*)message
237                         context:(NSDictionary*)context;
238 // Handles 'window.unload' message.
239 - (BOOL)handleWindowUnloadMessage:(base::DictionaryValue*)message
240                           context:(NSDictionary*)context;
241 @end
243 namespace {
245 // Utility to help catch unwanted JavaScript re-entries. An instance should
246 // be created on the stack any time JS will be executed.
247 // It uses an instance variable (passed in as a pointer to boolean) that needs
248 // to be initialized to false.
249 class ScopedReentryGuard {
250  public:
251   explicit ScopedReentryGuard(BOOL* is_inside_javascript_context)
252       : is_inside_javascript_context_(is_inside_javascript_context) {
253     DCHECK(!*is_inside_javascript_context_);
254     *is_inside_javascript_context_ = YES;
255   }
256   ~ScopedReentryGuard() {
257     DCHECK(*is_inside_javascript_context_);
258     *is_inside_javascript_context_ = NO;
259   }
261  private:
262   BOOL* is_inside_javascript_context_;
263   DISALLOW_IMPLICIT_CONSTRUCTORS(ScopedReentryGuard);
266 // Class allowing to selectively enable caching of |currentURL| on a
267 // CRWUIWebViewWebController. As long as an instance of this class lives,
268 // the CRWUIWebViewWebController passed as parameter will cache the result for
269 // |currentURL| calls.
270 class ScopedCachedCurrentUrl {
271  public:
272   explicit ScopedCachedCurrentUrl(CRWUIWebViewWebController* web_controller)
273       : web_controller_(web_controller),
274         had_cached_current_url_([web_controller urlCachingEnabled]) {
275     if (!had_cached_current_url_)
276       [web_controller_ setURLCachingEnabled:YES];
277   }
279   ~ScopedCachedCurrentUrl() {
280     if (!had_cached_current_url_)
281       [web_controller_ setURLCachingEnabled:NO];
282   }
284  private:
285   CRWUIWebViewWebController* web_controller_;
286   bool had_cached_current_url_;
287   DISALLOW_IMPLICIT_CONSTRUCTORS(ScopedCachedCurrentUrl);
290 // Normalizes the URL for the purposes of identifying the origin page (remove
291 // any parameters, fragments, etc.) and return an absolute string of the URL.
292 std::string NormalizedUrl(const GURL& url) {
293   GURL::Replacements replacements;
294   replacements.ClearQuery();
295   replacements.ClearRef();
296   replacements.ClearUsername();
297   replacements.ClearPassword();
298   GURL page_url(url.ReplaceComponents(replacements));
300   return page_url.spec();
303 // The maximum size of JSON message passed from JavaScript to ObjC.
304 // 256kB is an arbitrary number that was chosen to be a magnitude larger than
305 // any legitimate message.
306 const size_t kMaxMessageQueueSize = 262144;
308 }  // namespace
310 @implementation CRWUIWebViewWebController
312 - (instancetype)initWithWebState:(scoped_ptr<web::WebStateImpl>)webState {
313   self = [super initWithWebState:webState.Pass()];
314   if (self) {
315     _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
317     NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
318     [defaultCenter addObserver:self
319                       selector:@selector(moviePlayerDidEnterFullscreen:)
320                           name:
321         @"UIMoviePlayerControllerDidEnterFullscreenNotification"
322                         object:nil];
323     [defaultCenter addObserver:self
324                       selector:@selector(moviePlayerDidExitFullscreen:)
325                           name:
326         @"UIMoviePlayerControllerDidExitFullscreenNotification"
327                         object:nil];
328     _recurringTaskDelegate = self;
329   }
330   return self;
333 - (void)dealloc {
334   [[NSNotificationCenter defaultCenter] removeObserver:self];
335   [super dealloc];
338 #pragma mark - CRWWebController public method implementations
340 - (void)loadCompleteWithSuccess:(BOOL)loadSuccess {
341   [super loadCompleteWithSuccess:loadSuccess];
342   [self removeDocumentLoadCommandsFromQueue];
345 - (BOOL)keyboardDisplayRequiresUserAction {
346   return [_uiWebView keyboardDisplayRequiresUserAction];
349 - (void)setKeyboardDisplayRequiresUserAction:(BOOL)requiresUserAction {
350   [_uiWebView setKeyboardDisplayRequiresUserAction:requiresUserAction];
353 - (void)evaluateUserJavaScript:(NSString*)script {
354   [self setUserInteractionRegistered:YES];
355   // A script which contains alert() call executed by UIWebView from gcd block
356   // freezes the app (crbug.com/444106), hence this uses the NSObject API.
357   [_uiWebView performSelectorOnMainThread:
358       @selector(stringByEvaluatingJavaScriptFromString:)
359                                withObject:script
360                             waitUntilDone:NO];
363 #pragma mark Overridden public methods
365 - (void)wasShown {
366   if (_uiWebView) {
367     // Turn the timer back on, and do an immediate check for anything missed
368     // while the timer was off.
369     [self.recurringTaskDelegate runRecurringTask];
370     _continuousCheckTimer->Reset();
371   }
372   [super wasShown];
375 - (void)wasHidden {
376   if (self.webView) {
377     // Turn the timer off, to cut down on work being done by background tabs.
378     _continuousCheckTimer->Stop();
379   }
380   [super wasHidden];
382   // The video player is not quit/dismissed when the home button is pressed and
383   // Chrome is backgrounded (crbug.com/277206).
384   if (_inFullscreenVideo)
385     [self exitFullscreenVideo];
389 - (void)close {
390   [super close];
392   // The timers must not exist at this point, otherwise this object will leak.
393   DCHECK(!_continuousCheckTimer);
394   DCHECK(!_lowerFrequencyTimer);
397 - (void)childWindowClosed:(NSString*)windowName {
398   // Get the substring of the window name after the hash.
399   NSRange range = [windowName rangeOfString:
400       base::SysUTF8ToNSString(web::kWindowNameSeparator)];
401   if (range.location != NSNotFound) {
402     NSString* target = [windowName substringFromIndex:(range.location + 1)];
403     [self stringByEvaluatingJavaScriptFromString:
404         [NSString stringWithFormat:@"__gCrWeb.windowClosed('%@');", target]];
405   }
408 #pragma mark -
409 #pragma mark Testing-Only Methods
411 - (void)injectWebView:(id)webView {
412   [super injectWebView:webView];
413   [self setWebView:webView];
416 #pragma mark CRWJSInjectionEvaluatorMethods
418 - (void)evaluateJavaScript:(NSString*)script
419        stringResultHandler:(web::JavaScriptCompletion)handler {
420   NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script];
421   web::EvaluateJavaScript(_uiWebView, safeScript, handler);
424 - (web::WebViewType)webViewType {
425   return web::UI_WEB_VIEW_TYPE;
428 #pragma mark - Protected property implementations
430 - (UIView*)webView {
431   return _uiWebView.get();
434 - (UIScrollView*)webScrollView {
435   return [_uiWebView scrollView];
438 - (BOOL)urlCachingEnabled {
439   return _urlCachingEnabled;
442 - (void)setURLCachingEnabled:(BOOL)enabled {
443   if (enabled == _urlCachingEnabled)
444     return;
445   _urlCachingEnabled = enabled;
446   _cachedURL.first = GURL();
449 - (BOOL)ignoreURLVerificationFailures {
450   return _spoofableRequest;
453 - (NSString*)title {
454   return [self stringByEvaluatingJavaScriptFromString:
455       @"document.title.length ? document.title : ''"];
458 #pragma mark Protected method implementations
460 - (void)ensureWebViewCreated {
461   if (!_uiWebView) {
462     [self setWebView:[self createWebView]];
463     // Notify super class about created web view. -webViewDidChange is not
464     // called from -setWebView: as the latter used in unit tests with fake
465     // web view, which cannot be added to view hierarchy.
466     [self webViewDidChange];
467   }
470 - (void)resetWebView {
471   [self setWebView:nil];
474 - (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
475   if (_cachedURL.first.is_valid()) {
476     *trustLevel = _cachedURL.second;
477     return _cachedURL.first;
478   }
479   ScopedReentryGuard reentryGuard(&_inJavaScriptContext);
480   const GURL url =
481       [CRWURLVerifyingProtocolHandler currentURLForWebView:_uiWebView
482                                                 trustLevel:trustLevel];
484   // If verification succeeded, or the URL has changed, then the UIWebView is no
485   // longer in the initial state.
486   if (*trustLevel != web::URLVerificationTrustLevel::kNone ||
487       url != GURL(url::kAboutBlankURL))
488     _spoofableRequest = NO;
490   if (*trustLevel == web::URLVerificationTrustLevel::kAbsolute) {
491     _lastCorrectURLTime = base::TimeTicks::Now();
492     if (self.urlCachingEnabled) {
493       _cachedURL.first = url;
494       _cachedURL.second = *trustLevel;
495     }
496   } else if (*trustLevel == web::URLVerificationTrustLevel::kNone &&
497              (base::TimeTicks::Now() - _lastCorrectURLTime) <
498                  base::TimeDelta::FromMilliseconds(
499                      web::kContinuousCheckIntervalMSLow)) {
500     // URL is not trusted, but the last time it was trusted is within
501     // kContinuousCheckIntervalMSLow.
502     *trustLevel = web::URLVerificationTrustLevel::kMixed;
503   }
505   return url;
508 - (void)registerUserAgent {
509   web::BuildAndRegisterUserAgentForUIWebView(
510       self.webStateImpl->GetRequestGroupID(),
511       [self useDesktopUserAgent]);
514 // The core.js cannot pass messages back to obj-c  if it is injected
515 // to |WEB_VIEW_DOCUMENT| because it does not support iframe creation used
516 // by core.js to communicate back. That functionality is only supported
517 // by |WEB_VIEW_HTML_DOCUMENT|. |WEB_VIEW_DOCUMENT| is used when displaying
518 // non-HTML contents (e.g. PDF documents).
519 - (web::WebViewDocumentType)webViewDocumentType {
520   // This happens during tests.
521   if (!_uiWebView) {
522     return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
523   }
524   NSString* documentType =
525       [_uiWebView stringByEvaluatingJavaScriptFromString:
526           @"'' + document"];
527   if ([documentType isEqualToString:@"[object HTMLDocument]"])
528     return web::WEB_VIEW_DOCUMENT_TYPE_HTML;
529   else if ([documentType isEqualToString:@"[object Document]"])
530     return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
531   return web::WEB_VIEW_DOCUMENT_TYPE_UNKNOWN;
534 - (void)loadWebRequest:(NSURLRequest*)request {
535   [_uiWebView loadRequest:request];
538 - (void)loadWebHTMLString:(NSString*)html forURL:(const GURL&)URL {
539   [_uiWebView loadHTMLString:html baseURL:net::NSURLWithGURL(URL)];
542 - (BOOL)scriptHasBeenInjectedForClass:(Class)jsInjectionManagerClass
543                        presenceBeacon:(NSString*)beacon {
544   NSString* beaconCheckScript = [NSString stringWithFormat:
545       @"try { typeof %@; } catch (e) { 'undefined'; }", beacon];
546   NSString* result =
547       [self stringByEvaluatingJavaScriptFromString:beaconCheckScript];
548   return [result isEqualToString:@"object"];
551 - (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass {
552   // Skip evaluation if there's no content (e.g., if what's being injected is
553   // an umbrella manager).
554   if ([script length]) {
555     [super injectScript:script forClass:JSInjectionManagerClass];
556     [self stringByEvaluatingJavaScriptFromString:script];
557   }
560 - (void)willLoadCurrentURLInWebView {
561 #if !defined(NDEBUG)
562   if (_uiWebView) {
563     // This code uses non-documented API, but is not compiled in release.
564     id documentView = [_uiWebView valueForKey:@"documentView"];
565     id webView = [documentView valueForKey:@"webView"];
566     NSString* userAgent = [webView performSelector:@selector(userAgentForURL:)
567                                         withObject:nil];
568     DCHECK(userAgent);
569     const bool wrongRequestGroupID =
570         ![self.webStateImpl->GetRequestGroupID()
571             isEqualToString:web::ExtractRequestGroupIDFromUserAgent(userAgent)];
572     DLOG_IF(ERROR, wrongRequestGroupID) << "Incorrect user agent in UIWebView";
573   }
574 #endif  // !defined(NDEBUG)
577 - (void)setPageChangeProbability:(web::PageChangeProbability)probability {
578   if (probability == web::PAGE_CHANGE_PROBABILITY_LOW) {
579     // Reduce check interval to precautionary frequency.
580     [self setContinuousCheckTimerInterval:
581         base::TimeDelta::FromMilliseconds(web::kContinuousCheckIntervalMSLow)];
582   } else {
583     // Increase the timer frequency, as a window change is anticipated shortly.
584     [self setContinuousCheckTimerInterval:
585         base::TimeDelta::FromMilliseconds(web::kContinuousCheckIntervalMSHigh)];
587     if (probability != web::PAGE_CHANGE_PROBABILITY_VERY_HIGH) {
588       // The timer frequency is automatically lowered after a set duration in
589       // case the guess was wrong, to avoid wedging in high-frequency mode.
590       base::Closure closure = base::BindBlock(^{
591           [self setContinuousCheckTimerInterval:
592               base::TimeDelta::FromMilliseconds(
593                   web::kContinuousCheckIntervalMSLow)];
594       });
595       _lowerFrequencyTimer.reset(
596           new base::Timer(FROM_HERE,
597                           base::TimeDelta::FromMilliseconds(
598                               web::kContinuousCheckHighFrequencyMSMaxDuration),
599                           closure, false /* not repeating */));
600       _lowerFrequencyTimer->Reset();
601     }
602   }
605 - (BOOL)checkForUnexpectedURLChange {
606     // The check makes no sense without an active web view.
607   if (!self.webView)
608     return NO;
610   // Change to UIWebView default page is not considered a 'real' change and
611   // URL changes are not reported.
612   if ([self isDefaultPage])
613     return NO;
615   // Check if currentURL is unexpected (not the incoming page).
616   // This is necessary to notice page changes if core.js injection is disabled
617   // by a malicious page.
618   if (!self.URLOnStartLoading.is_empty() &&
619       [self currentURL] == self.URLOnStartLoading) {
620     return NO;
621   }
623   // If the URL has changed, handle page load mechanics.
624   ScopedCachedCurrentUrl scopedCurrentURL(self);
625   [self webPageChanged];
626   [self checkDocumentLoaded];
627   [self titleDidChange];
629   return YES;
632 - (void)abortWebLoad {
633   // Current load will not complete; this should be communicated upstream to
634   // the delegate, and flagged in the WebView so further messages can be
635   // prevented (which may be confused for messages from newer pages).
636   [_uiWebView stringByEvaluatingJavaScriptFromString:
637       @"document._cancelled = true;"];
638   [_uiWebView stopLoading];
641 - (void)resetLoadState {
642   _loadCount = 0;
643   _unloadCount = 0;
646 - (void)setSuppressDialogsWithHelperScript:(NSString*)script {
647   [self stringByEvaluatingJavaScriptFromString:script];
650 - (void)checkDocumentLoaded {
651   NSString* loaded =
652       [self stringByEvaluatingJavaScriptFromString:
653            @"document.readyState === 'loaded' || "
654             "document.readyState === 'complete'"];
655   if ([loaded isEqualToString:@"true"]) {
656     [self didFinishNavigation];
657   }
660 - (NSString*)currentReferrerString {
661   return [self stringByEvaluatingJavaScriptFromString:@"document.referrer"];
664 - (void)titleDidChange {
665   if (![self.delegate respondsToSelector:
666            @selector(webController:titleDidChange:)]) {
667     return;
668   }
670   // Checking the URL trust level is expensive. For performance reasons, the
671   // current URL and the trust level cache must be enabled.
672   // NOTE: Adding a ScopedCachedCurrentUrl here is not the right way to solve
673   // this DCHECK.
674   DCHECK(self.urlCachingEnabled);
676   // Change to UIWebView default page is not considered a 'real' change and
677   // title changes are not reported.
678   if ([self isDefaultPage])
679     return;
681   // The title can be retrieved from the document only if the URL can be
682   // trusted.
683   web::URLVerificationTrustLevel trustLevel =
684       web::URLVerificationTrustLevel::kNone;
685   [self currentURLWithTrustLevel:&trustLevel];
686   if (trustLevel != web::URLVerificationTrustLevel::kAbsolute)
687     return;
689   NSString* title = self.title;
690   if ([title length])
691     [self.delegate webController:self titleDidChange:title];
694 - (void)teminateNetworkActivity {
695   [super terminateNetworkActivity];
696   _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
699 - (void)fetchWebPageSizeWithCompletionHandler:(void(^)(CGSize))handler {
700   if (!self.webView) {
701     handler(CGSizeZero);
702     return;
703   }
705   // Ensure that JavaScript has been injected.
706   [self.recurringTaskDelegate runRecurringTask];
707   [super fetchWebPageSizeWithCompletionHandler:handler];
710 - (void)documentPresent {
711   if (self.loadPhase != web::PAGE_LOADED &&
712       self.loadPhase != web::LOAD_REQUESTED) {
713     return;
714   }
716   ScopedCachedCurrentUrl scopedCurrentURL(self);
718   // This is a good time to check if the URL has changed.
719   BOOL urlChanged = [self checkForUnexpectedURLChange];
721   // This is a good time to check if the page has refreshed.
722   if (!urlChanged && self.windowId != self.lastSeenWindowID)
723     [self webPageChanged];
725   // Set initial title.
726   [self titleDidChange];
729 - (void)webPageChanged {
730   if (self.loadPhase != web::LOAD_REQUESTED ||
731       self.lastRegisteredRequestURL.is_empty() ||
732       self.lastRegisteredRequestURL != [self currentURL]) {
733     // The page change was unexpected (not already messaged to
734     // webWillStartLoadingURL), so fill in the load request.
735     [self generateMissingLoadRequestWithURL:[self currentURL]
736                                    referrer:[self currentReferrer]];
737   }
739   [super webPageChanged];
742 - (CGFloat)absoluteZoomScaleForScrollState:
743     (const web::PageScrollState&)scrollState {
744   CGFloat zoomScale = NAN;
745   if (scrollState.IsZoomScaleValid()) {
746     if (scrollState.IsZoomScaleLegacyFormat())
747       zoomScale = scrollState.zoom_scale();
748     else
749       zoomScale = scrollState.zoom_scale() / scrollState.minimum_zoom_scale();
750   }
751   return zoomScale;
754 - (void)applyWebViewScrollZoomScaleFromScrollState:
755     (const web::PageScrollState&)scrollState {
756   // A UIWebView's scroll view uses zoom scales in a non-standard way.  The
757   // scroll view's |zoomScale| property is always equal to 1.0, and the
758   // |minimumZoomScale| and |maximumZoomScale| properties are adjusted
759   // proportionally to reflect the relative zoom scale.  Setting the |zoomScale|
760   // property here scales the page by the value set (i.e. setting zoomScale to
761   // 2.0 will update the zoom to twice its initial scale). The maximum-scale or
762   // minimum-scale meta tags of a page may have changed since the state was
763   // recorded, so clamp the zoom scale to the current range if necessary.
764   DCHECK(scrollState.IsZoomScaleValid());
765   CGFloat zoomScale = scrollState.IsZoomScaleLegacyFormat()
766                           ? scrollState.zoom_scale()
767                           : self.webScrollView.minimumZoomScale /
768                                 scrollState.minimum_zoom_scale();
769   if (zoomScale < self.webScrollView.minimumZoomScale)
770     zoomScale = self.webScrollView.minimumZoomScale;
771   if (zoomScale > self.webScrollView.maximumZoomScale)
772     zoomScale = self.webScrollView.maximumZoomScale;
773   self.webScrollView.zoomScale = zoomScale;
776 #pragma mark - JS to ObjC messaging
778 - (void)respondToJSInvoke {
779   // This call is asynchronous. If the web view has been removed, there is
780   // nothing left to do, so just discard the queued messages and return.
781   if (!self.webView) {
782     _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
783     return;
784   }
785   // Messages are queued and processed asynchronously. However, user
786   // may initiate JavaScript at arbitrary times (e.g. through Omnibox
787   // "javascript:alert('foo')"). This delays processing of queued messages
788   // until JavaScript execution is completed.
789   // TODO(pkl): This should have a unit test or UI Automation test case.
790   // See crbug.com/228125
791   if (_inJavaScriptContext) {
792     [self performSelector:@selector(respondToJSInvoke)
793                withObject:nil
794                afterDelay:0];
795     return;
796   }
797   DCHECK(_jsInvokeParameterQueue);
798   while (![_jsInvokeParameterQueue isEmpty]) {
799     CRWJSInvokeParameters* parameters =
800         [_jsInvokeParameterQueue popInvokeParameters];
801     if (!parameters)
802       return;
803     // TODO(stuartmorgan): Some messages (e.g., window.write) should be
804     // processed even if the page has already changed by the time they are
805     // received. crbug.com/228275
806     if ([parameters windowId] != [self windowId]) {
807       // If there is a windowID mismatch, the document has been changed since
808       // messages were added to the queue. Ignore the incoming messages.
809       DLOG(WARNING) << "Messages from JS ignored due to non-matching windowID: "
810                     << [[parameters windowId] UTF8String]
811                     << " != " << [[self windowId] UTF8String];
812       continue;
813     }
814     if (![self respondToMessageQueue:[parameters commandString]
815                    userIsInteracting:[parameters userIsInteracting]
816                            originURL:[parameters originURL]]) {
817       DLOG(WARNING) << "Messages from JS not handled due to invalid format";
818     }
819   }
822 - (void)handleWebInvokeURL:(const GURL&)url request:(NSURLRequest*)request {
823   DCHECK([self urlSchemeIsWebInvoke:url]);
824   NSURL* nsurl = request.URL;
825   // TODO(stuartmorgan): Remove the NSURL usage here. Will require a logic
826   // change since GURL doesn't parse non-standard URLs into host and fragment
827   if (![nsurl.host isEqualToString:[self windowId]]) {
828     // If there is a windowID mismatch, we may be under attack from a
829     // malicious page, so a defense is to reset the page.
830     DLOG(WARNING) << "Messages from JS ignored due to non-matching windowID: "
831                   << nsurl.host << " != " << [[self windowId] UTF8String];
832     DLOG(WARNING) << "Page reset as security precaution";
833     [self performSelector:@selector(reload) withObject:nil afterDelay:0];
834     return;
835   }
836   if (url.spec().length() > kMaxMessageQueueSize) {
837     DLOG(WARNING) << "Messages from JS ignored due to excessive length";
838     return;
839   }
840   NSString* commandString = [[nsurl fragment]
841       stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
843   GURL originURL(net::GURLWithNSURL(request.mainDocumentURL));
845   if (url.SchemeIs("crwebinvokeimmediate")) {
846     [self respondToMessageQueue:commandString
847               userIsInteracting:[self userIsInteracting]
848                       originURL:originURL];
849   } else {
850     [_jsInvokeParameterQueue addCommandString:commandString
851                             userIsInteracting:[self userIsInteracting]
852                                     originURL:originURL
853                                   forWindowId:[super windowId]];
854     if (!_jsMessageQueueThrottled) {
855       [self performSelector:@selector(respondToJSInvoke)
856                  withObject:nil
857                  afterDelay:0];
858     }
859   }
862 - (void)setJsMessageQueueThrottled:(BOOL)throttle {
863   _jsMessageQueueThrottled = throttle;
864   if (!throttle)
865     [self respondToJSInvoke];
868 - (void)removeDocumentLoadCommandsFromQueue {
869   [_jsInvokeParameterQueue removeCommandString:@"document.present"];
870   [_jsInvokeParameterQueue removeCommandString:@"document.loaded"];
873 - (BOOL)urlSchemeIsWebInvoke:(const GURL&)url {
874   return url.SchemeIs("crwebinvoke") || url.SchemeIs("crwebinvokeimmediate");
877 - (CRWJSInvokeParameterQueue*)jsInvokeParameterQueue {
878   return _jsInvokeParameterQueue;
881 - (BOOL)respondToMessageQueue:(NSString*)messageQueue
882             userIsInteracting:(BOOL)userIsInteracting
883                     originURL:(const GURL&)originURL {
884   ScopedCachedCurrentUrl scopedCurrentURL(self);
886   int errorCode = 0;
887   std::string errorMessage;
888   scoped_ptr<base::Value> inputJSONData(base::JSONReader::ReadAndReturnError(
889       base::SysNSStringToUTF8(messageQueue), false, &errorCode, &errorMessage));
890   if (errorCode) {
891     DLOG(WARNING) << "JSON parse error: %s" << errorMessage.c_str();
892     return NO;
893   }
894   // MessageQueues pass messages as a list.
895   base::ListValue* messages = nullptr;
896   if (!inputJSONData->GetAsList(&messages)) {
897     DLOG(WARNING) << "Message queue not a list";
898     return NO;
899   }
900   for (size_t idx = 0; idx != messages->GetSize(); ++idx) {
901     // The same-origin check has to be done for every command to mitigate the
902     // risk of command sequences where the first command would change the page
903     // and the subsequent commands would have unlimited access to it.
904     if (originURL.GetOrigin() != self.currentURL.GetOrigin()) {
905       DLOG(WARNING) << "Message source URL origin: " << originURL.GetOrigin()
906                     << " does not match current URL origin: "
907                     << self.currentURL.GetOrigin();
908       return NO;
909     }
911     base::DictionaryValue* message = nullptr;
912     if (!messages->GetDictionary(idx, &message)) {
913       DLOG(WARNING) << "Message could not be retrieved";
914       return NO;
915     }
916     BOOL messageHandled = [self respondToMessage:message
917                                userIsInteracting:userIsInteracting
918                                        originURL:originURL];
919     if (!messageHandled)
920       return NO;
922     // If handling the message caused this page to be closed, stop processing
923     // messages.
924     // TODO(stuartmorgan): Ideally messages should continue to be handled until
925     // the end of the event loop (e.g., window.close(); window.open(...);
926     // should do both things). That would require knowing which messages came
927     // in the same event loop, however.
928     if ([self isBeingDestroyed])
929       return YES;
930   }
931   return YES;
934 - (SEL)selectorToHandleJavaScriptCommand:(const std::string&)command {
935   static std::map<std::string, SEL>* handlers = nullptr;
936   static dispatch_once_t onceToken;
937   dispatch_once(&onceToken, ^{
938     handlers = new std::map<std::string, SEL>();
939     (*handlers)["anchor.click"] = @selector(handleAnchorClickMessage:context:);
940     (*handlers)["document.loaded"] =
941         @selector(handleDocumentLoadedMessage:context:);
942     (*handlers)["document.present"] =
943         @selector(handleDocumentPresentMessage:context:);
944     (*handlers)["document.retitled"] =
945         @selector(handleDocumentRetitledMessage:context:);
946     (*handlers)["window.close"] = @selector(handleWindowCloseMessage:context:);
947     (*handlers)["window.document.write"] =
948         @selector(handleWindowDocumentWriteMessage:context:);
949     (*handlers)["window.location"] =
950         @selector(handleWindowLocationMessage:context:);
951     (*handlers)["window.open"] = @selector(handleWindowOpenMessage:context:);
952     (*handlers)["window.stop"] = @selector(handleWindowStopMessage:context:);
953     (*handlers)["window.unload"] =
954         @selector(handleWindowUnloadMessage:context:);
955   });
956   DCHECK(handlers);
957   auto iter = handlers->find(command);
958   return iter != handlers->end()
959              ? iter->second
960              : [super selectorToHandleJavaScriptCommand:command];
963 - (NSString*)windowNameFromMessage:(base::DictionaryValue*)message
964                            context:(NSDictionary*)context {
965   std::string target;
966   if(!message->GetString("target", &target)) {
967     DLOG(WARNING) << "JS message parameter not found: target";
968     return nil;
969   }
970   DCHECK(&target);
971   DCHECK(context[web::kOriginURLKey]);
972   const GURL& originURL = net::GURLWithNSURL(context[web::kOriginURLKey]);
974   // Unique string made for page/target combination.
975   // Safe to delimit unique string with # since page references won't
976   // contain #.
977   return base::SysUTF8ToNSString(
978       NormalizedUrl(originURL) + web::kWindowNameSeparator + target);
981 #pragma mark -
982 #pragma mark JavaScript message handlers
984 - (BOOL)handleAnchorClickMessage:(base::DictionaryValue*)message
985                          context:(NSDictionary*)context {
986   // Reset the external click request.
987   [self resetExternalRequest];
989   std::string href;
990   if (!message->GetString("href", &href)) {
991     DLOG(WARNING) << "JS message parameter not found: href";
992     return NO;
993   }
994   const GURL targetURL(href);
995   const GURL currentURL([self currentURL]);
996   if (currentURL != targetURL) {
997     if (web::UrlHasWebScheme(targetURL)) {
998       // The referrer is not known yet, and will be updated later.
999       const web::Referrer emptyReferrer;
1000       [self registerLoadRequest:targetURL
1001                        referrer:emptyReferrer
1002                      transition:ui::PAGE_TRANSITION_LINK];
1003       [self setPageChangeProbability:web::PAGE_CHANGE_PROBABILITY_HIGH];
1004     } else if (web::GetWebClient()->IsAppSpecificURL(targetURL) &&
1005                web::GetWebClient()->IsAppSpecificURL(currentURL)) {
1006       // Allow navigations between app-specific URLs
1007       [self removeWebViewAllowingCachedReconstruction:NO];
1008       ui::PageTransition pageTransitionLink =
1009           ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK);
1010       const web::Referrer referrer(currentURL, web::ReferrerPolicyDefault);
1011       web::WebState::OpenURLParams openParams(targetURL, referrer, CURRENT_TAB,
1012                                               pageTransitionLink, true);
1013       [self.delegate openURLWithParams:openParams];
1014     }
1015   }
1016   return YES;
1019 - (BOOL)handleDocumentLoadedMessage:(base::DictionaryValue*)message
1020                             context:(NSDictionary*)context {
1021   // Very early hashchange events can be missed, hence this extra explicit
1022   // check.
1023   [self checkForUnexpectedURLChange];
1024   [self didFinishNavigation];
1025   return YES;
1028 - (BOOL)handleDocumentPresentMessage:(base::DictionaryValue*)message
1029                              context:(NSDictionary*)context {
1030   NSString* documentCancelled =
1031       [self stringByEvaluatingJavaScriptFromString:@"document._cancelled"];
1032   if (![documentCancelled isEqualToString:@"true"])
1033     [self documentPresent];
1034   return YES;
1037 - (BOOL)handleDocumentRetitledMessage:(base::DictionaryValue*)message
1038                               context:(NSDictionary*)context {
1039   [self titleDidChange];
1040   return YES;
1043 - (BOOL)handleWindowCloseMessage:(base::DictionaryValue*)message
1044                          context:(NSDictionary*)context {
1045   NSString* windowName = [self windowNameFromMessage:message
1046                                              context:context];
1047   if (!windowName)
1048     return NO;
1049   [[self scriptingInterfaceForWindowNamed:windowName] orderClose];
1050   return YES;
1053 - (BOOL)handleWindowDocumentWriteMessage:(base::DictionaryValue*)message
1054                                  context:(NSDictionary*)context {
1055   NSString* windowName = [self windowNameFromMessage:message
1056                                              context:context];
1057   if (!windowName)
1058     return NO;
1059   std::string HTML;
1060   if (!message->GetString("html", &HTML)) {
1061     DLOG(WARNING) << "JS message parameter not found: html";
1062     return NO;
1063   }
1064   [[self scriptingInterfaceForWindowNamed:windowName]
1065       loadHTML:base::SysUTF8ToNSString(HTML)];
1066   return YES;
1069 - (BOOL)handleWindowLocationMessage:(base::DictionaryValue*)message
1070                             context:(NSDictionary*)context {
1071   NSString* windowName = [self windowNameFromMessage:message
1072                                              context:context];
1073   if (!windowName)
1074     return NO;
1075   std::string command;
1076   if (!message->GetString("command", &command)) {
1077     DLOG(WARNING) << "JS message parameter not found: command";
1078     return NO;
1079   }
1080   std::string value;
1081   if (!message->GetString("value", &value)) {
1082     DLOG(WARNING) << "JS message parameter not found: value";
1083     return NO;
1084   }
1085   std::string escapedValue;
1086   base::EscapeJSONString(value, true, &escapedValue);
1087   NSString* HTML =
1088       [NSString stringWithFormat:@"<script>%s = %s;</script>",
1089                                  command.c_str(),
1090                                  escapedValue.c_str()];
1091   [[self scriptingInterfaceForWindowNamed:windowName] loadHTML:HTML];
1092   return YES;
1095 - (BOOL)handleWindowOpenMessage:(base::DictionaryValue*)message
1096                         context:(NSDictionary*)context {
1097   NSString* windowName = [self windowNameFromMessage:message
1098                                              context:context];
1099   if (!windowName)
1100     return NO;
1101   std::string targetURL;
1102   if (!message->GetString("url", &targetURL)) {
1103     DLOG(WARNING) << "JS message parameter not found: url";
1104     return NO;
1105   }
1106   std::string referrerPolicy;
1107   if (!message->GetString("referrerPolicy", &referrerPolicy)) {
1108     DLOG(WARNING) << "JS message parameter not found: referrerPolicy";
1109     return NO;
1110   }
1111   GURL resolvedURL = targetURL.empty() ?
1112       self.defaultURL :
1113       GURL(net::GURLWithNSURL(context[web::kOriginURLKey])).Resolve(targetURL);
1114   DCHECK(&resolvedURL);
1115   web::NewWindowInfo
1116       windowInfo(resolvedURL,
1117                  windowName,
1118                  [self referrerPolicyFromString:referrerPolicy],
1119                  [context[web::kUserIsInteractingKey] boolValue]);
1121   [self openPopupWithInfo:windowInfo];
1122   return YES;
1125 - (BOOL)handleWindowStopMessage:(base::DictionaryValue*)message
1126                         context:(NSDictionary*)context {
1127   NSString* windowName = [self windowNameFromMessage:message
1128                                              context:context];
1129   if (!windowName)
1130     return NO;
1131   [[self scriptingInterfaceForWindowNamed:windowName] stopLoading];
1132   return YES;
1135 - (BOOL)handleWindowUnloadMessage:(base::DictionaryValue*)message
1136                           context:(NSDictionary*)context {
1137   [self setPageChangeProbability:web::PAGE_CHANGE_PROBABILITY_VERY_HIGH];
1138   return YES;
1141 #pragma mark Private methods
1143 - (BOOL)isDefaultPage {
1144   if ([[self stringByEvaluatingJavaScriptFromString:@"document._defaultPage"]
1145        isEqualToString:@"true"]) {
1146     return self.currentURL == self.defaultURL;
1147   }
1148   return NO;
1151 - (BOOL)isLinkNavigation:(UIWebViewNavigationType)navigationType {
1152   switch (navigationType) {
1153     case UIWebViewNavigationTypeLinkClicked:
1154       return YES;
1155     case UIWebViewNavigationTypeOther:
1156       return [self userClickedRecently];
1157     default:
1158       return NO;
1159   }
1162 - (void)setContinuousCheckTimerInterval:(const base::TimeDelta&)interval {
1163   // The timer should never be set when there's no web view.
1164   DCHECK(_uiWebView);
1166   BOOL shouldStartTimer =
1167       !_continuousCheckTimer.get() || _continuousCheckTimer->IsRunning();
1168   base::Closure closure = base::BindBlock(^{
1169       // Only perform JS checks if CRWWebController is not already in JavaScript
1170       // context. This is possible when "javascript:..." is executed from
1171       // Omnibox and this block is run from the timer.
1172       if (!_inJavaScriptContext)
1173         [self.recurringTaskDelegate runRecurringTask];
1174   });
1175   _continuousCheckTimer.reset(
1176       new base::Timer(FROM_HERE, interval, closure, true));
1177   if (shouldStartTimer)
1178     _continuousCheckTimer->Reset();
1179   if (_lowerFrequencyTimer &&
1180       interval == base::TimeDelta::FromMilliseconds(
1181           web::kContinuousCheckIntervalMSLow)) {
1182     _lowerFrequencyTimer.reset();
1183   }
1186 - (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script {
1187   if (!_uiWebView)
1188     return nil;
1190   ScopedReentryGuard reentryGuard(&_inJavaScriptContext);
1191   return [_uiWebView stringByEvaluatingJavaScriptFromString:script];
1194 - (NSString*)stringByEvaluatingUserJavaScriptFromString:(NSString*)script {
1195   [self setUserInteractionRegistered:YES];
1196   return [self stringByEvaluatingJavaScriptFromString:script];
1199 - (UIWebView*)createWebView {
1200   UIWebView* webView = web::CreateWebView(
1201       CGRectZero,
1202       self.webStateImpl->GetRequestGroupID(),
1203       [self useDesktopUserAgent]);
1205   // Mark the document object of the default page as such, so that it is not
1206   // mistaken for a 'real' page by change detection mechanisms.
1207   [webView stringByEvaluatingJavaScriptFromString:
1208       @"document._defaultPage = true;"];
1210   [webView setScalesPageToFit:YES];
1211   // Turn off data-detectors. MobileSafari does the same thing.
1212   [webView setDataDetectorTypes:UIDataDetectorTypeNone];
1214   return [webView autorelease];
1217 - (void)setWebView:(UIWebView*)webView {
1218   DCHECK_NE(_uiWebView.get(), webView);
1219   // Per documentation, must clear the delegate before releasing the UIWebView
1220   // to avoid errant dangling pointers.
1221   [_uiWebView setDelegate:nil];
1222   _uiWebView.reset([webView retain]);
1223   [_uiWebView setDelegate:self];
1224   // Clear out the trusted URL cache.
1225   _lastCorrectURLTime = base::TimeTicks();
1226   _cachedURL.first = GURL();
1227   // Reset the spoofable state (see declaration comment).
1228   // TODO(stuartmorgan): Fix the fact that there's no guarantee that no
1229   // navigation has happened before the UIWebView is set here (ideally by
1230   // unifying the creation and setting flow).
1231   _spoofableRequest = YES;
1232   _inJavaScriptContext = NO;
1234   if (webView) {
1235     // Do initial injection even before loading another page, since the window
1236     // object is re-used.
1237     [self injectEarlyInjectionScripts];
1238   } else {
1239     _continuousCheckTimer.reset();
1240     // This timer exists only to change the frequency of the main timer, so it
1241     // should not outlive the main timer.
1242     _lowerFrequencyTimer.reset();
1243   }
1246 - (void)generateMissingDocumentLifecycleEvents {
1247   // The webView can be removed between this method being queued and invoked.
1248   if (!self.webView)
1249     return;
1250   if ([self webViewDocumentType] == web::WEB_VIEW_DOCUMENT_TYPE_GENERIC) {
1251     [self documentPresent];
1252     [self didFinishNavigation];
1253   }
1256 - (void)generateMissingLoadRequestWithURL:(const GURL&)currentURL
1257                                  referrer:(const web::Referrer&)referrer {
1258   [self loadCancelled];
1259   // Initialize transition based on whether the request is user-initiated or
1260   // not. This is a best guess to replace lost transition type informationj.
1261   ui::PageTransition transition = self.userInteractionRegistered
1262                                       ? ui::PAGE_TRANSITION_LINK
1263                                       : ui::PAGE_TRANSITION_CLIENT_REDIRECT;
1264   // If the URL agrees with session state, use the session's transition.
1265   if (currentURL == [self currentNavigationURL]) {
1266     transition = [self currentTransition];
1267   }
1269   [self registerLoadRequest:currentURL referrer:referrer transition:transition];
1272 - (id<CRWWebControllerScripting>)scriptingInterfaceForWindowNamed:
1273     (NSString*)name {
1274   if (![self.delegate respondsToSelector:
1275         @selector(webController:scriptingInterfaceForWindowNamed:)]) {
1276     return nil;
1277   }
1278   return [self.delegate webController:self
1279      scriptingInterfaceForWindowNamed:name];
1282 #pragma mark FullscreenVideo
1284 - (void)moviePlayerDidEnterFullscreen:(NSNotification*)notification {
1285   _inFullscreenVideo = YES;
1288 - (void)moviePlayerDidExitFullscreen:(NSNotification*)notification {
1289   _inFullscreenVideo = NO;
1292 - (void)exitFullscreenVideo {
1293   [self stringByEvaluatingJavaScriptFromString:
1294       @"__gCrWeb.exitFullscreenVideo();"];
1297 #pragma mark -
1298 #pragma mark CRWRecurringTaskDelegate
1300 // Checks for page changes are made continuously.
1301 - (void)runRecurringTask {
1302   if (!self.webView)
1303     return;
1305   [self injectEarlyInjectionScripts];
1306   [self checkForUnexpectedURLChange];
1309 #pragma mark -
1310 #pragma mark UIWebViewDelegate Methods
1312 // Called when a load begins, and for subsequent subpages.
1313 - (BOOL)webView:(UIWebView*)webView
1314     shouldStartLoadWithRequest:(NSURLRequest*)request
1315                 navigationType:(UIWebViewNavigationType)navigationType {
1316   DVLOG(5) << "webViewShouldStartLoadWithRequest "
1317            << net::FormatUrlRequestForLogging(request);
1319   if (self.isBeingDestroyed)
1320     return NO;
1322   GURL url = net::GURLWithNSURL(request.URL);
1324   // The crwebnull protocol is used where an element requires a URL but it
1325   // should not trigger any activity on the WebView.
1326   if (url.SchemeIs("crwebnull"))
1327     return NO;
1329   if ([self urlSchemeIsWebInvoke:url]) {
1330     [self handleWebInvokeURL:url request:request];
1331     return NO;
1332   }
1334   // ##### IMPORTANT NOTE #####
1335   // Do not add new code above this line unless you're certain about what you're
1336   // doing with respect to JS re-entry.
1337   ScopedReentryGuard javaScriptReentryGuard(&_inJavaScriptContext);
1339   BOOL isLinkClick = [self isLinkNavigation:navigationType];
1340   return [self shouldAllowLoadWithRequest:request isLinkClick:isLinkClick];
1343 // Called at multiple points during a load, such as at the start of loading a
1344 // page, and every time an iframe loads. Not called again for server-side
1345 // redirects.
1346 - (void)webViewDidStartLoad:(UIWebView *)webView {
1347   NSURLRequest* request = webView.request;
1348   DVLOG(5) << "webViewDidStartLoad "
1349            << net::FormatUrlRequestForLogging(request);
1350   // |webView:shouldStartLoad| may not be called or called with different URL
1351   // and mainDocURL for the request in certain page navigations. There
1352   // are at least 2 known page navigations where this occurs, in these cases it
1353   // is imperative the URL verification timer is started here.
1354   // The 2 known cases are:
1355   // 1) A malicious page suppressing core.js injection and calling
1356   //    window.history.back() or window.history.forward()
1357   // 2) An iframe loading a URL using target=_blank.
1358   // TODO(shreyasv): crbug.com/349155. Understand further why this happens
1359   // in some case and not in others.
1360   if (webView != self.webView) {
1361     // This happens sometimes as tests are brought down.
1362     // TODO(jimblackler): work out why and fix the problem at source.
1363     LOG(WARNING) << " UIWebViewDelegate message received for inactive WebView.";
1364     return;
1365   }
1366   DCHECK(!self.isBeingDestroyed);
1367   // Increment the number of pending loads. This will be balanced by either
1368   // a |-webViewDidFinishLoad:| or |-webView:didFailLoadWithError:|.
1369   ++_loadCount;
1370   [self.recurringTaskDelegate runRecurringTask];
1373 // Called when the page (or one of its subframes) finishes loading. This is
1374 // called multiple times during a page load, with varying frequency depending
1375 // on the action (going back, loading a page with frames, redirecting).
1376 // See http://goto/efrmm for a summary of why this is so painful.
1377 - (void)webViewDidFinishLoad:(UIWebView*)webView {
1378   DVLOG(5) << "webViewDidFinishLoad "
1379            << net::FormatUrlRequestForLogging(webView.request);
1380   DCHECK(!self.isHalted);
1381   // Occasionally this delegate is invoked as a side effect during core.js
1382   // injection. It is necessary to ensure we do not attempt to start the
1383   // injection process a second time.
1384   if (!_inJavaScriptContext)
1385     [self.recurringTaskDelegate runRecurringTask];
1387   [self performSelector:@selector(generateMissingDocumentLifecycleEvents)
1388              withObject:nil
1389              afterDelay:0];
1391   ++_unloadCount;
1392   if ((_loadCount == _unloadCount) && (self.loadPhase != web::LOAD_REQUESTED))
1393     [self checkDocumentLoaded];
1396 // Called when there is an error loading the page. Some errors aren't actual
1397 // errors, but are caused by user actions such as stopping a page load
1398 // prematurely.
1399 - (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
1400   DVLOG(5) << "webViewDidFailLoadWithError "
1401            << net::FormatUrlRequestForLogging(webView.request);
1402   ++_unloadCount;
1404   // Under unknown circumstances navigation item can be null. In that case the
1405   // state of web/ will not be valid and app will crash. Early return avoid a
1406   // crash (crbug.com/411912).
1407   if (!self.webStateImpl ||
1408       !self.webStateImpl->GetNavigationManagerImpl().GetVisibleItem()) {
1409     return;
1410   }
1412   // There's no reliable way to know if a load is for the main frame, so make a
1413   // best-effort guess.
1414   // |_loadCount| is reset to 0 before starting loading a new page, and is
1415   // incremented in each call to |-webViewDidStartLoad:|. The main request
1416   // is the first one to be loaded, and thus has a |_loadCount| of 1.
1417   // Sub-requests have a |_loadCount| > 1.
1418   // An iframe loading after the main page also has a |_loadCount| of 1, as
1419   // |_loadCount| is reset at the end of the main page load. In that case,
1420   // |loadPhase_| is web::PAGE_LOADED (as opposed to web::PAGE_LOADING for a
1421   // main request).
1422   const bool isMainFrame = (_loadCount == 1 &&
1423                             self.loadPhase != web::PAGE_LOADED);
1424   [self handleLoadError:error inMainFrame:isMainFrame];
1427 #pragma mark -
1428 #pragma mark Testing methods
1430 -(id<CRWRecurringTaskDelegate>)recurringTaskDelegate {
1431   return _recurringTaskDelegate;
1434 @end