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"
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;
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
66 // TODO(stuartmorgan): Change this to a struct so code using it is more
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
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
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:
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
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
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;
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 {
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;
256 ~ScopedReentryGuard() {
257 DCHECK(*is_inside_javascript_context_);
258 *is_inside_javascript_context_ = NO;
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 {
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];
279 ~ScopedCachedCurrentUrl() {
280 if (!had_cached_current_url_)
281 [web_controller_ setURLCachingEnabled:NO];
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;
310 @implementation CRWUIWebViewWebController
312 - (instancetype)initWithWebState:(scoped_ptr<web::WebStateImpl>)webState {
313 self = [super initWithWebState:webState.Pass()];
315 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
317 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
318 [defaultCenter addObserver:self
319 selector:@selector(moviePlayerDidEnterFullscreen:)
321 @"UIMoviePlayerControllerDidEnterFullscreenNotification"
323 [defaultCenter addObserver:self
324 selector:@selector(moviePlayerDidExitFullscreen:)
326 @"UIMoviePlayerControllerDidExitFullscreenNotification"
328 _recurringTaskDelegate = self;
334 [[NSNotificationCenter defaultCenter] removeObserver:self];
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:)
363 #pragma mark Overridden public methods
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();
377 // Turn the timer off, to cut down on work being done by background tabs.
378 _continuousCheckTimer->Stop();
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];
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]];
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
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)
445 _urlCachingEnabled = enabled;
446 _cachedURL.first = GURL();
449 - (BOOL)ignoreURLVerificationFailures {
450 return _spoofableRequest;
454 return [self stringByEvaluatingJavaScriptFromString:
455 @"document.title.length ? document.title : ''"];
458 #pragma mark Protected method implementations
460 - (void)ensureWebViewCreated {
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];
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;
479 ScopedReentryGuard reentryGuard(&_inJavaScriptContext);
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;
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;
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.
522 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
524 NSString* documentType =
525 [_uiWebView stringByEvaluatingJavaScriptFromString:
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];
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];
560 - (void)willLoadCurrentURLInWebView {
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:)
569 const bool wrongRequestGroupID =
570 ![self.webStateImpl->GetRequestGroupID()
571 isEqualToString:web::ExtractRequestGroupIDFromUserAgent(userAgent)];
572 DLOG_IF(ERROR, wrongRequestGroupID) << "Incorrect user agent in UIWebView";
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)];
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)];
595 _lowerFrequencyTimer.reset(
596 new base::Timer(FROM_HERE,
597 base::TimeDelta::FromMilliseconds(
598 web::kContinuousCheckHighFrequencyMSMaxDuration),
599 closure, false /* not repeating */));
600 _lowerFrequencyTimer->Reset();
605 - (BOOL)checkForUnexpectedURLChange {
606 // The check makes no sense without an active web view.
610 // Change to UIWebView default page is not considered a 'real' change and
611 // URL changes are not reported.
612 if ([self isDefaultPage])
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) {
623 // If the URL has changed, handle page load mechanics.
624 ScopedCachedCurrentUrl scopedCurrentURL(self);
625 [self webPageChanged];
626 [self checkDocumentLoaded];
627 [self titleDidChange];
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 {
646 - (void)setSuppressDialogsWithHelperScript:(NSString*)script {
647 [self stringByEvaluatingJavaScriptFromString:script];
650 - (void)checkDocumentLoaded {
652 [self stringByEvaluatingJavaScriptFromString:
653 @"document.readyState === 'loaded' || "
654 "document.readyState === 'complete'"];
655 if ([loaded isEqualToString:@"true"]) {
656 [self didFinishNavigation];
660 - (NSString*)currentReferrerString {
661 return [self stringByEvaluatingJavaScriptFromString:@"document.referrer"];
664 - (void)titleDidChange {
665 if (![self.delegate respondsToSelector:
666 @selector(webController:titleDidChange:)]) {
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
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])
681 // The title can be retrieved from the document only if the URL can be
683 web::URLVerificationTrustLevel trustLevel =
684 web::URLVerificationTrustLevel::kNone;
685 [self currentURLWithTrustLevel:&trustLevel];
686 if (trustLevel != web::URLVerificationTrustLevel::kAbsolute)
689 NSString* title = self.title;
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 {
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) {
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]];
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();
749 zoomScale = scrollState.zoom_scale() / scrollState.minimum_zoom_scale();
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.
782 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
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)
797 DCHECK(_jsInvokeParameterQueue);
798 while (![_jsInvokeParameterQueue isEmpty]) {
799 CRWJSInvokeParameters* parameters =
800 [_jsInvokeParameterQueue popInvokeParameters];
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];
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";
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];
836 if (url.spec().length() > kMaxMessageQueueSize) {
837 DLOG(WARNING) << "Messages from JS ignored due to excessive length";
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];
850 [_jsInvokeParameterQueue addCommandString:commandString
851 userIsInteracting:[self userIsInteracting]
853 forWindowId:[super windowId]];
854 if (!_jsMessageQueueThrottled) {
855 [self performSelector:@selector(respondToJSInvoke)
862 - (void)setJsMessageQueueThrottled:(BOOL)throttle {
863 _jsMessageQueueThrottled = 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);
887 std::string errorMessage;
888 scoped_ptr<base::Value> inputJSONData(base::JSONReader::ReadAndReturnError(
889 base::SysNSStringToUTF8(messageQueue), false, &errorCode, &errorMessage));
891 DLOG(WARNING) << "JSON parse error: %s" << errorMessage.c_str();
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";
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();
911 base::DictionaryValue* message = nullptr;
912 if (!messages->GetDictionary(idx, &message)) {
913 DLOG(WARNING) << "Message could not be retrieved";
916 BOOL messageHandled = [self respondToMessage:message
917 userIsInteracting:userIsInteracting
918 originURL:originURL];
922 // If handling the message caused this page to be closed, stop processing
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])
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:);
957 auto iter = handlers->find(command);
958 return iter != handlers->end()
960 : [super selectorToHandleJavaScriptCommand:command];
963 - (NSString*)windowNameFromMessage:(base::DictionaryValue*)message
964 context:(NSDictionary*)context {
966 if(!message->GetString("target", &target)) {
967 DLOG(WARNING) << "JS message parameter not found: 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
977 return base::SysUTF8ToNSString(
978 NormalizedUrl(originURL) + web::kWindowNameSeparator + target);
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];
990 if (!message->GetString("href", &href)) {
991 DLOG(WARNING) << "JS message parameter not found: href";
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];
1019 - (BOOL)handleDocumentLoadedMessage:(base::DictionaryValue*)message
1020 context:(NSDictionary*)context {
1021 // Very early hashchange events can be missed, hence this extra explicit
1023 [self checkForUnexpectedURLChange];
1024 [self didFinishNavigation];
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];
1037 - (BOOL)handleDocumentRetitledMessage:(base::DictionaryValue*)message
1038 context:(NSDictionary*)context {
1039 [self titleDidChange];
1043 - (BOOL)handleWindowCloseMessage:(base::DictionaryValue*)message
1044 context:(NSDictionary*)context {
1045 NSString* windowName = [self windowNameFromMessage:message
1049 [[self scriptingInterfaceForWindowNamed:windowName] orderClose];
1053 - (BOOL)handleWindowDocumentWriteMessage:(base::DictionaryValue*)message
1054 context:(NSDictionary*)context {
1055 NSString* windowName = [self windowNameFromMessage:message
1060 if (!message->GetString("html", &HTML)) {
1061 DLOG(WARNING) << "JS message parameter not found: html";
1064 [[self scriptingInterfaceForWindowNamed:windowName]
1065 loadHTML:base::SysUTF8ToNSString(HTML)];
1069 - (BOOL)handleWindowLocationMessage:(base::DictionaryValue*)message
1070 context:(NSDictionary*)context {
1071 NSString* windowName = [self windowNameFromMessage:message
1075 std::string command;
1076 if (!message->GetString("command", &command)) {
1077 DLOG(WARNING) << "JS message parameter not found: command";
1081 if (!message->GetString("value", &value)) {
1082 DLOG(WARNING) << "JS message parameter not found: value";
1085 std::string escapedValue;
1086 base::EscapeJSONString(value, true, &escapedValue);
1088 [NSString stringWithFormat:@"<script>%s = %s;</script>",
1090 escapedValue.c_str()];
1091 [[self scriptingInterfaceForWindowNamed:windowName] loadHTML:HTML];
1095 - (BOOL)handleWindowOpenMessage:(base::DictionaryValue*)message
1096 context:(NSDictionary*)context {
1097 NSString* windowName = [self windowNameFromMessage:message
1101 std::string targetURL;
1102 if (!message->GetString("url", &targetURL)) {
1103 DLOG(WARNING) << "JS message parameter not found: url";
1106 std::string referrerPolicy;
1107 if (!message->GetString("referrerPolicy", &referrerPolicy)) {
1108 DLOG(WARNING) << "JS message parameter not found: referrerPolicy";
1111 GURL resolvedURL = targetURL.empty() ?
1113 GURL(net::GURLWithNSURL(context[web::kOriginURLKey])).Resolve(targetURL);
1114 DCHECK(&resolvedURL);
1116 windowInfo(resolvedURL,
1118 [self referrerPolicyFromString:referrerPolicy],
1119 [context[web::kUserIsInteractingKey] boolValue]);
1121 [self openPopupWithInfo:windowInfo];
1125 - (BOOL)handleWindowStopMessage:(base::DictionaryValue*)message
1126 context:(NSDictionary*)context {
1127 NSString* windowName = [self windowNameFromMessage:message
1131 [[self scriptingInterfaceForWindowNamed:windowName] stopLoading];
1135 - (BOOL)handleWindowUnloadMessage:(base::DictionaryValue*)message
1136 context:(NSDictionary*)context {
1137 [self setPageChangeProbability:web::PAGE_CHANGE_PROBABILITY_VERY_HIGH];
1141 #pragma mark Private methods
1143 - (BOOL)isDefaultPage {
1144 if ([[self stringByEvaluatingJavaScriptFromString:@"document._defaultPage"]
1145 isEqualToString:@"true"]) {
1146 return self.currentURL == self.defaultURL;
1151 - (BOOL)isLinkNavigation:(UIWebViewNavigationType)navigationType {
1152 switch (navigationType) {
1153 case UIWebViewNavigationTypeLinkClicked:
1155 case UIWebViewNavigationTypeOther:
1156 return [self userClickedRecently];
1162 - (void)setContinuousCheckTimerInterval:(const base::TimeDelta&)interval {
1163 // The timer should never be set when there's no web view.
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];
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();
1186 - (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script {
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(
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;
1235 // Do initial injection even before loading another page, since the window
1236 // object is re-used.
1237 [self injectEarlyInjectionScripts];
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();
1246 - (void)generateMissingDocumentLifecycleEvents {
1247 // The webView can be removed between this method being queued and invoked.
1250 if ([self webViewDocumentType] == web::WEB_VIEW_DOCUMENT_TYPE_GENERIC) {
1251 [self documentPresent];
1252 [self didFinishNavigation];
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];
1269 [self registerLoadRequest:currentURL referrer:referrer transition:transition];
1272 - (id<CRWWebControllerScripting>)scriptingInterfaceForWindowNamed:
1274 if (![self.delegate respondsToSelector:
1275 @selector(webController:scriptingInterfaceForWindowNamed:)]) {
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();"];
1298 #pragma mark CRWRecurringTaskDelegate
1300 // Checks for page changes are made continuously.
1301 - (void)runRecurringTask {
1305 [self injectEarlyInjectionScripts];
1306 [self checkForUnexpectedURLChange];
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)
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"))
1329 if ([self urlSchemeIsWebInvoke:url]) {
1330 [self handleWebInvokeURL:url request:request];
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
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.";
1366 DCHECK(!self.isBeingDestroyed);
1367 // Increment the number of pending loads. This will be balanced by either
1368 // a |-webViewDidFinishLoad:| or |-webView:didFailLoadWithError:|.
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)
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
1399 - (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
1400 DVLOG(5) << "webViewDidFailLoadWithError "
1401 << net::FormatUrlRequestForLogging(webView.request);
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()) {
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
1422 const bool isMainFrame = (_loadCount == 1 &&
1423 self.loadPhase != web::PAGE_LOADED);
1424 [self handleLoadError:error inMainFrame:isMainFrame];
1428 #pragma mark Testing methods
1430 -(id<CRWRecurringTaskDelegate>)recurringTaskDelegate {
1431 return _recurringTaskDelegate;