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 #import "ios/web/navigation/navigation_item_impl.h"
23 #include "ios/web/net/clients/crw_redirect_network_client_factory.h"
24 #import "ios/web/net/crw_url_verifying_protocol_handler.h"
25 #include "ios/web/net/request_group_util.h"
26 #include "ios/web/public/url_scheme_util.h"
27 #include "ios/web/public/web_client.h"
28 #import "ios/web/public/web_state/ui/crw_web_view_content_view.h"
29 #import "ios/web/ui_web_view_util.h"
30 #include "ios/web/web_state/frame_info.h"
31 #import "ios/web/web_state/js/crw_js_invoke_parameter_queue.h"
32 #import "ios/web/web_state/ui/crw_context_menu_provider.h"
33 #import "ios/web/web_state/ui/crw_web_controller+protected.h"
34 #import "ios/web/web_state/ui/crw_web_controller.h"
35 #import "ios/web/web_state/ui/web_view_js_utils.h"
36 #import "ios/web/web_state/web_state_impl.h"
37 #import "ios/web/web_state/web_view_internal_creation_util.h"
38 #import "net/base/mac/url_conversions.h"
39 #include "net/base/net_errors.h"
40 #include "url/url_constants.h"
44 // The following continuous check timer frequency constants are externally
45 // available for the purpose of performance tests.
46 // Frequency for the continuous checks when a reset in the page object is
47 // anticipated shortly. In milliseconds.
48 const int64 kContinuousCheckIntervalMSHigh = 100;
50 // The maximum duration that the CRWWebController can run in high-frequency
51 // check mode before being changed back to the low frequency.
52 const int64 kContinuousCheckHighFrequencyMSMaxDuration = 5000;
54 // Frequency for the continuous checks when a reset in the page object is not
55 // anticipated; checks are only made as a precaution.
56 // The URL could be out of date for this many milliseconds, so this should not
57 // be increased without careful consideration.
58 const int64 kContinuousCheckIntervalMSLow = 3000;
62 @interface CRWUIWebViewWebController () <CRWRedirectClientDelegate,
64 // The UIWebView managed by this instance.
65 base::scoped_nsobject<UIWebView> _uiWebView;
67 // Whether caching of the current URL is enabled or not.
68 BOOL _urlCachingEnabled;
70 // Temporarily cached current URL. Only valid/set while urlCachingEnabled
72 // TODO(stuartmorgan): Change this to a struct so code using it is more
74 std::pair<GURL, web::URLVerificationTrustLevel> _cachedURL;
76 // The last time a URL with absolute trust level was computed.
77 // When an untrusted URL is retrieved from the
78 // |CRWURLVerifyingProtocolHandler|, if the last trusted URL is within
79 // |kContinuousCheckIntervalMSLow|, the trustLevel is upgraded to Mixed.
80 // The reason is that it is sometimes temporarily impossible to do a
81 // AsyncXMLHttpRequest on a web page. When this happen, it is not possible
82 // to check for the validity of the current URL. Because the checker is
83 // only checking every |kContinuousCheckIntervalMSLow| anyway, waiting this
84 // amount of time before triggering an interstitial does not weaken the
85 // security of the browser.
86 base::TimeTicks _lastCorrectURLTime;
88 // Each new UIWebView starts in a state where:
89 // - window.location.href is equal to about:blank
90 // - Ajax requests seem to be impossible
91 // Because Ajax requests are used to determine is a URL is verified, this
92 // means it is impossible to do this check until the UIWebView is in a more
93 // sane state. This variable tracks whether verifying the URL is currently
94 // impossible. It starts at YES when a new UIWebView is created,
95 // and will change to NO, as soon as either window.location.href is not
96 // about:blank anymore, or an URL verification request succeeds. This means
97 // that a malicious site that is able to change the value of
98 // window.location.href and that is loaded as the first request will be able
99 // to change its URL to about:blank. As this is not an interesting URL, it is
100 // considered acceptable.
101 BOOL _spoofableRequest;
103 // Timer used to make continuous checks on the UIWebView. Timer is
104 // running only while |webView| is non-nil.
105 scoped_ptr<base::Timer> _continuousCheckTimer;
106 // Timer to lower the check frequency automatically.
107 scoped_ptr<base::Timer> _lowerFrequencyTimer;
109 // Counts of calls to |-webViewDidStartLoad:| and |-webViewDidFinishLoad|.
110 // When |_loadCount| is equal to |_unloadCount|, the page is no longer
111 // loading. Used as a fallback to determine when the page is done loading in
112 // case document.readyState isn't sufficient.
113 // When |_loadCount| is 1, the main page is loading (as opposed to a
118 // Backs the property of the same name.
119 BOOL _inJavaScriptContext;
121 // Backs the property of the same name.
122 base::scoped_nsobject<CRWJSInvokeParameterQueue> _jsInvokeParameterQueue;
124 // Blocks message queue processing (for testing).
125 BOOL _jsMessageQueueThrottled;
127 // YES if a video is playing in fullscreen.
128 BOOL _inFullscreenVideo;
130 // Backs the property of the same name.
131 id<CRWRecurringTaskDelegate>_recurringTaskDelegate;
133 // Redirect client factory.
134 base::scoped_nsobject<CRWRedirectNetworkClientFactory>
135 redirect_client_factory_;
138 // Whether or not URL caching is enabled. Between enabling and disabling
139 // caching, calls to webURLWithTrustLevel: after the first may return the same
140 // answer without re-checking the URL.
141 @property(nonatomic, setter=setURLCachingEnabled:) BOOL urlCachingEnabled;
143 // Returns whether the current page is the web views initial (default) page.
144 - (BOOL)isDefaultPage;
146 // Returns whether the given navigation is triggered by a user link click.
147 - (BOOL)isLinkNavigation:(UIWebViewNavigationType)navigationType;
149 // Starts (at the given interval) the timer that drives runRecurringTasks to see
150 // whether or not the page has changed. This is used to work around the fact
151 // that UIWebView callbacks are not sufficiently reliable to catch every page
153 - (void)setContinuousCheckTimerInterval:(const base::TimeDelta&)interval;
155 // Evaluates the supplied JavaScript and returns the result. Will return nil
156 // if it is unable to evaluate the JavaScript.
157 - (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script;
159 // Evaluates the user-entered JavaScript in the WebView and returns the result.
160 // Will return nil if the web view is currently not available.
161 - (NSString*)stringByEvaluatingUserJavaScriptFromString:(NSString*)script;
163 // Checks if the document is loaded, and if so triggers any necessary logic.
164 - (void)checkDocumentLoaded;
166 // Returns a new autoreleased UIWebView.
167 - (UIWebView*)createWebView;
169 // Sets value to web view property.
170 - (void)setWebView:(UIWebView*)webView;
172 // Simulates the events generated by core.js during document loading process.
173 // Used for non-HTML documents (e.g. PDF) that do not support data flow from
174 // JavaScript to obj-c via iframe injection.
175 - (void)generateMissingDocumentLifecycleEvents;
177 // Makes a best-effort attempt to retroactively construct a load request for an
178 // observed-but-unexpected navigation. Should be called any time a page
179 // change is detected as having happened without the current internal state
180 // indicating it was expected.
181 - (void)generateMissingLoadRequestWithURL:(const GURL&)currentURL
182 referrer:(const web::Referrer&)referrer;
184 // Returns a child scripting CRWWebController with the given window name.
185 - (id<CRWWebControllerScripting>)scriptingInterfaceForWindowNamed:
188 // Called when UIMoviePlayerControllerDidEnterFullscreenNotification is posted.
189 - (void)moviePlayerDidEnterFullscreen:(NSNotification*)notification;
191 // Called when UIMoviePlayerControllerDidExitFullscreenNotification is posted.
192 - (void)moviePlayerDidExitFullscreen:(NSNotification*)notification;
194 // Exits fullscreen mode for any playing videos.
195 - (void)exitFullscreenVideo;
197 // Handles presentation of the web document. Checks and handles URL changes,
198 // page refreshes, and title changes.
199 - (void)documentPresent;
201 // Handles queued JS to ObjC messages.
202 // All commands are passed via JSON strings, including parameters.
203 - (void)respondToJSInvoke;
205 // Pauses (|throttle|=YES) or resumes (|throttle|=NO) crwebinvoke message
207 - (void)setJsMessageQueueThrottled:(BOOL)throttle;
209 // Removes document load commands from the queue. Otherwise they could be
210 // handled after a new page load has begun, which would cause an unwanted
212 - (void)removeDocumentLoadCommandsFromQueue;
214 // Returns YES if the given URL has a scheme associated with JS->native calls.
215 - (BOOL)urlSchemeIsWebInvoke:(const GURL&)url;
217 // Returns window name given a message and its context.
218 - (NSString*)windowNameFromMessage:(base::DictionaryValue*)message
219 context:(NSDictionary*)context;
221 // Handles 'anchor.click' message.
222 - (BOOL)handleAnchorClickMessage:(base::DictionaryValue*)message
223 context:(NSDictionary*)context;
224 // Handles 'document.loaded' message.
225 - (BOOL)handleDocumentLoadedMessage:(base::DictionaryValue*)message
226 context:(NSDictionary*)context;
227 // Handles 'document.present' message.
228 - (BOOL)handleDocumentPresentMessage:(base::DictionaryValue*)message
229 context:(NSDictionary*)context;
230 // Handles 'document.retitled' message.
231 - (BOOL)handleDocumentRetitledMessage:(base::DictionaryValue*)message
232 context:(NSDictionary*)context;
233 // Handles 'window.close' message.
234 - (BOOL)handleWindowCloseMessage:(base::DictionaryValue*)message
235 context:(NSDictionary*)context;
236 // Handles 'window.document.write' message.
237 - (BOOL)handleWindowDocumentWriteMessage:(base::DictionaryValue*)message
238 context:(NSDictionary*)context;
239 // Handles 'window.location' message.
240 - (BOOL)handleWindowLocationMessage:(base::DictionaryValue*)message
241 context:(NSDictionary*)context;
242 // Handles 'window.open' message.
243 - (BOOL)handleWindowOpenMessage:(base::DictionaryValue*)message
244 context:(NSDictionary*)context;
245 // Handles 'window.stop' message.
246 - (BOOL)handleWindowStopMessage:(base::DictionaryValue*)message
247 context:(NSDictionary*)context;
248 // Handles 'window.unload' message.
249 - (BOOL)handleWindowUnloadMessage:(base::DictionaryValue*)message
250 context:(NSDictionary*)context;
255 // Utility to help catch unwanted JavaScript re-entries. An instance should
256 // be created on the stack any time JS will be executed.
257 // It uses an instance variable (passed in as a pointer to boolean) that needs
258 // to be initialized to false.
259 class ScopedReentryGuard {
261 explicit ScopedReentryGuard(BOOL* is_inside_javascript_context)
262 : is_inside_javascript_context_(is_inside_javascript_context) {
263 DCHECK(!*is_inside_javascript_context_);
264 *is_inside_javascript_context_ = YES;
266 ~ScopedReentryGuard() {
267 DCHECK(*is_inside_javascript_context_);
268 *is_inside_javascript_context_ = NO;
272 BOOL* is_inside_javascript_context_;
273 DISALLOW_IMPLICIT_CONSTRUCTORS(ScopedReentryGuard);
276 // Class allowing to selectively enable caching of |currentURL| on a
277 // CRWUIWebViewWebController. As long as an instance of this class lives,
278 // the CRWUIWebViewWebController passed as parameter will cache the result for
279 // |currentURL| calls.
280 class ScopedCachedCurrentUrl {
282 explicit ScopedCachedCurrentUrl(CRWUIWebViewWebController* web_controller)
283 : web_controller_(web_controller),
284 had_cached_current_url_([web_controller urlCachingEnabled]) {
285 if (!had_cached_current_url_)
286 [web_controller_ setURLCachingEnabled:YES];
289 ~ScopedCachedCurrentUrl() {
290 if (!had_cached_current_url_)
291 [web_controller_ setURLCachingEnabled:NO];
295 CRWUIWebViewWebController* web_controller_;
296 bool had_cached_current_url_;
297 DISALLOW_IMPLICIT_CONSTRUCTORS(ScopedCachedCurrentUrl);
300 // Normalizes the URL for the purposes of identifying the origin page (remove
301 // any parameters, fragments, etc.) and return an absolute string of the URL.
302 std::string NormalizedUrl(const GURL& url) {
303 GURL::Replacements replacements;
304 replacements.ClearQuery();
305 replacements.ClearRef();
306 replacements.ClearUsername();
307 replacements.ClearPassword();
308 GURL page_url(url.ReplaceComponents(replacements));
310 return page_url.spec();
313 // The maximum size of JSON message passed from JavaScript to ObjC.
314 // 256kB is an arbitrary number that was chosen to be a magnitude larger than
315 // any legitimate message.
316 const size_t kMaxMessageQueueSize = 262144;
320 @implementation CRWUIWebViewWebController
322 - (instancetype)initWithWebState:(scoped_ptr<web::WebStateImpl>)webState {
323 self = [super initWithWebState:webState.Pass()];
325 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
327 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
328 [defaultCenter addObserver:self
329 selector:@selector(moviePlayerDidEnterFullscreen:)
331 @"UIMoviePlayerControllerDidEnterFullscreenNotification"
333 [defaultCenter addObserver:self
334 selector:@selector(moviePlayerDidExitFullscreen:)
336 @"UIMoviePlayerControllerDidExitFullscreenNotification"
338 _recurringTaskDelegate = self;
340 // UIWebViews require a redirect network client in order to accurately
341 // detect server redirects.
342 scoped_refptr<web::RequestTrackerImpl> requestTracker =
343 self.webStateImpl->GetRequestTracker();
344 if (requestTracker) {
345 redirect_client_factory_.reset(
346 [[CRWRedirectNetworkClientFactory alloc] initWithDelegate:self]);
347 requestTracker->PostIOTask(
348 base::Bind(&net::RequestTracker::AddNetworkClientFactory,
349 requestTracker, redirect_client_factory_));
356 [[NSNotificationCenter defaultCenter] removeObserver:self];
360 #pragma mark - CRWWebController public method implementations
362 - (void)loadCompleteWithSuccess:(BOOL)loadSuccess {
363 [super loadCompleteWithSuccess:loadSuccess];
364 [self removeDocumentLoadCommandsFromQueue];
367 - (BOOL)keyboardDisplayRequiresUserAction {
368 return [_uiWebView keyboardDisplayRequiresUserAction];
371 - (void)setKeyboardDisplayRequiresUserAction:(BOOL)requiresUserAction {
372 [_uiWebView setKeyboardDisplayRequiresUserAction:requiresUserAction];
375 - (void)evaluateUserJavaScript:(NSString*)script {
376 [self setUserInteractionRegistered:YES];
377 // A script which contains alert() call executed by UIWebView from gcd block
378 // freezes the app (crbug.com/444106), hence this uses the NSObject API.
379 [_uiWebView performSelectorOnMainThread:
380 @selector(stringByEvaluatingJavaScriptFromString:)
385 #pragma mark Overridden public methods
389 // Turn the timer back on, and do an immediate check for anything missed
390 // while the timer was off.
391 [self.recurringTaskDelegate runRecurringTask];
392 _continuousCheckTimer->Reset();
399 // Turn the timer off, to cut down on work being done by background tabs.
400 _continuousCheckTimer->Stop();
404 // The video player is not quit/dismissed when the home button is pressed and
405 // Chrome is backgrounded (crbug.com/277206).
406 if (_inFullscreenVideo)
407 [self exitFullscreenVideo];
414 // The timers must not exist at this point, otherwise this object will leak.
415 DCHECK(!_continuousCheckTimer);
416 DCHECK(!_lowerFrequencyTimer);
419 - (void)childWindowClosed:(NSString*)windowName {
420 // Get the substring of the window name after the hash.
421 NSRange range = [windowName rangeOfString:
422 base::SysUTF8ToNSString(web::kWindowNameSeparator)];
423 if (range.location != NSNotFound) {
424 NSString* target = [windowName substringFromIndex:(range.location + 1)];
425 [self stringByEvaluatingJavaScriptFromString:
426 [NSString stringWithFormat:@"__gCrWeb.windowClosed('%@');", target]];
431 #pragma mark Testing-Only Methods
433 - (void)injectWebViewContentView:(CRWWebViewContentView*)webViewContentView {
434 [super injectWebViewContentView:webViewContentView];
435 [self setWebView:static_cast<UIWebView*>(webViewContentView.webView)];
438 #pragma mark CRWJSInjectionEvaluatorMethods
440 - (void)evaluateJavaScript:(NSString*)script
441 stringResultHandler:(web::JavaScriptCompletion)handler {
442 NSString* safeScript = [self scriptByAddingWindowIDCheckForScript:script];
443 web::EvaluateJavaScript(_uiWebView, safeScript, handler);
446 - (web::WebViewType)webViewType {
447 return web::UI_WEB_VIEW_TYPE;
450 #pragma mark - Protected property implementations
453 return _uiWebView.get();
456 - (UIScrollView*)webScrollView {
457 return [_uiWebView scrollView];
460 - (BOOL)urlCachingEnabled {
461 return _urlCachingEnabled;
464 - (void)setURLCachingEnabled:(BOOL)enabled {
465 if (enabled == _urlCachingEnabled)
467 _urlCachingEnabled = enabled;
468 _cachedURL.first = GURL();
471 - (BOOL)ignoreURLVerificationFailures {
472 return _spoofableRequest;
476 return [self stringByEvaluatingJavaScriptFromString:
477 @"document.title.length ? document.title : ''"];
480 #pragma mark Protected method implementations
482 - (void)ensureWebViewCreated {
484 [self setWebView:[self createWebView]];
485 // Notify super class about created web view. -webViewDidChange is not
486 // called from -setWebView: as the latter used in unit tests with fake
487 // web view, which cannot be added to view hierarchy.
488 [self webViewDidChange];
492 - (void)resetWebView {
493 [self setWebView:nil];
496 - (GURL)webURLWithTrustLevel:(web::URLVerificationTrustLevel*)trustLevel {
497 if (_cachedURL.first.is_valid()) {
498 *trustLevel = _cachedURL.second;
499 return _cachedURL.first;
501 ScopedReentryGuard reentryGuard(&_inJavaScriptContext);
503 [CRWURLVerifyingProtocolHandler currentURLForWebView:_uiWebView
504 trustLevel:trustLevel];
506 // If verification succeeded, or the URL has changed, then the UIWebView is no
507 // longer in the initial state.
508 if (*trustLevel != web::URLVerificationTrustLevel::kNone ||
509 url != GURL(url::kAboutBlankURL))
510 _spoofableRequest = NO;
512 if (*trustLevel == web::URLVerificationTrustLevel::kAbsolute) {
513 _lastCorrectURLTime = base::TimeTicks::Now();
514 if (self.urlCachingEnabled) {
515 _cachedURL.first = url;
516 _cachedURL.second = *trustLevel;
518 } else if (*trustLevel == web::URLVerificationTrustLevel::kNone &&
519 (base::TimeTicks::Now() - _lastCorrectURLTime) <
520 base::TimeDelta::FromMilliseconds(
521 web::kContinuousCheckIntervalMSLow)) {
522 // URL is not trusted, but the last time it was trusted is within
523 // kContinuousCheckIntervalMSLow.
524 *trustLevel = web::URLVerificationTrustLevel::kMixed;
530 - (void)registerUserAgent {
531 web::BuildAndRegisterUserAgentForUIWebView(
532 self.webStateImpl->GetRequestGroupID(),
533 [self useDesktopUserAgent]);
536 - (BOOL)isCurrentNavigationItemPOST {
537 DCHECK([self currentSessionEntry]);
538 NSData* currentPOSTData =
539 [self currentSessionEntry].navigationItemImpl->GetPostData();
540 return currentPOSTData != nil;
543 // The core.js cannot pass messages back to obj-c if it is injected
544 // to |WEB_VIEW_DOCUMENT| because it does not support iframe creation used
545 // by core.js to communicate back. That functionality is only supported
546 // by |WEB_VIEW_HTML_DOCUMENT|. |WEB_VIEW_DOCUMENT| is used when displaying
547 // non-HTML contents (e.g. PDF documents).
548 - (web::WebViewDocumentType)webViewDocumentType {
549 // This happens during tests.
551 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
553 NSString* documentType =
554 [_uiWebView stringByEvaluatingJavaScriptFromString:
556 if ([documentType isEqualToString:@"[object HTMLDocument]"])
557 return web::WEB_VIEW_DOCUMENT_TYPE_HTML;
558 else if ([documentType isEqualToString:@"[object Document]"])
559 return web::WEB_VIEW_DOCUMENT_TYPE_GENERIC;
560 return web::WEB_VIEW_DOCUMENT_TYPE_UNKNOWN;
563 - (void)loadRequest:(NSMutableURLRequest*)request {
564 DCHECK(web::GetWebClient());
565 GURL requestURL = net::GURLWithNSURL(request.URL);
566 // If the request is for WebUI, add information to let the network stack
567 // access the requestGroupID.
568 if (web::GetWebClient()->IsAppSpecificURL(requestURL)) {
569 // Sub requests of a chrome:// page will not contain the user agent.
570 // Instead use the username part of the URL to allow the network stack to
571 // associate a request to the correct tab.
572 request.URL = web::AddRequestGroupIDToURL(
573 request.URL, self.webStateImpl->GetRequestGroupID());
575 [_uiWebView loadRequest:request];
578 - (void)loadWebHTMLString:(NSString*)html forURL:(const GURL&)URL {
579 [_uiWebView loadHTMLString:html baseURL:net::NSURLWithGURL(URL)];
582 - (BOOL)scriptHasBeenInjectedForClass:(Class)jsInjectionManagerClass
583 presenceBeacon:(NSString*)beacon {
584 NSString* beaconCheckScript = [NSString stringWithFormat:
585 @"try { typeof %@; } catch (e) { 'undefined'; }", beacon];
587 [self stringByEvaluatingJavaScriptFromString:beaconCheckScript];
588 return [result isEqualToString:@"object"];
591 - (void)injectScript:(NSString*)script forClass:(Class)JSInjectionManagerClass {
592 // Skip evaluation if there's no content (e.g., if what's being injected is
593 // an umbrella manager).
594 if ([script length]) {
595 [super injectScript:script forClass:JSInjectionManagerClass];
596 [self stringByEvaluatingJavaScriptFromString:script];
600 - (void)willLoadCurrentURLInWebView {
603 // This code uses non-documented API, but is not compiled in release.
604 id documentView = [_uiWebView valueForKey:@"documentView"];
605 id webView = [documentView valueForKey:@"webView"];
606 NSString* userAgent = [webView performSelector:@selector(userAgentForURL:)
609 const bool wrongRequestGroupID =
610 ![self.webStateImpl->GetRequestGroupID()
611 isEqualToString:web::ExtractRequestGroupIDFromUserAgent(userAgent)];
612 DLOG_IF(ERROR, wrongRequestGroupID) << "Incorrect user agent in UIWebView";
614 #endif // !defined(NDEBUG)
617 - (void)loadRequestForCurrentNavigationItem {
618 DCHECK(self.webView && !self.nativeController);
619 NSMutableURLRequest* request = [self requestForCurrentNavigationItem];
621 ProceduralBlock GETBlock = ^{
622 [self registerLoadRequest:[self currentNavigationURL]
623 referrer:[self currentSessionEntryReferrer]
624 transition:[self currentTransition]];
625 [self loadRequest:request];
628 // If there is no POST data, load the request as a GET right away.
629 DCHECK([self currentSessionEntry]);
630 web::NavigationItemImpl* currentItem =
631 [self currentSessionEntry].navigationItemImpl;
632 NSData* POSTData = currentItem->GetPostData();
638 ProceduralBlock POSTBlock = ^{
639 [request setHTTPMethod:@"POST"];
640 [request setHTTPBody:POSTData];
641 [request setAllHTTPHeaderFields:[self currentHTTPHeaders]];
642 [self registerLoadRequest:[self currentNavigationURL]
643 referrer:[self currentSessionEntryReferrer]
644 transition:[self currentTransition]];
645 [self loadRequest:request];
648 // If POST data is empty or the user does not need to confirm,
649 // load the request right away.
650 if (!POSTData.length || currentItem->ShouldSkipResubmitDataConfirmation()) {
655 // Prompt the user to confirm the POST request.
656 [self.delegate webController:self
657 onFormResubmissionForRequest:request
658 continueBlock:POSTBlock
659 cancelBlock:GETBlock];
662 - (void)setPageChangeProbability:(web::PageChangeProbability)probability {
663 if (probability == web::PAGE_CHANGE_PROBABILITY_LOW) {
664 // Reduce check interval to precautionary frequency.
665 [self setContinuousCheckTimerInterval:
666 base::TimeDelta::FromMilliseconds(web::kContinuousCheckIntervalMSLow)];
668 // Increase the timer frequency, as a window change is anticipated shortly.
669 [self setContinuousCheckTimerInterval:
670 base::TimeDelta::FromMilliseconds(web::kContinuousCheckIntervalMSHigh)];
672 if (probability != web::PAGE_CHANGE_PROBABILITY_VERY_HIGH) {
673 // The timer frequency is automatically lowered after a set duration in
674 // case the guess was wrong, to avoid wedging in high-frequency mode.
675 base::Closure closure = base::BindBlock(^{
676 [self setContinuousCheckTimerInterval:
677 base::TimeDelta::FromMilliseconds(
678 web::kContinuousCheckIntervalMSLow)];
680 _lowerFrequencyTimer.reset(
681 new base::Timer(FROM_HERE,
682 base::TimeDelta::FromMilliseconds(
683 web::kContinuousCheckHighFrequencyMSMaxDuration),
684 closure, false /* not repeating */));
685 _lowerFrequencyTimer->Reset();
690 - (BOOL)checkForUnexpectedURLChange {
691 // The check makes no sense without an active web view.
695 // Change to UIWebView default page is not considered a 'real' change and
696 // URL changes are not reported.
697 if ([self isDefaultPage])
700 // Check if currentURL is unexpected (not the incoming page).
701 // This is necessary to notice page changes if core.js injection is disabled
702 // by a malicious page.
703 if (!self.URLOnStartLoading.is_empty() &&
704 [self currentURL] == self.URLOnStartLoading) {
708 // If the URL has changed, handle page load mechanics.
709 ScopedCachedCurrentUrl scopedCurrentURL(self);
710 [self webPageChanged];
711 [self checkDocumentLoaded];
712 [self titleDidChange];
717 - (void)abortWebLoad {
718 // Current load will not complete; this should be communicated upstream to
719 // the delegate, and flagged in the WebView so further messages can be
720 // prevented (which may be confused for messages from newer pages).
721 [_uiWebView stringByEvaluatingJavaScriptFromString:
722 @"document._cancelled = true;"];
723 [_uiWebView stopLoading];
726 - (void)resetLoadState {
731 - (void)setSuppressDialogsWithHelperScript:(NSString*)script {
732 [self stringByEvaluatingJavaScriptFromString:script];
735 - (void)checkDocumentLoaded {
737 [self stringByEvaluatingJavaScriptFromString:
738 @"document.readyState === 'loaded' || "
739 "document.readyState === 'complete'"];
740 if ([loaded isEqualToString:@"true"]) {
741 [self didFinishNavigation];
745 - (NSString*)currentReferrerString {
746 return [self stringByEvaluatingJavaScriptFromString:@"document.referrer"];
749 - (void)titleDidChange {
750 if (![self.delegate respondsToSelector:
751 @selector(webController:titleDidChange:)]) {
755 // Checking the URL trust level is expensive. For performance reasons, the
756 // current URL and the trust level cache must be enabled.
757 // NOTE: Adding a ScopedCachedCurrentUrl here is not the right way to solve
759 DCHECK(self.urlCachingEnabled);
761 // Change to UIWebView default page is not considered a 'real' change and
762 // title changes are not reported.
763 if ([self isDefaultPage])
766 // The title can be retrieved from the document only if the URL can be
768 web::URLVerificationTrustLevel trustLevel =
769 web::URLVerificationTrustLevel::kNone;
770 [self currentURLWithTrustLevel:&trustLevel];
771 if (trustLevel != web::URLVerificationTrustLevel::kAbsolute)
774 NSString* title = self.title;
776 [self.delegate webController:self titleDidChange:title];
779 - (void)teminateNetworkActivity {
780 [super terminateNetworkActivity];
781 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
784 - (void)fetchWebPageSizeWithCompletionHandler:(void(^)(CGSize))handler {
790 // Ensure that JavaScript has been injected.
791 [self.recurringTaskDelegate runRecurringTask];
792 [super fetchWebPageSizeWithCompletionHandler:handler];
795 - (void)documentPresent {
796 if (self.loadPhase != web::PAGE_LOADED &&
797 self.loadPhase != web::LOAD_REQUESTED) {
801 ScopedCachedCurrentUrl scopedCurrentURL(self);
803 // This is a good time to check if the URL has changed.
804 BOOL urlChanged = [self checkForUnexpectedURLChange];
806 // This is a good time to check if the page has refreshed.
807 if (!urlChanged && self.windowId != self.lastSeenWindowID)
808 [self webPageChanged];
810 // Set initial title.
811 [self titleDidChange];
814 - (void)webPageChanged {
815 if (self.loadPhase != web::LOAD_REQUESTED ||
816 self.lastRegisteredRequestURL.is_empty() ||
817 self.lastRegisteredRequestURL != [self currentURL]) {
818 // The page change was unexpected (not already messaged to
819 // webWillStartLoadingURL), so fill in the load request.
820 [self generateMissingLoadRequestWithURL:[self currentURL]
821 referrer:[self currentReferrer]];
824 [super webPageChanged];
827 - (void)applyWebViewScrollZoomScaleFromZoomState:
828 (const web::PageZoomState&)zoomState {
829 // A UIWebView's scroll view uses zoom scales in a non-standard way. The
830 // scroll view's |zoomScale| property is always equal to 1.0, and the
831 // |minimumZoomScale| and |maximumZoomScale| properties are adjusted
832 // proportionally to reflect the relative zoom scale. Setting the |zoomScale|
833 // property here scales the page by the value set (i.e. setting zoomScale to
834 // 2.0 will update the zoom to twice its initial scale). The maximum-scale or
835 // minimum-scale meta tags of a page may have changed since the state was
836 // recorded, so clamp the zoom scale to the current range if necessary.
837 DCHECK(zoomState.IsValid());
838 CGFloat zoomScale = zoomState.IsLegacyFormat()
839 ? zoomState.zoom_scale()
840 : self.webScrollView.minimumZoomScale /
841 zoomState.minimum_zoom_scale();
842 if (zoomScale < self.webScrollView.minimumZoomScale)
843 zoomScale = self.webScrollView.minimumZoomScale;
844 if (zoomScale > self.webScrollView.maximumZoomScale)
845 zoomScale = self.webScrollView.maximumZoomScale;
846 self.webScrollView.zoomScale = zoomScale;
849 - (void)handleCancelledError:(NSError*)error {
850 // NSURLErrorCancelled errors generated by the Chrome net stack should be
851 // aborted. If the error was generated by the UIWebView, it will not have
852 // an underlying net error and will be automatically retried by the web view.
853 DCHECK_EQ(error.code, NSURLErrorCancelled);
854 NSError* underlyingError = base::ios::GetFinalUnderlyingErrorFromError(error);
855 NSString* netDomain = base::SysUTF8ToNSString(net::kErrorDomain);
856 BOOL shouldAbortLoadForCancelledError =
857 [underlyingError.domain isEqualToString:netDomain];
858 if (!shouldAbortLoadForCancelledError)
861 // NSURLCancelled errors with underlying errors are generated from the
862 // Chrome network stack. Abort the load in this case.
865 switch (underlyingError.code) {
866 case net::ERR_ABORTED:
867 // |NSURLErrorCancelled| errors with underlying net error code
868 // |net::ERR_ABORTED| are used by the Chrome network stack to
869 // indicate that the current load should be aborted and the pending
870 // entry should be discarded.
871 [[self sessionController] discardNonCommittedEntries];
873 case net::ERR_BLOCKED_BY_CLIENT:
874 // |NSURLErrorCancelled| errors with underlying net error code
875 // |net::ERR_BLOCKED_BY_CLIENT| are used by the Chrome network stack
876 // to indicate that the current load should be aborted and the pending
877 // entry should be kept.
884 #pragma mark - JS to ObjC messaging
886 - (void)respondToJSInvoke {
887 // This call is asynchronous. If the web view has been removed, there is
888 // nothing left to do, so just discard the queued messages and return.
890 _jsInvokeParameterQueue.reset([[CRWJSInvokeParameterQueue alloc] init]);
893 // Messages are queued and processed asynchronously. However, user
894 // may initiate JavaScript at arbitrary times (e.g. through Omnibox
895 // "javascript:alert('foo')"). This delays processing of queued messages
896 // until JavaScript execution is completed.
897 // TODO(pkl): This should have a unit test or UI Automation test case.
898 // See crbug.com/228125
899 if (_inJavaScriptContext) {
900 [self performSelector:@selector(respondToJSInvoke)
905 DCHECK(_jsInvokeParameterQueue);
906 while (![_jsInvokeParameterQueue isEmpty]) {
907 CRWJSInvokeParameters* parameters =
908 [_jsInvokeParameterQueue popInvokeParameters];
911 // TODO(stuartmorgan): Some messages (e.g., window.write) should be
912 // processed even if the page has already changed by the time they are
913 // received. crbug.com/228275
914 if ([parameters windowId] != [self windowId]) {
915 // If there is a windowID mismatch, the document has been changed since
916 // messages were added to the queue. Ignore the incoming messages.
917 DLOG(WARNING) << "Messages from JS ignored due to non-matching windowID: "
918 << [[parameters windowId] UTF8String]
919 << " != " << [[self windowId] UTF8String];
922 if (![self respondToMessageQueue:[parameters commandString]
923 userIsInteracting:[parameters userIsInteracting]
924 originURL:[parameters originURL]]) {
925 DLOG(WARNING) << "Messages from JS not handled due to invalid format";
930 - (void)handleWebInvokeURL:(const GURL&)url request:(NSURLRequest*)request {
931 DCHECK([self urlSchemeIsWebInvoke:url]);
932 NSURL* nsurl = request.URL;
933 // TODO(stuartmorgan): Remove the NSURL usage here. Will require a logic
934 // change since GURL doesn't parse non-standard URLs into host and fragment
935 if (![nsurl.host isEqualToString:[self windowId]]) {
936 // If there is a windowID mismatch, we may be under attack from a
937 // malicious page, so a defense is to reset the page.
938 DLOG(WARNING) << "Messages from JS ignored due to non-matching windowID: "
939 << nsurl.host << " != " << [[self windowId] UTF8String];
940 DLOG(WARNING) << "Page reset as security precaution";
941 [self performSelector:@selector(reload) withObject:nil afterDelay:0];
944 if (url.spec().length() > kMaxMessageQueueSize) {
945 DLOG(WARNING) << "Messages from JS ignored due to excessive length";
948 NSString* commandString = [[nsurl fragment]
949 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
951 GURL originURL(net::GURLWithNSURL(request.mainDocumentURL));
953 if (url.SchemeIs("crwebinvokeimmediate")) {
954 [self respondToMessageQueue:commandString
955 userIsInteracting:[self userIsInteracting]
956 originURL:originURL];
958 [_jsInvokeParameterQueue addCommandString:commandString
959 userIsInteracting:[self userIsInteracting]
961 forWindowId:[super windowId]];
962 if (!_jsMessageQueueThrottled) {
963 [self performSelector:@selector(respondToJSInvoke)
970 - (void)setJsMessageQueueThrottled:(BOOL)throttle {
971 _jsMessageQueueThrottled = throttle;
973 [self respondToJSInvoke];
976 - (void)removeDocumentLoadCommandsFromQueue {
977 [_jsInvokeParameterQueue removeCommandString:@"document.present"];
978 [_jsInvokeParameterQueue removeCommandString:@"document.loaded"];
981 - (BOOL)urlSchemeIsWebInvoke:(const GURL&)url {
982 return url.SchemeIs("crwebinvoke") || url.SchemeIs("crwebinvokeimmediate");
985 - (CRWJSInvokeParameterQueue*)jsInvokeParameterQueue {
986 return _jsInvokeParameterQueue;
989 - (BOOL)respondToMessageQueue:(NSString*)messageQueue
990 userIsInteracting:(BOOL)userIsInteracting
991 originURL:(const GURL&)originURL {
992 ScopedCachedCurrentUrl scopedCurrentURL(self);
995 std::string errorMessage;
996 scoped_ptr<base::Value> inputJSONData(base::JSONReader::ReadAndReturnError(
997 base::SysNSStringToUTF8(messageQueue), false, &errorCode, &errorMessage));
999 DLOG(WARNING) << "JSON parse error: %s" << errorMessage.c_str();
1002 // MessageQueues pass messages as a list.
1003 base::ListValue* messages = nullptr;
1004 if (!inputJSONData->GetAsList(&messages)) {
1005 DLOG(WARNING) << "Message queue not a list";
1008 for (size_t idx = 0; idx != messages->GetSize(); ++idx) {
1009 // The same-origin check has to be done for every command to mitigate the
1010 // risk of command sequences where the first command would change the page
1011 // and the subsequent commands would have unlimited access to it.
1012 if (originURL.GetOrigin() != self.currentURL.GetOrigin()) {
1013 DLOG(WARNING) << "Message source URL origin: " << originURL.GetOrigin()
1014 << " does not match current URL origin: "
1015 << self.currentURL.GetOrigin();
1019 base::DictionaryValue* message = nullptr;
1020 if (!messages->GetDictionary(idx, &message)) {
1021 DLOG(WARNING) << "Message could not be retrieved";
1024 BOOL messageHandled = [self respondToMessage:message
1025 userIsInteracting:userIsInteracting
1026 originURL:originURL];
1027 if (!messageHandled)
1030 // If handling the message caused this page to be closed, stop processing
1032 // TODO(stuartmorgan): Ideally messages should continue to be handled until
1033 // the end of the event loop (e.g., window.close(); window.open(...);
1034 // should do both things). That would require knowing which messages came
1035 // in the same event loop, however.
1036 if ([self isBeingDestroyed])
1042 - (SEL)selectorToHandleJavaScriptCommand:(const std::string&)command {
1043 static std::map<std::string, SEL>* handlers = nullptr;
1044 static dispatch_once_t onceToken;
1045 dispatch_once(&onceToken, ^{
1046 handlers = new std::map<std::string, SEL>();
1047 (*handlers)["anchor.click"] = @selector(handleAnchorClickMessage:context:);
1048 (*handlers)["document.loaded"] =
1049 @selector(handleDocumentLoadedMessage:context:);
1050 (*handlers)["document.present"] =
1051 @selector(handleDocumentPresentMessage:context:);
1052 (*handlers)["document.retitled"] =
1053 @selector(handleDocumentRetitledMessage:context:);
1054 (*handlers)["window.close"] = @selector(handleWindowCloseMessage:context:);
1055 (*handlers)["window.document.write"] =
1056 @selector(handleWindowDocumentWriteMessage:context:);
1057 (*handlers)["window.location"] =
1058 @selector(handleWindowLocationMessage:context:);
1059 (*handlers)["window.open"] = @selector(handleWindowOpenMessage:context:);
1060 (*handlers)["window.stop"] = @selector(handleWindowStopMessage:context:);
1061 (*handlers)["window.unload"] =
1062 @selector(handleWindowUnloadMessage:context:);
1065 auto iter = handlers->find(command);
1066 return iter != handlers->end()
1068 : [super selectorToHandleJavaScriptCommand:command];
1071 - (NSString*)windowNameFromMessage:(base::DictionaryValue*)message
1072 context:(NSDictionary*)context {
1074 if(!message->GetString("target", &target)) {
1075 DLOG(WARNING) << "JS message parameter not found: target";
1079 DCHECK(context[web::kOriginURLKey]);
1080 const GURL& originURL = net::GURLWithNSURL(context[web::kOriginURLKey]);
1082 // Unique string made for page/target combination.
1083 // Safe to delimit unique string with # since page references won't
1085 return base::SysUTF8ToNSString(
1086 NormalizedUrl(originURL) + web::kWindowNameSeparator + target);
1090 #pragma mark JavaScript message handlers
1092 - (BOOL)handleAnchorClickMessage:(base::DictionaryValue*)message
1093 context:(NSDictionary*)context {
1094 // Reset the external click request.
1095 [self resetExternalRequest];
1098 if (!message->GetString("href", &href)) {
1099 DLOG(WARNING) << "JS message parameter not found: href";
1102 const GURL targetURL(href);
1103 const GURL currentURL([self currentURL]);
1104 if (currentURL != targetURL) {
1105 if (web::UrlHasWebScheme(targetURL)) {
1106 // The referrer is not known yet, and will be updated later.
1107 const web::Referrer emptyReferrer;
1108 [self registerLoadRequest:targetURL
1109 referrer:emptyReferrer
1110 transition:ui::PAGE_TRANSITION_LINK];
1111 [self setPageChangeProbability:web::PAGE_CHANGE_PROBABILITY_HIGH];
1112 } else if (web::GetWebClient()->IsAppSpecificURL(targetURL) &&
1113 web::GetWebClient()->IsAppSpecificURL(currentURL)) {
1114 // Allow navigations between app-specific URLs
1115 [self removeWebViewAllowingCachedReconstruction:NO];
1116 ui::PageTransition pageTransitionLink =
1117 ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK);
1118 const web::Referrer referrer(currentURL, web::ReferrerPolicyDefault);
1119 web::WebState::OpenURLParams openParams(targetURL, referrer, CURRENT_TAB,
1120 pageTransitionLink, true);
1121 [self.delegate openURLWithParams:openParams];
1127 - (BOOL)handleDocumentLoadedMessage:(base::DictionaryValue*)message
1128 context:(NSDictionary*)context {
1129 // Very early hashchange events can be missed, hence this extra explicit
1131 [self checkForUnexpectedURLChange];
1132 [self didFinishNavigation];
1136 - (BOOL)handleDocumentPresentMessage:(base::DictionaryValue*)message
1137 context:(NSDictionary*)context {
1138 NSString* documentCancelled =
1139 [self stringByEvaluatingJavaScriptFromString:@"document._cancelled"];
1140 if (![documentCancelled isEqualToString:@"true"])
1141 [self documentPresent];
1145 - (BOOL)handleDocumentRetitledMessage:(base::DictionaryValue*)message
1146 context:(NSDictionary*)context {
1147 [self titleDidChange];
1151 - (BOOL)handleWindowCloseMessage:(base::DictionaryValue*)message
1152 context:(NSDictionary*)context {
1153 NSString* windowName = [self windowNameFromMessage:message
1157 [[self scriptingInterfaceForWindowNamed:windowName] orderClose];
1161 - (BOOL)handleWindowDocumentWriteMessage:(base::DictionaryValue*)message
1162 context:(NSDictionary*)context {
1163 NSString* windowName = [self windowNameFromMessage:message
1168 if (!message->GetString("html", &HTML)) {
1169 DLOG(WARNING) << "JS message parameter not found: html";
1172 [[self scriptingInterfaceForWindowNamed:windowName]
1173 loadHTML:base::SysUTF8ToNSString(HTML)];
1177 - (BOOL)handleWindowLocationMessage:(base::DictionaryValue*)message
1178 context:(NSDictionary*)context {
1179 NSString* windowName = [self windowNameFromMessage:message
1183 std::string command;
1184 if (!message->GetString("command", &command)) {
1185 DLOG(WARNING) << "JS message parameter not found: command";
1189 if (!message->GetString("value", &value)) {
1190 DLOG(WARNING) << "JS message parameter not found: value";
1193 std::string escapedValue;
1194 base::EscapeJSONString(value, true, &escapedValue);
1196 [NSString stringWithFormat:@"<script>%s = %s;</script>",
1198 escapedValue.c_str()];
1199 [[self scriptingInterfaceForWindowNamed:windowName] loadHTML:HTML];
1203 - (BOOL)handleWindowOpenMessage:(base::DictionaryValue*)message
1204 context:(NSDictionary*)context {
1205 NSString* windowName = [self windowNameFromMessage:message
1209 std::string targetURL;
1210 if (!message->GetString("url", &targetURL)) {
1211 DLOG(WARNING) << "JS message parameter not found: url";
1214 std::string referrerPolicy;
1215 if (!message->GetString("referrerPolicy", &referrerPolicy)) {
1216 DLOG(WARNING) << "JS message parameter not found: referrerPolicy";
1219 GURL resolvedURL = targetURL.empty() ?
1221 GURL(net::GURLWithNSURL(context[web::kOriginURLKey])).Resolve(targetURL);
1222 DCHECK(&resolvedURL);
1224 windowInfo(resolvedURL,
1226 [self referrerPolicyFromString:referrerPolicy],
1227 [context[web::kUserIsInteractingKey] boolValue]);
1229 [self openPopupWithInfo:windowInfo];
1233 - (BOOL)handleWindowStopMessage:(base::DictionaryValue*)message
1234 context:(NSDictionary*)context {
1235 NSString* windowName = [self windowNameFromMessage:message
1239 [[self scriptingInterfaceForWindowNamed:windowName] stopLoading];
1243 - (BOOL)handleWindowUnloadMessage:(base::DictionaryValue*)message
1244 context:(NSDictionary*)context {
1245 [self setPageChangeProbability:web::PAGE_CHANGE_PROBABILITY_VERY_HIGH];
1249 #pragma mark Private methods
1251 - (BOOL)isDefaultPage {
1252 if ([[self stringByEvaluatingJavaScriptFromString:@"document._defaultPage"]
1253 isEqualToString:@"true"]) {
1254 return self.currentURL == self.defaultURL;
1259 - (BOOL)isLinkNavigation:(UIWebViewNavigationType)navigationType {
1260 switch (navigationType) {
1261 case UIWebViewNavigationTypeLinkClicked:
1263 case UIWebViewNavigationTypeOther:
1264 return [self userClickedRecently];
1270 - (void)setContinuousCheckTimerInterval:(const base::TimeDelta&)interval {
1271 // The timer should never be set when there's no web view.
1274 BOOL shouldStartTimer =
1275 !_continuousCheckTimer.get() || _continuousCheckTimer->IsRunning();
1276 base::Closure closure = base::BindBlock(^{
1277 // Only perform JS checks if CRWWebController is not already in JavaScript
1278 // context. This is possible when "javascript:..." is executed from
1279 // Omnibox and this block is run from the timer.
1280 if (!_inJavaScriptContext)
1281 [self.recurringTaskDelegate runRecurringTask];
1283 _continuousCheckTimer.reset(
1284 new base::Timer(FROM_HERE, interval, closure, true));
1285 if (shouldStartTimer)
1286 _continuousCheckTimer->Reset();
1287 if (_lowerFrequencyTimer &&
1288 interval == base::TimeDelta::FromMilliseconds(
1289 web::kContinuousCheckIntervalMSLow)) {
1290 _lowerFrequencyTimer.reset();
1294 - (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script {
1298 ScopedReentryGuard reentryGuard(&_inJavaScriptContext);
1299 return [_uiWebView stringByEvaluatingJavaScriptFromString:script];
1302 - (NSString*)stringByEvaluatingUserJavaScriptFromString:(NSString*)script {
1303 [self setUserInteractionRegistered:YES];
1304 return [self stringByEvaluatingJavaScriptFromString:script];
1307 - (UIWebView*)createWebView {
1308 UIWebView* webView = web::CreateWebView(
1310 self.webStateImpl->GetRequestGroupID(),
1311 [self useDesktopUserAgent]);
1313 // Mark the document object of the default page as such, so that it is not
1314 // mistaken for a 'real' page by change detection mechanisms.
1315 [webView stringByEvaluatingJavaScriptFromString:
1316 @"document._defaultPage = true;"];
1318 [webView setScalesPageToFit:YES];
1319 // Turn off data-detectors. MobileSafari does the same thing.
1320 [webView setDataDetectorTypes:UIDataDetectorTypeNone];
1322 return [webView autorelease];
1325 - (void)setWebView:(UIWebView*)webView {
1326 DCHECK_NE(_uiWebView.get(), webView);
1327 // Per documentation, must clear the delegate before releasing the UIWebView
1328 // to avoid errant dangling pointers.
1329 [_uiWebView setDelegate:nil];
1330 _uiWebView.reset([webView retain]);
1331 [_uiWebView setDelegate:self];
1332 // Clear out the trusted URL cache.
1333 _lastCorrectURLTime = base::TimeTicks();
1334 _cachedURL.first = GURL();
1335 // Reset the spoofable state (see declaration comment).
1336 // TODO(stuartmorgan): Fix the fact that there's no guarantee that no
1337 // navigation has happened before the UIWebView is set here (ideally by
1338 // unifying the creation and setting flow).
1339 _spoofableRequest = YES;
1342 _inJavaScriptContext = NO;
1343 // Do initial injection even before loading another page, since the window
1344 // object is re-used.
1345 [self injectEarlyInjectionScripts];
1347 _continuousCheckTimer.reset();
1348 // This timer exists only to change the frequency of the main timer, so it
1349 // should not outlive the main timer.
1350 _lowerFrequencyTimer.reset();
1354 - (void)generateMissingDocumentLifecycleEvents {
1355 // The webView can be removed between this method being queued and invoked.
1358 if ([self webViewDocumentType] == web::WEB_VIEW_DOCUMENT_TYPE_GENERIC) {
1359 [self documentPresent];
1360 [self didFinishNavigation];
1364 - (void)generateMissingLoadRequestWithURL:(const GURL&)currentURL
1365 referrer:(const web::Referrer&)referrer {
1366 [self loadCancelled];
1367 // Initialize transition based on whether the request is user-initiated or
1368 // not. This is a best guess to replace lost transition type informationj.
1369 ui::PageTransition transition = self.userInteractionRegistered
1370 ? ui::PAGE_TRANSITION_LINK
1371 : ui::PAGE_TRANSITION_CLIENT_REDIRECT;
1372 // If the URL agrees with session state, use the session's transition.
1373 if (currentURL == [self currentNavigationURL]) {
1374 transition = [self currentTransition];
1377 [self registerLoadRequest:currentURL referrer:referrer transition:transition];
1380 - (id<CRWWebControllerScripting>)scriptingInterfaceForWindowNamed:
1382 if (![self.delegate respondsToSelector:
1383 @selector(webController:scriptingInterfaceForWindowNamed:)]) {
1386 return [self.delegate webController:self
1387 scriptingInterfaceForWindowNamed:name];
1390 #pragma mark FullscreenVideo
1392 - (void)moviePlayerDidEnterFullscreen:(NSNotification*)notification {
1393 _inFullscreenVideo = YES;
1396 - (void)moviePlayerDidExitFullscreen:(NSNotification*)notification {
1397 _inFullscreenVideo = NO;
1400 - (void)exitFullscreenVideo {
1401 [self stringByEvaluatingJavaScriptFromString:
1402 @"__gCrWeb.exitFullscreenVideo();"];
1406 #pragma mark CRWRecurringTaskDelegate
1408 // Checks for page changes are made continuously.
1409 - (void)runRecurringTask {
1413 [self injectEarlyInjectionScripts];
1414 [self checkForUnexpectedURLChange];
1418 #pragma mark CRWRedirectClientDelegate
1420 - (void)wasRedirectedToRequest:(NSURLRequest*)request
1421 redirectResponse:(NSURLResponse*)response {
1422 // This callback can be received after -close is called; ignore it.
1423 if (self.isBeingDestroyed)
1426 // Register the redirected load request if it originated from the main page
1428 GURL redirectedURL = net::GURLWithNSURL(response.URL);
1429 if ([self currentNavigationURL] == redirectedURL) {
1430 [self registerLoadRequest:net::GURLWithNSURL(request.URL)
1431 referrer:[self currentReferrer]
1432 transition:ui::PAGE_TRANSITION_SERVER_REDIRECT];
1437 #pragma mark UIWebViewDelegate Methods
1439 // Called when a load begins, and for subsequent subpages.
1440 - (BOOL)webView:(UIWebView*)webView
1441 shouldStartLoadWithRequest:(NSURLRequest*)request
1442 navigationType:(UIWebViewNavigationType)navigationType {
1443 DVLOG(5) << "webViewShouldStartLoadWithRequest "
1444 << net::FormatUrlRequestForLogging(request);
1446 if (self.isBeingDestroyed)
1449 GURL url = net::GURLWithNSURL(request.URL);
1451 // The crwebnull protocol is used where an element requires a URL but it
1452 // should not trigger any activity on the WebView.
1453 if (url.SchemeIs("crwebnull"))
1456 if ([self urlSchemeIsWebInvoke:url]) {
1457 [self handleWebInvokeURL:url request:request];
1461 // ##### IMPORTANT NOTE #####
1462 // Do not add new code above this line unless you're certain about what you're
1463 // doing with respect to JS re-entry.
1464 ScopedReentryGuard javaScriptReentryGuard(&_inJavaScriptContext);
1465 web::FrameInfo* targetFrame = nullptr; // No reliable way to get this info.
1466 BOOL isLinkClick = [self isLinkNavigation:navigationType];
1467 return [self shouldAllowLoadWithRequest:request
1468 targetFrame:targetFrame
1469 isLinkClick:isLinkClick];
1472 // Called at multiple points during a load, such as at the start of loading a
1473 // page, and every time an iframe loads. Not called again for server-side
1475 - (void)webViewDidStartLoad:(UIWebView *)webView {
1476 NSURLRequest* request = webView.request;
1477 DVLOG(5) << "webViewDidStartLoad "
1478 << net::FormatUrlRequestForLogging(request);
1479 // |webView:shouldStartLoad| may not be called or called with different URL
1480 // and mainDocURL for the request in certain page navigations. There
1481 // are at least 2 known page navigations where this occurs, in these cases it
1482 // is imperative the URL verification timer is started here.
1483 // The 2 known cases are:
1484 // 1) A malicious page suppressing core.js injection and calling
1485 // window.history.back() or window.history.forward()
1486 // 2) An iframe loading a URL using target=_blank.
1487 // TODO(shreyasv): crbug.com/349155. Understand further why this happens
1488 // in some case and not in others.
1489 if (webView != self.webView) {
1490 // This happens sometimes as tests are brought down.
1491 // TODO(jimblackler): work out why and fix the problem at source.
1492 LOG(WARNING) << " UIWebViewDelegate message received for inactive WebView.";
1495 DCHECK(!self.isBeingDestroyed);
1496 // Increment the number of pending loads. This will be balanced by either
1497 // a |-webViewDidFinishLoad:| or |-webView:didFailLoadWithError:|.
1499 [self.recurringTaskDelegate runRecurringTask];
1502 // Called when the page (or one of its subframes) finishes loading. This is
1503 // called multiple times during a page load, with varying frequency depending
1504 // on the action (going back, loading a page with frames, redirecting).
1505 // See http://goto/efrmm for a summary of why this is so painful.
1506 - (void)webViewDidFinishLoad:(UIWebView*)webView {
1507 DVLOG(5) << "webViewDidFinishLoad "
1508 << net::FormatUrlRequestForLogging(webView.request);
1509 DCHECK(!self.isHalted);
1510 // Occasionally this delegate is invoked as a side effect during core.js
1511 // injection. It is necessary to ensure we do not attempt to start the
1512 // injection process a second time.
1513 if (!_inJavaScriptContext)
1514 [self.recurringTaskDelegate runRecurringTask];
1516 [self performSelector:@selector(generateMissingDocumentLifecycleEvents)
1521 if ((_loadCount == _unloadCount) && (self.loadPhase != web::LOAD_REQUESTED))
1522 [self checkDocumentLoaded];
1525 // Called when there is an error loading the page. Some errors aren't actual
1526 // errors, but are caused by user actions such as stopping a page load
1528 - (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
1529 DVLOG(5) << "webViewDidFailLoadWithError "
1530 << net::FormatUrlRequestForLogging(webView.request);
1533 // Under unknown circumstances navigation item can be null. In that case the
1534 // state of web/ will not be valid and app will crash. Early return avoid a
1535 // crash (crbug.com/411912).
1536 if (!self.webStateImpl ||
1537 !self.webStateImpl->GetNavigationManagerImpl().GetVisibleItem()) {
1541 // There's no reliable way to know if a load is for the main frame, so make a
1542 // best-effort guess.
1543 // |_loadCount| is reset to 0 before starting loading a new page, and is
1544 // incremented in each call to |-webViewDidStartLoad:|. The main request
1545 // is the first one to be loaded, and thus has a |_loadCount| of 1.
1546 // Sub-requests have a |_loadCount| > 1.
1547 // An iframe loading after the main page also has a |_loadCount| of 1, as
1548 // |_loadCount| is reset at the end of the main page load. In that case,
1549 // |loadPhase_| is web::PAGE_LOADED (as opposed to web::PAGE_LOADING for a
1551 const bool isMainFrame = (_loadCount == 1 &&
1552 self.loadPhase != web::PAGE_LOADED);
1553 [self handleLoadError:error inMainFrame:isMainFrame];
1557 #pragma mark Testing methods
1559 -(id<CRWRecurringTaskDelegate>)recurringTaskDelegate {
1560 return _recurringTaskDelegate;