I was working on a userscript last week to facilitate downloading and saving images from a facebook album. I wanted to automate this, rather than have to manually open and save each picture. The Greasemonkey add-on to Firefox seemed like the best way to get this done, and before long I had a bit of JavaScript that could fetch and save a file to my local disk with no user interaction. In the spirit of logging things I might want to come back to later, join saving other people the frustration I experienced with all my searches finding metablogs referring to the same ghostblog, I briefly document what I needed.
First, it seems sensible to cover downloading binary data via Javascript. I used the XMLHttpRequest object for this, which I created in this cross-browser way:
var oXML=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();
My first attempts at downloading images would produce data, but not a viable image. It turned out to be affected by the way XMLHttpRequest process the HTTP response, which seemed hopeless at first, but proved to be easy to resolve, given the right information. Fetching binary data is a two-step process. The request needs to be configured properly, and the response requires some processing to restore the data to it's original form. The configuration of the request is simple, one additional line of code:
oXML.overrideMimeType('text/plain; charset=x-user-defined');
Post-processing is as simple as striping the high-order bytes from each character in the response text and building a new (binary) string from the low-order bytes:
function repairBinary(sIn){
var sOut=[];
for(var i=0;i<sIn.length;i++){
sOut[i]=String.fromCharCode(sIn.charCodeAt(i) & 0xff);
}
return sOut.join('');
}
Finally, I need a method for automated file saving:
function writeFile(sFilename,sData) {
if(!sFilename)return;
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
file.initWithPath( sFilename );
stream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
stream.init(file, -1, -1, null);
stream.write(sData, sData.length);
stream.flush();
stream.close();
}
The above requires a configuration change in Firefox. Open
about:config
and check that
[signed.applets.codebase_principal_support]
is set
to "true". This will allow the browser to prompt the user for
access to write the local file. Clicking "Remember this
decision" and "Allow" will save the user from having to answer
the question for each file, so long as they stay on the same
page. It is not globally remembered, so there is no worry
about other sites abusing this permission. Of course, since
this is a user script, one could just unset the option when
done.