Reading Local Files With Javascript

Security Conscious Javascript

Normally, javascript does not have access to local files.  Rightfully so because almost every web server should be untrusted and allowing anybody to read your files is a large security risk.  Mission Control Desktop, however is a javascript application with access to XPCOM, Mozilla’s component object model. This article is an example of how to use it to read local files.

Solaris Printing Configuration

I use MCD to configure thousands of users’ browser and mail clients.  In Solaris, printer preferences are set according to the Mozilla Postscript subsystem in a .printers file in the home directory.  Firefox and Thunderbird do not support this file interface so I use autoconfig to read the file and set printer preferences accordingly.

I wrote an object prototype to read and parse $HOME/.printers which the autoconfig uses to set the print.printer_list preference.  The prototype searches the file for a string indicating the list of printers to use:

# $HOME/.printers
# Set the default printer to riemann
_default riemann
# Display a chooser for three printers, ignoring all others.
_all newton,poincare,riemann

The line beginning with _all is quite important in my environment with 1,700 printers configured through LDAP.  If Thunderbird or Firefox were to try to load the entire printer list, it would become overwhelmed and crash.

extract_allPrinters utility

First, a look at the utility function I use to find the right line.

if( typeof(FileInterface) == "undefined" )  // Barf if the object prototype is undefined
   throw("autoconfig constructed improperly: need classes/file_interface.js before extract_all_printers.js");
 
function extract_allPrinters(path)
{  if( typeof(path) == "undefined" )
      path = env_home + "/.printers";
 
   // Regular expression to search .printers for _all
   re = /^\s*_all\s+(.+)\s*/i;
   lines = new FileInterface(path).grep(re);
 
   if( typeof(lines) != "undefined" )
   {  // Take the first match
      allPrinters = re.exec( lines[0] )[1].replace(/,\s*/g, " ");
   }
   return allPrinters;
}
 
/*
   *snip*
*/
 
// Tell FF & TB which printers to offer a print dialogue for
defaultPref("print.printer_list", extract_allPrinters() );

The first lines check that the FileInterface object prototype is defined.  This is necessary because my autoconfig script broken into component scripts in the filesystem.  Besides making it easier to maintain, I can share functionality between Firefox and Thunderbird while excluding application-specific parts from the wrong autoconfig.

This function uses a prototype called FileInterface (explained later) to grep .printers for lines beginning with _all. If grep returns a match, the function takes the first one. print.printer_list takes a space-separated list of printers while _all is comma-separated, so the final thing to do is replace the commas with spaces.

Later on in the script I call defaultPref to set the printer list.

The FileInterface object prototype

nsILocalFile is the XPCOM interface used to access files on the client-side filesystem.  This object prototype provides only enough functionality to search files, but remains extensible should I need to create or modify.

// All you need is a (string) path
function FileInterface(path)
{  if( typeof(path) == "undefined" )
     throw("Need a path for FileInterface()");
   this.path = path;
}
 
/* Emulate the functionality of unix grep
   Takes a RegExp as it's only argument
   Returns an array of lines matching the RegExp
     E.G.: passwd = new FileInterface("/etc/passwd");
           lines = passwd.grep(/brundage/);
           // lines[0] = brundage:x:1002:1002::/home/brundage:/bin/zsh
*/
FileInterface.prototype.grep = function(re)
{  matches = undefined;
   if( re )
   {  line = {};
      matches = [];
      this.initIStream();
      do
      {  hasMore = this.iStream.readLine(line);
         if( re.test(line.value) )
            matches.push(line.value);
      } while(hasMore);
      this.iStream.close();
   }
   return matches;
}
 
/* Initializes a nsILineInputStream for higher-level functions
   Takes three arguments, assigning read-only defaults to undefined arguments.
   For possible values see: https://developer.mozilla.org/en/NsIFileInputStream
*/
FileInterface.prototype.initIStream = function(ioFlags,perm,behaviorFlags)
{  if( ! ioFlags )
     ioFlags = -1;  // Default mode (PR_READONLY)
   if( ! perm )
     perm = -1;  // Default mode (0)
   if( ! behaviorFlags )
     behaviorFlags = 0;
 
   // Initializing the stream requires an nsILocalFile.  Make one out of the path attribute.
   this.initLocalFile();
 
   // Get the nsIFileInputStream instance from the global Components variable
   this.iStream =  Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);
   if( ! this.iStream )  // Bad Things
      throw("network/file-input-stream component does not exist");
 
   // Point the stream at the iLocalFile
   this.iStream.init(this.iLocalFile, ioFlags, perm, behaviorFlags);
   // Transform iStream into a nsILineInputStream
   this.iStream.QueryInterface(Components.interfaces.nsILineInputStream);
}
 
/* Initialize an nsILocalFile instance with the path attribute of this object
   Required for streams
*/
FileInterface.prototype.initLocalFile = function()
{  this.iLocalFile = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
   if( ! this.iLocalFile )  // Bad Things
     throw("file/local component does not exist");
 
   this.iLocalFile.initWithPath(this.path);
}

There you have it

I have only encountered this single situation that requires access to the local filesystem.  Can you think of others?

One thought on “Reading Local Files With Javascript”

Leave a Reply