Tag Archives: preferences

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;
   }
}

Mapping Firefox & Thunderbird Behaviors to Preference Settings

Thunderbird settings

Individual in the Enterprise

Firefox, Thunderbird have full-fledged graphical settings editors.  It is easy for a user to change the behavior of his or her web browser with a few mouse clicks.  While this approach is sensible for the home user, GUIs hamper software configuration in the corporate enterprise.  Although Mozilla products textually represent preferences in a flat file, discovering the right text and value is not always simple.  I will outline some techniques I use to determine preference strings for a given behavior.

Preferences in Mission Control Desktop

Mozilla MCD autoconfig is an invaluable tool to the software administrator.  It runs at browser startup setting preferences according to corporate policy.  After starting, Firefox saves all settings to the user’s local prefs.js file.  The autoconfig API and user  prefs.js work with text preferences.  When you decide to change the application’s behavior your prefs.js is a good place to look.

Start clean

Quit Firefox.  Backup then remove your Firefox profile directory. On linux it is in $HOME/.mozilla/firefox. Then launch the browser starting with a good autoconfigured profile.

Compare

Take a backup of your user prefs.js from Firefox”s profile directory. ($HOME/.mozilla/firefox//prefs.js)  Make the behavior change and apply.  Use diff to compare the preference files.

An example

Suppose your company decides that Firefox should only keep third-party cookies for the lifetime of the browser.  Once the user closes Firefox, it will delete all third-party cookies.  Make the change (for version 3.6.3) in Settings –> Privacy –> Check Accept cookies from sites, then check Accept third-party cookies and change the dropdown to Keep until: I close Firefox.

Your preferences should look something like this:

0 apollo firefox/kzssiknu.default % diff -u prefs.js.pre prefs.js
--- prefs.js.pre    2010-04-22 12:57:30.000000000 -0500
+++ prefs.js        2010-04-22 12:57:39.000000000 -0500
@@ -230,6 +230,7 @@
 user_pref("lightweightThemes.persisted.footerURL", true);
 user_pref("lightweightThemes.persisted.headerURL", true);
 user_pref("metrics.upload.enable", false);
+user_pref("network.cookie.lifetimePolicy", 2);
 user_pref("network.cookie.prefsMigrated", true);
 user_pref("nglayout.debug.disable_xul_cache", true);
 user_pref("nglayout.debug.disable_xul_fastload", true);

See that a new preference called network.cookie.lifetimePolicy was inserted with value 2.  Use these in autoconfig, calling

// Remember third-party cookies until the browser closes
lockPref("network.cookie.lifetimePolicy", 2);

Sometimes it’s not that easy

The above method does not work all the time.  For example, Thunderbird’s password settings are not so obvious.  My company does not allow password storage so I needed to lock out that behavior.
Save Passwords

Inspecting the preferences GUI leads nowhere.  My first resource in these cases is the About:config_entries page on MozillaZine.  It’s a wild wiki page containing mostly-complete setting documentation for all the Mozilla products.  There you will find a table with the right information.

Name Type Meaning of Values
signon. rememberSignons Boolean True: (default): Enable the Password Manager
False Opposite of the above

The About:config_entries page also has pointers to the wiki’s Category:Preferences and The Preferential Project.  Each page has something the others lack.  When these resources fail, I sign on to irc.mozilla.org (as OccamRazor, old-school IRC etiquette rules apply.) or as a last resort, hit a search engine.

Document it

When you find the right text to twiddle document the behavior in your autoconfig as I did for the cookie lifetime above.