aboutsummaryrefslogtreecommitdiffstats
path: root/iPhone/CordovaLib/Classes/CDVWebViewDelegate.m
diff options
context:
space:
mode:
Diffstat (limited to 'iPhone/CordovaLib/Classes/CDVWebViewDelegate.m')
-rwxr-xr-xiPhone/CordovaLib/Classes/CDVWebViewDelegate.m315
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];
}
}