12

I'm working on Fahrii, which is, essentially, a userscript enabled mobile browser. At the moment, I'm having trouble invoking the scripts on a web page that's loaded into a UIWebView. I'd like to keep this public APIs if possible. If not, a proof of concept would at least be nice, so I can get a better understanding of how this works. I'm trying to do it in the webViewDidFinishLoad: method.

I've tried injecting by reading out the webView's content (using JavaScript) and then loading it back with loadHTML:baseURL:, but that causes infinite recursion. (A completed load causes a script to be injected, which is in turn causing a completed "load".)

Next, I've tried something like this:

    [self.browser stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"document.body.innerHTML = %@", originalHTMLWithScriptsInjected]];

The script seems to be injected, but not run.

The only way I got this to work was to use the ASIHTTPRequest library to make the actual requests, but then I ended up having trouble with login forms, besides for which, I have to make each request twice in that case.

I might be doing something that's either impossible, or I'm just doing it the wrong away. So, how can I invoke a user script on an existing UIWebView with content loaded from the web?

Edit:

Here's some more code, including the changes proposed by @rich:

 //
 //  Run the remaining scripts
 //

NSMutableString *scriptTextToInject = [[NSMutableString alloc]init];

//
//  Add each script into the page
//

for (Userscript *script in scriptsToExecute) {

    //
    //  Read the userscript
    //

    NSString *scriptToLoad = [NSString stringWithContentsOfURL:[NSURL URLWithString:script.pathToScript] encoding:NSUTF8StringEncoding error:&error];

    NSLog(@"Script to load: %@", scriptToLoad);

    [scriptTextToInject appendFormat:@"%@\n\n",scriptToLoad];
}

NSString *documentHeadBefore = [self.browser stringByEvaluatingJavaScriptFromString:@"document.getElementsByTagName('head')[0].innerHTML"];

//
//  Inject the scripts
//

NSString *scriptWrappedInATag = [NSString stringWithFormat:@"window.onload() = function(){var script = document.createElement('script');\n"
                                 "script.charset = 'UTF-8'"
                                 "script.setAttribute(\"type\",\"text/javascript\");\n"
                                 "text = \"function u(){\n"
                                 "%@"
                                 "}\";\n"
                                 "document.getElementsByTagName('head')[0].appendChild(script);}", scriptTextToInject];
NSLog(@"Scripts wrapped in a tag: %@", scriptWrappedInATag);

NSString *runResults = [self.browser stringByEvaluatingJavaScriptFromString:@"u();"];

NSString *documentHeadAfter = [self.browser stringByEvaluatingJavaScriptFromString:@"document.getElementsByTagName('head')[0].innerHTML"];

NSLog(@"Before: %@ \n\n\n\n After: %@", documentHeadBefore, documentHeadAfter);
Moshe
  • 57,511
  • 78
  • 272
  • 425
  • -stringByEvaluatingJavaScriptFromString: is a proper way to call javascript on a webview, but it looks like you can't set document.body.innerHTML that way. I guess Apple wants you to do that with -loadHTMLString:baseURL: method. But describe what you want to do, or show us the javascript code you want to run. – Filip Radelic Aug 05 '11 at 22:53
  • The idea is to inject userscripts into the page. The only way I've been successful at this is by using a separate request and loading the response into the WebView manually. – Moshe Aug 07 '11 at 01:31
  • Beware DO NOT treat that edit as anything more than "more code" (ie the "including the changes proposed by @rich" could be misleading) 1) Why is it wrapped in `window.onload`? I thought this implied a timing gotcha (there isn't one) 2) That's not how you would assign something to `window.onload` anyway (extraneous parenthesis) 3) The code placed into `scriptWrappedInATag` is not ever called / passed to `stringByEvauluatingJavaScriptFromString`.*just skip to rich's answer below* – WiseOldDuck Oct 21 '16 at 17:24

2 Answers2

5

An example of doing just this is below. I had the same problem until I realized that the js needed to be injected into the page. To properly inject the js you need to write it into the head of the dom as shown below.

- (void)webViewDidFinishLoad:(UIWebView *)webView {
 [webView stringByEvaluatingJavaScriptFromString:@"var script = document.createElement('script');"  
 "script.type = 'text/javascript';"  
 "script.text = \"function myFunction() { "  
 "window.scrollTo(100,100)"
 "}\";"  
 "document.getElementsByTagName('head')[0].appendChild(script);"];  

 [webView stringByEvaluatingJavaScriptFromString:@"myFunction();"];
}    
rich
  • 2,136
  • 19
  • 23
  • Does this circumvent the 10 second limit on execution time, imposed by Apple on UIWebView? – Moshe Aug 08 '11 at 00:45
  • I have used this in several apps, it executes before the page is loaded so it is good to go by Apple standards – rich Aug 08 '11 at 02:46
  • Interesting, thanks! I'll check it out and let you know how it goes. – Moshe Aug 08 '11 at 13:10
  • Have you made sure to set the webView delegate to self? That is a common mishap in this situation – rich Aug 08 '11 at 15:09
  • Yes, the code is running, but it is not working. The script does not appear to be injecting properly. – Moshe Aug 08 '11 at 16:19
  • The code is contained in a method which is invoked by `webViewDidFinishLoad:`. The code is being called, but it's not behaving as expected. – Moshe Aug 08 '11 at 22:50
2

You should directly pass the string to the stringByEvaluatingJavaScriptFromString: method, like this:

NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"Script To Be Run"];