diff options
Diffstat (limited to 'iPhone/CordovaLib/Classes/CDVWebViewDelegate.m')
-rwxr-xr-x | iPhone/CordovaLib/Classes/CDVWebViewDelegate.m | 315 |
1 files changed, 269 insertions, 46 deletions
diff --git a/iPhone/CordovaLib/Classes/CDVWebViewDelegate.m b/iPhone/CordovaLib/Classes/CDVWebViewDelegate.m index 9ee8186..7254c2e 100755 --- a/iPhone/CordovaLib/Classes/CDVWebViewDelegate.m +++ b/iPhone/CordovaLib/Classes/CDVWebViewDelegate.m @@ -17,14 +17,77 @@ under the License. */ +// +// Testing shows: +// +// In all cases, webView.request.URL is the previous page's URL (or empty) during the didStartLoad callback. +// When loading a page with a redirect: +// 1. shouldStartLoading (requestURL is target page) +// 2. didStartLoading +// 3. shouldStartLoading (requestURL is redirect target) +// 4. didFinishLoad (request.URL is redirect target) +// +// Note the lack of a second didStartLoading ** +// +// When loading a page with iframes: +// 1. shouldStartLoading (requestURL is main page) +// 2. didStartLoading +// 3. shouldStartLoading (requestURL is one of the iframes) +// 4. didStartLoading +// 5. didFinishLoad +// 6. didFinishLoad +// +// Note there is no way to distinguish which didFinishLoad maps to which didStartLoad ** +// +// Loading a page by calling window.history.go(-1): +// 1. didStartLoading +// 2. didFinishLoad +// +// Note the lack of a shouldStartLoading call ** +// Actually - this is fixed on iOS6. iOS6 has a shouldStart. ** +// +// Loading a page by calling location.reload() +// 1. shouldStartLoading +// 2. didStartLoading +// 3. didFinishLoad +// +// Loading a page with an iframe that fails to load: +// 1. shouldStart (main page) +// 2. didStart +// 3. shouldStart (iframe) +// 4. didStart +// 5. didFailWithError +// 6. didFinish +// +// Loading a page with an iframe that fails to load due to an invalid URL: +// 1. shouldStart (main page) +// 2. didStart +// 3. shouldStart (iframe) +// 5. didFailWithError +// 6. didFinish +// +// This case breaks our logic since there is a missing didStart. To prevent this, +// we check URLs in shouldStart and return NO if they are invalid. +// +// Loading a page with an invalid URL +// 1. shouldStart (main page) +// 2. didFailWithError +// +// TODO: Record order when page is re-navigated before the first navigation finishes. +// + #import "CDVWebViewDelegate.h" #import "CDVAvailability.h" +// #define VerboseLog NSLog +#define VerboseLog(...) do {} while (0) + typedef enum { - STATE_NORMAL, - STATE_SHOULD_LOAD_MISSING, - STATE_WAITING_FOR_START, - STATE_WAITING_FOR_FINISH + STATE_IDLE, + STATE_WAITING_FOR_LOAD_START, + STATE_WAITING_FOR_LOAD_FINISH, + STATE_IOS5_POLLING_FOR_LOAD_START, + STATE_IOS5_POLLING_FOR_LOAD_FINISH } State; @implementation CDVWebViewDelegate @@ -35,11 +98,50 @@ typedef enum { if (self != nil) { _delegate = delegate; _loadCount = -1; - _state = STATE_NORMAL; + _state = STATE_IDLE; } return self; } +- (BOOL)request:(NSURLRequest*)newRequest isFragmentIdentifierToRequest:(NSURLRequest*)originalRequest +{ + if (originalRequest.URL && newRequest.URL) { + NSString* originalRequestUrl = [originalRequest.URL absoluteString]; + NSString* newRequestUrl = [newRequest.URL absoluteString]; + + // no fragment, easy + if (newRequest.URL.fragment == nil) { + return NO; + } + + // if the urls have fragments and they are equal + if ((originalRequest.URL.fragment && newRequest.URL.fragment) && [originalRequestUrl isEqualToString:newRequestUrl]) { + return YES; + } + + NSString* urlFormat = @"%@://%@:%d/%@#%@"; + // reconstruct the URLs (ignoring basic auth credentials, query string) + NSString* baseOriginalRequestUrl = [NSString stringWithFormat:urlFormat, + [originalRequest.URL scheme], + [originalRequest.URL host], + [[originalRequest.URL port] intValue], + [originalRequest.URL path], + [newRequest.URL fragment] // add the new request's fragment + ]; + NSString* baseNewRequestUrl = [NSString stringWithFormat:urlFormat, + [newRequest.URL scheme], + [newRequest.URL host], + [[newRequest.URL port] intValue], + [newRequest.URL path], + [newRequest.URL fragment] + ]; + + return [baseOriginalRequestUrl isEqualToString:baseNewRequestUrl]; + } + + return NO; +} + - (BOOL)isPageLoaded:(UIWebView*)webView { NSString* readyState = [webView stringByEvaluatingJavaScriptFromString:@"document.readyState"]; @@ -62,95 +164,216 @@ typedef enum { - (void)pollForPageLoadStart:(UIWebView*)webView { - if ((_state != STATE_WAITING_FOR_START) && (_state != STATE_SHOULD_LOAD_MISSING)) { + if (_state != STATE_IOS5_POLLING_FOR_LOAD_START) { return; } if (![self isJsLoadTokenSet:webView]) { - _state = STATE_WAITING_FOR_FINISH; + VerboseLog(@"Polled for page load start. result = YES!"); + _state = STATE_IOS5_POLLING_FOR_LOAD_FINISH; [self setLoadToken:webView]; - [_delegate webViewDidStartLoad:webView]; + if ([_delegate respondsToSelector:@selector(webViewDidStartLoad:)]) { + [_delegate webViewDidStartLoad:webView]; + } [self pollForPageLoadFinish:webView]; + } else { + VerboseLog(@"Polled for page load start. result = NO"); + // Poll only for 1 second, and then fall back on checking only when delegate methods are called. + ++_loadStartPollCount; + if (_loadStartPollCount < (1000 * .05)) { + [self performSelector:@selector(pollForPageLoadStart:) withObject:webView afterDelay:.05]; + } } } - (void)pollForPageLoadFinish:(UIWebView*)webView { - if (_state != STATE_WAITING_FOR_FINISH) { + if (_state != STATE_IOS5_POLLING_FOR_LOAD_FINISH) { return; } if ([self isPageLoaded:webView]) { - _state = STATE_SHOULD_LOAD_MISSING; - [_delegate webViewDidFinishLoad:webView]; + VerboseLog(@"Polled for page load finish. result = YES!"); + _state = STATE_IDLE; + if ([_delegate respondsToSelector:@selector(webViewDidFinishLoad:)]) { + [_delegate webViewDidFinishLoad:webView]; + } } else { - [self performSelector:@selector(pollForPageLoaded) withObject:webView afterDelay:50]; + VerboseLog(@"Polled for page load finish. result = NO"); + [self performSelector:@selector(pollForPageLoadFinish:) withObject:webView afterDelay:.05]; } } - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { - BOOL shouldLoad = [_delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; + BOOL shouldLoad = YES; + + if ([_delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { + shouldLoad = [_delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; + } + + VerboseLog(@"webView shouldLoad=%d (before) state=%d loadCount=%d URL=%@", shouldLoad, _state, _loadCount, request.URL); if (shouldLoad) { BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; if (isTopLevelNavigation) { - _loadCount = 0; - _state = STATE_NORMAL; + switch (_state) { + case STATE_WAITING_FOR_LOAD_FINISH: + // Redirect case. + // We expect loadCount == 1. + if (_loadCount != 1) { + NSLog(@"CDVWebViewDelegate: Detected redirect when loadCount=%d", _loadCount); + } + break; + + case STATE_IDLE: + case STATE_IOS5_POLLING_FOR_LOAD_START: + // Page navigation start. + _loadCount = 0; + _state = STATE_WAITING_FOR_LOAD_START; + break; + + default: + { + NSString* description = [NSString stringWithFormat:@"CDVWebViewDelegate: Navigation started when state=%d", _state]; + NSLog(@"%@", description); + _loadCount = 0; + _state = STATE_WAITING_FOR_LOAD_START; + if (![self request:request isFragmentIdentifierToRequest:webView.request]) { + if ([_delegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) { + NSDictionary* errorDictionary = @{NSLocalizedDescriptionKey : description}; + NSError* error = [[NSError alloc] initWithDomain:@"CDVWebViewDelegate" code:1 userInfo:errorDictionary]; + [_delegate webView:webView didFailLoadWithError:error]; + } + } + } + } + } else { + // Deny invalid URLs so that we don't get the case where we go straight from + // webViewShouldLoad -> webViewDidFailLoad (messes up _loadCount). + shouldLoad = shouldLoad && [NSURLConnection canHandleRequest:request]; } + VerboseLog(@"webView shouldLoad=%d (after) isTopLevelNavigation=%d state=%d loadCount=%d", shouldLoad, isTopLevelNavigation, _state, _loadCount); } return shouldLoad; } - (void)webViewDidStartLoad:(UIWebView*)webView { - if (_state == STATE_NORMAL) { - if (_loadCount == 0) { - [_delegate webViewDidStartLoad:webView]; - _loadCount += 1; - } else if (_loadCount > 0) { - _loadCount += 1; - } else if (!IsAtLeastiOSVersion(@"6.0")) { + VerboseLog(@"webView didStartLoad (before). state=%d loadCount=%d", _state, _loadCount); + BOOL fireCallback = NO; + switch (_state) { + case STATE_IDLE: + if (IsAtLeastiOSVersion(@"6.0")) { + break; + } // If history.go(-1) is used pre-iOS6, the shouldStartLoadWithRequest function is not called. // Without shouldLoad, we can't distinguish an iframe from a top-level navigation. // We could try to distinguish using [UIWebView canGoForward], but that's too much complexity, // and would work only on the first time it was used. // Our work-around is to set a JS variable and poll until it disappears (from a naviagtion). - _state = STATE_WAITING_FOR_START; + _state = STATE_IOS5_POLLING_FOR_LOAD_START; + _loadStartPollCount = 0; [self setLoadToken:webView]; - } - } else { - [self pollForPageLoadStart:webView]; - [self pollForPageLoadFinish:webView]; + [self pollForPageLoadStart:webView]; + break; + + case STATE_WAITING_FOR_LOAD_START: + if (_loadCount != 0) { + NSLog(@"CDVWebViewDelegate: Unexpected loadCount in didStart. count=%d", _loadCount); + } + fireCallback = YES; + _state = STATE_WAITING_FOR_LOAD_FINISH; + _loadCount = 1; + break; + + case STATE_WAITING_FOR_LOAD_FINISH: + _loadCount += 1; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_START: + [self pollForPageLoadStart:webView]; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_FINISH: + [self pollForPageLoadFinish:webView]; + break; + + default: + NSLog(@"CDVWebViewDelegate: Unexpected didStart with state=%d loadCount=%d", _state, _loadCount); + } + VerboseLog(@"webView didStartLoad (after). state=%d loadCount=%d fireCallback=%d", _state, _loadCount, fireCallback); + if (fireCallback && [_delegate respondsToSelector:@selector(webViewDidStartLoad:)]) { + [_delegate webViewDidStartLoad:webView]; } } - (void)webViewDidFinishLoad:(UIWebView*)webView { - if (_state == STATE_NORMAL) { - if (_loadCount == 1) { - [_delegate webViewDidFinishLoad:webView]; - _loadCount -= 1; - } else if (_loadCount > 1) { + VerboseLog(@"webView didFinishLoad (before). state=%d loadCount=%d", _state, _loadCount); + BOOL fireCallback = NO; + switch (_state) { + case STATE_IDLE: + break; + + case STATE_WAITING_FOR_LOAD_START: + NSLog(@"CDVWebViewDelegate: Unexpected didFinish while waiting for load start."); + break; + + case STATE_WAITING_FOR_LOAD_FINISH: + if (_loadCount == 1) { + fireCallback = YES; + _state = STATE_IDLE; + } _loadCount -= 1; - } - } else { - [self pollForPageLoadStart:webView]; - [self pollForPageLoadFinish:webView]; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_START: + [self pollForPageLoadStart:webView]; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_FINISH: + [self pollForPageLoadFinish:webView]; + break; + } + VerboseLog(@"webView didFinishLoad (after). state=%d loadCount=%d fireCallback=%d", _state, _loadCount, fireCallback); + if (fireCallback && [_delegate respondsToSelector:@selector(webViewDidFinishLoad:)]) { + [_delegate webViewDidFinishLoad:webView]; } } - (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error { - if (_state == STATE_NORMAL) { - if (_loadCount == 1) { - [_delegate webView:webView didFailLoadWithError:error]; - _loadCount -= 1; - } else if (_loadCount > 1) { - _loadCount -= 1; - } - } else { - [self pollForPageLoadStart:webView]; - [self pollForPageLoadFinish:webView]; + VerboseLog(@"webView didFailLoad (before). state=%d loadCount=%d", _state, _loadCount); + BOOL fireCallback = NO; + + switch (_state) { + case STATE_IDLE: + break; + + case STATE_WAITING_FOR_LOAD_START: + _state = STATE_IDLE; + fireCallback = YES; + break; + + case STATE_WAITING_FOR_LOAD_FINISH: + if (_loadCount == 1) { + _state = STATE_IDLE; + fireCallback = YES; + } + _loadCount = -1; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_START: + [self pollForPageLoadStart:webView]; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_FINISH: + [self pollForPageLoadFinish:webView]; + break; + } + VerboseLog(@"webView didFailLoad (after). state=%d loadCount=%d, fireCallback=%d", _state, _loadCount, fireCallback); + if (fireCallback && [_delegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) { + [_delegate webView:webView didFailLoadWithError:error]; } } |