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/ns_error_util.h"
8 #import "base/ios/weak_nsobject.h"
9 #include "base/json/json_reader.h"
10 #include "base/json/string_escape.h"
11 #include "base/mac/bind_objc_block.h"
12 #import "base/mac/scoped_nsobject.h"
13 #include "base/memory/scoped_ptr.h"
14 #include "base/metrics/field_trial.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 #include "ios/web/net/clients/crw_redirect_network_client_factory.h"
23 #import "ios/web/net/crw_url_verifying_protocol_handler.h"
24 #include "ios/web/net/request_group_util.h"
25 #include "ios/web/public/url_scheme_util.h"
26 #include "ios/web/public/web_client.h"
27 #import "ios/web/public/web_state/ui/crw_web_view_content_view.h"
28 #import "ios/web/ui_web_view_util.h"
29 #include "ios/web/web_state/frame_info.h"
30 #import "ios/web/web_state/js/crw_js_invoke_parameter_queue.h"
31 #import "ios/web/web_state/ui/crw_context_menu_provider.h"
32 #import "ios/web/web_state/ui/crw_web_controller+protected.h"
33 #import "ios/web/web_state/ui/crw_web_controller.h"
34 #import "ios/web/web_state/ui/web_view_js_utils.h"
35 #import "ios/web/web_state/web_state_impl.h"
36 #import "ios/web/web_state/web_view_internal_creation_util.h"
37 #import "net/base/mac/url_conversions.h"
38 #include "net/base/net_errors.h"
39 #include "url/url_constants.h"
43 // The following continuous check timer frequency constants are externally
44 // available for the purpose of performance tests.
45 // Frequency for the continuous checks when a reset in the page object is
46 // anticipated shortly. In milliseconds.
47 const int64 kContinuousCheckIntervalMSHigh = 100;
49 // The maximum duration that the CRWWebController can run in high-frequency
50 // check mode before being changed back to the low frequency.
51 const int64 kContinuousCheckHighFrequencyMSMaxDuration = 5000;
53 // Frequency for the continuous checks when a reset in the page object is not
54 // anticipated; checks are only made as a precaution.
55 // The URL could be out of date for this many milliseconds, so this should not
56 // be increased without careful consideration.
57 const int64 kContinuousCheckIntervalMSLow = 3000;
61 @interface CRWUIWebViewWebController () <CRWRedirectClientDelegate,
63 // The UIWebView managed by this instance.
64 base::scoped_nsobject<UIWebView> _uiWebView;
66 // Whether caching of the current URL is enabled or not.
67 BOOL _urlCachingEnabled;
69 // Temporarily cached current URL. Only valid/set while urlCachingEnabled
71 // TODO(stuartmorgan): Change this to a struct so code using it is more
73 std::pair<GURL, web::URLVerificationTrustLevel> _cachedURL;
75 // The last time a URL with absolute trust level was computed.
76 // When an untrusted URL is retrieved from the
77 // |CRWURLVerifyingProtocolHandler|, if the last trusted URL is within
78 // |kContinuousCheckIntervalMSLow|, the trustLevel is upgraded to Mixed.
79 // The reason is that it is sometimes temporarily impossible to do a
80 // AsyncXMLHttpRequest on a web page. When this happen, it is not possible
81 // to check for the validity of the current URL. Because the checker is
82 // only checking every |kContinuousCheckIntervalMSLow| anyway, waiting this
83 // amount of time before triggering an interstitial does not weaken the
84 // security of the browser.
85 base::TimeTicks _lastCorrectURLTime;
87 // Each new UIWebView starts in a state where:
88 // - window.location.href is equal to about:blank
89 // - Ajax requests seem to be impossible
90 // Because Ajax requests are used to determine is a URL is verified, this
91 // means it is impossible to do this check until the UIWebView is in a more
92 // sane state. This variable tracks whether verifying the URL is currently
93 // impossible. It starts at YES when a new UIWebView is created,
94 // and will change to NO, as soon as either window.location.href is not
95 // about:blank anymore, or an URL verification request succeeds. This means
96 // that a malicious site that is able to change the value of
97 // window.location.href and that is loaded as the first request will be able
98 // to change its URL to about:blank. As this is not an interesting URL, it is
99 // considered acceptable.
100 BOOL _spoofableRequest;
102 // Timer used to make continuous checks on the UIWebView. Timer is
103 // running only while |webView| is non-nil.
104 scoped_ptr<base::Timer> _continuousCheckTimer;
105 // Timer to lower the check frequency automatically.
106 scoped_ptr<base::Timer> _lowerFrequencyTimer;
108 // Counts of calls to |-webViewDidStartLoad:| and |-webViewDidFinishLoad|.
109 // When |_loadCount| is equal to |_unloadCount|, the page is no longer
110 // loading. Used as a fallback to determine when the page is done loading in
111 // case document.readyState isn't sufficient.
112 // When |_loadCount| is 1, the main page is loading (as opposed to a
117 // Backs the property of the same name.
118 BOOL _inJavaScriptContext;
120 // Backs the property of the same name.
121 base::scoped_nsobject<CRWJSInvokeParameterQueue> _jsInvokeParameterQueue;
123 // Blocks message queue processing (for testing).
124 BOOL _jsMessageQueueThrottled;
126 // YES if a video is playing in fullscreen.
127 BOOL _inFullscreenVideo;
129 // Backs the property of the same name.
130 id<CRWRecurringTaskDelegate>_recurringTaskDelegate;
132 // Redirect client factory.
133 base::scoped_nsobject<CRWRedirectNetworkClientFactory>
134 redirect_client_factory_;
137 // Whether or not URL caching is enabled. Between enabling and disabling
138 // caching, calls to webURLWithTrustLevel: after the first may return the same
139 // answer without re-checking the URL.
140 @property(nonatomic, setter=setURLCachingEnabled:) BOOL urlCachingEnabled;
142 // Returns whether the current page is the web views initial (default) page.
143 - (BOOL)isDefaultPage;
145 // Returns whether the given navigation is triggered by a user link click.
146 - (BOOL)isLinkNavigation:(UIWebViewNavigationType)navigationType;
148 // Starts (at the given interval) the timer that drives runRecurringTasks to see
149 // whether or not the page has changed. This is used to work around the fact
150 // that UIWebView callbacks are not sufficiently reliable to catch every page
152 - (void)setContinuousCheckTimerInterval:(const base::TimeDelta&)interval;
154 // Evaluates the supplied JavaScript and returns the result. Will return nil
155 // if it is unable to evaluate the JavaScript.
156 - (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script;
158 // Evaluates the user-entered JavaScript in the WebView and returns the result.
159 // Will return nil if the web view is currently not available.
160 - (NSString*)stringByEvaluatingUserJavaScriptFromString:(NSString*)script;
162 // Checks if the document is loaded, and if so triggers any necessary logic.
163 - (void)checkDocumentLoaded;
165 // Returns a new autoreleased UIWebView.
166 - (UIWebView*)createWebView;
168 // Sets value to web view property.
169 - (void)setWebView:(UIWebView*)webView;
171 // Simulates the events generated by core.js during document loading process.
172 // Used for non-HTML documents (e.g. PDF) that do not support data flow from
173 // JavaScript to obj-c via iframe injection.
174 - (void)generateMissingDocumentLifecycleEvents;
176 // Makes a best-effort attempt to retroactively construct a load request for an
177 // observed-but-unexpected navigation. Should be called any time a page
178 // change is detected as having happened without the current internal state
179 // indicating it was expected.
180 - (void)generateMissingLoadRequestWithURL:(const GURL&)currentURL
181 referrer:(const web::Referrer&)referrer;
183 // Returns a child scripting CRWWebController with the given window name.
184 - (id<CRWWebControllerScripting>)scriptingInterfaceForWindowNamed:
187 // Called when UIMoviePlayerControllerDidEnterFullscreenNotification is posted.
188 - (void)moviePlayerDidEnterFullscreen:(NSNotification*)notification;
190 // Called when UIMoviePlayerControllerDidExitFullscreenNotification is posted.
191 - (void)moviePlayerDidExitFullscreen:(NSNotification*)notification;
193 // Exits fullscreen mode for any playing videos.
194 - (void)exitFullscreenVideo;
196 // Handles presentation of the web document. Checks and handles URL changes,
197 // page refreshes, and title changes.
198 - (void)documentPresent;
200 // Handles queued JS to ObjC messages.
201 // All commands are passed via JSON strings, including parameters.
202 - (void)respondToJSInvoke;
204 // Pauses (|throttle|=YES) or resumes (|throttle|=NO) crwebinvoke message
206 - (void)setJsMessageQueueThrottled:(BOOL)throttle;
208 // Removes document load commands from the queue. Otherwise they could be
209 // handled after a new page load has begun, which would cause an unwanted
211 - (void)removeDocumentLoadCommandsFromQueue;
213 // Returns YES if the given URL has a scheme associated with JS->native calls.
214 - (BOOL)urlSchemeIsWebInvoke:(const GURL&)url;
216 // Returns window name given a message and its context.
217 - (NSString*)windowNameFromMessage:(base::DictionaryValue*)message
218 context:(NSDictionary*)context;
220 // Handles 'anchor.click' message.
221 - (BOOL)handleAnchorClickMessage:(base::DictionaryValue*)message
222 context:(NSDictionary*)context;
223 // Handles 'document.loaded' message.
224 - (BOOL)handleDocumentLoadedMessage:(base::DictionaryValue*)message
225 context:(NSDictionary*)context;
226 // Handles 'document.present' message.
227 - (BOOL)handleDocumentPresentMessage:(base::DictionaryValue*)message
228 context:(NSDictionary*)context;
229 // Handles 'document.retitled' message.
230 - (BOOL)handleDocumentRetitledMessage:(base::DictionaryValue*)message
231 context:(NSDictionary*)context;
232 // Handles 'window.close' message.
233 - (BOOL)handleWindowCloseMessage:(base::DictionaryValue*)message
234 context:(NSDictionary*)context;
235 // Handles 'window.document.write' message.
236 - (BOOL)handleWindowDocumentWriteMessage:(base::DictionaryValue*)message
237 context:(NSDictionary*)context;
238 // Handles 'window.location' message.
239 - (BOOL)handleWindowLocationMessage:(base::DictionaryValue*)message
240 context:(NSDictionary*)context;
241 // Handles 'window.open' message.
242 - (BOOL)handleWindowOpenMessage:(base::DictionaryValue*)message
243 context:(NSDictionary*)context;
244 // Handles 'window.stop' message.
245 - (BOOL)handleWindowStopMessage:(base::DictionaryValue*)message
246 context:(NSDictionary*)context;
247 // Handles 'window.unload' message.
248 - (BOOL)handleWindowUnloadMessage:(base::DictionaryValue*)message
249 context:(NSDictionary*)context;
254 // Utility to help catch unwanted JavaScript re-entries. An instance should
255 // be created on the stack any time JS will be executed.
256 // It uses an instance variable (passed in as a pointer to boolean) that needs
257 // to be initialized to false.
258 class ScopedReentryGuard {
260 explicit ScopedReentryGuard(BOOL* is_inside_javascript_context)
261 : is_inside_javascript_context_(is_inside_javascript_context) {
262 DCHECK(!*is_inside_javascript_context_);
263 *is_inside_javascript_context_ = YES;
265 ~ScopedReentryGuard() {
266 DCHECK(*is_inside_javascript_context_);
267 *is_inside_javascript_context_ = NO;
271 BOOL* is_inside_javascript_context_;
272 DISALLOW_IMPLICIT_CONSTRUCTORS(ScopedReentryGuard);
275 // Class allowing to selectively enable caching of |currentURL| on a
276 // CRWUIWebViewWebController. As long as an instance of this class lives,
277 // the CRWUIWebViewWebController passed as parameter will cache the result for
278 // |currentURL| calls.
279 class ScopedCachedCurrentUrl {
281 explicit ScopedCachedCurrentUrl(CRWUIWebViewWebController* web_controller)
282 : web_controller_(web_controller),
283 had_cached_current_url_([web_controller urlCachingEnabled]) {
284 if (!had_cached_current_url_)
285 [web_controller_ setURLCachingEnabled:YES];
288 ~ScopedCachedCurrentUrl() {
289 if (!had_cached_current_url_)
290 [web_controller_ setURLCachingEnabled:NO];
294 CRWUIWebViewWebController* web_controller_;
295 bool had_cached_current_url_;
296 DISALLOW_IMPLICIT_CONSTRUCTORS(ScopedCachedCurrentUrl);
299 // Normalizes the URL for the purposes of identifying the origin page (remove
300 // any parameters, fragments, etc.) and return an absolute string of the URL.
301 std::string NormalizedUrl(const GURL& url) {
302 GURL::Replacements replacements;
303 replacements.ClearQuery();
304 replacements.ClearRef();
305 replacements.ClearUsername();
306 replacements.ClearPassword();
307 GURL page_url(url.ReplaceComponents(replacements));
309 return page_url.spec();
312 // The maximum size of JSON message passed from JavaScript to ObjC.
313 // 256kB is an arbitrary number that was chosen to be a magnitude larger than
314 // any legitimate message.
315 const size_t kMaxMessageQueueSize = 262144;
319 @implementation CRWUIWebViewWebController
321 - (instancetype)initWithWebState:(scoped_ptr<web::WebStateImpl>)webState {
322 self = [super initWithWebState:webState.Pass()];
324 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
326 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
327 [defaultCenter addObserver:self
328 selector:@selector(moviePlayerDidEnterFullscreen:)
330 @"UIMoviePlayerControllerDidEnterFullscreenNotification"
332 [defaultCenter addObserver:self
333 selector:@selector(moviePlayerDidExitFullscreen:)
335 @"UIMoviePlayerControllerDidExitFullscreenNotification"
337 _recurringTaskDelegate = self;
339 // UIWebViews require a redirect network client in order to accurately
340 // detect server redirects.
341 redirect_client_factory_.reset(
342 [[CRWRedirectNetworkClientFactory alloc] initWithDelegate:self]);
343 // WeakNSObjects cannot be dereferenced outside of the main thread, and
344 // CRWWebController must be deallocated from the main thread. Keep a
345 // reference to self on the main thread and release it after successfully
346 // adding the redirect client factory to the RequestTracker on the IO
348 __block base::scoped_nsobject<CRWUIWebViewWebController> scopedSelf(
350 web::WebThread::PostTaskAndReply(
351 web::WebThread::IO, FROM_HERE, base::BindBlock(^{
352 // Only add the factory if there is a valid request tracker.
353 web::WebStateImpl* webState = [scopedSelf webStateImpl];
354 if (webState && webState->GetRequestTracker()) {
355 webState->GetRequestTracker()->AddNetworkClientFactory(
356 redirect_client_factory_);
367 [[NSNotificationCenter defaultCenter] removeObserver:self];
371 #pragma mark - CRWWebController public method implementations
373 - (void)loadCompleteWithSuccess:(BOOL)loadSuccess {
374 [super loadCompleteWithSuccess:loadSuccess];
375 [self removeDocumentLoadCommandsFromQueue];
378 - (BOOL)keyboardDisplayRequiresUserAction {
379 return [_uiWebView keyboardDisplayRequiresUserAction];
382 - (void)setKeyboardDisplayRequiresUserAction:(BOOL)requiresUserAction {
383 [_uiWebView setKeyboardDisplayRequiresUserAction:requiresUserAction];
386 - (void)evaluateUserJavaScript:(NSString*)script {
387 [self setUserInteractionRegistered:YES];
388 // A script which contains alert() call executed by UIWebView from gcd block
389 // freezes the app (crbug.com/444106), hence this uses the NSObject API.
390 [_uiWebView performSelectorOnMainThread:
391 @selector(stringByEvaluatingJavaScriptFromString:)
396 #pragma mark Overridden public methods
400 // Turn the timer back on, and do an immediate check for anything missed
401 // while the timer was off.
402 [self.recurringTaskDelegate runRecurringTask];
403 _continuousCheckTimer->Reset();
410 // Turn the timer off, to cut down on work being done by background tabs.
411 _continuousCheckTimer->Stop();
415 // The video player is not quit/dismissed when the home button is pressed and
416 // Chrome is backgrounded (crbug.com/277206).
417 if (_inFullscreenVideo)
418 [self exitFullscreenVideo];
425 // The timers must not exist at this point, otherwise this object will leak.
426 DCHECK(!_continuousCheckTimer);
427 DCHECK(!_lowerFrequencyTimer);
430 - (void)childWindowClosed:(NSString*)windowName {
431 // Get the substring of the window name after the hash.
432 NSRange range = [windowName rangeOfString:
433 base::SysUTF8ToNSString(web::kWindowNameSeparator)];
434 if (range.location != NSNotFound) {
435 NSString* target = [windowName substringFromIndex:(range.location + 1)];
436 [self stringByEvaluatingJavaScriptFromString:
437 [NSString stringWithFormat:@"__gCrWeb.windowClosed('%@');", target]];
442 #pragma mark Testing-Only Methods
444 - (void)injectWebViewContentView:(CRWWebViewContentView*)webViewContentView {
445 [super injectWebViewContentView:webViewContentView];
446 [self setWebView:static_cast<UIWebView*>(webViewContentView.webView)];
449 #pragma mark CRWJSInjectionEvaluatorMethods
451 - (void)evaluateJavaScript:(NSString*)script
452 stringResultHandler:(web::JavaScriptCompletion)handler {
453 NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script];
454 web::EvaluateJavaScript(_uiWebView, safeScript, handler);
457 - (web::WebViewType)webViewType {
458 return web::UI_WEB_VIEW_TYPE;
461 #pragma mark - Protected property implementations
464 return _uiWebView.get();
467 - (UIScrollView*)webScrollView {
468 return [_uiWebView scrollView];
471 - (BOOL)urlCachingEnabled {
472 return _urlCachingEnabled;
475 - (void)setURLCachingEnabled:(BOOL)enabled {
476 if (enabled == _urlCachingEnabled)
478 _urlCachingEnabled = enabled;
479 _cachedURL.first = GURL();
482 - (BOOL)ignoreURLVerificationFailures {
483 return _spoofableRequest;
487 return [self stringByEvaluatingJavaScriptFromString:
488 @"document.title.length ? document.title : ''"];
491 #pragma mark Protected method implementations
493 - (void)ensureWebViewCreated {
495 [self setWebView:[self createWebView]];
496 // Notify super class about created web view. -webViewDidChange is not
497 // called from -setWebView: as the latter used in unit tests with fake
498 // web view, which cannot be added to view hierarchy.
499 [self webViewDidChange];
503 - (void)resetWebView {
504 [self setWebView:nil];
507 - (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
508 if (_cachedURL.first.is_valid()) {
509 *trustLevel = _cachedURL.second;
510 return _cachedURL.first;
512 ScopedReentryGuard reentryGuard(&_inJavaScriptContext);
514 [CRWURLVerifyingProtocolHandler currentURLForWebView:_uiWebView
515 trustLevel:trustLevel];
517 // If verification succeeded, or the URL has changed, then the UIWebView is no
518 // longer in the initial state.
519 if (*trustLevel != web::URLVerificationTrustLevel::kNone ||
520 url != GURL(url::kAboutBlankURL))
521 _spoofableRequest = NO;
523 if (*trustLevel == web::URLVerificationTrustLevel::kAbsolute) {
524 _lastCorrectURLTime = base::TimeTicks::Now();
525 if (self.urlCachingEnabled) {
526 _cachedURL.first = url;
527 _cachedURL.second = *trustLevel;
529 } else if (*trustLevel == web::URLVerificationTrustLevel::kNone &&
530 (base::TimeTicks::Now() - _lastCorrectURLTime) <
531 base::TimeDelta::FromMilliseconds(
532 web::kContinuousCheckIntervalMSLow)) {
533 // URL is not trusted, but the last time it was trusted is within
534 // kContinuousCheckIntervalMSLow.
535 *trustLevel = web::URLVerificationTrustLevel::kMixed;
541 - (void)registerUserAgent {
542 web::BuildAndRegisterUserAgentForUIWebView(
543 self.webStateImpl->GetRequestGroupID(),
544 [self useDesktopUserAgent]);
547 // The core.js cannot pass messages back to obj-c if it is injected
548 // to |WEB_VIEW_DOCUMENT| because it does not support iframe creation used
549 // by core.js to communicate back. That functionality is only supported
550 // by |WEB_VIEW_HTML_DOCUMENT|. |WEB_VIEW_DOCUMENT| is used when displaying
551 // non-HTML contents (e.g. PDF documents).
552 - (web::WebViewDocumentType)webViewDocumentType {
553 // This happens during tests.
555 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
557 NSString* documentType =
558 [_uiWebView stringByEvaluatingJavaScriptFromString:
560 if ([documentType isEqualToString:@"[object HTMLDocument]"])
561 return web::WEB_VIEW_DOCUMENT_TYPE_HTML;
562 else if ([documentType isEqualToString:@"[object Document]"])
563 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
564 return web::WEB_VIEW_DOCUMENT_TYPE_UNKNOWN;
567 - (void)loadRequest:(NSMutableURLRequest*)request {
568 DCHECK(web::GetWebClient());
569 GURL requestURL = net::GURLWithNSURL(request.URL);
570 // If the request is for WebUI, add information to let the network stack
571 // access the requestGroupID.
572 if (web::GetWebClient()->IsAppSpecificURL(requestURL)) {
573 // Sub requests of a chrome:// page will not contain the user agent.
574 // Instead use the username part of the URL to allow the network stack to
575 // associate a request to the correct tab.
576 request.URL = web::AddRequestGroupIDToURL(
577 request.URL, self.webStateImpl->GetRequestGroupID());
579 [_uiWebView loadRequest:request];
582 - (void)loadWebHTMLString:(NSString*)html forURL:(const GURL&)URL {
583 [_uiWebView loadHTMLString:html baseURL:net::NSURLWithGURL(URL)];
586 - (BOOL)scriptHasBeenInjectedForClass:(Class)jsInjectionManagerClass
587 presenceBeacon:(NSString*)beacon {
588 NSString* beaconCheckScript = [NSString stringWithFormat:
589 @"try { typeof %@; } catch (e) { 'undefined'; }", beacon];
591 [self stringByEvaluatingJavaScriptFromString:beaconCheckScript];
592 return [result isEqualToString:@"object"];
595 - (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass {
596 // Skip evaluation if there's no content (e.g., if what's being injected is
597 // an umbrella manager).
598 if ([script length]) {
599 [super injectScript:script forClass:JSInjectionManagerClass];
600 [self stringByEvaluatingJavaScriptFromString:script];
604 - (void)willLoadCurrentURLInWebView {
607 // This code uses non-documented API, but is not compiled in release.
608 id documentView = [_uiWebView valueForKey:@"documentView"];
609 id webView = [documentView valueForKey:@"webView"];
610 NSString* userAgent = [webView performSelector:@selector(userAgentForURL:)
613 const bool wrongRequestGroupID =
614 ![self.webStateImpl->GetRequestGroupID()
615 isEqualToString:web::ExtractRequestGroupIDFromUserAgent(userAgent)];
616 DLOG_IF(ERROR, wrongRequestGroupID) << "Incorrect user agent in UIWebView";
618 #endif // !defined(NDEBUG)
621 - (void)setPageChangeProbability:(web::PageChangeProbability)probability {
622 if (probability == web::PAGE_CHANGE_PROBABILITY_LOW) {
623 // Reduce check interval to precautionary frequency.
624 [self setContinuousCheckTimerInterval:
625 base::TimeDelta::FromMilliseconds(web::kContinuousCheckIntervalMSLow)];
627 // Increase the timer frequency, as a window change is anticipated shortly.
628 [self setContinuousCheckTimerInterval:
629 base::TimeDelta::FromMilliseconds(web::kContinuousCheckIntervalMSHigh)];
631 if (probability != web::PAGE_CHANGE_PROBABILITY_VERY_HIGH) {
632 // The timer frequency is automatically lowered after a set duration in
633 // case the guess was wrong, to avoid wedging in high-frequency mode.
634 base::Closure closure = base::BindBlock(^{
635 [self setContinuousCheckTimerInterval:
636 base::TimeDelta::FromMilliseconds(
637 web::kContinuousCheckIntervalMSLow)];
639 _lowerFrequencyTimer.reset(
640 new base::Timer(FROM_HERE,
641 base::TimeDelta::FromMilliseconds(
642 web::kContinuousCheckHighFrequencyMSMaxDuration),
643 closure, false /* not repeating */));
644 _lowerFrequencyTimer->Reset();
649 - (BOOL)checkForUnexpectedURLChange {
650 // The check makes no sense without an active web view.
654 // Change to UIWebView default page is not considered a 'real' change and
655 // URL changes are not reported.
656 if ([self isDefaultPage])
659 // Check if currentURL is unexpected (not the incoming page).
660 // This is necessary to notice page changes if core.js injection is disabled
661 // by a malicious page.
662 if (!self.URLOnStartLoading.is_empty() &&
663 [self currentURL] == self.URLOnStartLoading) {
667 // If the URL has changed, handle page load mechanics.
668 ScopedCachedCurrentUrl scopedCurrentURL(self);
669 [self webPageChanged];
670 [self checkDocumentLoaded];
671 [self titleDidChange];
676 - (void)abortWebLoad {
677 // Current load will not complete; this should be communicated upstream to
678 // the delegate, and flagged in the WebView so further messages can be
679 // prevented (which may be confused for messages from newer pages).
680 [_uiWebView stringByEvaluatingJavaScriptFromString:
681 @"document._cancelled = true;"];
682 [_uiWebView stopLoading];
685 - (void)resetLoadState {
690 - (void)setSuppressDialogsWithHelperScript:(NSString*)script {
691 [self stringByEvaluatingJavaScriptFromString:script];
694 - (void)checkDocumentLoaded {
696 [self stringByEvaluatingJavaScriptFromString:
697 @"document.readyState === 'loaded' || "
698 "document.readyState === 'complete'"];
699 if ([loaded isEqualToString:@"true"]) {
700 [self didFinishNavigation];
704 - (NSString*)currentReferrerString {
705 return [self stringByEvaluatingJavaScriptFromString:@"document.referrer"];
708 - (void)titleDidChange {
709 if (![self.delegate respondsToSelector:
710 @selector(webController:titleDidChange:)]) {
714 // Checking the URL trust level is expensive. For performance reasons, the
715 // current URL and the trust level cache must be enabled.
716 // NOTE: Adding a ScopedCachedCurrentUrl here is not the right way to solve
718 DCHECK(self.urlCachingEnabled);
720 // Change to UIWebView default page is not considered a 'real' change and
721 // title changes are not reported.
722 if ([self isDefaultPage])
725 // The title can be retrieved from the document only if the URL can be
727 web::URLVerificationTrustLevel trustLevel =
728 web::URLVerificationTrustLevel::kNone;
729 [self currentURLWithTrustLevel:&trustLevel];
730 if (trustLevel != web::URLVerificationTrustLevel::kAbsolute)
733 NSString* title = self.title;
735 [self.delegate webController:self titleDidChange:title];
738 - (void)teminateNetworkActivity {
739 [super terminateNetworkActivity];
740 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
743 - (void)fetchWebPageSizeWithCompletionHandler:(void(^)(CGSize))handler {
749 // Ensure that JavaScript has been injected.
750 [self.recurringTaskDelegate runRecurringTask];
751 [super fetchWebPageSizeWithCompletionHandler:handler];
754 - (void)documentPresent {
755 if (self.loadPhase != web::PAGE_LOADED &&
756 self.loadPhase != web::LOAD_REQUESTED) {
760 ScopedCachedCurrentUrl scopedCurrentURL(self);
762 // This is a good time to check if the URL has changed.
763 BOOL urlChanged = [self checkForUnexpectedURLChange];
765 // This is a good time to check if the page has refreshed.
766 if (!urlChanged && self.windowId != self.lastSeenWindowID)
767 [self webPageChanged];
769 // Set initial title.
770 [self titleDidChange];
773 - (void)webPageChanged {
774 if (self.loadPhase != web::LOAD_REQUESTED ||
775 self.lastRegisteredRequestURL.is_empty() ||
776 self.lastRegisteredRequestURL != [self currentURL]) {
777 // The page change was unexpected (not already messaged to
778 // webWillStartLoadingURL), so fill in the load request.
779 [self generateMissingLoadRequestWithURL:[self currentURL]
780 referrer:[self currentReferrer]];
783 [super webPageChanged];
786 - (void)applyWebViewScrollZoomScaleFromZoomState:
787 (const web::PageZoomState&)zoomState {
788 // A UIWebView's scroll view uses zoom scales in a non-standard way. The
789 // scroll view's |zoomScale| property is always equal to 1.0, and the
790 // |minimumZoomScale| and |maximumZoomScale| properties are adjusted
791 // proportionally to reflect the relative zoom scale. Setting the |zoomScale|
792 // property here scales the page by the value set (i.e. setting zoomScale to
793 // 2.0 will update the zoom to twice its initial scale). The maximum-scale or
794 // minimum-scale meta tags of a page may have changed since the state was
795 // recorded, so clamp the zoom scale to the current range if necessary.
796 DCHECK(zoomState.IsValid());
797 CGFloat zoomScale = zoomState.IsLegacyFormat()
798 ? zoomState.zoom_scale()
799 : self.webScrollView.minimumZoomScale /
800 zoomState.minimum_zoom_scale();
801 if (zoomScale < self.webScrollView.minimumZoomScale)
802 zoomScale = self.webScrollView.minimumZoomScale;
803 if (zoomScale > self.webScrollView.maximumZoomScale)
804 zoomScale = self.webScrollView.maximumZoomScale;
805 self.webScrollView.zoomScale = zoomScale;
808 - (void)handleCancelledError:(NSError*)error {
809 // NSURLErrorCancelled errors generated by the Chrome net stack should be
810 // aborted. If the error was generated by the UIWebView, it will not have
811 // an underlying net error and will be automatically retried by the web view.
812 DCHECK_EQ(error.code, NSURLErrorCancelled);
813 NSError* underlyingError = base::ios::GetFinalUnderlyingErrorFromError(error);
814 NSString* netDomain = base::SysUTF8ToNSString(net::kErrorDomain);
815 BOOL shouldAbortLoadForCancelledError =
816 [underlyingError.domain isEqualToString:netDomain];
817 if (!shouldAbortLoadForCancelledError)
820 // NSURLCancelled errors with underlying errors are generated from the
821 // Chrome network stack. Abort the load in this case.
824 switch (underlyingError.code) {
825 case net::ERR_ABORTED:
826 // |NSURLErrorCancelled| errors with underlying net error code
827 // |net::ERR_ABORTED| are used by the Chrome network stack to
828 // indicate that the current load should be aborted and the pending
829 // entry should be discarded.
830 [[self sessionController] discardNonCommittedEntries];
832 case net::ERR_BLOCKED_BY_CLIENT:
833 // |NSURLErrorCancelled| errors with underlying net error code
834 // |net::ERR_BLOCKED_BY_CLIENT| are used by the Chrome network stack
835 // to indicate that the current load should be aborted and the pending
836 // entry should be kept.
843 #pragma mark - JS to ObjC messaging
845 - (void)respondToJSInvoke {
846 // This call is asynchronous. If the web view has been removed, there is
847 // nothing left to do, so just discard the queued messages and return.
849 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
852 // Messages are queued and processed asynchronously. However, user
853 // may initiate JavaScript at arbitrary times (e.g. through Omnibox
854 // "javascript:alert('foo')"). This delays processing of queued messages
855 // until JavaScript execution is completed.
856 // TODO(pkl): This should have a unit test or UI Automation test case.
857 // See crbug.com/228125
858 if (_inJavaScriptContext) {
859 [self performSelector:@selector(respondToJSInvoke)
864 DCHECK(_jsInvokeParameterQueue);
865 while (![_jsInvokeParameterQueue isEmpty]) {
866 CRWJSInvokeParameters* parameters =
867 [_jsInvokeParameterQueue popInvokeParameters];
870 // TODO(stuartmorgan): Some messages (e.g., window.write) should be
871 // processed even if the page has already changed by the time they are
872 // received. crbug.com/228275
873 if ([parameters windowId] != [self windowId]) {
874 // If there is a windowID mismatch, the document has been changed since
875 // messages were added to the queue. Ignore the incoming messages.
876 DLOG(WARNING) << "Messages from JS ignored due to non-matching windowID: "
877 << [[parameters windowId] UTF8String]
878 << " != " << [[self windowId] UTF8String];
881 if (![self respondToMessageQueue:[parameters commandString]
882 userIsInteracting:[parameters userIsInteracting]
883 originURL:[parameters originURL]]) {
884 DLOG(WARNING) << "Messages from JS not handled due to invalid format";
889 - (void)handleWebInvokeURL:(const GURL&)url request:(NSURLRequest*)request {
890 DCHECK([self urlSchemeIsWebInvoke:url]);
891 NSURL* nsurl = request.URL;
892 // TODO(stuartmorgan): Remove the NSURL usage here. Will require a logic
893 // change since GURL doesn't parse non-standard URLs into host and fragment
894 if (![nsurl.host isEqualToString:[self windowId]]) {
895 // If there is a windowID mismatch, we may be under attack from a
896 // malicious page, so a defense is to reset the page.
897 DLOG(WARNING) << "Messages from JS ignored due to non-matching windowID: "
898 << nsurl.host << " != " << [[self windowId] UTF8String];
899 DLOG(WARNING) << "Page reset as security precaution";
900 [self performSelector:@selector(reload) withObject:nil afterDelay:0];
903 if (url.spec().length() > kMaxMessageQueueSize) {
904 DLOG(WARNING) << "Messages from JS ignored due to excessive length";
907 NSString* commandString = [[nsurl fragment]
908 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
910 GURL originURL(net::GURLWithNSURL(request.mainDocumentURL));
912 if (url.SchemeIs("crwebinvokeimmediate")) {
913 [self respondToMessageQueue:commandString
914 userIsInteracting:[self userIsInteracting]
915 originURL:originURL];
917 [_jsInvokeParameterQueue addCommandString:commandString
918 userIsInteracting:[self userIsInteracting]
920 forWindowId:[super windowId]];
921 if (!_jsMessageQueueThrottled) {
922 [self performSelector:@selector(respondToJSInvoke)
929 - (void)setJsMessageQueueThrottled:(BOOL)throttle {
930 _jsMessageQueueThrottled = throttle;
932 [self respondToJSInvoke];
935 - (void)removeDocumentLoadCommandsFromQueue {
936 [_jsInvokeParameterQueue removeCommandString:@"document.present"];
937 [_jsInvokeParameterQueue removeCommandString:@"document.loaded"];
940 - (BOOL)urlSchemeIsWebInvoke:(const GURL&)url {
941 return url.SchemeIs("crwebinvoke") || url.SchemeIs("crwebinvokeimmediate");
944 - (CRWJSInvokeParameterQueue*)jsInvokeParameterQueue {
945 return _jsInvokeParameterQueue;
948 - (BOOL)respondToMessageQueue:(NSString*)messageQueue
949 userIsInteracting:(BOOL)userIsInteracting
950 originURL:(const GURL&)originURL {
951 ScopedCachedCurrentUrl scopedCurrentURL(self);
954 std::string errorMessage;
955 scoped_ptr<base::Value> inputJSONData(base::JSONReader::ReadAndReturnError(
956 base::SysNSStringToUTF8(messageQueue), false, &errorCode, &errorMessage));
958 DLOG(WARNING) << "JSON parse error: %s" << errorMessage.c_str();
961 // MessageQueues pass messages as a list.
962 base::ListValue* messages = nullptr;
963 if (!inputJSONData->GetAsList(&messages)) {
964 DLOG(WARNING) << "Message queue not a list";
967 for (size_t idx = 0; idx != messages->GetSize(); ++idx) {
968 // The same-origin check has to be done for every command to mitigate the
969 // risk of command sequences where the first command would change the page
970 // and the subsequent commands would have unlimited access to it.
971 if (originURL.GetOrigin() != self.currentURL.GetOrigin()) {
972 DLOG(WARNING) << "Message source URL origin: " << originURL.GetOrigin()
973 << " does not match current URL origin: "
974 << self.currentURL.GetOrigin();
978 base::DictionaryValue* message = nullptr;
979 if (!messages->GetDictionary(idx, &message)) {
980 DLOG(WARNING) << "Message could not be retrieved";
983 BOOL messageHandled = [self respondToMessage:message
984 userIsInteracting:userIsInteracting
985 originURL:originURL];
989 // If handling the message caused this page to be closed, stop processing
991 // TODO(stuartmorgan): Ideally messages should continue to be handled until
992 // the end of the event loop (e.g., window.close(); window.open(...);
993 // should do both things). That would require knowing which messages came
994 // in the same event loop, however.
995 if ([self isBeingDestroyed])
1001 - (SEL)selectorToHandleJavaScriptCommand:(const std::string&)command {
1002 static std::map<std::string, SEL>* handlers = nullptr;
1003 static dispatch_once_t onceToken;
1004 dispatch_once(&onceToken, ^{
1005 handlers = new std::map<std::string, SEL>();
1006 (*handlers)["anchor.click"] = @selector(handleAnchorClickMessage:context:);
1007 (*handlers)["document.loaded"] =
1008 @selector(handleDocumentLoadedMessage:context:);
1009 (*handlers)["document.present"] =
1010 @selector(handleDocumentPresentMessage:context:);
1011 (*handlers)["document.retitled"] =
1012 @selector(handleDocumentRetitledMessage:context:);
1013 (*handlers)["window.close"] = @selector(handleWindowCloseMessage:context:);
1014 (*handlers)["window.document.write"] =
1015 @selector(handleWindowDocumentWriteMessage:context:);
1016 (*handlers)["window.location"] =
1017 @selector(handleWindowLocationMessage:context:);
1018 (*handlers)["window.open"] = @selector(handleWindowOpenMessage:context:);
1019 (*handlers)["window.stop"] = @selector(handleWindowStopMessage:context:);
1020 (*handlers)["window.unload"] =
1021 @selector(handleWindowUnloadMessage:context:);
1024 auto iter = handlers->find(command);
1025 return iter != handlers->end()
1027 : [super selectorToHandleJavaScriptCommand:command];
1030 - (NSString*)windowNameFromMessage:(base::DictionaryValue*)message
1031 context:(NSDictionary*)context {
1033 if(!message->GetString("target", &target)) {
1034 DLOG(WARNING) << "JS message parameter not found: target";
1038 DCHECK(context[web::kOriginURLKey]);
1039 const GURL& originURL = net::GURLWithNSURL(context[web::kOriginURLKey]);
1041 // Unique string made for page/target combination.
1042 // Safe to delimit unique string with # since page references won't
1044 return base::SysUTF8ToNSString(
1045 NormalizedUrl(originURL) + web::kWindowNameSeparator + target);
1049 #pragma mark JavaScript message handlers
1051 - (BOOL)handleAnchorClickMessage:(base::DictionaryValue*)message
1052 context:(NSDictionary*)context {
1053 // Reset the external click request.
1054 [self resetExternalRequest];
1057 if (!message->GetString("href", &href)) {
1058 DLOG(WARNING) << "JS message parameter not found: href";
1061 const GURL targetURL(href);
1062 const GURL currentURL([self currentURL]);
1063 if (currentURL != targetURL) {
1064 if (web::UrlHasWebScheme(targetURL)) {
1065 // The referrer is not known yet, and will be updated later.
1066 const web::Referrer emptyReferrer;
1067 [self registerLoadRequest:targetURL
1068 referrer:emptyReferrer
1069 transition:ui::PAGE_TRANSITION_LINK];
1070 [self setPageChangeProbability:web::PAGE_CHANGE_PROBABILITY_HIGH];
1071 } else if (web::GetWebClient()->IsAppSpecificURL(targetURL) &&
1072 web::GetWebClient()->IsAppSpecificURL(currentURL)) {
1073 // Allow navigations between app-specific URLs
1074 [self removeWebViewAllowingCachedReconstruction:NO];
1075 ui::PageTransition pageTransitionLink =
1076 ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK);
1077 const web::Referrer referrer(currentURL, web::ReferrerPolicyDefault);
1078 web::WebState::OpenURLParams openParams(targetURL, referrer, CURRENT_TAB,
1079 pageTransitionLink, true);
1080 [self.delegate openURLWithParams:openParams];
1086 - (BOOL)handleDocumentLoadedMessage:(base::DictionaryValue*)message
1087 context:(NSDictionary*)context {
1088 // Very early hashchange events can be missed, hence this extra explicit
1090 [self checkForUnexpectedURLChange];
1091 [self didFinishNavigation];
1095 - (BOOL)handleDocumentPresentMessage:(base::DictionaryValue*)message
1096 context:(NSDictionary*)context {
1097 NSString* documentCancelled =
1098 [self stringByEvaluatingJavaScriptFromString:@"document._cancelled"];
1099 if (![documentCancelled isEqualToString:@"true"])
1100 [self documentPresent];
1104 - (BOOL)handleDocumentRetitledMessage:(base::DictionaryValue*)message
1105 context:(NSDictionary*)context {
1106 [self titleDidChange];
1110 - (BOOL)handleWindowCloseMessage:(base::DictionaryValue*)message
1111 context:(NSDictionary*)context {
1112 NSString* windowName = [self windowNameFromMessage:message
1116 [[self scriptingInterfaceForWindowNamed:windowName] orderClose];
1120 - (BOOL)handleWindowDocumentWriteMessage:(base::DictionaryValue*)message
1121 context:(NSDictionary*)context {
1122 NSString* windowName = [self windowNameFromMessage:message
1127 if (!message->GetString("html", &HTML)) {
1128 DLOG(WARNING) << "JS message parameter not found: html";
1131 [[self scriptingInterfaceForWindowNamed:windowName]
1132 loadHTML:base::SysUTF8ToNSString(HTML)];
1136 - (BOOL)handleWindowLocationMessage:(base::DictionaryValue*)message
1137 context:(NSDictionary*)context {
1138 NSString* windowName = [self windowNameFromMessage:message
1142 std::string command;
1143 if (!message->GetString("command", &command)) {
1144 DLOG(WARNING) << "JS message parameter not found: command";
1148 if (!message->GetString("value", &value)) {
1149 DLOG(WARNING) << "JS message parameter not found: value";
1152 std::string escapedValue;
1153 base::EscapeJSONString(value, true, &escapedValue);
1155 [NSString stringWithFormat:@"<script>%s = %s;</script>",
1157 escapedValue.c_str()];
1158 [[self scriptingInterfaceForWindowNamed:windowName] loadHTML:HTML];
1162 - (BOOL)handleWindowOpenMessage:(base::DictionaryValue*)message
1163 context:(NSDictionary*)context {
1164 NSString* windowName = [self windowNameFromMessage:message
1168 std::string targetURL;
1169 if (!message->GetString("url", &targetURL)) {
1170 DLOG(WARNING) << "JS message parameter not found: url";
1173 std::string referrerPolicy;
1174 if (!message->GetString("referrerPolicy", &referrerPolicy)) {
1175 DLOG(WARNING) << "JS message parameter not found: referrerPolicy";
1178 GURL resolvedURL = targetURL.empty() ?
1180 GURL(net::GURLWithNSURL(context[web::kOriginURLKey])).Resolve(targetURL);
1181 DCHECK(&resolvedURL);
1183 windowInfo(resolvedURL,
1185 [self referrerPolicyFromString:referrerPolicy],
1186 [context[web::kUserIsInteractingKey] boolValue]);
1188 [self openPopupWithInfo:windowInfo];
1192 - (BOOL)handleWindowStopMessage:(base::DictionaryValue*)message
1193 context:(NSDictionary*)context {
1194 NSString* windowName = [self windowNameFromMessage:message
1198 [[self scriptingInterfaceForWindowNamed:windowName] stopLoading];
1202 - (BOOL)handleWindowUnloadMessage:(base::DictionaryValue*)message
1203 context:(NSDictionary*)context {
1204 [self setPageChangeProbability:web::PAGE_CHANGE_PROBABILITY_VERY_HIGH];
1208 #pragma mark Private methods
1210 - (BOOL)isDefaultPage {
1211 if ([[self stringByEvaluatingJavaScriptFromString:@"document._defaultPage"]
1212 isEqualToString:@"true"]) {
1213 return self.currentURL == self.defaultURL;
1218 - (BOOL)isLinkNavigation:(UIWebViewNavigationType)navigationType {
1219 switch (navigationType) {
1220 case UIWebViewNavigationTypeLinkClicked:
1222 case UIWebViewNavigationTypeOther:
1223 return [self userClickedRecently];
1229 - (void)setContinuousCheckTimerInterval:(const base::TimeDelta&)interval {
1230 // The timer should never be set when there's no web view.
1233 BOOL shouldStartTimer =
1234 !_continuousCheckTimer.get() || _continuousCheckTimer->IsRunning();
1235 base::Closure closure = base::BindBlock(^{
1236 // Only perform JS checks if CRWWebController is not already in JavaScript
1237 // context. This is possible when "javascript:..." is executed from
1238 // Omnibox and this block is run from the timer.
1239 if (!_inJavaScriptContext)
1240 [self.recurringTaskDelegate runRecurringTask];
1242 _continuousCheckTimer.reset(
1243 new base::Timer(FROM_HERE, interval, closure, true));
1244 if (shouldStartTimer)
1245 _continuousCheckTimer->Reset();
1246 if (_lowerFrequencyTimer &&
1247 interval == base::TimeDelta::FromMilliseconds(
1248 web::kContinuousCheckIntervalMSLow)) {
1249 _lowerFrequencyTimer.reset();
1253 - (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script {
1257 ScopedReentryGuard reentryGuard(&_inJavaScriptContext);
1258 return [_uiWebView stringByEvaluatingJavaScriptFromString:script];
1261 - (NSString*)stringByEvaluatingUserJavaScriptFromString:(NSString*)script {
1262 [self setUserInteractionRegistered:YES];
1263 return [self stringByEvaluatingJavaScriptFromString:script];
1266 - (UIWebView*)createWebView {
1267 UIWebView* webView = web::CreateWebView(
1269 self.webStateImpl->GetRequestGroupID(),
1270 [self useDesktopUserAgent]);
1272 // Mark the document object of the default page as such, so that it is not
1273 // mistaken for a 'real' page by change detection mechanisms.
1274 [webView stringByEvaluatingJavaScriptFromString:
1275 @"document._defaultPage = true;"];
1277 [webView setScalesPageToFit:YES];
1278 // Turn off data-detectors. MobileSafari does the same thing.
1279 [webView setDataDetectorTypes:UIDataDetectorTypeNone];
1281 return [webView autorelease];
1284 - (void)setWebView:(UIWebView*)webView {
1285 DCHECK_NE(_uiWebView.get(), webView);
1286 // Per documentation, must clear the delegate before releasing the UIWebView
1287 // to avoid errant dangling pointers.
1288 [_uiWebView setDelegate:nil];
1289 _uiWebView.reset([webView retain]);
1290 [_uiWebView setDelegate:self];
1291 // Clear out the trusted URL cache.
1292 _lastCorrectURLTime = base::TimeTicks();
1293 _cachedURL.first = GURL();
1294 // Reset the spoofable state (see declaration comment).
1295 // TODO(stuartmorgan): Fix the fact that there's no guarantee that no
1296 // navigation has happened before the UIWebView is set here (ideally by
1297 // unifying the creation and setting flow).
1298 _spoofableRequest = YES;
1301 _inJavaScriptContext = NO;
1302 // Do initial injection even before loading another page, since the window
1303 // object is re-used.
1304 [self injectEarlyInjectionScripts];
1306 _continuousCheckTimer.reset();
1307 // This timer exists only to change the frequency of the main timer, so it
1308 // should not outlive the main timer.
1309 _lowerFrequencyTimer.reset();
1313 - (void)generateMissingDocumentLifecycleEvents {
1314 // The webView can be removed between this method being queued and invoked.
1317 if ([self webViewDocumentType] == web::WEB_VIEW_DOCUMENT_TYPE_GENERIC) {
1318 [self documentPresent];
1319 [self didFinishNavigation];
1323 - (void)generateMissingLoadRequestWithURL:(const GURL&)currentURL
1324 referrer:(const web::Referrer&)referrer {
1325 [self loadCancelled];
1326 // Initialize transition based on whether the request is user-initiated or
1327 // not. This is a best guess to replace lost transition type informationj.
1328 ui::PageTransition transition = self.userInteractionRegistered
1329 ? ui::PAGE_TRANSITION_LINK
1330 : ui::PAGE_TRANSITION_CLIENT_REDIRECT;
1331 // If the URL agrees with session state, use the session's transition.
1332 if (currentURL == [self currentNavigationURL]) {
1333 transition = [self currentTransition];
1336 [self registerLoadRequest:currentURL referrer:referrer transition:transition];
1339 - (id<CRWWebControllerScripting>)scriptingInterfaceForWindowNamed:
1341 if (![self.delegate respondsToSelector:
1342 @selector(webController:scriptingInterfaceForWindowNamed:)]) {
1345 return [self.delegate webController:self
1346 scriptingInterfaceForWindowNamed:name];
1349 #pragma mark FullscreenVideo
1351 - (void)moviePlayerDidEnterFullscreen:(NSNotification*)notification {
1352 _inFullscreenVideo = YES;
1355 - (void)moviePlayerDidExitFullscreen:(NSNotification*)notification {
1356 _inFullscreenVideo = NO;
1359 - (void)exitFullscreenVideo {
1360 [self stringByEvaluatingJavaScriptFromString:
1361 @"__gCrWeb.exitFullscreenVideo();"];
1365 #pragma mark CRWRecurringTaskDelegate
1367 // Checks for page changes are made continuously.
1368 - (void)runRecurringTask {
1372 [self injectEarlyInjectionScripts];
1373 [self checkForUnexpectedURLChange];
1377 #pragma mark CRWRedirectClientDelegate
1379 - (void)wasRedirectedToRequest:(NSURLRequest*)request
1380 redirectResponse:(NSURLResponse*)response {
1381 // This callback can be received after -close is called; ignore it.
1382 if (self.isBeingDestroyed)
1385 // Register the redirected load request if it originated from the main page
1387 GURL redirectedURL = net::GURLWithNSURL(response.URL);
1388 if ([self currentNavigationURL] == redirectedURL) {
1389 [self registerLoadRequest:net::GURLWithNSURL(request.URL)
1390 referrer:[self currentReferrer]
1391 transition:ui::PAGE_TRANSITION_SERVER_REDIRECT];
1396 #pragma mark UIWebViewDelegate Methods
1398 // Called when a load begins, and for subsequent subpages.
1399 - (BOOL)webView:(UIWebView*)webView
1400 shouldStartLoadWithRequest:(NSURLRequest*)request
1401 navigationType:(UIWebViewNavigationType)navigationType {
1402 DVLOG(5) << "webViewShouldStartLoadWithRequest "
1403 << net::FormatUrlRequestForLogging(request);
1405 if (self.isBeingDestroyed)
1408 GURL url = net::GURLWithNSURL(request.URL);
1410 // The crwebnull protocol is used where an element requires a URL but it
1411 // should not trigger any activity on the WebView.
1412 if (url.SchemeIs("crwebnull"))
1415 if ([self urlSchemeIsWebInvoke:url]) {
1416 [self handleWebInvokeURL:url request:request];
1420 // ##### IMPORTANT NOTE #####
1421 // Do not add new code above this line unless you're certain about what you're
1422 // doing with respect to JS re-entry.
1423 ScopedReentryGuard javaScriptReentryGuard(&_inJavaScriptContext);
1424 web::FrameInfo* targetFrame = nullptr; // No reliable way to get this info.
1425 BOOL isLinkClick = [self isLinkNavigation:navigationType];
1426 return [self shouldAllowLoadWithRequest:request
1427 targetFrame:targetFrame
1428 isLinkClick:isLinkClick];
1431 // Called at multiple points during a load, such as at the start of loading a
1432 // page, and every time an iframe loads. Not called again for server-side
1434 - (void)webViewDidStartLoad:(UIWebView *)webView {
1435 NSURLRequest* request = webView.request;
1436 DVLOG(5) << "webViewDidStartLoad "
1437 << net::FormatUrlRequestForLogging(request);
1438 // |webView:shouldStartLoad| may not be called or called with different URL
1439 // and mainDocURL for the request in certain page navigations. There
1440 // are at least 2 known page navigations where this occurs, in these cases it
1441 // is imperative the URL verification timer is started here.
1442 // The 2 known cases are:
1443 // 1) A malicious page suppressing core.js injection and calling
1444 // window.history.back() or window.history.forward()
1445 // 2) An iframe loading a URL using target=_blank.
1446 // TODO(shreyasv): crbug.com/349155. Understand further why this happens
1447 // in some case and not in others.
1448 if (webView != self.webView) {
1449 // This happens sometimes as tests are brought down.
1450 // TODO(jimblackler): work out why and fix the problem at source.
1451 LOG(WARNING) << " UIWebViewDelegate message received for inactive WebView.";
1454 DCHECK(!self.isBeingDestroyed);
1455 // Increment the number of pending loads. This will be balanced by either
1456 // a |-webViewDidFinishLoad:| or |-webView:didFailLoadWithError:|.
1458 [self.recurringTaskDelegate runRecurringTask];
1461 // Called when the page (or one of its subframes) finishes loading. This is
1462 // called multiple times during a page load, with varying frequency depending
1463 // on the action (going back, loading a page with frames, redirecting).
1464 // See http://goto/efrmm for a summary of why this is so painful.
1465 - (void)webViewDidFinishLoad:(UIWebView*)webView {
1466 DVLOG(5) << "webViewDidFinishLoad "
1467 << net::FormatUrlRequestForLogging(webView.request);
1468 DCHECK(!self.isHalted);
1469 // Occasionally this delegate is invoked as a side effect during core.js
1470 // injection. It is necessary to ensure we do not attempt to start the
1471 // injection process a second time.
1472 if (!_inJavaScriptContext)
1473 [self.recurringTaskDelegate runRecurringTask];
1475 [self performSelector:@selector(generateMissingDocumentLifecycleEvents)
1480 if ((_loadCount == _unloadCount) && (self.loadPhase != web::LOAD_REQUESTED))
1481 [self checkDocumentLoaded];
1484 // Called when there is an error loading the page. Some errors aren't actual
1485 // errors, but are caused by user actions such as stopping a page load
1487 - (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
1488 DVLOG(5) << "webViewDidFailLoadWithError "
1489 << net::FormatUrlRequestForLogging(webView.request);
1492 // Under unknown circumstances navigation item can be null. In that case the
1493 // state of web/ will not be valid and app will crash. Early return avoid a
1494 // crash (crbug.com/411912).
1495 if (!self.webStateImpl ||
1496 !self.webStateImpl->GetNavigationManagerImpl().GetVisibleItem()) {
1500 // There's no reliable way to know if a load is for the main frame, so make a
1501 // best-effort guess.
1502 // |_loadCount| is reset to 0 before starting loading a new page, and is
1503 // incremented in each call to |-webViewDidStartLoad:|. The main request
1504 // is the first one to be loaded, and thus has a |_loadCount| of 1.
1505 // Sub-requests have a |_loadCount| > 1.
1506 // An iframe loading after the main page also has a |_loadCount| of 1, as
1507 // |_loadCount| is reset at the end of the main page load. In that case,
1508 // |loadPhase_| is web::PAGE_LOADED (as opposed to web::PAGE_LOADING for a
1510 const bool isMainFrame = (_loadCount == 1 &&
1511 self.loadPhase != web::PAGE_LOADED);
1512 [self handleLoadError:error inMainFrame:isMainFrame];
1516 #pragma mark Testing methods
1518 -(id<CRWRecurringTaskDelegate>)recurringTaskDelegate {
1519 return _recurringTaskDelegate;