EPx professional blog and repository for braindumps

2009/03/02

Reading and writing local files in Firefox 3

A semmingly simple question haunted the best part of my weekend: how can a "Web 2.0" AJAX application to read from and write to local files?

Yes, it is, but it is not a trivial thing, and must not be, since it is a potencial security hole. It seems that every browser offers a particular (and non-standard) way to allow local file manipulation. All those methods have a common feature: they employ a "trampoline" applet in Java, Flash or ActiveX, since those tools have ways to get privileged access to the filesystem.

Note that I am not talking about local storage here. "Local storage" refers to the ability of storing client-side data. Cookies are a form of this, but they are too limited (4k bytes), so several non-standard methods emerged. But this is a pretty much solved thing (even though not a standard among browsers yet), I just use dojo.storage module that takes care of selecting the best technique for the user's browser.

Client-side storage is nice but it has the same life expectancy as cookies. Data can be wiped out anytime, and it is not trivial to save a copy of it. So, the only way to safely save data is writing it to a real file.


There are even almost-ready-to-use solutions like TiddlyWiki's TiddlySaver, a Java applet that can write local files, provided Java virtual machine has permission to do so. But I really wanted a pure Javascript solution ;)

At least for Firefox, I have found the answers I wanted after googling a countless number of sites. The knowledge about dealing with local files is very scattered, so I thought it would be nice to condense everything in a single article.

First, the easy part: reading a file from the local filesystem. Firefox 3 made that really easy. You just need a file input field, which allows the user to choose a file:

<input type="file" name="xxx" value="">

Then, read the file in Javascript code with something like this:

document.f.xxx.files.item(0).getAsBinary()

Using a file chooser field as a "trampoline" to read the file is probably a security measure, so Javascript can only read a file that the user explicitely chose.

Now the difficult part: writing to a file in untrusted Javascript code. As I said before, we need a priviledged trampoline to do that. Fortunately, Firefox offers a way to create the trampoline in Javascript itself: the add-ons system. A Firefox plugin can create and write files at will.

The first problem is: how to make the untrusted script communicate with the privileged plug-in. This can be achieved using events. For example, the HTML side sends an event:

// HTML page script

var ev = document.createEvent("Event");
ev.initEvent("EventWithMySpecialName", true, false);
document.f.xxx.dispatchEvent(ev);

At the plugin side, I need of course to listen to that event, and act upon. In the example below, I shoot back another event (which the HTML script must listen to, so it gets the add-on response).

// ADD-ON CODE

function plugin_responder(e)
{
var ev = window.document.createEvent("Event");
ev.initEvent("MyAnswerEvent", true, false);
e.target.dispatchEvent(ev);
}


document.addEventListener("EventWithMySpecialName", plugin_responder, false, true);

One important thing here: since it is not trivial to create a new DOM object at plugin side, I recycled the HTML object (e.target) to send the response event back. The add-on code can manipulate the e.target object in almost all ways that a normal script can -- so it can be used as a crude form of data exchange (think about a hidden field).

Probably, a better way to exchange data between script and add-on is to use specific events that can carry data, like MessageEvent. I tried to use MessageEvent and it failed on Firefox, I am not sure it is supported, so I gave up early and resorted to data exchange via form fields.

This event exchange is also useful for the unprivileged script to detect whether the add-on is installed. If it is not, the script can offer the add-on to the user. There are other ways to detect a Firefox add-on (e.g. using chrome: protocol to try to open an image inside add-on bundle), but since we control the code of both sides here, it seems better to me to ask directly.

Ok, but we still need the code at plugin side that effectively writes a file. The following function saves a file to the Desktop.

// ADD-ON CODE

function savefile(name, data)
{
var fileContractId = "@mozilla.org/file/local;1";
var fileInterface = Components.interfaces.nsILocalFile;
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
var localFileClass = Components.classes[fileContractId];
var file = localFileClass.createInstance(fileInterface);
var desk = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties)
.get("Desk", Components.interfaces.nsIFile);

try {
desk.append(name);
} catch (exp) {
alert("Bad file name");
return;
}

name = desk.path;

try {
file.initWithPath(name);
} catch (exp) {
alert("Bad file name");
return;
}

try {
file.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 420);
} catch (exp) {
//
}

var stream = Components.classes["@mozilla.org/network/file-output-stream;1"]
.createInstance(Components.interfaces.nsIFileOutputStream);

try {
stream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
} catch (exp) {
alert("Cannot create file");
return;
}

stream.write(data, data.length);
stream.close();
}


Note the heavy use of XPCOM components and the request to escalate the privilege. Of course we can read files in add-ons too. So if your application needs to read arbitrary files from the file system (instead of a user-chosen file), a Firefox add-on is a good place to do it.

Some articles show similar code and say that you can run that code in an unprivileged HTML script, by tweaking some key in about:config page. I tried that and it did not work, and offering an add-on is far more user-friendly, after all.