views:

220

answers:

1

I'm trying to build a javascript bookmarklet for a special URL shortening service we've built at http://esv.to for shortening scripture references (i.e. "Matthew 5" becomes "http://esv.to/Mt5". The bookmarklet is supposed to do a GET request to http://api.esv.to/Matthew+5, which returns a text/plain response of http://esv.to/Mt5.

The code for the bookmarklet itself looks like this (expanded for readability):

var body = document.getElementsByTagName('body')[0], script = document.createElement('script');     
script.type = 'text/javascript';
script.src = 'http://esv.to/media/js/bookmarklet.js';
body.appendChild(script);
void(0);

The code from http://esv.to/media/js/bookmarklet.js looks like this:

(function() {

    function shorten(ref, callback) {
      var url = "http://esv.to/api/" + escape(ref);
      var req = new XMLHttpRequest(); 
      req.onreadystatechange = function shortenIt() {
        if ( this.readyState == 4 && this.status == 200 ) {
          callback(req.responseText);
        };
      };
      req.open( "GET", url );
      req.send();
    };

    function doBookmarklet() {
      var ref = prompt("Enter a scripture reference or keyword search to link to:", "")
      shorten(ref, function (short) {
        prompt("Here is your shortened ESV URL:", short);
      });
    };

    doBookmarklet();

})();

When called from http://esv.to itself, the bookmarklet works correctly. But when used on another page, it does not. The strange thing is, when I watch the request from Firebug, the response is 200 OK, the browser downloads 17 bytes (the length of the returned string), but the response body is empty! No error is thrown, just an empty responseText on the XmlHttpRequest object.

Now, according to http://stackoverflow.com/questions/664689/ajax-call-from-bookmarklet, GET shouldn't violate the same origin policy. Is this a bug? Is there a workaround?

+2  A: 

Cross-site XMLHttpRequests can only be done in browsers that implement the W3C Cross-Origin Resource Sharing spec and if the server returns the appropriate access control headers (see MDC article), e.g.:

Access-Control-Allow-Origin: *

But this is not implemented by all browsers. The only sure-fire way to do cross-site requests is to use JSONP, for (untested) example:

(function() {
    function shorten(ref, callback){
        var callbackFuncName = 'esvapiJSONPCallback' + (new Date()).valueOf();
        var script = document.createElement('script');
        script.type = "text/javascript";
        script.src = "http://esv.to/api/" + escape(ref) + "?callback=" + callbackFuncName;
        window[callbackFuncName] = function(shorturl){
            script.parentNode.removeChild(script);
            window.callbackFuncName = null;
            delete window[callbackFuncName];
            callback(shorturl);
        };
        document.getElementsByTagName("head")[0].appendChild(script); 
    }

    var ref = prompt("Enter a scripture reference or keyword search to link to:", "");
    shorten(ref, function(shorturl) {
        prompt("Here is your shortened ESV URL:", shorturl);
    });
})();

When the server sees the callback parameter it would then need to return text/javascript instead of text/plain, and the response body would need to be wrapped in an invocation of the provided callback, for example:

<?php
#... after $shorturl is set ...
if(isset($_GET['callback'])){
    header('Content-Type: text/javascript');
    $callback = preg_replace('/\W+/', '', $_GET['callback']); #sanitize
    print $callback . "(" . json_encode($shorturl) . ");";
}
else {
    header("Content-Type: text/plain");
    print $shorturl;
}
?>
Weston Ruter
Aside from a few syntax errors, that works quite well! I have to wait a day before accepting this as the answer, to give others a chance. Thanks!
David Eyk
Sorry for the syntax errors; I think I corrected them. I didn't actually run the code, except on my MVM (mental virtual machine).
Weston Ruter