Sunday, May 25, 2008

Make JSONP work in Firefox Chrome

I have been doing some Firefox extension and I needed to do some JSONP Ajax call in the sidebar and then show the result. jQuery makes it easy, you do something like:




$.ajax({
type: "GET",
dataType: "jsonp",
url: "http://someurl.com",
data: aRequestData,
cache: false,
error: function (XMLHttpRequest, textStatus, errorThrown) {
// typically only one of textStatus or errorThrown
// will have info
alert("Error occured textStatus=" + textStatus + " errorThrown=" + errorThrown);
},
success: function(data) {
alert('success');
}
});




Plain and simple and it works in browser window, however, to my big surprise it did not work in Chrome, never got to the success alert. After turning on Firebug I got an error from jQuery: head is null. OK, What is JSONP?
How it works is that script is injected into the head element of the HTML document. Source of the script is a link to the external server that is providing customized javascript function call with the retrieved data. Upon receiving data jQuery is deleting that script element.
So head is null indeed since document.getElementsByTag("head") is not returning enything since I don't have head element in my XUL file:



<?xml version="1.0"?>
<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css" ?>
<?xml-stylesheet href="chrome://intranetfeedreader/skin/sidebar.css" type="text/css"?>
<!DOCTYPE prefwindow SYSTEM "chrome://intranetfeedreader/locale/intranetfeedreader.dtd">

<page id="sbIntranetFeedReaderSidebar" title="&sidebar.title;"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
<script src="jquery-1.2.3.js"/>
<script src="sidebar.js"/>
<vbox flex="1">
<label id="atest" value="&sidebar.title;" />
</vbox>
</page>



So nothing fancy but no head tag. Well lets add head tag and see if we can trick the jQuery:



<?xml version="1.0"?>
<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css" ?>
<?xml-stylesheet href="chrome://intranetfeedreader/skin/sidebar.css" type="text/css"?>
<!DOCTYPE prefwindow SYSTEM "chrome://intranetfeedreader/locale/intranetfeedreader.dtd">

<page id="sbIntranetFeedReaderSidebar" title="&sidebar.title;"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
<head>
<script src="jquery-1.2.3.js"/>
<script src="sidebar.js"/>
</head>
<vbox flex="1">
<label id="atest" value="&sidebar.title;" />
</vbox>
</page>



And it worked :) I got rid of the error, but still no success alert. To make the long story short: injection of scripts this way is not working in Chrome. It will allow only urls starting with chrome://. Well it should work that way, working from browser window is pretty harmless (well it depends ...) compared to working in chrome where the code has much more privileges and can do some serious damage. I am not really convinced that getting the data this way is really smart thing to do, simply injecting javascript code from another server is scary - I can imagine that it is not too hard to inject whatever harmful code this way.
So I guess one big warning is due for the reminder of the post:

WARNING!!!! The following code should be used only with trusted servers. Ideally only in intranet. Never, never, never use this code with server that you do not trust 100%. Of course you are using this code on your own risk.

Since the injection of the script by purely adding script element to the document does not work in chrome then we have to do the script loading manually. Idea is to download the script ourselves and then execute it ourselves. That is not hard at all, and here is (almost) all the code:



var scriptCollection = document.getElementsByTagName("script");
var reader = new httpReader(scriptCollection[scriptCollection.length - 1].src);

function evaluateJs(aReader){
if(aReader && aReader.mData)
eval(aReader.mData);
}

reader.AsyncLoad(bind(evaluateJs, this, reader));


The code is really simple, firstly we get all the script elements from the document, then we initialize httpReader object with the source of the last script (jQuery is appending it to the head, well keep all your scripts enclosed in the head tag or modify the code to get the correct script). evaluateJs is simply helper function that is calling eval function on the script we get from the server. And at the end we asyncronuosly load the url with the script.

This code executes javascript code as browser would in the normal html document - get the script and then execute it. Some further testing should probably be involved here to check the code we are going to evaluate but anyhow if you plan on using this approach you should really take security seriously into consideration.

The last bit is httpReader, it is simple implementation from the article: Creating Sandboxed HTTP Connections from the developer.mozilla.org. bind functions are based on Firebug code. And the file is here.

3 comments:

  1. I have been visiting various blogs for my term papers writing research. I have found your blog to be quite useful. Keep updating your blog with valuable information... Regards

    ReplyDelete
  2. I included the jQuery in the chrome package
    and use this:
    <script type="application/x-javascript"
    src="chrome://plugin/content/jquery-1.4.4.min.js" />
    to import jquery... the ajax call gets to the XSite, but I never get any success or error callback...
    Any suggestions ?

    ReplyDelete
  3. I was just playing with jsonp and was trying to port some code. The approach above shouldn't be used and it probably doesn't work with what you need. I think it's your question:

    http://stackoverflow.com/questions/5168255/how-to-make-an-ajax-call-in-xul-environment

    I would've given you the same answer that you came up with.

    If you still want to use jQuery and your ajax call is going through to the site but your callback is not called I suggest you reference full jQuery version not minimal, and debug it with FireBug if possible (or change jQuery code and add alerts) and see where the hiccup is.

    ReplyDelete