Archive for the ‘Greasemonkey’ Category

Working around the same-origin policy in Greasemonkey

Wednesday, September 16th, 2009

The same-origin policy prevents scripts on a page from communicating with servers on a different domain from the page. Implemented by all browsers, this prevents some cross-site scripting attacks.

Of course, communicating with different web services is a common goal for mashups like the ones we’re building in this class. So if you need to communicate with a different server from the current page, you have three options:

  1. Use a server-side proxy.
  2. Make a JSONP request to a server that supports it.
  3. Use the Greasemonkey chrome to make the cross-site request for you.

Ryan’s walkthrough of the Delicious Trailmaker has code for both of the first two options, so I’ll focus on the third. Also, if you are writing a Greasemonkey script, the third option is the most flexible and straightforward.

First, let’s start with a stub of a Greasemonkey script. This script loads jQuery and inserts some simple CSS and HTML into the page to add a bar to the bottom of the page.

Now, let’s use AJAX to request something on another page. You can read extensive documentation on the jQuery AJAX commands, but for the most part, you can just call $.get() with the URL you want and a callback function for handling the XML response. For example, if you run this on http://sfbay.craigslist.org you should see the full contents of the sublets page outputted in your console.

$.get('/sub/', function(xml){
    console.log(xml);
});

But if you modify that to request the contents of delicious.com while still on craigslist.org, you’ll see nothing in the console. (You can try this yourself to convince yourself that the same-origin restriction really works.)

$.get('http://delicious.com/', function(xml){
    console.log(xml);
});

Now, to workaround this restriction, we’ll add a Javascript file which tells jQuery to use Greasemonkey to make its XmlHttpRequests. The Greasemonkey-jQuery-XHR Bridge is available on our course website.

To add it to our Greasemonkey script, we’ll add a new @require statement to the metadata block at the top of the script:

...
// @require	http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js
// @require	http://courses.ischool.berkeley.edu/i290-4/f09/resources/gm_jq_xhr.js
// ==/UserScript==

In order to get these changes, you must uninstall and re-install your script. To verify that it works, refresh that same craigslist.org page and confirm that the contents of delicious.com appear in the console.

Now that I can talk to other domains, let’s consider my ultimate goal: I want to fetch all the recent tags for the current page from Delicious. To do this, I’ll use the Delicious feeds API. That API requires that I send the MD5 hash of my URL rather than the raw URL itself, and to compute an MD5 hash, I’ll need to include an additional jQuery plugin.

There are jQuery plugins for almost everything, including MD5 hashing. This requires another @require and another reinstallation of the script.

// @require	http://plugins.jquery.com/files/jquery.md5.js.txt

Now I can produce the URL that the Delicious feeds API requires, and see on the console what kind of output I’ll receive.

var theUrl = $.md5(window.location.href);

$.get('http://feeds.delicious.com/v2/xml/url' + theUrl, function(xml){
    console.log(xml);
});
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" version="2.0">
	<channel>
		<title>Delicious/url/ad1706731c138951229c69f68c6a1c23</title>
		<link>http://delicious.com/url/ad1706731c138951229c69f68c6a1c23</link>
		<description>bookmark history for http://sfbay.craigslist.org/</description>
		<atom:link rel="self" type="application/rss+xml" href="http://feeds.delicious.com/v2/xml/url/ad1706731c138951229c69f68c6a1c23"/>
		<item>
			<title>[from icresource] craigslist - san francisco, east bay, south bay, marin county, california</title>
			<pubDate>Wed, 16 Sep 2009 17:57:38 +0000</pubDate>
			<guid isPermaLink="false">http://delicious.com/url/ad1706731c138951229c69f68c6a1c23#icresource</guid>
			<link>http://sfbay.craigslist.org/</link>
			<dc:creator><![CDATA[icresource]]></dc:creator>
			<comments>http://delicious.com/url/ad1706731c138951229c69f68c6a1c23</comments>
			<wfw:commentRss>http://feeds.delicious.com/v2/rss/url/ad1706731c138951229c69f68c6a1c23</wfw:commentRss>
			<source url="http://feeds.delicious.com/v2/rss/icresource">icresource's bookmarks</source>
			<category domain="http://delicious.com/icresource/">San_Francisco-RC</category>
			<category domain="http://delicious.com/icresource/">East_Bay-RC</category>
			<category domain="http://delicious.com/icresource/">South_Bay-RC</category>
			<category domain="http://delicious.com/icresource/">Marin_County-RC</category>
			<category domain="http://delicious.com/icresource/">events-calendar</category>
			<category domain="http://delicious.com/icresource/">jobs</category>
			<category domain="http://delicious.com/icresource/">housing</category>
			<category domain="http://delicious.com/icresource/">rideshare</category>
			<category domain="http://delicious.com/icresource/">personals</category>
			<category domain="http://delicious.com/icresource/">classifieds,</category>
			<category domain="http://delicious.com/icresource/">forums</category>
		</item>
		<item>
...

I can see from that output that it’s the <category> elements that I’m interested in. We can use the same jQuery selection tools on this XML output that we do on the Document Object Model by wrapping the xml variable in $() and running the find method. And for each <category> element that I find, I’ll add a list item to the bar.

$(xml).find("category").each(function(){
	$("#bar ul").append('<li>' + $(this).text() + '</li>');
});

(In the above code, this refers to the DOM object that we’ve selected from the XML file. By wrapping this in $() I can convert it to a jQuery object and I can use all the jQuery functions that I’m familiar with, like .text().)

At this point, you should be successfully grabbing all the recent tags for the current page from Delicious and inserting them in a bar on the page. And besides the @require lines, your code style should be no different than if you were doing same-origin requests.

Here’s the completed script. If you have any questions, problems, corrections or suggestions, please leave a comment and I’ll respond here.

Add jQuery to any (or every) webpage

Saturday, September 12th, 2009

We’ve already seen how Firebug makes it incredibly easy to inspect the current page loaded in Firefox and run jQuery commands to quickly modify items on the page or test different selectors.

But what if the webpage you’re interested in doesn’t already have jQuery installed? jQuery is becoming more and more widespread (the iSchool website has it, heck, even Craigslist, that paragon of simplicity, has it) but not all websites have it loaded. And if the webpage you visit doesn’t load jQuery then you won’t be able to use the jQuery commands from Firebug on that page.

But it’s easy to write a Greasemonkey script that will insert jQuery into any page. We can’t just use @require — that loads jQuery into the Greasemonkey script but then jQuery won’t be around when the Greasemonkey script ends and we’re trying to run our debug commands in Firebug. Instead, we’ll add a <script> element to the head of the page itself.

// ==UserScript==
// @name           Add jQuery
// @namespace      http://people.ischool.berkeley.edu/~npdoty
// @description    Insert the jQuery script so that we can run commands in Firebug
// @include        http://*
// @include        https://*
// ==/UserScript==

var GM_JQ = document.createElement('script');
GM_JQ.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js';
GM_JQ.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(GM_JQ);

Easy as pie. (I’ve used plain Javascript so that we don’t have to load the whole jQuery library just to load jQuery.) With this userscript installed, you can test any jQuery command on any page you might want to investigate or modify with your own Greasemonkey script.

More details about this technique from Joan Piedra (from whom I’ve adapted our code).