Tag Archives: javascript

Manufacturing User Preferences For MCD

Bottling Line
From vissago

Nobody likes boring code

Mozilla products like Thunderbird and Firefox represent setting choices in a textual “tree” system. (Read the intro.)  Preferences that begin with print. live on the tree branch dealing with printing while those that begin with app.update. are on the auto-update system’s branch.  An easy concept to grasp. In practice, however, the simplicity will easily become a drone of defalutPref(“app.update.auto”, false); if you are not careful.

// This is a boring autoconfig script
defaultPref("browser.dom.window.dump.enabled", true);
defaultPref("browser.download.manager.retention", 0);
defaultPref("browser.download.manager.showAlertOnComplete", false);
defaultPref("browser.download.manager.showWhenStarting", false);
defaultPref("browser.download.save_converter_index", 0);
defaultPref("browser.feeds.handler", "reader");
defaultPref("browser.feeds.handler.default", "web");
defaultPref("browser.feeds.showFirstRunUI", false);
defaultPref("browser.history_expire_days.mirror", 180);
// and so on....

Working at The Preference Factory

Instead of hand-cranking preferences I developed an object prototype to mechanize large blocks of related settings.  The resulting code is more readable and easier to maintain when it looks something like this:

// Some hypothetical printing preferences
var printPrefs = new PreferenceFactory("print");
 
// Lock out printing background or in color and don't allow font download
printPrefs.setPrefs( { print_bgcolor: false,
                       print_bgimages: false,
                       print_downoadfonts: false,
                       print_in_color: false }, "lock" );
 
// Make some sensible defaults
printPrefs.setPrefs( { print_orientation: 0,  // Letter
                       print_to_file: false } );

Whitespace focuses your attention on the block and the object eliminates monotonous pref-branch statement repetition. For the experienced programmer this approach is the familiar object-oriented one of sending messages to receivers. One goal behind this prototype was to help create elegant code, which can be difficult to do in javascript. There is another purpose to PreferenceFactory which I will cover in a later post.

The Bottling Line

The heart of PreferenceFactory is a wrapper around the autoconfig API.  It performs lockPref(), defaultPref(), pref() and getPref() on a branch of the preference tree that you specify when creating the object. Here is the engine driving the factory:

function PreferenceFactory(arg)
{  this.prefNodes = [];   // Nodes in the preference tree
   this.prefBranch = "";  // String representation of the branch
   if(arg)
      this.addPrefBranch(arg);   // Add nodes to the tree and update the string representation
}
 
/* Preference setting helper function
   myServer.setPref("type", "imap", "lock");
   Arguments
     key: The preference string to set
     value: Value to assign to key
     lockLevel: Locking level.
            Valid values are "default", "lock" and "pref"
            Default level is "defulat"
*/
PreferenceFactory.prototype.setPref = function(key,value,lockLevel)
{  if( lockLevel )
   {  switch( lockLevel.toLowerCase() )
      {  case "default":
            this.defaultPref(key,value);
            break;
         case "lock":
            this.lockPref(key,value);
            break;
         case "pref":
            this.pref(key,value);
            break;
         default:
            throw("Unrecognized locking level: " + lockLevel );
      }
   } else
   {  this.defaultPref(key,value);  }
}
 
PreferenceFactory.prototype.defaultPref = function(key,value)
{  if( key )
      defaultPref( this.prefBranch + "." + key, value );
}
 
PreferenceFactory.prototype.getPref = function(key)
{  if( key )
      getPref( this.prefBranch + "." + key );
}
 
PreferenceFactory.prototype.lockPref = function(key,value)
{  if( key )
      lockPref( this.prefBranch + "." + key, value );
}
 
PreferenceFactory.prototype.pref = function(key,value)
{  if( key )
      pref( this.prefBranch + "." + key, value );
}

The “hypothetical print” code example gets its readability from a final function to set multiple preferences.  It uses a function I copied from JsUnit called trueTypeOf().  Its function is much like javascript’s typeof, but handles more than just the built-in data types.  setPrefs() accepts an array or “hash” and iterates over its elements, calling the above setPref() on each 2-tuple.

/* Sets multiple preferences
   Accepts an array or object "hash" and an optional locking level
*/
PreferenceFactory.prototype.setPrefs = function(prefs,lockLevel)
{  switch( trueTypeOf(prefs) )
   {  case "Object":
         for( thing in prefs )
         {  this.setPref(thing, prefs[thing], lockLevel);  }
         break;
      case "Array":
         if( prefs.length % 2 != 0 )
         {  throw("Need an even number of strings to set multiple preferences with an array");  }
         for( i = 0; i < prefs.length; i = i + 2 )
         {  this.setPref(prefs[i], prefs[i+1], lockLevel);  }
         break;
      default:
         throw("I don't know how to set multiple prefs with a " + trueTypeOf(prefs) );
   }
}

Download the full source

Scene from an upcoming post

I alluded to another purpose for this prototype and this is a glimpse of how I use it with Thunderbird in the real world.  Stay tuned.

   // Create an email account
   var brewingAccount = AccountManager.newAccount({ isDefault: true, type: "imap" });
 
   // Lock Preferences on the IMAP server
   brewingAccount.server.setPrefs({ capability: 81,
                                    hostname: "mail.example.com",
                                    port: 993,
                                    realhostname: "mail.example.com",
                                    realusername: brewingMail,
                                    remember_password: false,
                                    socketType: 3,
                                    type: "imap",
                                    userName: brewingMail }, "lock" );
   // Default Preferences
   brewingAccount.server.setPrefs({ check_new_mail: true,
                                    check_time: 10,
                                    cleanup_inbox_on_exit: true,
                                    delete_model: 1,   /* Move to trash */
                                    directory: userInfo.env_home + "/Mail",
                                    "directory-rel": "[ProfD]../../Mail",
                                    empty_trash_on_exit: false,
                                    empty_trash_threshhold: 0,
                                    name: "Brewing Mail",
                                    login_at_startup: true,
                                    using_subscription: false });
 
  // Lock Preferences on the SMTP server
  brewingAccount.smtpServer.setPrefs({ auth_method: 1,    /* User/pass */
                                       port: 465,
                                       username: brewingMail }, "lock" );
  // Default Preferences
  brewingAccount.smtpServer.setPrefs({ description: "Brewing SMTP server",
                                       hostname: "mail.example.com",
                                       try_ssl: 3 });

The source

/**
   By Dean Brundage
   Originally published here:
     http://blog.deanandadie.net/2010/05/manufacturing-user-preferences-for-mcd/
 
   Mix-in prototype for other objects that want to set defaultPref, lockPref
     or just plain old pref().
   Use the utility function copyPrototype() to copy this object's prototype
     functions to other objects.  This can also be used as a stand-alone object.
     Pass the prefix string to the constructor
   Example [mix-in]:
      function Mail.Server()
      {  // It's very important to update the object's preference path
         this.Mail();
         this.addPrefBranch("server");
         // Continue to define Mail.Server
      }
      copyPrototype(Mail.Server, PreferenceFactory);
      // Define the rest of Mail.Server's prototypes
 
      // Then you can set preferences on your Mail.Server object
      myServer = new Mail.Server();
      myServer.setPref("type", "imap", "lock");  // Lock the type of server to IMAP
 
   Example [stand-alone]:
      var prefFact = new PreferenceFactory( ["mail", "accountmanager"] );
      prefFact.setPref("localfoldersserver", "server2" );
 
*/
function PreferenceFactory(arg)
{  this.prefNodes = [];
   this.prefBranch = "";
   if(arg)
      this.addPrefBranch(arg);
}
 
 
// Add a string or many strings to the preference branch
PreferenceFactory.prototype.addPrefBranch = function(nodes)
{  switch( trueTypeOf(nodes) )
   {  case "String":
         this.prefNodes.push(nodes);
         break;
      case "Array":
         for( i = 0; i < nodes.length; i++ )
            this.prefNodes.push(nodes[i]);
         break;
      default:
         throw("Don't know how to addPrefBranch for a " + trueTypeOf(nodes));
         break;
   }
   this.prefBranch = this.prefNodes.join(".");
}
 
 
PreferenceFactory.prototype.defaultPref = function(key,value)
{  if( key )
      defaultPref( this.prefBranch + "." + key, value );
}
 
 
PreferenceFactory.prototype.getPref = function(key)
{  if( key )
      getPref( this.prefBranch + "." + key );
}
 
 
PreferenceFactory.prototype.lockPref = function(key,value)
{  if( key )
      lockPref( this.prefBranch + "." + key, value );
}
 
 
PreferenceFactory.prototype.pref = function(key,value)
{  if( key )
      pref( this.prefBranch + "." + key, value );
}
 
 
/*
   Preference setting helper function
   myServer.setPref("type", "imap", "lock");
   Arguments
     key: The preference string to set
     value: Value to assign to key
     lockLevel: Locking level.
            Valid values are "default", "lock" and "pref"
            Default level is "defulat"
*/
PreferenceFactory.prototype.setPref = function(key,value,lockLevel)
{  if( lockLevel )
   {  switch(lockLevel.toLowerCase())
      {  case "default":
            this.defaultPref(key,value);
            break;
         case "lock":
            this.lockPref(key,value);
            break;
         case "pref":
            this.pref(key,value);
            break;
         default:
            throw("Unrecognized locking level: " + lockLevel );
      }
   } else
   {  this.defaultPref(key,value);  }
}
 
 
/* Sets multiple preferences
   Accepts an array or object "hash" and an optional locking level
*/
PreferenceFactory.prototype.setPrefs = function(prefs,lockLevel)
{  switch( trueTypeOf(prefs) )
   {  case "Object":
         for( thing in prefs )
         {  this.setPref(thing, prefs[thing], lockLevel);  }
         break;
      case "Array":
         if( prefs.length % 2 != 0 )
         {  throw("Need an even number of strings to set multiple preferences with an array");  }
         for( i = 0; i < prefs.length; i = i + 2 )
         {  this.setPref(prefs[i], prefs[i+1], lockLevel);  }
         break;
      default:
         throw("I don't know how to set multiple prefs with a " + trueTypeOf(prefs) );
   }
}
 
function trueTypeOf(something)
{  // Borrowed from jsUnitCore.js.  Thank you.
   // http://github.com/pivotal/jsunit/blob/master/app/jsUnitCore.js
   var result = typeof something;
   try
   {  switch (result)
      {  case 'string':
            break;
         case 'boolean':
            break;
         case 'number':
            break;
         case 'object':
         case 'function':
            switch (something.constructor)
            {  case new String().constructor:
                        result = 'String';
                        break;
               case new Boolean().constructor:
                        result = 'Boolean';
                        break;
               case new Number().constructor:
                        result = 'Number';
                        break;
               case new Array().constructor:
                        result = 'Array';
                        break;
               case new RegExp().constructor:
                        result = 'RegExp';
                        break;
               case new Date().constructor:
                        result = 'Date';
                        break;
               case Function:
                        result = 'Function';
                        break;
               default:
                  var m = something.constructor.toString().match(/function\s*([^( ]+)\(/);
                  if (m)
                     result = m[1];
                  else
                     break;
            }
            break;
      }
   }
   finally
   {  result = result.substr(0, 1).toUpperCase() + result.substr(1);
      return result;
   }
}

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 &amp; 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?

Problematic Decimal Arithmetic in Javascript

It is a pretty well known fact that using javascript to add decimals 0.1 with 0.2 does not result in 0.3. [1] Try it yourself with the FireBug console. For the uninitiated, the problem stems from javascript’s internal representation of numbers. They are actually binary numbers that are usually exact, but sometimes for example, are 0.00000000000000004 off. This is particularly aggravating when writing calculators that rely on js to give accurate results.

In my text inputs I was using toFixed() and some magic HTML attributes to keep decimals nice and clean. However, this method breaks down when a user enters a number with more significant figures than initially set up or you try to operate on two numbers with different sig figs. It was probably inevitable that I use a little arithmetic library extending Number to make decimals play nice.

Since javascript is 13 years old I thought it would be a simple thing to find such a library. I was wrong. After four days learning, looking and lamenting I had no library. After putting this one together in about a day and a half I am not surprised that nobody published theirs. Most of the eleven functions are one-liners, yet it bothers me that there are probably thirty-odd implementations of the this out there and not one found through Google.

// decimal_arithmetic.js
String.prototype.digitsAfterDecimal = function()
{  var parts = this.split(".", 2);  // FIXME: Not international!
   if( ! parts[1] )
   {  parts[1] = "";  }
   return parts[1].length;
};
 
Number.prototype.biggerScalar = function(n)
{  return n.scale() > this.scale() ? n.scale() : this.scale();  };
 
Number.prototype.digitsAfterDecimal = function()
{  return this.toString().digitsAfterDecimal();  };
 
Number.prototype.divided = function(n)
{  return this.dividedBy(n);  };
 
Number.prototype.dividedBy = function(n)
{  return this.multiply( n.reciprocal() );  };
 
Number.prototype.minus = function(n)
{  return this.plus( n.negative() );  };
 
Number.prototype.multiply = function(n)
{  var s = this.biggerScalar(n);
   return (Math.round(s*this,0) * Math.round(s*n,0)) / (s*s);
};
 
Number.prototype.negative = function()
{  return -1 * this;  };
 
Number.prototype.plus = function(n)
{  var s = this.biggerScalar(n);
   return (Math.round(s*this,0) + Math.round(s*n,0)) / s;
};
 
Number.prototype.reciprocal = function()
{  return 1 / this;  };
 
Number.prototype.scale = function()
{  return Math.pow(10, this.digitsAfterDecimal() );  };

Now you can do magical things like:

0.1.plus(0.2)
// 0.3

yielding the correct results.

I am looking forward to javascript 2.0 when I can override the + operator. Maybe I won’t go that far since binary arithmetic is still faster.

[1] http://groups.google.com/group/comp.lang.javascript/…

It doesn’t have to start out so advanced

Merry Christmas. Here’s a little javascript brewsession present.

http://ror.deanandadie.net/table.html

I was thinking about the ingredients table Greg designed. While it is good looking and we’ll definitely use it, I would rather see a very simple list to start with and give the option to have an “advanced view”. It occurred to me to let the user choose which columns they want to see. So I restarted work on BrewSession again with this prototype. It’s a work in progress, but a solid start.

–Dean