After my recent announcement of the release of version 1.2 of MakeLink it became clear that there was no intuitive way of installing the extension. While I promptly offered an excuse and a workaround I haven’t, until now, offered any form of fix. The problem was that I had simply linked to the .XPIs and relied on the Firefox to be able to determine what type of file it was being given. More accurately I was relying on the server—in this case netsoc’s server which I still use to host these files, out of habit—to tell Firefox what type of files it was sending. The server, not having been configured for this use, had no idea. Consequently Firefox had no idea. Therefore, unless the user—who can never be relied on—had some knowledge of the transactions going on, the result was a clueless browser asking a clueless user what to do with a file served by a clueless server. Step in the cluefull developer to the rescue.
It turns out there are better ways to hold this whole process together. Ways that don’t rely on servers or users knowing what’s really going on. Ben Goodger, lead developer of Firefox, posted a guide on mozilla.org on Installing Extensions and Themes from Web Pages. The guide asks that developers use the method described to install extensions and themes because it "provides the most seamless experience to users". It may very well provide a seamless experience but it is not without problems. One, it’s not generalised—it requires seperate scripts for each extension or theme you want to serve. Two, it requires invalid HTML. (It’s also missing a closing brace but I don’t count that as it’s a simple typo.)
Ben’s solution is to present a link like this:
<a href="http://www.foo.com/bar.xpi"
iconURL="http://www.foo.com/bar.png"
onclick="return install(event);">Install Extension!</a>
which calls a function install()
defined by this:
function install (aEvent)
{
var params = {
"Bar": { URL: aEvent.target.href,
IconURL: aEvent.target.getAttribute("iconURL"),
toString: function () { return this.URL; }
};
InstallTrigger.install(params);
return false;
}
The lack of generalisation comes from the function itself, where the name of the extension is hardcoded ("Bar"), making it impossible to use the same function for more than one extension. You would need one copy of the function for each extension you wanted to deliver—installFoo()
, installBar()
etc. with a single line altered in each. To be honest the generalisation aspect wasn’t of major importance to me since I have only one extension to worry about. But the invalid HTML—the made-up attribute iconURL
—is not something I want to see when Firefox is the most standards-compliant browser in the world. So I set out to replace Ben’s approach with my own. I placed three requirements on my replacement:
- That it be generalisable to any number of extensions.
- That it only use valid HTML.
- That it retain the "seamless experience" that Ben wanted from his original.
None of these requirements are negotiable.
My first attempt was simple. I removed the iconURL
attribute from the link and made it an extra parameter of the function:
function install(aEvent , iconURL)
, changed the line that determined the icon URL from the link’s attribute so that it simply adopted the value passed to the function:
IconURL: iconURL,
, and made sure the link had the extra parameter when it called the function:
<a href="http://www.foo.com/bar.xpi"
onclick="return install(event,'http://www.foo.com/bar.png');">Install Extension!</a>
That took care of the validity issue neatly, but left the generalisation issue unresolved. The obvious step was to do something similar with the name of the extension as I had done with its icon URL, that is to add another parameter to the function specifying the name of the extension. So I ended up with this function:
function install(aEvent , extName, iconURL)
{
var params = {
extName: {
URL: aEvent.target.href,
IconURL: iconURL,
toString: function () { return this.URL; }
}
};
InstallTrigger.install(params);
return false;
}
which was called by this link:
<a href="http://www.foo.com/bar.xpi"
onclick="return install(event,'Bar','http://www.foo.com/bar.png');">Install Extension!</a>
This seemed reasonable. All I did was replace a literal string ("Bar") with a variable that refered to a string. Unfortunately the result was a dialog that presented the name of the extension as "extName". That’s not a solution at all. I consulted the nearest reference I could find on javascript, the Core JavaScript Guide on DevEdge. About the first thing it mentions about objects in javascript is the remarkable reature that an object and an associative array are the same thing. That meant I could define the object params
as an empty array and then assign the actual parameters object to an element of the array who’s name was determined by the value of the name
parameter. The end result is this script:
function install(aEvent, extName, iconURL)
{
var params = new Array();
params[extName] = {
URL: aEvent.target.href,
IconURL: iconURL,
toString: function () { return this.URL; }
};
InstallTrigger.install(params);
return false;
}
This fits all of my criteria for a useful replacement of Ben’s version without adding any complexity. I use a simplified version of this script—I don’t use iconURL since MakeLink has no icon—to make installation of MakeLink about as easy as it could reasonably be expected to be.
Updated 17 May 2005: I fixed the code as per the comments. Sorry for the delay.