text: /* ***** BEGIN LICENSE BLOCK *****
   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
   *
   * The contents of this file are subject to the Mozilla Public License Version
   * 1.1 (the "License"); you may not use this file except in compliance with
   * the License. You may obtain a copy of the License at
   * http://www.mozilla.org/MPL/
   *
   * Software distributed under the License is distributed on an "AS IS" basis,
   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
   * for the specific language governing rights and limitations under the
   * License.
   *
   * The Original Code is Content Preferences (cpref).
   *
   * The Initial Developer of the Original Code is Mozilla.
   * Portions created by the Initial Developer are Copyright (C) 2006
   * the Initial Developer. All Rights Reserved.
   *
   * Contributor(s):
   *   Myk Melez <myk@mozilla.org>
   *   Ehsan Akhgari <ehsan.akhgari@gmail.com>
   *
   * Alternatively, the contents of this file may be used under the terms of
   * either the GNU General Public License Version 2 or later (the "GPL"), or
   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
   * in which case the provisions of the GPL or the LGPL are applicable instead
   * of those above. If you wish to allow use of your version of this file only
   * under the terms of either the GPL or the LGPL, and not to allow others to
   * use your version of this file under the terms of the MPL, indicate your
   * decision by deleting the provisions above and replace them with the notice
   * and other provisions required by the GPL or the LGPL. If you do not delete
   * the provisions above, a recipient may use your version of this file under
   * the terms of any one of the MPL, the GPL or the LGPL.
   *
   * ***** END LICENSE BLOCK ***** */
  
  const Ci = Components.interfaces;
  const Cc = Components.classes;
  const Cr = Components.results;
  const Cu = Components.utils;
  
  Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  
  function ContentPrefService() {
    // If this throws an exception, it causes the getService call to fail,
    // but the next time a consumer tries to retrieve the service, we'll try
    // to initialize the database again, which might work if the failure
    // was due to a temporary condition (like being out of disk space).
    this._dbInit();
  
    // Observe shutdown so we can shut down the database connection.
    this._observerSvc.addObserver(this, "xpcom-shutdown", false);
  }
  
  ContentPrefService.prototype = {
    //**************************************************************************//
    // XPCOM Plumbing
  
    classDescription: "Content Pref Service",
    classID:          Components.ID("{e6a3f533-4ffa-4615-8eb4-d4e72d883fa7}"),
    contractID:       "@mozilla.org/content-pref/service;1",
    QueryInterface:   XPCOMUtils.generateQI([Ci.nsIContentPrefService]),
  
  
    //**************************************************************************//
    // Convenience Getters
  
    // Observer Service
    __observerSvc: null,
    get _observerSvc ContentPrefService_get__observerSvc() {
      if (!this.__observerSvc)
        this.__observerSvc = Cc["@mozilla.org/observer-service;1"].
                             getService(Ci.nsIObserverService);
      return this.__observerSvc;
    },
  
    // Console Service
    __consoleSvc: null,
    get _consoleSvc ContentPrefService_get__consoleSvc() {
      if (!this.__consoleSvc)
        this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"].
                            getService(Ci.nsIConsoleService);
      return this.__consoleSvc;
    },
  
    // Preferences Service
    __prefSvc: null,
    get _prefSvc ContentPrefService_get__prefSvc() {
      if (!this.__prefSvc)
        this.__prefSvc = Cc["@mozilla.org/preferences-service;1"].
                         getService(Ci.nsIPrefBranch2);
      return this.__prefSvc;
    },
  
  
    //**************************************************************************//
    // Destruction
  
    _destroy: function ContentPrefService__destroy() {
      this._observerSvc.removeObserver(this, "xpcom-shutdown");
  
      // Delete references to XPCOM components to make sure we don't leak them
      // (although we haven't observed leakage in tests).  Also delete references
      // in _observers and _genericObservers to avoid cycles with those that
      // refer to us and don't remove themselves from those observer pools.
      for (var i in this) {
        try { this[i] = null }
        // Ignore "setting a property that has only a getter" exceptions.
        catch(ex) {}
      }
    },
  
  
    //**************************************************************************//
    // nsIObserver
  
    observe: function ContentPrefService_observe(subject, topic, data) {
      switch (topic) {
        case "xpcom-shutdown":
          this._destroy();
          break;
      }
    },
  
  
    //**************************************************************************//
    // nsIContentPrefService
  
    getPref: function ContentPrefService_getPref(aURI, aName) {
      if (aURI) {
        var group = this.grouper.group(aURI);
        return this._selectPref(group, aName);
      }
  
      return this._selectGlobalPref(aName);
    },
  
    setPref: function ContentPrefService_setPref(aURI, aName, aValue) {
      // If the pref is already set to the value, there's nothing more to do.
      var currentValue = this.getPref(aURI, aName);
      if (typeof currentValue != "undefined") {
        if (currentValue == aValue)
          return;
      }
      else {
        // If we are in private browsing mode, refuse to set new prefs
        var inPrivateBrowsing = false;
        try { // The Private Browsing service might not be available.
          var pbs = Cc["@mozilla.org/privatebrowsing;1"].
                    getService(Ci.nsIPrivateBrowsingService);
          inPrivateBrowsing = pbs.privateBrowsingEnabled;
        } catch (e) {}
        if (inPrivateBrowsing)
          return;
      }
  
      var settingID = this._selectSettingID(aName) || this._insertSetting(aName);
      var group, groupID, prefID;
      if (aURI) {
        group = this.grouper.group(aURI);
        groupID = this._selectGroupID(group) || this._insertGroup(group);
        prefID = this._selectPrefID(groupID, settingID);
      }
      else {
        group = null;
        groupID = null;
        prefID = this._selectGlobalPrefID(settingID);
      }
  
      // Update the existing record, if any, or create a new one.
      if (prefID)
        this._updatePref(prefID, aValue);
      else
        this._insertPref(groupID, settingID, aValue);
  
      for each (var observer in this._getObservers(aName)) {
        try {
          observer.onContentPrefSet(group, aName, aValue);
        }
        catch(ex) {
          Cu.reportError(ex);
        }
      }
    },
  
    hasPref: function ContentPrefService_hasPref(aURI, aName) {
      // XXX If consumers end up calling this method regularly, then we should
      // optimize this to query the database directly.
      return (typeof this.getPref(aURI, aName) != "undefined");
    },
  
    removePref: function ContentPrefService_removePref(aURI, aName) {
      // If there's no old value, then there's nothing to remove.
      if (!this.hasPref(aURI, aName))
        return;
  
      var settingID = this._selectSettingID(aName);
      var group, groupID, prefID;
      if (aURI) {
        group = this.grouper.group(aURI);
        groupID = this._selectGroupID(group);
        prefID = this._selectPrefID(groupID, settingID);
      }
      else {
        group = null;
        groupID = null;
        prefID = this._selectGlobalPrefID(settingID);
      }
  
      this._deletePref(prefID);
  
      // Get rid of extraneous records that are no longer being used.
      this._deleteSettingIfUnused(settingID);
      if (groupID)
        this._deleteGroupIfUnused(groupID);
  
      this._notifyPrefRemoved(group, aName);
    },
  
    removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs() {
      this._dbConnection.beginTransaction();
      try {
        this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE groupID IS NOT NULL");
        this._dbConnection.executeSimpleSQL("DELETE FROM groups");
        this._dbConnection.executeSimpleSQL(
          "DELETE FROM settings " +
          "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)"
        );
        this._dbConnection.commitTransaction();
      }
      catch(ex) {
        this._dbConnection.rollbackTransaction();
        throw ex;
      }
    },
  
    removePrefsByName: function ContentPrefService_removePrefsByName(aName) {
      var settingID = this._selectSettingID(aName);
      if (!settingID) {
        return;
      }
      
      var selectGroupsStmt = this._dbCreateStatement(
        "SELECT groups.id AS groupID, groups.name AS groupName " +
        "FROM prefs " +
        "JOIN groups ON prefs.groupID = groups.id " +
        "WHERE prefs.settingID = :setting "
      );
      
      var groupNames = [];
      var groupIDs = [];
      try {
        selectGroupsStmt.params.setting = settingID;
      
        while (selectGroupsStmt.step()) {
          groupIDs.push(selectGroupsStmt.row["groupID"]);
          groupNames.push(selectGroupsStmt.row["groupName"]);
        }
      }
      finally {
        selectGroupsStmt.reset();
      }
      
      if (this.hasPref(null, aName)) {
        groupNames.push(null);
      }
  
      this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE settingID = " + settingID);
      this._dbConnection.executeSimpleSQL("DELETE FROM settings WHERE id = " + settingID);
  
      for (var i = 0; i < groupNames.length; i++) {
        this._notifyPrefRemoved(groupNames[i], aName);
        if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length)
          this._deleteGroupIfUnused(groupIDs[i]);
      }
    },
  
    getPrefs: function ContentPrefService_getPrefs(aURI) {
      if (aURI) {
        var group = this.grouper.group(aURI);
        return this._selectPrefs(group);
      }
  
      return this._selectGlobalPrefs();
    },
  
    getPrefsByName: function ContentPrefService_getPrefsByName(aName) {
      return this._selectPrefsByName(aName);
    },
  
    // A hash of arrays of observers, indexed by setting name.
    _observers: {},
  
    // An array of generic observers, which observe all settings.
    _genericObservers: [],
  
    addObserver: function ContentPrefService_addObserver(aName, aObserver) {
      var observers;
      if (aName) {
        if (!this._observers[aName])
          this._observers[aName] = [];
        observers = this._observers[aName];
      }
      else
        observers = this._genericObservers;
  
      if (observers.indexOf(aObserver) == -1)
        observers.push(aObserver);
    },
  
    removeObserver: function ContentPrefService_removeObserver(aName, aObserver) {
      var observers;
      if (aName) {
        if (!this._observers[aName])
          return;
        observers = this._observers[aName];
      }
      else
        observers = this._genericObservers;
  
      if (observers.indexOf(aObserver) != -1)
        observers.splice(observers.indexOf(aObserver), 1);
    },
  
    /**
     * Construct a list of observers to notify about a change to some setting,
     * putting setting-specific observers before before generic ones, so observers
     * that initialize individual settings (like the page style controller)
     * execute before observers that display multiple settings and depend on them
     * being initialized first (like the content prefs sidebar).
     */
    _getObservers: function ContentPrefService__getObservers(aName) {
      var observers = [];
  
      if (aName && this._observers[aName])
        observers = observers.concat(this._observers[aName]);
      observers = observers.concat(this._genericObservers);
  
      return observers;
    },
    
    _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(aGroup, aName) {
      for each (var observer in this._getObservers(aName)) {
        try {
          observer.onContentPrefRemoved(aGroup, aName);
        }
        catch(ex) {
          Cu.reportError(ex);
        }
      }
    },
  
    _grouper: null,
    get grouper ContentPrefService_get_grouper() {
      if (!this._grouper)
        this._grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"].
                        getService(Ci.nsIContentURIGrouper);
      return this._grouper;
    },
  
    get DBConnection ContentPrefService_get_DBConnection() {
      return this._dbConnection;
    },
  
  
    //**************************************************************************//
    // Data Retrieval & Modification
  
    __stmtSelectPref: null,
    get _stmtSelectPref ContentPrefService_get__stmtSelectPref() {
      if (!this.__stmtSelectPref)
        this.__stmtSelectPref = this._dbCreateStatement(
          "SELECT prefs.value AS value " +
          "FROM prefs " +
          "JOIN groups ON prefs.groupID = groups.id " +
          "JOIN settings ON prefs.settingID = settings.id " +
          "WHERE groups.name = :group " +
          "AND settings.name = :setting"
        );
  
      return this.__stmtSelectPref;
    },
  
    _selectPref: function ContentPrefService__selectPref(aGroup, aSetting) {
      var value;
  
      try {
        this._stmtSelectPref.params.group = aGroup;
        this._stmtSelectPref.params.setting = aSetting;
  
        if (this._stmtSelectPref.step())
          value = this._stmtSelectPref.row["value"];
      }
      finally {
        this._stmtSelectPref.reset();
      }
  
      return value;
    },
  
    __stmtSelectGlobalPref: null,
    get _stmtSelectGlobalPref ContentPrefService_get__stmtSelectGlobalPref() {
      if (!this.__stmtSelectGlobalPref)
        this.__stmtSelectGlobalPref = this._dbCreateStatement(
          "SELECT prefs.value AS value " +
          "FROM prefs " +
          "JOIN settings ON prefs.settingID = settings.id " +
          "WHERE prefs.groupID IS NULL " +
          "AND settings.name = :name"
        );
  
      return this.__stmtSelectGlobalPref;
    },
  
    _selectGlobalPref: function ContentPrefService__selectGlobalPref(aName) {
      var value;
  
      try {
        this._stmtSelectGlobalPref.params.name = aName;
  
        if (this._stmtSelectGlobalPref.step())
          value = this._stmtSelectGlobalPref.row["value"];
      }
      finally {
        this._stmtSelectGlobalPref.reset();
      }
  
      return value;
    },
  
    __stmtSelectGroupID: null,
    get _stmtSelectGroupID ContentPrefService_get__stmtSelectGroupID() {
      if (!this.__stmtSelectGroupID)
        this.__stmtSelectGroupID = this._dbCreateStatement(
          "SELECT groups.id AS id " +
          "FROM groups " +
          "WHERE groups.name = :name "
        );
  
      return this.__stmtSelectGroupID;
    },
  
    _selectGroupID: function ContentPrefService__selectGroupID(aName) {
      var id;
  
      try {
        this._stmtSelectGroupID.params.name = aName;
  
        if (this._stmtSelectGroupID.step())
          id = this._stmtSelectGroupID.row["id"];
      }
      finally {
        this._stmtSelectGroupID.reset();
      }
  
      return id;
    },
  
    __stmtInsertGroup: null,
    get _stmtInsertGroup ContentPrefService_get__stmtInsertGroup() {
      if (!this.__stmtInsertGroup)
        this.__stmtInsertGroup = this._dbCreateStatement(
          "INSERT INTO groups (name) VALUES (:name)"
        );
  
      return this.__stmtInsertGroup;
    },
  
    _insertGroup: function ContentPrefService__insertGroup(aName) {
      this._stmtInsertGroup.params.name = aName;
      this._stmtInsertGroup.execute();
      return this._dbConnection.lastInsertRowID;
    },
  
    __stmtSelectSettingID: null,
    get _stmtSelectSettingID ContentPrefService_get__stmtSelectSettingID() {
      if (!this.__stmtSelectSettingID)
        this.__stmtSelectSettingID = this._dbCreateStatement(
          "SELECT id FROM settings WHERE name = :name"
        );
  
      return this.__stmtSelectSettingID;
    },
  
    _selectSettingID: function ContentPrefService__selectSettingID(aName) {
      var id;
  
      try {
        this._stmtSelectSettingID.params.name = aName;
  
        if (this._stmtSelectSettingID.step())
          id = this._stmtSelectSettingID.row["id"];
      }
      finally {
        this._stmtSelectSettingID.reset();
      }
  
      return id;
    },
  
    __stmtInsertSetting: null,
    get _stmtInsertSetting ContentPrefService_get__stmtInsertSetting() {
      if (!this.__stmtInsertSetting)
        this.__stmtInsertSetting = this._dbCreateStatement(
          "INSERT INTO settings (name) VALUES (:name)"
        );
  
      return this.__stmtInsertSetting;
    },
  
    _insertSetting: function ContentPrefService__insertSetting(aName) {
      this._stmtInsertSetting.params.name = aName;
      this._stmtInsertSetting.execute();
      return this._dbConnection.lastInsertRowID;
    },
  
    __stmtSelectPrefID: null,
    get _stmtSelectPrefID ContentPrefService_get__stmtSelectPrefID() {
      if (!this.__stmtSelectPrefID)
        this.__stmtSelectPrefID = this._dbCreateStatement(
          "SELECT id FROM prefs WHERE groupID = :groupID AND settingID = :settingID"
        );
  
      return this.__stmtSelectPrefID;
    },
  
    _selectPrefID: function ContentPrefService__selectPrefID(aGroupID, aSettingID) {
      var id;
  
      try {
        this._stmtSelectPrefID.params.groupID = aGroupID;
        this._stmtSelectPrefID.params.settingID = aSettingID;
  
        if (this._stmtSelectPrefID.step())
          id = this._stmtSelectPrefID.row["id"];
      }
      finally {
        this._stmtSelectPrefID.reset();
      }
  
      return id;
    },
  
    __stmtSelectGlobalPrefID: null,
    get _stmtSelectGlobalPrefID ContentPrefService_get__stmtSelectGlobalPrefID() {
      if (!this.__stmtSelectGlobalPrefID)
        this.__stmtSelectGlobalPrefID = this._dbCreateStatement(
          "SELECT id FROM prefs WHERE groupID IS NULL AND settingID = :settingID"
        );
  
      return this.__stmtSelectGlobalPrefID;
    },
  
    _selectGlobalPrefID: function ContentPrefService__selectGlobalPrefID(aSettingID) {
      var id;
  
      try {
        this._stmtSelectGlobalPrefID.params.settingID = aSettingID;
  
        if (this._stmtSelectGlobalPrefID.step())
          id = this._stmtSelectGlobalPrefID.row["id"];
      }
      finally {
        this._stmtSelectGlobalPrefID.reset();
      }
  
      return id;
    },
  
    __stmtInsertPref: null,
    get _stmtInsertPref ContentPrefService_get__stmtInsertPref() {
      if (!this.__stmtInsertPref)
        this.__stmtInsertPref = this._dbCreateStatement(
          "INSERT INTO prefs (groupID, settingID, value) " +
          "VALUES (:groupID, :settingID, :value)"
        );
  
      return this.__stmtInsertPref;
    },
  
    _insertPref: function ContentPrefService__insertPref(aGroupID, aSettingID, aValue) {
      this._stmtInsertPref.params.groupID = aGroupID;
      this._stmtInsertPref.params.settingID = aSettingID;
      this._stmtInsertPref.params.value = aValue;
      this._stmtInsertPref.execute();
      return this._dbConnection.lastInsertRowID;
    },
  
    __stmtUpdatePref: null,
    get _stmtUpdatePref ContentPrefService_get__stmtUpdatePref() {
      if (!this.__stmtUpdatePref)
        this.__stmtUpdatePref = this._dbCreateStatement(
          "UPDATE prefs SET value = :value WHERE id = :id"
        );
  
      return this.__stmtUpdatePref;
    },
  
    _updatePref: function ContentPrefService__updatePref(aPrefID, aValue) {
      this._stmtUpdatePref.params.id = aPrefID;
      this._stmtUpdatePref.params.value = aValue;
      this._stmtUpdatePref.execute();
    },
  
    __stmtDeletePref: null,
    get _stmtDeletePref ContentPrefService_get__stmtDeletePref() {
      if (!this.__stmtDeletePref)
        this.__stmtDeletePref = this._dbCreateStatement(
          "DELETE FROM prefs WHERE id = :id"
        );
  
      return this.__stmtDeletePref;
    },
  
    _deletePref: function ContentPrefService__deletePref(aPrefID) {
      this._stmtDeletePref.params.id = aPrefID;
      this._stmtDeletePref.execute();
    },
  
    __stmtDeleteSettingIfUnused: null,
    get _stmtDeleteSettingIfUnused ContentPrefService_get__stmtDeleteSettingIfUnused() {
      if (!this.__stmtDeleteSettingIfUnused)
        this.__stmtDeleteSettingIfUnused = this._dbCreateStatement(
          "DELETE FROM settings WHERE id = :id " +
          "AND id NOT IN (SELECT DISTINCT settingID FROM prefs)"
        );
  
      return this.__stmtDeleteSettingIfUnused;
    },
  
    _deleteSettingIfUnused: function ContentPrefService__deleteSettingIfUnused(aSettingID) {
      this._stmtDeleteSettingIfUnused.params.id = aSettingID;
      this._stmtDeleteSettingIfUnused.execute();
    },
  
    __stmtDeleteGroupIfUnused: null,
    get _stmtDeleteGroupIfUnused ContentPrefService_get__stmtDeleteGroupIfUnused() {
      if (!this.__stmtDeleteGroupIfUnused)
        this.__stmtDeleteGroupIfUnused = this._dbCreateStatement(
          "DELETE FROM groups WHERE id = :id " +
          "AND id NOT IN (SELECT DISTINCT groupID FROM prefs)"
        );
  
      return this.__stmtDeleteGroupIfUnused;
    },
  
    _deleteGroupIfUnused: function ContentPrefService__deleteGroupIfUnused(aGroupID) {
      this._stmtDeleteGroupIfUnused.params.id = aGroupID;
      this._stmtDeleteGroupIfUnused.execute();
    },
  
    __stmtSelectPrefs: null,
    get _stmtSelectPrefs ContentPrefService_get__stmtSelectPrefs() {
      if (!this.__stmtSelectPrefs)
        this.__stmtSelectPrefs = this._dbCreateStatement(
          "SELECT settings.name AS name, prefs.value AS value " +
          "FROM prefs " +
          "JOIN groups ON prefs.groupID = groups.id " +
          "JOIN settings ON prefs.settingID = settings.id " +
          "WHERE groups.name = :group "
        );
  
      return this.__stmtSelectPrefs;
    },
  
    _selectPrefs: function ContentPrefService__selectPrefs(aGroup) {
      var prefs = Cc["@mozilla.org/hash-property-bag;1"].
                  createInstance(Ci.nsIWritablePropertyBag);
  
      try {
        this._stmtSelectPrefs.params.group = aGroup;
  
        while (this._stmtSelectPrefs.step())
          prefs.setProperty(this._stmtSelectPrefs.row["name"],
                            this._stmtSelectPrefs.row["value"]);
      }
      finally {
        this._stmtSelectPrefs.reset();
      }
  
      return prefs;
    },
  
    __stmtSelectGlobalPrefs: null,
    get _stmtSelectGlobalPrefs ContentPrefService_get__stmtSelectGlobalPrefs() {
      if (!this.__stmtSelectGlobalPrefs)
        this.__stmtSelectGlobalPrefs = this._dbCreateStatement(
          "SELECT settings.name AS name, prefs.value AS value " +
          "FROM prefs " +
          "JOIN settings ON prefs.settingID = settings.id " +
          "WHERE prefs.groupID IS NULL"
        );
  
      return this.__stmtSelectGlobalPrefs;
    },
  
    _selectGlobalPrefs: function ContentPrefService__selectGlobalPrefs() {
      var prefs = Cc["@mozilla.org/hash-property-bag;1"].
                  createInstance(Ci.nsIWritablePropertyBag);
  
      try {
        while (this._stmtSelectGlobalPrefs.step())
          prefs.setProperty(this._stmtSelectGlobalPrefs.row["name"],
                            this._stmtSelectGlobalPrefs.row["value"]);
      }
      finally {
        this._stmtSelectGlobalPrefs.reset();
      }
  
      return prefs;
    },
  
    __stmtSelectPrefsByName: null,
    get _stmtSelectPrefsByName ContentPrefService_get__stmtSelectPrefsByName() {
      if (!this.__stmtSelectPrefsByName)
        this.__stmtSelectPrefsByName = this._dbCreateStatement(
          "SELECT groups.name AS groupName, prefs.value AS value " +
          "FROM prefs " +
          "JOIN groups ON prefs.groupID = groups.id " +
          "JOIN settings ON prefs.settingID = settings.id " +
          "WHERE settings.name = :setting "
        );
  
      return this.__stmtSelectPrefsByName;
    },
  
    _selectPrefsByName: function ContentPrefService__selectPrefsByName(aName) {
      var prefs = Cc["@mozilla.org/hash-property-bag;1"].
                  createInstance(Ci.nsIWritablePropertyBag);
  
      try {
        this._stmtSelectPrefsByName.params.setting = aName;
  
        while (this._stmtSelectPrefsByName.step())
          prefs.setProperty(this._stmtSelectPrefsByName.row["groupName"],
                            this._stmtSelectPrefsByName.row["value"]);
      }
      finally {
        this._stmtSelectPrefsByName.reset();
      }
      
      var global = this._selectGlobalPref(aName);
      if (typeof global != "undefined") {
        prefs.setProperty(null, global);
      }
  
      return prefs;
    },
  
  
    //**************************************************************************//
    // Database Creation & Access
  
    _dbVersion: 3,
  
    _dbSchema: {
      tables: {
        groups:     "id           INTEGER PRIMARY KEY, \
                     name         TEXT NOT NULL",
    
        settings:   "id           INTEGER PRIMARY KEY, \
                     name         TEXT NOT NULL",
    
        prefs:      "id           INTEGER PRIMARY KEY, \
                     groupID      INTEGER REFERENCES groups(id), \
                     settingID    INTEGER NOT NULL REFERENCES settings(id), \
                     value        BLOB"
      },
      indices: {
        groups_idx: {
          table: "groups",
          columns: ["name"]
        },
        settings_idx: {
          table: "settings",
          columns: ["name"]
        },
        prefs_idx: {
          table: "prefs",
          columns: ["groupID", "settingID"]
        }
      }
    },
  
    _dbConnection: null,
  
    _dbCreateStatement: function ContentPrefService__dbCreateStatement(aSQLString) {
      try {
        var statement = this._dbConnection.createStatement(aSQLString);
      }
      catch(ex) {
        Cu.reportError("error creating statement " + aSQLString + ": " +
                       this._dbConnection.lastError + " - " +
                       this._dbConnection.lastErrorString);
        throw ex;
      }
  
      var wrappedStatement = Cc["@mozilla.org/storage/statement-wrapper;1"].
                             createInstance(Ci.mozIStorageStatementWrapper);
      wrappedStatement.initialize(statement);
      return wrappedStatement;
    },
  
    // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version-
    // specific migration methods) must be careful not to call any method
    // of the service that assumes the database connection has already been
    // initialized, since it won't be initialized until at the end of _dbInit.
  
    _dbInit: function ContentPrefService__dbInit() {
      var dirService = Cc["@mozilla.org/file/directory_service;1"].
                       getService(Ci.nsIProperties);
      var dbFile = dirService.get("ProfD", Ci.nsIFile);
      dbFile.append("content-prefs.sqlite");
  
      var dbService = Cc["@mozilla.org/storage/service;1"].
                      getService(Ci.mozIStorageService);
  
      var dbConnection;
  
      if (!dbFile.exists())
        dbConnection = this._dbCreate(dbService, dbFile);
      else {
        try {
          dbConnection = dbService.openDatabase(dbFile);
        }
        // If the connection isn't ready after we open the database, that means
        // the database has been corrupted, so we back it up and then recreate it.
        catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
          dbConnection = this._dbBackUpAndRecreate(dbService, dbFile,
                                                   dbConnection);
        }
  
        // Get the version of the schema in the file.
        var version = dbConnection.schemaVersion;
  
        // Try to migrate the schema in the database to the current schema used by
        // the service.  If migration fails, back up the database and recreate it.
        if (version != this._dbVersion) {
          try {
            this._dbMigrate(dbConnection, version, this._dbVersion);
          }
          catch(ex) {
            Cu.reportError("error migrating DB: " + ex + "; backing up and recreating");
            dbConnection = this._dbBackUpAndRecreate(dbService, dbFile, dbConnection);
          }
        }
      }
  
      // Turn off disk synchronization checking to reduce disk churn and speed up
      // operations when prefs are changed rapidly (such as when a user repeatedly
      // changes the value of the browser zoom setting for a site).
      //
      // Note: this could cause database corruption if the OS crashes or machine
      // loses power before the data gets written to disk, but this is considered
      // a reasonable risk for the not-so-critical data stored in this database.
      //
      // If you really don't want to take this risk, however, just set the
      // toolkit.storage.synchronous pref to 1 (NORMAL synchronization) or 2
      // (FULL synchronization), in which case mozStorageConnection::Initialize
      // will use that value, and we won't override it here.
      if (!this._prefSvc.prefHasUserValue("toolkit.storage.synchronous"))
        dbConnection.executeSimpleSQL("PRAGMA synchronous = OFF");
  
      this._dbConnection = dbConnection;
    },
  
    _dbCreate: function ContentPrefService__dbCreate(aDBService, aDBFile) {
      var dbConnection = aDBService.openDatabase(aDBFile);
  
      try {
        this._dbCreateSchema(dbConnection);
        dbConnection.schemaVersion = this._dbVersion;
      }
      catch(ex) {
        // If we failed to create the database (perhaps because the disk ran out
        // of space), then remove the database file so we don't leave it in some
        // half-created state from which we won't know how to recover.
        dbConnection.close();
        aDBFile.remove(false);
        throw ex;
      }
  
      return dbConnection;
    },
  
    _dbCreateSchema: function ContentPrefService__dbCreateSchema(aDBConnection) {
      this._dbCreateTables(aDBConnection);
      this._dbCreateIndices(aDBConnection);
    },
  
    _dbCreateTables: function ContentPrefService__dbCreateTables(aDBConnection) {
      for (let name in this._dbSchema.tables)
        aDBConnection.createTable(name, this._dbSchema.tables[name]);
    },
  
    _dbCreateIndices: function ContentPrefService__dbCreateIndices(aDBConnection) {
      for (let name in this._dbSchema.indices) {
        let index = this._dbSchema.indices[name];
        let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
                        "(" + index.columns.join(", ") + ")";
        aDBConnection.executeSimpleSQL(statement);
      }
    },
  
    _dbBackUpAndRecreate: function ContentPrefService__dbBackUpAndRecreate(aDBService,
                                                                           aDBFile,
                                                                           aDBConnection) {
      aDBService.backupDatabaseFile(aDBFile, "content-prefs.sqlite.corrupt");
  
      // Close the database, ignoring the "already closed" exception, if any.
      // It'll be open if we're here because of a migration failure but closed
      // if we're here because of database corruption.
      try { aDBConnection.close() } catch(ex) {}
  
      aDBFile.remove(false);
  
      let dbConnection = this._dbCreate(aDBService, aDBFile);
  
      return dbConnection;
    },
  
    _dbMigrate: function ContentPrefService__dbMigrate(aDBConnection, aOldVersion, aNewVersion) {
      if (this["_dbMigrate" + aOldVersion + "To" + aNewVersion]) {
        aDBConnection.beginTransaction();
        try {
          this["_dbMigrate" + aOldVersion + "To" + aNewVersion](aDBConnection);
          aDBConnection.schemaVersion = aNewVersion;
          aDBConnection.commitTransaction();
        }
        catch(ex) {
          aDBConnection.rollbackTransaction();
          throw ex;
        }
      }
      else
        throw("no migrator function from version " + aOldVersion +
              " to version " + aNewVersion);
    },
  
    /**
     * If the schema version is 0, that means it was never set, which means
     * the database was somehow created without the schema being applied, perhaps
     * because the system ran out of disk space (although we check for this
     * in _createDB) or because some other code created the database file without
     * applying the schema.  In any case, recover by simply reapplying the schema.
     */
    _dbMigrate0To3: function ContentPrefService___dbMigrate0To3(aDBConnection) {
      this._dbCreateSchema(aDBConnection);
    },
  
    _dbMigrate1To3: function ContentPrefService___dbMigrate1To3(aDBConnection) {
      aDBConnection.executeSimpleSQL("ALTER TABLE groups RENAME TO groupsOld");
      aDBConnection.createTable("groups", this._dbSchema.tables.groups);
      aDBConnection.executeSimpleSQL(
        "INSERT INTO groups (id, name) " +
        "SELECT id, name FROM groupsOld"
      );
  
      aDBConnection.executeSimpleSQL("DROP TABLE groupers");
      aDBConnection.executeSimpleSQL("DROP TABLE groupsOld");
  
      this._dbCreateIndices(aDBConnection);
    },
  
    _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) {
      this._dbCreateIndices(aDBConnection);
    }
  
  };
  
  
  function HostnameGrouper() {}
  
  HostnameGrouper.prototype = {
    //**************************************************************************//
    // XPCOM Plumbing
    
    classDescription: "Hostname Grouper",
    classID:          Components.ID("{8df290ae-dcaa-4c11-98a5-2429a4dc97bb}"),
    contractID:       "@mozilla.org/content-pref/hostname-grouper;1",
    QueryInterface:   XPCOMUtils.generateQI([Ci.nsIContentURIGrouper]),
  
  
    //**************************************************************************//
    // nsIContentURIGrouper
  
    group: function HostnameGrouper_group(aURI) {
      var group;
  
      try {
        // Accessing the host property of the URI will throw an exception
        // if the URI is of a type that doesn't have a host property.
        // Otherwise, we manually throw an exception if the host is empty,
        // since the effect is the same (we can't derive a group from it).
  
        group = aURI.host;
        if (!group)
          throw("can't derive group from host; no host in URI");
      }
      catch(ex) {
        // If we don't have a host, then use the entire URI (minus the query,
        // reference, and hash, if possible) as the group.  This means that URIs
        // like about:mozilla and about:blank will be considered separate groups,
        // but at least they'll be grouped somehow.
        
        // This also means that each individual file: URL will be considered
        // its own group.  This seems suboptimal, but so does treating the entire
        // file: URL space as a single group (especially if folks start setting
        // group-specific capabilities prefs).
  
        // XXX Is there something better we can do here?
  
        try {
          var url = aURI.QueryInterface(Ci.nsIURL);
          group = aURI.prePath + url.filePath;
        }
        catch(ex) {
          group = aURI.spec;
        }
      }
  
      return group;
    }
  };
  
  
  //****************************************************************************//
  // XPCOM Plumbing
  
  var components = [ContentPrefService, HostnameGrouper];
  var NSGetModule = function ContentPrefService_NSGetModule(compMgr, fileSpec) {
    return XPCOMUtils.generateModule(components);
  }
  