Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIWebViewDelegate not monitoring XMLHttpRequest?

Is it true that the UIWebViewDelegate does not monitor requests made by using a XMLHttpRequest? If so, is there a way to monitor these kind of requests?

e.g. UIWebViewDelegate does not catch this in -(BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSMutableURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

var xhr = new XMLHttpRequest();
xhr.open("GET", "http://www.google.com", true);

xhr.onreadystatechange=function() 
{
    if (xhr.readyState==4) 
    {
        alert(xhr.responseText);
    }
}

xhr.send();
like image 438
Thizzer Avatar asked Mar 18 '11 14:03

Thizzer


4 Answers

Interesting question.

There are two parts to make this work: a JavaScript handler and UIWebView delegate methods. In JavaScript, we can modify prototype methods to trigger events when an AJAX request is created. With our UIWebView delegate, we can capture these events.

JavaScript Handler

We need to be notified when an AJAX request is made. I found the solution here.

In our case, to make the code work, I put the following JavaScript in a resource called ajax_handler.js which is bundled with my app.

var s_ajaxListener = new Object();
s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
s_ajaxListener.callback = function () {
    window.location='mpAjaxHandler://' + this.url;
};

XMLHttpRequest.prototype.open = function(a,b) {
  if (!a) var a='';
  if (!b) var b='';
  s_ajaxListener.tempOpen.apply(this, arguments);
  s_ajaxListener.method = a;  
  s_ajaxListener.url = b;
  if (a.toLowerCase() == 'get') {
    s_ajaxListener.data = b.split('?');
    s_ajaxListener.data = s_ajaxListener.data[1];
  }
}

XMLHttpRequest.prototype.send = function(a,b) {
  if (!a) var a='';
  if (!b) var b='';
  s_ajaxListener.tempSend.apply(this, arguments);
  if(s_ajaxListener.method.toLowerCase() == 'post')s_ajaxListener.data = a;
  s_ajaxListener.callback();
}

What this will actually do is change the location of the browser to some made up URL scheme (in this case, mpAjaxHandle) with info about the request made. Don't worry, our delegate with catch this and the location won't change.

UIWebView Delegate

First, we need to read our JavaScript file. I suggest doing storing it in a static variable. I'm in the habit of using +initialize.

static NSString *JSHandler;

+ (void)initialize {
    JSHandler = [[NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ajax_handler" withExtension:@"js"] encoding:NSUTF8StringEncoding error:nil] retain];
}

Next, we want to inject this JavaScript before a page is done loading so we can receive all events.

- (void)webViewDidStartLoad:(UIWebView *)webView {
    [webView stringByEvaluatingJavaScriptFromString:JSHandler];
}

Finally, we want to capture the event.

Since the URL Scheme is made up, we don't want to actually follow it. We return NO and all is well.

#define CocoaJSHandler          @"mpAjaxHandler"

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if ([[[request URL] scheme] isEqual:CocoaJSHandler]) {
        NSString *requestedURLString = [[[request URL] absoluteString] substringFromIndex:[CocoaJSHandler length] + 3];

        NSLog(@"ajax request: %@", requestedURLString);
        return NO;
    }

    return YES;
}

I created a sample project with the solution but have nowhere to host it. You can message me if you can host it and I'll edit this post accordingly.

like image 112
Mikey Avatar answered Nov 11 '22 19:11

Mikey


You can use an NSURLProtocol. For instance if you call XMLHttpRequest with http://localhost/path you can handle it with the following:

@interface YourProtocol: NSURLProtocol

Then for the implementation:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request 
{
    return [request.URL.host isEqualToString:@"localhost"];
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

- (void) startLoading
{
    // Here you handle self.request 
}

- (void)stopLoading
{
}

You need to register the protocol as follows:

    [NSURLProtocol registerClass:[YourProtocol class]];
like image 20
George Avatar answered Nov 11 '22 19:11

George


By implementing and registering a subclass of NSURLProtocol you can capture all the request from your UIWebView. It may be overkill in some cases but if you are not able to modify the javascript actually being run it is your best bet.

In my case I need to capture all the request and insert a specific HTTP header to every one of them. I have done this by implementing NSURLProtocol, registering it using registerClass and answering YES in my subclass to + (BOOL)canInitWithRequest:(NSURLRequest *)request if the request corresponds to the URLs I am interested in. You then have to implement the other methods of the protocol this can be done by using an NSURLConnection, setting the protocol class as the delegate and redirecting the delegate methods of NSURLConnection to NSURLProtocolClient

like image 3
terrinecold Avatar answered Nov 11 '22 18:11

terrinecold


It does appear to be true. There is no way to monitor what a UIWebView is doing beyond what UIWebViewDelegate provides, unless perhaps you can figure out a way to use stringByEvaluatingJavaScriptFromString: to inject some Javascript to do what you need.

like image 1
Anomie Avatar answered Nov 11 '22 20:11

Anomie