Greasemonkey & jQuery

23 March 2010
4:13 PM

I taught a class this past fall where we used Greasemonkey as a tool for building prototypes of new browser functionality. We also used jQuery extensively to make JavaScript in the browser simpler. There are some tricks to getting the two to work together that I’ve been meaning to share.

Loading jQuery in a Greasemonkey script

Simply loading jQuery within Greasemonkey hasn’t always been straightforward. I think they should just bundle jQuery with the Greasemonkey package since just about everyone is using it already, but that’s a separate issue.

The old way to include jQuery in your Greasemonkey scripts was to add a <script> tag to every page you visit linking to the jQuery library. Although this works, it’s a clunky solution. It introduces timing issues: since you can’t be sure when the script would load, you have to poll continually, checking to see if jQuery has downloaded yet. Once you detect that jQuery is available, you can run your script. This approach also might clobber other JavaScript running on the main page (like the Prototype library) before you have a chance to call jQuery.noConflict.

Fortunately, later versions of Greasemonkey made this hack unnecessary by adding the @require attribute to scripts. When you install a Greasemonkey script that has an @require statement, the listed resource is downloaded once and included in the script as if it were pasted directly into the document. Unfortunately, there’s a small stumbling block for people testing their scripts: the @require statement is only executed when you install a script. This means that if you create a new Greasemonkey script in Firefox, add the @require line, and save your file nothing will happen. You have to uninstall your script and reinstall it because @require is only processed at installation. Once you do this, you can use jQuery all you want in your script. Except for…

jQuery AJAX and Greasemonkey

By default, jQuery’s helpful AJAX methods like $.get and $.post don’t work in Greasemonkey. jQuery acts as a wrapper around browser’s varying implementations of XMLHttpRequest (XHR), which is what makes AJAX possible. In a Greasemonkey script you don’t have XMLHttpRequest. Instead you get GM_xmlhttpRequest. You can use jQuery’s .ajaxSetup() to specify a different object for requests, but GM\_XMLHttpRequest actually operates differently than a normal XHR.

When you create a new XMLHttpRequest you get an object that you can use to set options like the URL, HTTP verb, callback functions, and so forth. Once everything is set up, you start the XHR with the .send() method. With GM_XMLHttpRequest, you set all these options when you create the object because it sends immediately when you create it. In order to use GM_XMLHttpRequest with jQuery, we need a way to encapsulate its functionality. This script does that:

// Wrapper for GM_xmlhttpRequest
function GM_XHR() {
    this.type = null;
    this.url = null;
    this.async = null;
    this.username = null;
    this.password = null;
    this.status = null;
    this.headers = {};
    this.readyState = null;

    this.open = function(type, url, async, username, password) {
        this.type = type ? type : null;
        this.url = url ? url : null;
        this.async = async ? async : null;
        this.username = username ? username : null;
        this.password = password ? password : null;
        this.readyState = 1;
    };

    this.setRequestHeader = function(name, value) {
        this.headers[name] = value;
    };

    this.abort = function() {
        this.readyState = 0;
    };

    this.getResponseHeader = function(name) {
        return this.headers[name];
    };

    this.send = function(data) {
        this.data = data;
        var that = this;
        GM_xmlhttpRequest({
            method: this.type,
            url: this.url,
            headers: this.headers,
            data: this.data,
            onload: function(rsp) {
                // Populate wrapper object with returned data
                for (k in rsp) {
                    that[k] = rsp[k];
                }
            },
            onerror: function(rsp) {
                for (k in rsp) {
                    that[k] = rsp[k];
                }
            },
            onreadystatechange: function(rsp) {
                for (k in rsp) {
                    that[k] = rsp[k];
                }
            }
        });
    };
};

Once we have this wrapper object, we need to tell jQuery to use it instead of the standard browser XHR:

$.ajaxSetup({
    xhr: function(){return new GM_XHR;}
});

If you include this code in your Greasemonkey script (or @require it—see above), you will be able to use $.ajax, $.get, and $.post in your scripts. I’m not sure about $.getScript, and I know that $.getJSON will often fail.

Cross-site scripting with Greasemonkey and jQuery

The short story: using $.getJSON in Greasemonkey will fail anytime you request a resource on another site. To avoid this, just use $.get and parse the returned JSON yourself (using eval() or—better yet—JSON.parse() (see also json2.js).

Update 2010-04-11: jQuery 1.4 added the $.parseJSON method which is what you should use to handle JSON responses (via Paul Tarjan).

For cross-domain JSON requests jQuery uses <script> tags and JSONP as a workaround for the same origin policy. JSONP works because the responding server wraps its response with the name of a user-defined callback function. Instead of returning:

{"param": "value"}

the server will respond with

callback_function_name({"param": "value"})

where callback_function_name is something jQuery creates automatically. When you use Greasemonkey, jQuery defines this function in the scope of the Greasemonkey window, but the browser looks for the function in the main window’s scope and you get an error. The solution is simply to avoid $.getJSON and use $.get instead. Since GM_xmlhttprequest doesn’t have to abide by the same origin policy you don’t need JSONP.

I hope these tips are helpful for people familiar with jQuery who want to try their hand at making a Greasemonkey script.

Comments