I was home for a few days sick, and in my Advil Cold & Sinus-induced haze I decided I’d hack about in an embedded scripting language that relied heavily on text-file bindings in XML. Yup, Firefox extensions!
I checked out GooglePreview, and modified it for use with MSN Search. — called MSNPreview.
Cracking open the .xpi
Firefox extensions are .xpi files, which is a fancy .zip file that Firefox knows contains a bunch of files to install. Cracking open msnpreview.xpi, we have:
>unzip -l msnpreview.xpi
Archive: msnpreview.xpi
Length Date Time Name
-------- ---- ---- ----
0 03-26-05 16:06 chrome/
43690 03-26-05 16:08 chrome/msnpreview.jar
224 03-26-05 16:08 install.js
996 03-26-05 16:08 install.rdf
——– ——-
44910 4 files
install.js is a legacy file for older Firefox installs, and can probably get deleted. install.rdf is the instructions to install this extension, and chrome/msnpreview.jar is the contents of the actual extension.
.jar files are also just fancy .zip files. Normally, in a Java world (jars are Java ARchives), they contain a MANIFEST and some other files to make them useful. But, as it turns out, this .jar is just a .zip file. Cracking it open, we have:
>unzip -l msnpreview.jar
Archive: msnpreview.jar
Length Date Time Name
-------- ---- ---- ----
11081 03-26-05 16:08 content/msnpreview/browserOverlay.js
700 03-25-05 16:55 content/msnpreview/browserOverlay.xul
777 03-25-05 16:35 content/msnpreview/contents.rdf
206 03-25-05 16:35 content/msnpreview/gp.png
43 03-25-05 16:35 content/msnpreview/pixel.gif
1200 03-25-05 17:14 content/msnpreview/options.xul
864 03-25-05 16:35 content/msnpreview/msnoff.png
343 03-25-05 16:38 content/msnpreview/msnpreview.css
226 03-25-05 16:35 content/msnpreview/nopreview.gif
1243 03-25-05 17:38 content/msnpreview/options.js
644 03-25-05 16:35 content/msnpreview/msnon.png
13731 03-25-05 16:35 content/msnpreview/msnoff.psd
10390 03-25-05 16:35 content/msnpreview/msnon.psd
——– ——-
41448 13 files
Key Files
- content/msnpreview/browserOverlay.js - this is where most of the code is
- content/msnpreview/browserOverlay.xul - XML bindings for the code
- content/msnpreview/contents.rdf - header file for the extensions
- content/msnpreview/options.xul - XML bindings for the options dialog
- content/msnpreview/options.js - code for the options (pretty minor)
Support Files / Images
- content/msnpreview/gp.png - legacy image
- content/msnpreview/pixel.gif - single transparent pixel
- content/msnpreview/msnoff.png - status bar icon, butterfly w/ X through it
- content/msnpreview/nopreview.gif - blank preview thumbnail
- content/msnpreview/msnon.png - status bar icon, butterfly
- content/msnpreview/msnoff.psd - Photoshop PSD for msnoff.png. Not used normally, just for dev.
- content/msnpreview/msnon.psd - Photoshop PSD for msnon.png. Not used normally, just for dev.
Key Code
Take a look at content/msnpreview/browserOverlay.js
. In it, you’ll see the following bit of key code:
} else if (isMsn(url)) {
// alert("Activating MSN Thumbnails");
/***************************************************/
/* MSN Search Pic Insertions */
/***************************************************/
var i = 0;
var results = getDocument().getElementById("results");
var a = results.getElementsByTagName("a")[i++];
while (a != null) {
var href = a.href;
url = href;
// if (url) href = unescape(url[1]);
if ((href.indexOf(”http://”) == 0) &&
(href.indexOf(”cc.msnscache.com”) == -1) &&
(href.indexOf(”.msn.com”) == -1)) {
var aParent = a.parentNode;
if (”done” != a.getAttribute(”done”)) {
var thumb = createThumbnail(href);
var linka = getDocument().createElement(”a”);
linka.href = href;
linka.insertBefore(thumb, linka.firstChild);
aParent.insertBefore(linka, aParent.firstChild);
a.setAttribute(”done”, “done”);
}
}
a = results.getElementsByTagName(”a”)[i++];
}
head = getDocument().getElementsByTagName(”head”)[0];
style = getDocument().createElement(”style”);
style.setAttribute(”type”, ‘text/css’);
style.innerHTML = “\n#results>UL>LI { height: 100px; }\n”;
head.insertBefore(style, head.lastChild);
}
Welcome to the wide world of extension scripting! OK, here’s the model: the input is a DOM tree parsed from the HTML document. It assumes that there is a heavy use of <DIV>’s, name, id, and class attributes (used by style sheets). Effectively, what we’re going to do is find the DOM objects we want, create some new ones, and insert them into the tree in appropriate places. In this case, we’re going to look for anchors in the results section that point outside of msn.com and stick in image in front of ‘em. Plus, we’ll muck about with the global style sheet to ensure that the height of a snippet is tall enough for the picture, so we don’t get some weird indentation effects.
This assumes a tight binding with the underlying HTML input. If that changes, this breaks. Horribly.
Let’s go through it. First off, look at the initialization:
var i = 0;
var results = getDocument().getElementById("results");
var a = results.getElementsByTagName("a")[i++];
What we’re gonna do is find the DOM object identified by id="results", or in this case <div id="results>. Then, we’ll work from within that node of the tree. We’re then going to look for all elements in that subtree that are anchors, i.e. <a href=…>.
Now let’s look at that loop:
while (a != null) {
var href = a.href;
if ((href.indexOf("http://") == 0) &&
(href.indexOf("cc.msnscache.com") == -1) &&
(href.indexOf(".msn.com") == -1)) {
var aParent = a.parentNode;
if ("done" != a.getAttribute("done")) {
var thumb = createThumbnail(href);
var linka = getDocument().createElement("a");
linka.href = href;
linka.insertBefore(thumb, linka.firstChild);
aParent.insertBefore(linka, aParent.firstChild);
a.setAttribute("done", "done");
}
}
a = results.getElementsByTagName("a")[i++];
All this does is check to make sure that the href in question doesn’t point to msn.com or msnscache.com, and tack on the image. The get/set “done” attribute is just to make sure we don’t duplicate the effort, in case we’re somehow doubly-installed or we see the same URL somewhere else.
So, aside from a fair amount of glue code, that’s about it. Pretty sweet, huh?
But wait, there’s more!
Didja notice the following bit of code:
function amazonifiy() {
var allLinks = getDocument().getElementsByTagName("a");
for(i=0; i < alllinks .length; i++) {
var href = allLinks[i].href;
href = getRealURL(href);
if (isAmazonCOM(href)) {
var n = “http://www.amazon.com/exec/obidos/redirect?\
tag=gp04-20&path=” + escape(getAPath(href));
allLinks[i].setAttribute(”href”, n);
}
else if (isAmazonDE(href)) {
var n = “http://www.amazon.de/exec/obidos/redirect?t\
ag=gp0409-21&path=” + escape(getAPath(href));
allLinks[i].setAttribute(”href”, n);
}
else if (isAmazonUK(href)) {
var n = “http://www.amazon.co.uk/exec/obidos/redirec\
t?tag=gp04-21&path=” + escape(getAPath(href));
allLinks[i].setAttribute(”href”, n);
}
}
}
Amazonify? What’s up with this?
Well, as it turns out… there’s an interesting economy with Firefox extensions and Amazon. See, Amazon is pretty liberal about who can be an affiliate. So, what people have done is they’ve put in code in extensions to use their Amazon Affiliate tag on Amazon links. Thus, if you install their extension, when you shop at Amazon you’re gonna give them their 5% or whatever it is. In fact, there’s even a Greasemonkey script that let’s you override all this nonesense and put in your own Affiliate ID so you can save your own 5%.
I’m not fully sure about the ethics of this… seems somewhat dodgy, but also OK (and surprisingly entrepreneurial). But it seems that lots of people are glomming onto the idea — which means if you create the dominant search plugin for X, you also get the Amazon Affiliate Credits unless somebody does more work to stop you… which is unlikely to happen, as most people probably don’t even know it’s going on unless they’re extension authors themselves!
Conclusions
It took me about two days to get my environment set up, figure out what everything means, and be productive. Most of that was in understanding the Javascript classes and methods available. That’s really not a very steep learning curve. But from that I was able to do all sorts of cool things. This means that lots of other people will too — there are already a bunch, and more coming seemingly daily. What will really spur things is the Amazon thing. Nothing prompts scratching an itch like the possibility of making a few nickels while doing it. This also means there’s a competition for the “best” plugin of a type — there is already another GooglePreview-like extension which is a bit nicer, although more obnoxious about the Amazon tie-in.