Backed out changeset f92c249cae8b (bug 886907) for failing editor/libeditor/tests/test_bug1368544.html on Android 4.3 debug. r=backout
authorSebastian Hengst <archaeopteryx@coole-files.de>
Tue, 15 Aug 2017 16:35:07 +0200
changeset 647031 122fa30ec6605756d1221e91facd661e4b8069d7
parent 647030 420253115f0666ea0cb65a7b98dc752c1d1f9b91
child 647032 06ffe35b662bf5d6981a44a13fecb84726aa2d9f
push id74288
push userhikezoe@mozilla.com
push dateWed, 16 Aug 2017 00:19:57 +0000
reviewersbackout
bugs886907, 1368544
milestone57.0a1
backs outf92c249cae8b855ff46035135785b73474bbc4ac
Backed out changeset f92c249cae8b (bug 886907) for failing editor/libeditor/tests/test_bug1368544.html on Android 4.3 debug. r=backout
browser/base/content/test/static/browser_all_files_referenced.js
browser/installer/package-manifest.in
dom/html/HTMLInputElement.cpp
dom/interfaces/base/moz.build
dom/interfaces/base/nsIContentPrefService.idl
dom/interfaces/base/nsIContentPrefService2.idl
editor/composer/nsEditorSpellCheck.cpp
toolkit/components/contentprefs/ContentPrefInstance.jsm
toolkit/components/contentprefs/ContentPrefService2.js
toolkit/components/contentprefs/ContentPrefService2.jsm
toolkit/components/contentprefs/ContentPrefService2.manifest
toolkit/components/contentprefs/moz.build
toolkit/components/contentprefs/nsContentPrefService.js
toolkit/components/contentprefs/nsContentPrefService.manifest
toolkit/components/contentprefs/tests/unit/.eslintrc.js
toolkit/components/contentprefs/tests/unit/head_contentPrefs.js
toolkit/components/contentprefs/tests/unit/test_bug248970.js
toolkit/components/contentprefs/tests/unit/test_bug503971.js
toolkit/components/contentprefs/tests/unit/test_bug679784.js
toolkit/components/contentprefs/tests/unit/test_contentPrefs.js
toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js
toolkit/components/contentprefs/tests/unit/test_stringGroups.js
toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js
toolkit/components/contentprefs/tests/unit/xpcshell.ini
toolkit/components/contentprefs/tests/unit_cps2/test_service.js
toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
toolkit/modules/Services.jsm
toolkit/modules/tests/xpcshell/test_Services.js
toolkit/mozapps/downloads/DownloadLastDir.jsm
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -159,16 +159,18 @@ var whitelist = [
   {file: "chrome://mozapps/skin/plugins/pluginBlocked.png"},
   // Bug 1348558
   {file: "chrome://mozapps/skin/update/downloadButtons.png",
    platforms: ["linux"]},
   // Bug 1348559
   {file: "chrome://pippki/content/resetpassword.xul"},
   // Bug 1351078
   {file: "resource://gre/modules/Battery.jsm"},
+  // Bug 1351070
+  {file: "resource://gre/modules/ContentPrefInstance.jsm"},
   // Bug 1351079
   {file: "resource://gre/modules/ISO8601DateUtils.jsm"},
   // Bug 1337345
   {file: "resource://gre/modules/Manifest.jsm"},
   // Bug 1351097
   {file: "resource://gre/modules/accessibility/AccessFu.jsm"},
   // Bug 1351637
   {file: "resource://gre/modules/sdk/bootstrap.js"},
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -459,18 +459,18 @@
 @RESPATH@/components/PageIconProtocolHandler.js
 @RESPATH@/components/PlacesCategoriesStarter.js
 @RESPATH@/components/ColorAnalyzer.js
 @RESPATH@/components/PageThumbsProtocol.js
 @RESPATH@/components/mozProtocolHandler.js
 @RESPATH@/components/mozProtocolHandler.manifest
 @RESPATH@/components/nsDefaultCLH.manifest
 @RESPATH@/components/nsDefaultCLH.js
-@RESPATH@/components/ContentPrefService2.manifest
-@RESPATH@/components/ContentPrefService2.js
+@RESPATH@/components/nsContentPrefService.manifest
+@RESPATH@/components/nsContentPrefService.js
 @RESPATH@/components/nsContentDispatchChooser.manifest
 @RESPATH@/components/nsContentDispatchChooser.js
 @RESPATH@/components/nsHandlerService-json.manifest
 @RESPATH@/components/nsHandlerService-json.js
 @RESPATH@/components/nsHandlerService.manifest
 @RESPATH@/components/nsHandlerService.js
 @RESPATH@/components/nsWebHandlerApp.manifest
 @RESPATH@/components/nsWebHandlerApp.js
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -81,17 +81,17 @@
 
 // input type=file
 #include "mozilla/dom/FileSystemEntry.h"
 #include "mozilla/dom/FileSystem.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/FileList.h"
 #include "nsIFile.h"
 #include "nsDirectoryServiceDefs.h"
-#include "nsIContentPrefService2.h"
+#include "nsIContentPrefService.h"
 #include "nsIMIMEService.h"
 #include "nsIObserverService.h"
 #include "nsIPopupWindowManager.h"
 #include "nsGlobalWindow.h"
 
 // input type=image
 #include "nsImageLoadingContent.h"
 #include "imgRequestProxy.h"
--- a/dom/interfaces/base/moz.build
+++ b/dom/interfaces/base/moz.build
@@ -7,16 +7,17 @@
 with Files("**"):
     BUG_COMPONENT = ("Core", "DOM")
 
 XPIDL_SOURCES += [
     'domstubs.idl',
     'nsIBrowser.idl',
     'nsIBrowserDOMWindow.idl',
     'nsIContentPermissionPrompt.idl',
+    'nsIContentPrefService.idl',
     'nsIContentPrefService2.idl',
     'nsIContentProcess.idl',
     'nsIContentURIGrouper.idl',
     'nsIDOMChromeWindow.idl',
     'nsIDOMClientRect.idl',
     'nsIDOMClientRectList.idl',
     'nsIDOMConstructor.idl',
     'nsIDOMCrypto.idl',
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/base/nsIContentPrefService.idl
@@ -0,0 +1,263 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIVariant;
+interface nsIPropertyBag2;
+interface nsIContentURIGrouper;
+interface nsILoadContext;
+interface mozIStorageConnection;
+
+[scriptable, uuid(43635c53-b445-4c4e-8cc5-562697299b55)]
+interface nsIContentPrefObserver : nsISupports
+{
+  /**
+   * Called when a content pref is set to a different value.
+   *
+   * @param    aGroup      the group to which the pref belongs, or null
+   *                       if it's a global pref (applies to all sites)
+   * @param    aName       the name of the pref that was set
+   * @param    aValue      the new value of the pref
+   * @param    aIsPrivate  an optional flag determining whether the
+   *                       original context is private or not
+   */
+  void onContentPrefSet(in AString aGroup,
+                        in AString aName,
+                        in nsIVariant aValue,
+                        [optional] in boolean aIsPrivate);
+
+  /**
+   * Called when a content pref is removed.
+   *
+   * @param    aGroup      the group to which the pref belongs, or null
+   *                       if it's a global pref (applies to all sites)
+   * @param    aName       the name of the pref that was removed
+   * @param    aIsPrivate  an optional flag determining whether the
+   *                       original context is private or not
+   */
+  void onContentPrefRemoved(in AString aGroup,
+                            in AString aName,
+                            [optional] in boolean aIsPrivate);
+};
+
+[scriptable, function, uuid(c1b3d6df-5373-4606-8494-8bcf14a7fc62)]
+interface nsIContentPrefCallback : nsISupports
+{
+  void onResult(in nsIVariant aResult);
+};
+
+/**
+ * @deprecated Please use nsIContentPrefService2 instead.
+ */
+[scriptable, uuid(e3f772f3-023f-4b32-b074-36cf0fd5d414)]
+interface nsIContentPrefService : nsISupports
+{
+  /**
+   * Get a pref.
+   *
+   * Besides the regular string, integer, boolean, etc. values, this method
+   * may return null (nsIDataType::VTYPE_EMPTY), which means the pref is set
+   * to NULL in the database, as well as undefined (nsIDataType::VTYPE_VOID),
+   * which means there is no record for this pref in the database.
+   *
+   * This method can be called from content processes in electrolysis builds.
+   * We have a whitelist of values that can be read in such a way.
+   *
+   * @param    aGroup      the group for which to get the pref, as an nsIURI
+   *                       from which the hostname will be used, a string
+   *                       (typically in the format of a hostname), or null
+   *                       to get the global pref (applies to all sites)
+   * @param    aName       the name of the pref to get
+   * @param    aPrivacyContext
+   *                       a context from which to determine the privacy status
+   *                       of the pref (ie. whether to search in memory or in
+   *                       permanent storage for it), obtained from a relevant
+   *                       window or channel.
+   * @param    aCallback   an optional nsIContentPrefCallback to receive the
+   *                       result. If desired, JavaScript callers can instead
+   *                       provide a function to call upon completion
+   *
+   * @returns  the value of the pref
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  nsIVariant getPref(in nsIVariant aGroup, in AString aName,
+                     in nsILoadContext aPrivacyContext,
+                     [optional] in nsIContentPrefCallback aCallback);
+
+  /**
+   * Set a pref.
+   *
+   * This method can be called from content processes in electrolysis builds.
+   * We have a whitelist of values that can be set in such a way.
+   *
+   * @param    aGroup      the group for which to set the pref, as an nsIURI
+   *                       from which the hostname will be used, a string
+   *                       (typically in the format of a hostname), or null
+   *                       to set the global pref (applies to all sites)
+   * @param    aName       the name of the pref to set
+   * @param    aValue      the new value of the pref
+   * @param    aPrivacyContext
+   *                       a context from which to determine the privacy status
+   *                       of the pref (ie. whether to store it in memory or in
+   *                       permanent storage), obtained from a relevant
+   *                       window or channel.
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  void setPref(in nsIVariant aGroup, in AString aName, in nsIVariant aValue, in nsILoadContext aPrivacyContext);
+
+  /**
+   * Check whether or not a pref exists.
+   *
+   * @param    aGroup      the group for which to check for the pref, as an nsIURI
+   *                       from which the hostname will be used, a string
+   *                       (typically in the format of a hostname), or null
+   *                       to check for the global pref (applies to all sites)
+   * @param    aName       the name of the pref to check for
+   * @param    aPrivacyContext
+   *                       a context from which to determine the privacy status
+   *                       of the pref (ie. whether to search in memory or in
+   *                       permanent storage for it), obtained from a relevant
+   *                       window or channel.
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  boolean hasPref(in nsIVariant aGroup, in AString aName, in nsILoadContext aContext);
+
+  /**
+   * Check whether or not the value of a pref (or its non-existance) is cached.
+   *
+   * @param    aGroup      the group for which to check for the pref, as an nsIURI
+   *                       from which the hostname will be used, a string
+   *                       (typically in the format of a hostname), or null
+   *                       to check for the global pref (applies to all sites)
+   * @param    aName       the name of the pref to check for
+   * @param    aPrivacyContext
+   *                       a context from which to determine the privacy status
+   *                       of the pref (ie. whether to search in memory or in
+   *                       permanent storage for it), obtained from a relevant
+   *                       window or channel.
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  boolean hasCachedPref(in nsIVariant aGroup, in AString aName, in nsILoadContext aContext);
+
+  /**
+   * Remove a pref.
+   *
+   * @param    aGroup      the group for which to remove the pref, as an nsIURI
+   *                       from which the hostname will be used, a string
+   *                       (typically in the format of a hostname), or null
+   *                       to remove the global pref (applies to all sites)
+   * @param    aName       the name of the pref to remove
+   * @param    aPrivacyContext
+   *                       a context from which to determine the privacy status
+   *                       of the pref (ie. whether to search in memory or in
+   *                       permanent storage for it), obtained from a relevant
+   *                       window or channel.
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  void removePref(in nsIVariant aGroup, in AString aName, in nsILoadContext aContext);
+
+  /**
+   * Remove all grouped prefs.  Useful for removing references to the sites
+   * the user has visited when the user clears their private data.
+   *
+   * @param    aPrivacyContext
+   *                       a context from which to determine the privacy status
+   *                       of the pref (ie. whether to remove prefs in memory or
+   *                       in permanent storage), obtained from a relevant
+   *                       window or channel.
+   */
+  void removeGroupedPrefs(in nsILoadContext aContext);
+
+  /**
+   * Remove all prefs with the given name.
+   *
+   * @param    aName        the setting name for which to remove prefs
+   * @param    aPrivacyContext
+   *                        a context from which to determine the privacy status
+   *                        of the prefs (ie. whether to remove prefs in memory or
+   *                        in permanent storage), obtained from a relevant
+   *                        window or channel.
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  void removePrefsByName(in AString aName, in nsILoadContext aContext);
+
+  /**
+   * Get the prefs that apply to the given site.
+   *
+   * @param    aGroup      the group for which to retrieve prefs, as an nsIURI
+   *                       from which the hostname will be used, a string
+   *                       (typically in the format of a hostname), or null
+   *                       to get the global prefs (apply to all sites)
+   * @param    aPrivacyContext
+   *                       a context from which to determine the privacy status
+   *                       of the pref (ie. whether to search for prefs in memory
+   *                       or in permanent storage), obtained from a relevant
+   *                       window or channel.
+   *
+   * @returns  a property bag of prefs
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
+   */
+  nsIPropertyBag2 getPrefs(in nsIVariant aGroup, in nsILoadContext aContext);
+
+  /**
+   * Get the prefs with the given name.
+   *
+   * @param    aName        the setting name for which to retrieve prefs
+   * @param    aPrivacyContext
+   *                        a context from which to determine the privacy status
+   *                        of the pref (ie. whether to search for prefs in memory
+   *                        or in permanent storage), obtained from a relevant
+   *                        window or channel.
+   *
+   * @returns  a property bag of prefs
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  nsIPropertyBag2 getPrefsByName(in AString aName, in nsILoadContext aContext);
+
+  /**
+   * Add an observer.
+   *
+   * @param    aName       the setting to observe, or null to add
+   *                       a generic observer that observes all settings
+   * @param    aObserver   the observer to add
+   */
+  void addObserver(in AString aName, in nsIContentPrefObserver aObserver);
+
+  /**
+   * Remove an observer.
+   *
+   * @param    aName       the setting being observed, or null to remove
+   *                       a generic observer that observes all settings
+   * @param    aObserver   the observer to remove
+   */
+  void removeObserver(in AString aName, in nsIContentPrefObserver aObserver);
+
+  /**
+   * The component that the service uses to determine the groups to which
+   * URIs belong.  By default this is the "hostname grouper", which groups
+   * URIs by full hostname (a.k.a. site).
+   */
+  readonly attribute nsIContentURIGrouper grouper;
+
+  /**
+   * The database connection to the content preferences database.
+   * Useful for accessing and manipulating preferences in ways that are caller-
+   * specific or for which there is not yet a generic method, although generic
+   * functionality useful to multiple callers should generally be added to this
+   * unfrozen interface.  Also useful for testing the database creation
+   * and migration code.
+   */
+  readonly attribute mozIStorageConnection DBConnection;
+};
+
+%{C++
+// The contractID for the generic implementation built in to xpcom.
+#define NS_CONTENT_PREF_SERVICE_CONTRACTID "@mozilla.org/content-pref/service;1"
+%}
--- a/dom/interfaces/base/nsIContentPrefService2.idl
+++ b/dom/interfaces/base/nsIContentPrefService2.idl
@@ -1,51 +1,20 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsIVariant;
+interface nsIContentPrefObserver;
 interface nsIContentPrefCallback2;
 interface nsILoadContext;
 interface nsIContentPref;
 
-[scriptable, uuid(43635c53-b445-4c4e-8cc5-562697299b55)]
-interface nsIContentPrefObserver : nsISupports
-{
-  /**
-   * Called when a content pref is set to a different value.
-   *
-   * @param    aGroup      the group to which the pref belongs, or null
-   *                       if it's a global pref (applies to all sites)
-   * @param    aName       the name of the pref that was set
-   * @param    aValue      the new value of the pref
-   * @param    aIsPrivate  an optional flag determining whether the
-   *                       original context is private or not
-   */
-  void onContentPrefSet(in AString aGroup,
-                        in AString aName,
-                        in nsIVariant aValue,
-                        [optional] in boolean aIsPrivate);
-
-  /**
-   * Called when a content pref is removed.
-   *
-   * @param    aGroup      the group to which the pref belongs, or null
-   *                       if it's a global pref (applies to all sites)
-   * @param    aName       the name of the pref that was removed
-   * @param    aIsPrivate  an optional flag determining whether the
-   *                       original context is private or not
-   */
-  void onContentPrefRemoved(in AString aGroup,
-                            in AString aName,
-                            [optional] in boolean aIsPrivate);
-};
-
 /**
  * Content Preferences
  *
  * Content preferences allow the application to associate arbitrary data, or
  * "preferences", with specific domains, or web "content".  Specifically, a
  * content preference is a structure with three values: a domain with which the
  * preference is associated, a name that identifies the preference within its
  * domain, and a value.  (See nsIContentPref below.)
@@ -429,13 +398,8 @@ interface nsIContentPrefCallback2 : nsIS
 
 [scriptable, function, uuid(9f24948d-24b5-4b1b-b554-7dbd58c1d792)]
 interface nsIContentPref : nsISupports
 {
   readonly attribute AString domain;
   readonly attribute AString name;
   readonly attribute nsIVariant value;
 };
-
-%{C++
-// The contractID for the generic implementation built in to xpcom.
-#define NS_CONTENT_PREF_SERVICE_CONTRACTID "@mozilla.org/content-pref/service;1"
-%}
--- a/editor/composer/nsEditorSpellCheck.cpp
+++ b/editor/composer/nsEditorSpellCheck.cpp
@@ -14,16 +14,17 @@
 #include "mozilla/mozalloc.h"           // for operator delete, etc
 #include "nsAString.h"                  // for nsAString::IsEmpty, etc
 #include "nsComponentManagerUtils.h"    // for do_CreateInstance
 #include "nsDebug.h"                    // for NS_ENSURE_TRUE, etc
 #include "nsDependentSubstring.h"       // for Substring
 #include "nsEditorSpellCheck.h"
 #include "nsError.h"                    // for NS_ERROR_NOT_INITIALIZED, etc
 #include "nsIContent.h"                 // for nsIContent
+#include "nsIContentPrefService.h"      // for nsIContentPrefService, etc
 #include "nsIContentPrefService2.h"     // for nsIContentPrefService2, etc
 #include "nsIDOMDocument.h"             // for nsIDOMDocument
 #include "nsIDOMElement.h"              // for nsIDOMElement
 #include "nsIDocument.h"                // for nsIDocument
 #include "nsIEditor.h"                  // for nsIEditor
 #include "nsIHTMLEditor.h"              // for nsIHTMLEditor
 #include "nsILoadContext.h"
 #include "nsISelection.h"               // for nsISelection
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefInstance.jsm
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+this.EXPORTED_SYMBOLS = ["ContentPrefInstance"];
+
+// This is a wrapper for nsIContentPrefService that alleviates the need to pass
+// an nsILoadContext argument to every method. Pass the context to the constructor
+// instead and continue on your way in blissful ignorance.
+
+this.ContentPrefInstance = function ContentPrefInstance(aContext) {
+  this._contentPrefSvc = Cc["@mozilla.org/content-pref/service;1"].
+                           getService(Ci.nsIContentPrefService);
+  this._context = aContext;
+};
+
+ContentPrefInstance.prototype = {
+  getPref: function ContentPrefInstance_init(aName, aGroup, aCallback) {
+    return this._contentPrefSvc.getPref(aName, aGroup, this._context, aCallback);
+  },
+
+  setPref: function ContentPrefInstance_setPref(aGroup, aName, aValue, aContext) {
+    return this._contentPrefSvc.setPref(aGroup, aName, aValue,
+                                        aContext ? aContext : this._context);
+  },
+
+  hasPref: function ContentPrefInstance_hasPref(aGroup, aName) {
+    return this._contentPrefSvc.hasPref(aGroup, aName, this._context);
+  },
+
+  hasCachedPref: function ContentPrefInstance_hasCachedPref(aGroup, aName) {
+    return this._contentPrefSvc.hasCachedPref(aGroup, aName, this._context);
+  },
+
+  removePref: function ContentPrefInstance_removePref(aGroup, aName) {
+    return this._contentPrefSvc.removePref(aGroup, aName, this._context);
+  },
+
+  removeGroupedPrefs: function ContentPrefInstance_removeGroupedPrefs() {
+    return this._contentPrefSvc.removeGroupedPrefs(this._context);
+  },
+
+  removePrefsByName: function ContentPrefInstance_removePrefsByName(aName) {
+    return this._contentPrefSvc.removePrefsByName(aName, this._context);
+  },
+
+  getPrefs: function ContentPrefInstance_getPrefs(aGroup) {
+    return this._contentPrefSvc.getPrefs(aGroup, this._context);
+  },
+
+  getPrefsByName: function ContentPrefInstance_getPrefsByName(aName) {
+    return this._contentPrefSvc.getPrefsByName(aName, this._context);
+  },
+
+  addObserver: function ContentPrefInstance_addObserver(aName, aObserver) {
+    return this._contentPrefSvc.addObserver(aName, aObserver);
+  },
+
+  removeObserver: function ContentPrefInstance_removeObserver(aName, aObserver) {
+    return this._contentPrefSvc.removeObserver(aName, aObserver);
+  },
+
+  get grouper() {
+    return this._contentPrefSvc.grouper;
+  },
+
+  get DBConnection() {
+    return this._contentPrefSvc.DBConnection;
+  },
+
+  set loadContext(aLoadContext) {
+    this._context = aLoadContext;
+  }
+};
rename from toolkit/components/contentprefs/ContentPrefService2.js
rename to toolkit/components/contentprefs/ContentPrefService2.jsm
--- a/toolkit/components/contentprefs/ContentPrefService2.js
+++ b/toolkit/components/contentprefs/ContentPrefService2.jsm
@@ -1,119 +1,56 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+// This file is an XPCOM component that implements nsIContentPrefService2.
+// Although it's a JSM, it's not intended to be imported by consumers like JSMs
+// are usually imported.  It's only a JSM so that nsContentPrefService.js can
+// easily use it.  Consumers should access this component with the usual XPCOM
+// rigmarole:
+//
+//   Cc["@mozilla.org/content-pref/service;1"].
+//   getService(Ci.nsIContentPrefService2);
+//
+// That contract ID actually belongs to nsContentPrefService.js, which, when
+// QI'ed to nsIContentPrefService2, returns an instance of this component.
+//
+// The plan is to eventually remove nsIContentPrefService and its
+// implementation, nsContentPrefService.js.  At such time this file can stop
+// being a JSM, and the "_cps" parts that ContentPrefService2 relies on and
+// NSGetFactory and all the other XPCOM initialization goop in
+// nsContentPrefService.js can be moved here.
+//
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=699859
+
+var EXPORTED_SYMBOLS = [
+  "ContentPrefService2",
+];
+
 const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
 Cu.import("resource://gre/modules/ContentPrefStore.jsm");
 
-const CACHE_MAX_GROUP_ENTRIES = 100;
-
 const GROUP_CLAUSE = `
   SELECT id
   FROM groups
   WHERE name = :group OR
         (:includeSubdomains AND name LIKE :pattern ESCAPE '/')
 `;
 
-function ContentPrefService2() {
-  if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
-    return Cu.import("resource://gre/modules/ContentPrefServiceChild.jsm")
-             .ContentPrefServiceChild;
-  }
-
-  // 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();
-
-  this._observerSvc.addObserver(this, "last-pb-context-exited");
-
-  // Observe shutdown so we can shut down the database connection.
-  this._observerSvc.addObserver(this, "xpcom-shutdown");
+function ContentPrefService2(cps) {
+  this._cps = cps;
+  this._cache = cps._cache;
+  this._pbStore = cps._privModeStorage;
 }
 
-const cache = new ContentPrefStore();
-cache.set = function CPS_cache_set(group, name, val) {
-  Object.getPrototypeOf(this).set.apply(this, arguments);
-  let groupCount = this._groups.size;
-  if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
-    // Clean half of the entries
-    for (let [group, name, ] of this) {
-      this.remove(group, name);
-      groupCount--;
-      if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2)
-        break;
-    }
-  }
-};
-
-const privModeStorage = new ContentPrefStore();
-
 ContentPrefService2.prototype = {
-  // XPCOM Plumbing
-
-  classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
-
-  // Convenience Getters
-
-  // Observer Service
-  __observerSvc: null,
-  get _observerSvc() {
-    if (!this.__observerSvc)
-      this.__observerSvc = Cc["@mozilla.org/observer-service;1"].
-                           getService(Ci.nsIObserverService);
-    return this.__observerSvc;
-  },
-
-  // Preferences Service
-  __prefSvc: null,
-  get _prefSvc() {
-    if (!this.__prefSvc)
-      this.__prefSvc = Cc["@mozilla.org/preferences-service;1"].
-                       getService(Ci.nsIPrefBranch);
-    return this.__prefSvc;
-  },
-
-
-  // Destruction
-
-  _destroy: function ContentPrefService__destroy() {
-    this._observerSvc.removeObserver(this, "xpcom-shutdown");
-    this._observerSvc.removeObserver(this, "last-pb-context-exited");
-
-    this.destroy();
-
-    this._dbConnection.asyncClose(() => {
-      Services.obs.notifyObservers(null, "content-prefs-db-closed");
-    });
-
-    // 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.
-    delete this._observers;
-    delete this._genericObservers;
-    delete this.__grouper;
-    delete this.__observerSvc;
-    delete this.__prefSvc;
-  },
-
-
-  // in-memory cache and private-browsing stores
-
-  _cache: cache,
-  _pbStore: privModeStorage,
-
-  // nsIContentPrefService
 
   getByName: function CPS2_getByName(name, context, callback) {
     checkNameArg(name);
     checkCallbackArg(callback, true);
 
     // Some prefs may be in both the database and the private browsing store.
     // Notify the caller of such prefs only once, using the values from private
     // browsing.
@@ -312,17 +249,17 @@ ContentPrefService2.prototype = {
     checkNameArg(name);
     checkValueArg(value);
     checkCallbackArg(callback, false);
 
     if (context && context.usePrivateBrowsing) {
       this._pbStore.set(group, name, value);
       this._schedule(function() {
         cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK);
-        this._notifyPrefSet(group, name, value, context.usePrivateBrowsing);
+        this._cps._notifyPrefSet(group, name, value, context.usePrivateBrowsing);
       });
       return;
     }
 
     // Invalidate the cached value so consumers accessing the cache between now
     // and when the operation finishes don't get old data.
     this._cache.remove(group, name);
 
@@ -384,17 +321,17 @@ ContentPrefService2.prototype = {
     stmts.push(stmt);
 
     this._execStmts(stmts, {
       onDone: function onDone(reason, ok) {
         if (ok)
           this._cache.setWithCast(group, name, value);
         cbHandleCompletion(callback, reason);
         if (ok)
-          this._notifyPrefSet(group, name, value, context && context.usePrivateBrowsing);
+          this._cps._notifyPrefSet(group, name, value, context && context.usePrivateBrowsing);
       },
       onError: function onError(nsresult) {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
   removeByDomainAndName: function CPS2_removeByDomainAndName(group, name,
@@ -464,17 +401,17 @@ ContentPrefService2.prototype = {
               prefs.set(sgroup, name, undefined);
               this._pbStore.remove(sgroup, name);
             }
           }
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, , ] of prefs) {
-            this._notifyPrefRemoved(sgroup, name, isPrivate);
+            this._cps._notifyPrefRemoved(sgroup, name, isPrivate);
           }
         }
       },
       onError: function onError(nsresult) {
         cbHandleError(callback, nsresult);
       }
     });
   },
@@ -577,17 +514,17 @@ ContentPrefService2.prototype = {
               prefs.set(sgroup, sname, undefined);
               this._pbStore.remove(sgroup, sname);
             }
           }
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, sname, ] of prefs) {
-            this._notifyPrefRemoved(sgroup, sname, isPrivate);
+            this._cps._notifyPrefRemoved(sgroup, sname, isPrivate);
           }
         }
       },
       onError: function onError(nsresult) {
         cbHandleError(callback, nsresult);
       }
     });
   },
@@ -643,17 +580,17 @@ ContentPrefService2.prototype = {
               prefs.set(sgroup, sname, undefined);
             }
           }
           this._pbStore.removeAllGroups();
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, sname, ] of prefs) {
-            this._notifyPrefRemoved(sgroup, sname, isPrivate);
+            this._cps._notifyPrefRemoved(sgroup, sname, isPrivate);
           }
         }
       },
       onError: function onError(nsresult) {
         cbHandleError(callback, nsresult);
       }
     });
   },
@@ -732,17 +669,17 @@ ContentPrefService2.prototype = {
               prefs.set(sgroup, name, undefined);
               this._pbStore.remove(sgroup, name);
             }
           }
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, , ] of prefs) {
-            this._notifyPrefRemoved(sgroup, name, isPrivate);
+            this._cps._notifyPrefRemoved(sgroup, name, isPrivate);
           }
         }
       },
       onError: function onError(nsresult) {
         cbHandleError(callback, nsresult);
       }
     });
   },
@@ -762,17 +699,17 @@ ContentPrefService2.prototype = {
    *
    * @param sql  The SQL query string.
    * @return     The cached, possibly new, statement.
    */
   _stmt: function CPS2__stmt(sql) {
     if (!this._statements)
       this._statements = {};
     if (!this._statements[sql])
-      this._statements[sql] = this._dbConnection.createAsyncStatement(sql);
+      this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql);
     return this._statements[sql];
   },
 
   /**
    * Executes some async statements.
    *
    * @param stmts      An array of mozIStorageAsyncStatements.
    * @param callbacks  An object with the following methods:
@@ -786,17 +723,17 @@ ContentPrefService2.prototype = {
    *                     didGetRow: True if onRow was ever called.
    *                   onError(nsresult) (optional)
    *                     Called on error.
    *                     nsresult: The error code.
    */
   _execStmts: function CPS2__execStmts(stmts, callbacks) {
     let self = this;
     let gotRow = false;
-    this._dbConnection.executeAsync(stmts, stmts.length, {
+    this._cps._dbConnection.executeAsync(stmts, stmts.length, {
       handleResult: function handleResult(results) {
         try {
           let row = null;
           while ((row = results.getNextRow())) {
             gotRow = true;
             if (callbacks.onRow)
               callbacks.onRow.call(self, row);
           }
@@ -821,24 +758,16 @@ ContentPrefService2.prototype = {
             callbacks.onError.call(self, Cr.NS_ERROR_FAILURE);
         } catch (err) {
           Cu.reportError(err);
         }
       }
     });
   },
 
-  __grouper: null,
-  get _grouper() {
-    if (!this.__grouper)
-      this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"].
-                       getService(Ci.nsIContentURIGrouper);
-    return this.__grouper;
-  },
-
   /**
    * Parses the domain (the "group", to use the database's term) from the given
    * string.
    *
    * @param groupStr  Assumed to be either a string or falsey.
    * @return          If groupStr is a valid URL string, returns the domain of
    *                  that URL.  If groupStr is some other nonempty string,
    *                  returns groupStr itself.  Otherwise returns null.
@@ -846,378 +775,85 @@ ContentPrefService2.prototype = {
   _parseGroup: function CPS2__parseGroup(groupStr) {
     if (!groupStr)
       return null;
     try {
       var groupURI = Services.io.newURI(groupStr);
     } catch (err) {
       return groupStr;
     }
-    return this._grouper.group(groupURI);
+    return this._cps._grouper.group(groupURI);
   },
 
   _schedule: function CPS2__schedule(fn) {
     Services.tm.dispatchToMainThread(fn.bind(this));
   },
 
-  // A hash of arrays of observers, indexed by setting name.
-  _observers: {},
-
-  // An array of generic observers, which observe all settings.
-  _genericObservers: [],
-
-  addObserverForName: function CPS2_addObserverForName(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);
-  },
-
-  removeObserverForName: function CPS2_removeObserverForName(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);
+  addObserverForName: function CPS2_addObserverForName(name, observer) {
+    this._cps._addObserver(name, observer);
   },
 
-  /**
-   * 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;
-  },
-
-  /**
-   * Notify all observers about the removal of a preference.
-   */
-  _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(aGroup, aName, aIsPrivate) {
-    for (var observer of this._getObservers(aName)) {
-      try {
-        observer.onContentPrefRemoved(aGroup, aName, aIsPrivate);
-      } catch (ex) {
-        Cu.reportError(ex);
-      }
-    }
-  },
-
-  /**
-   * Notify all observers about a preference change.
-   */
-  _notifyPrefSet: function ContentPrefService__notifyPrefSet(aGroup, aName, aValue, aIsPrivate) {
-    for (var observer of this._getObservers(aName)) {
-      try {
-        observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate);
-      } catch (ex) {
-        Cu.reportError(ex);
-      }
-    }
+  removeObserverForName: function CPS2_removeObserverForName(name, observer) {
+    this._cps._removeObserver(name, observer);
   },
 
   extractDomain: function CPS2_extractDomain(str) {
     return this._parseGroup(str);
   },
 
   /**
    * Tests use this as a backchannel by calling it directly.
    *
    * @param subj   This value depends on topic.
    * @param topic  The backchannel "method" name.
    * @param data   This value depends on topic.
    */
   observe: function CPS2_observe(subj, topic, data) {
     switch (topic) {
-    case "xpcom-shutdown":
-      this._destroy();
-      break;
-    case "last-pb-context-exited":
-      this._pbStore.removeAll();
-      break;
     case "test:reset":
       let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
       this._reset(fn);
       break;
     case "test:db":
       let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
-      obj.value = this._dbConnection;
+      obj.value = this._cps._dbConnection;
       break;
     }
   },
 
   /**
    * Removes all state from the service.  Used by tests.
    *
    * @param callback  A function that will be called when done.
    */
   _reset: function CPS2__reset(callback) {
     this._pbStore.removeAll();
     this._cache.removeAll();
 
-    this._observers = {};
-    this._genericObservers = [];
+    let cps = this._cps;
+    cps._observers = {};
+    cps._genericObservers = [];
 
     let tables = ["prefs", "groups", "settings"];
     let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
     this._execStmts(stmts, { onDone: () => callback() });
   },
 
   QueryInterface: function CPS2_QueryInterface(iid) {
     let supportedIIDs = [
       Ci.nsIContentPrefService2,
       Ci.nsIObserver,
       Ci.nsISupports,
     ];
     if (supportedIIDs.some(i => iid.equals(i)))
       return this;
+    if (iid.equals(Ci.nsIContentPrefService))
+      return this._cps;
     throw Cr.NS_ERROR_NO_INTERFACE;
   },
-
-
-  // Database Creation & Access
-
-  _dbVersion: 4,
-
-  _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, \
-                   timestamp    INTEGER NOT NULL DEFAULT 0" // Storage in seconds, API in ms. 0 for migrated values.
-    },
-    indices: {
-      groups_idx: {
-        table: "groups",
-        columns: ["name"]
-      },
-      settings_idx: {
-        table: "settings",
-        columns: ["name"]
-      },
-      prefs_idx: {
-        table: "prefs",
-        columns: ["timestamp", "groupID", "settingID"]
-      }
-    }
-  },
-
-  _dbConnection: null,
-
-  // _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);
-      } catch (e) {
-        // 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.
-        if (e.result != Cr.NS_ERROR_FILE_CORRUPTED)
-          throw e;
-        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) {
-    /**
-     * Migrations should follow the template rules in bug 1074817 comment 3 which are:
-     * 1. Migration should be incremental and non-breaking.
-     * 2. It should be idempotent because one can downgrade an upgrade again.
-     * On downgrade:
-     * 1. Decrement schema version so that upgrade runs the migrations again.
-     */
-    aDBConnection.beginTransaction();
-
-    try {
-       /**
-       * 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.
-       */
-      if (aOldVersion == 0) {
-        this._dbCreateSchema(aDBConnection);
-      } else {
-        for (let i = aOldVersion; i < aNewVersion; i++) {
-          let migrationName = "_dbMigrate" + i + "To" + (i + 1);
-          if (typeof this[migrationName] != "function") {
-            throw ("no migrator function from version " + aOldVersion + " to version " + aNewVersion);
-          }
-          this[migrationName](aDBConnection);
-        }
-      }
-      aDBConnection.schemaVersion = aNewVersion;
-      aDBConnection.commitTransaction();
-    } catch (ex) {
-      aDBConnection.rollbackTransaction();
-      throw ex;
-    }
-  },
-
-  _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(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");
-  },
-
-  _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) {
-    this._dbCreateIndices(aDBConnection);
-  },
-
-  _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) {
-    // Add timestamp column if it does not exist yet. This operation is idempotent.
-    try {
-      let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs");
-      stmt.finalize();
-    } catch (e) {
-      aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
-    }
-
-    // To modify prefs_idx drop it and create again.
-    aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx");
-    this._dbCreateIndices(aDBConnection);
-  },
 };
 
 function checkGroupArg(group) {
   if (!group || typeof(group) != "string")
     throw invalidArg("domain must be nonempty string.");
 }
 
 function checkNameArg(name) {
@@ -1235,61 +871,8 @@ function checkCallbackArg(callback, requ
     throw invalidArg("callback must be an nsIContentPrefCallback2.");
   if (!callback && required)
     throw invalidArg("callback must be given.");
 }
 
 function invalidArg(msg) {
   return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG);
 }
-
-
-function HostnameGrouper() {}
-
-HostnameGrouper.prototype = {
-  // XPCOM Plumbing
-
-  classID:          Components.ID("{8df290ae-dcaa-4c11-98a5-2429a4dc97bb}"),
-  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 = [ContentPrefService2, HostnameGrouper];
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
--- a/toolkit/components/contentprefs/moz.build
+++ b/toolkit/components/contentprefs/moz.build
@@ -1,28 +1,31 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += [
+    'tests/unit/xpcshell.ini',
     'tests/unit_cps2/xpcshell.ini',
 ]
 
 MOCHITEST_MANIFESTS += [
     'tests/mochitest/mochitest.ini'
 ]
 
 EXTRA_COMPONENTS += [
-    'ContentPrefService2.js',
-    'ContentPrefService2.manifest',
+    'nsContentPrefService.js',
+    'nsContentPrefService.manifest',
 ]
 
 EXTRA_JS_MODULES += [
+    'ContentPrefInstance.jsm',
+    'ContentPrefService2.jsm',
     'ContentPrefServiceChild.jsm',
     'ContentPrefServiceParent.jsm',
     'ContentPrefStore.jsm',
     'ContentPrefUtils.jsm',
 ]
 
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Preferences')
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/nsContentPrefService.js
@@ -0,0 +1,1309 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+const CACHE_MAX_GROUP_ENTRIES = 100;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function ContentPrefService() {
+  if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
+    return Cu.import("resource://gre/modules/ContentPrefServiceChild.jsm")
+             .ContentPrefServiceChild;
+  }
+
+  // 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();
+
+  this._observerSvc.addObserver(this, "last-pb-context-exited");
+
+  // Observe shutdown so we can shut down the database connection.
+  this._observerSvc.addObserver(this, "xpcom-shutdown");
+}
+
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+const cache = new ContentPrefStore();
+cache.set = function CPS_cache_set(group, name, val) {
+  Object.getPrototypeOf(this).set.apply(this, arguments);
+  let groupCount = this._groups.size;
+  if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
+    // Clean half of the entries
+    for (let [group, name, ] of this) {
+      this.remove(group, name);
+      groupCount--;
+      if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2)
+        break;
+    }
+  }
+};
+
+const privModeStorage = new ContentPrefStore();
+
+ContentPrefService.prototype = {
+  // XPCOM Plumbing
+
+  classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
+
+  QueryInterface: function CPS_QueryInterface(iid) {
+    let supportedIIDs = [
+      Ci.nsIContentPrefService,
+      Ci.nsISupports,
+    ];
+    if (supportedIIDs.some(i => iid.equals(i)))
+      return this;
+    if (iid.equals(Ci.nsIContentPrefService2)) {
+      if (!this._contentPrefService2) {
+        let s = {};
+        Cu.import("resource://gre/modules/ContentPrefService2.jsm", s);
+        this._contentPrefService2 = new s.ContentPrefService2(this);
+      }
+      return this._contentPrefService2;
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  // Convenience Getters
+
+  // Observer Service
+  __observerSvc: null,
+  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() {
+    if (!this.__consoleSvc)
+      this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"].
+                          getService(Ci.nsIConsoleService);
+    return this.__consoleSvc;
+  },
+
+  // Preferences Service
+  __prefSvc: null,
+  get _prefSvc() {
+    if (!this.__prefSvc)
+      this.__prefSvc = Cc["@mozilla.org/preferences-service;1"].
+                       getService(Ci.nsIPrefBranch);
+    return this.__prefSvc;
+  },
+
+
+  // Destruction
+
+  _destroy: function ContentPrefService__destroy() {
+    this._observerSvc.removeObserver(this, "xpcom-shutdown");
+    this._observerSvc.removeObserver(this, "last-pb-context-exited");
+
+    // Finalize statements which may have been used asynchronously.
+    // FIXME(696499): put them in an object cache like other components.
+    if (this.__stmtSelectPrefID) {
+      this.__stmtSelectPrefID.finalize();
+      this.__stmtSelectPrefID = null;
+    }
+    if (this.__stmtSelectGlobalPrefID) {
+      this.__stmtSelectGlobalPrefID.finalize();
+      this.__stmtSelectGlobalPrefID = null;
+    }
+    if (this.__stmtInsertPref) {
+      this.__stmtInsertPref.finalize();
+      this.__stmtInsertPref = null;
+    }
+    if (this.__stmtInsertGroup) {
+      this.__stmtInsertGroup.finalize();
+      this.__stmtInsertGroup = null;
+    }
+    if (this.__stmtInsertSetting) {
+      this.__stmtInsertSetting.finalize();
+      this.__stmtInsertSetting = null;
+    }
+    if (this.__stmtSelectGroupID) {
+      this.__stmtSelectGroupID.finalize();
+      this.__stmtSelectGroupID = null;
+    }
+    if (this.__stmtSelectSettingID) {
+      this.__stmtSelectSettingID.finalize();
+      this.__stmtSelectSettingID = null;
+    }
+    if (this.__stmtSelectPref) {
+      this.__stmtSelectPref.finalize();
+      this.__stmtSelectPref = null;
+    }
+    if (this.__stmtSelectGlobalPref) {
+      this.__stmtSelectGlobalPref.finalize();
+      this.__stmtSelectGlobalPref = null;
+    }
+    if (this.__stmtSelectPrefsByName) {
+      this.__stmtSelectPrefsByName.finalize();
+      this.__stmtSelectPrefsByName = null;
+    }
+    if (this.__stmtDeleteSettingIfUnused) {
+      this.__stmtDeleteSettingIfUnused.finalize();
+      this.__stmtDeleteSettingIfUnused = null;
+    }
+    if (this.__stmtSelectPrefs) {
+      this.__stmtSelectPrefs.finalize();
+      this.__stmtSelectPrefs = null;
+    }
+    if (this.__stmtDeleteGroupIfUnused) {
+      this.__stmtDeleteGroupIfUnused.finalize();
+      this.__stmtDeleteGroupIfUnused = null;
+    }
+    if (this.__stmtDeletePref) {
+      this.__stmtDeletePref.finalize();
+      this.__stmtDeletePref = null;
+    }
+    if (this.__stmtUpdatePref) {
+      this.__stmtUpdatePref.finalize();
+      this.__stmtUpdatePref = null;
+    }
+
+    if (this._contentPrefService2)
+      this._contentPrefService2.destroy();
+
+    this._dbConnection.asyncClose(() => {
+      Services.obs.notifyObservers(null, "content-prefs-db-closed");
+    });
+
+    // 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.
+    delete this._observers;
+    delete this._genericObservers;
+    delete this.__consoleSvc;
+    delete this.__grouper;
+    delete this.__observerSvc;
+    delete this.__prefSvc;
+  },
+
+
+  // nsIObserver
+
+  observe: function ContentPrefService_observe(subject, topic, data) {
+    switch (topic) {
+      case "xpcom-shutdown":
+        this._destroy();
+        break;
+      case "last-pb-context-exited":
+        this._privModeStorage.removeAll();
+        break;
+    }
+  },
+
+
+  // in-memory cache and private-browsing stores
+
+  _cache: cache,
+  _privModeStorage: privModeStorage,
+
+  // nsIContentPrefService
+
+  getPref: function ContentPrefService_getPref(aGroup, aName, aContext, aCallback) {
+    warnDeprecated();
+
+    if (!aName)
+      throw Components.Exception("aName cannot be null or an empty string",
+                                 Cr.NS_ERROR_ILLEGAL_VALUE);
+
+    var group = this._parseGroupParam(aGroup);
+
+    if (aContext && aContext.usePrivateBrowsing) {
+      if (this._privModeStorage.has(group, aName)) {
+        let value = this._privModeStorage.get(group, aName);
+        if (aCallback) {
+          this._scheduleCallback(function() { aCallback.onResult(value); });
+          return undefined;
+        }
+        return value;
+      }
+      // if we don't have a pref specific to this private mode browsing
+      // session, to try to get one from normal mode
+    }
+
+    if (group == null)
+      return this._selectGlobalPref(aName, aCallback);
+    return this._selectPref(group, aName, aCallback);
+  },
+
+  setPref: function ContentPrefService_setPref(aGroup, aName, aValue, aContext) {
+    warnDeprecated();
+
+    // If the pref is already set to the value, there's nothing more to do.
+    var currentValue = this.getPref(aGroup, aName, aContext);
+    if (typeof currentValue != "undefined") {
+      if (currentValue == aValue)
+        return;
+    }
+
+    var group = this._parseGroupParam(aGroup);
+
+    if (aContext && aContext.usePrivateBrowsing) {
+      this._privModeStorage.setWithCast(group, aName, aValue);
+      this._notifyPrefSet(group, aName, aValue, aContext.usePrivateBrowsing);
+      return;
+    }
+
+    var settingID = this._selectSettingID(aName) || this._insertSetting(aName);
+    var groupID, prefID;
+    if (group == null) {
+      groupID = null;
+      prefID = this._selectGlobalPrefID(settingID);
+    } else {
+      groupID = this._selectGroupID(group) || this._insertGroup(group);
+      prefID = this._selectPrefID(groupID, settingID);
+    }
+
+    // Update the existing record, if any, or create a new one.
+    if (prefID)
+      this._updatePref(prefID, aValue);
+    else
+      this._insertPref(groupID, settingID, aValue);
+
+    this._cache.setWithCast(group, aName, aValue);
+
+    this._notifyPrefSet(group, aName, aValue,
+                        aContext ? aContext.usePrivateBrowsing : false);
+  },
+
+  hasPref: function ContentPrefService_hasPref(aGroup, aName, aContext) {
+    warnDeprecated();
+
+    // XXX If consumers end up calling this method regularly, then we should
+    // optimize this to query the database directly.
+    return (typeof this.getPref(aGroup, aName, aContext) != "undefined");
+  },
+
+  hasCachedPref: function ContentPrefService_hasCachedPref(aGroup, aName, aContext) {
+    warnDeprecated();
+
+    if (!aName)
+      throw Components.Exception("aName cannot be null or an empty string",
+                                 Cr.NS_ERROR_ILLEGAL_VALUE);
+
+    let group = this._parseGroupParam(aGroup);
+    let storage = aContext && aContext.usePrivateBrowsing ? this._privModeStorage : this._cache;
+    return storage.has(group, aName);
+  },
+
+  removePref: function ContentPrefService_removePref(aGroup, aName, aContext) {
+    warnDeprecated();
+
+    // If there's no old value, then there's nothing to remove.
+    if (!this.hasPref(aGroup, aName, aContext))
+      return;
+
+    var group = this._parseGroupParam(aGroup);
+
+    if (aContext && aContext.usePrivateBrowsing) {
+      this._privModeStorage.remove(group, aName);
+      this._notifyPrefRemoved(group, aName, true);
+      return;
+    }
+
+    var settingID = this._selectSettingID(aName);
+    var groupID, prefID;
+    if (group == null) {
+      groupID = null;
+      prefID = this._selectGlobalPrefID(settingID);
+    } else {
+      groupID = this._selectGroupID(group);
+      prefID = this._selectPrefID(groupID, settingID);
+    }
+
+    this._deletePref(prefID);
+
+    // Get rid of extraneous records that are no longer being used.
+    this._deleteSettingIfUnused(settingID);
+    if (groupID)
+      this._deleteGroupIfUnused(groupID);
+
+    this._cache.remove(group, aName);
+    this._notifyPrefRemoved(group, aName, false);
+  },
+
+  removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs(aContext) {
+    warnDeprecated();
+
+    // will not delete global preferences
+    if (aContext && aContext.usePrivateBrowsing) {
+        // keep only global prefs
+        this._privModeStorage.removeAllGroups();
+    }
+    this._cache.removeAllGroups();
+    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, aContext) {
+    warnDeprecated();
+
+    if (!aName)
+      throw Components.Exception("aName cannot be null or an empty string",
+                                 Cr.NS_ERROR_ILLEGAL_VALUE);
+
+    if (aContext && aContext.usePrivateBrowsing) {
+      for (let [group, name, ] of this._privModeStorage) {
+        if (name === aName) {
+          this._privModeStorage.remove(group, aName);
+          this._notifyPrefRemoved(group, aName, true);
+        }
+      }
+    }
+
+    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.executeStep()) {
+        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._cache.remove(groupNames[i], aName);
+      if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length)
+        this._deleteGroupIfUnused(groupIDs[i]);
+      if (!aContext || !aContext.usePrivateBrowsing) {
+        this._notifyPrefRemoved(groupNames[i], aName, false);
+      }
+    }
+  },
+
+  getPrefs: function ContentPrefService_getPrefs(aGroup, aContext) {
+    warnDeprecated();
+
+    var group = this._parseGroupParam(aGroup);
+    if (aContext && aContext.usePrivateBrowsing) {
+        let prefs = Cc["@mozilla.org/hash-property-bag;1"].
+                    createInstance(Ci.nsIWritablePropertyBag);
+        for (let [sgroup, sname, sval] of this._privModeStorage) {
+          if (sgroup === group)
+            prefs.setProperty(sname, sval);
+        }
+        return prefs;
+    }
+
+    if (group == null)
+      return this._selectGlobalPrefs();
+    return this._selectPrefs(group);
+  },
+
+  getPrefsByName: function ContentPrefService_getPrefsByName(aName, aContext) {
+    warnDeprecated();
+
+    if (!aName)
+      throw Components.Exception("aName cannot be null or an empty string",
+                                 Cr.NS_ERROR_ILLEGAL_VALUE);
+
+    if (aContext && aContext.usePrivateBrowsing) {
+      let prefs = Cc["@mozilla.org/hash-property-bag;1"].
+                  createInstance(Ci.nsIWritablePropertyBag);
+      for (let [sgroup, sname, sval] of this._privModeStorage) {
+        if (sname === aName)
+          prefs.setProperty(sgroup, sval);
+      }
+      return prefs;
+    }
+
+    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) {
+    warnDeprecated();
+    this._addObserver.apply(this, arguments);
+  },
+
+  _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) {
+    warnDeprecated();
+    this._removeObserver.apply(this, arguments);
+  },
+
+  _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;
+  },
+
+  /**
+   * Notify all observers about the removal of a preference.
+   */
+  _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(aGroup, aName, aIsPrivate) {
+    for (var observer of this._getObservers(aName)) {
+      try {
+        observer.onContentPrefRemoved(aGroup, aName, aIsPrivate);
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+  },
+
+  /**
+   * Notify all observers about a preference change.
+   */
+  _notifyPrefSet: function ContentPrefService__notifyPrefSet(aGroup, aName, aValue, aIsPrivate) {
+    for (var observer of this._getObservers(aName)) {
+      try {
+        observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate);
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+  },
+
+  get grouper() {
+    warnDeprecated();
+    return this._grouper;
+  },
+  __grouper: null,
+  get _grouper() {
+    if (!this.__grouper)
+      this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"].
+                       getService(Ci.nsIContentURIGrouper);
+    return this.__grouper;
+  },
+
+  get DBConnection() {
+    warnDeprecated();
+    return this._dbConnection;
+  },
+
+
+  // Data Retrieval & Modification
+
+  __stmtSelectPref: null,
+  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;
+  },
+
+  _scheduleCallback(func) {
+    let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+    tm.dispatchToMainThread(func);
+  },
+
+  _selectPref: function ContentPrefService__selectPref(aGroup, aSetting, aCallback) {
+    let value = undefined;
+    if (this._cache.has(aGroup, aSetting)) {
+      value = this._cache.get(aGroup, aSetting);
+      if (aCallback) {
+        this._scheduleCallback(function() { aCallback.onResult(value); });
+        return undefined;
+      }
+      return value;
+    }
+
+    try {
+      this._stmtSelectPref.params.group = aGroup;
+      this._stmtSelectPref.params.setting = aSetting;
+
+      if (aCallback) {
+        let cache = this._cache;
+        new AsyncStatement(this._stmtSelectPref).execute({onResult(aResult) {
+          cache.set(aGroup, aSetting, aResult);
+          aCallback.onResult(aResult);
+        }});
+      } else {
+        if (this._stmtSelectPref.executeStep()) {
+          value = this._stmtSelectPref.row.value;
+        }
+        this._cache.set(aGroup, aSetting, value);
+      }
+    } finally {
+      this._stmtSelectPref.reset();
+    }
+
+    return value;
+  },
+
+  __stmtSelectGlobalPref: null,
+  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, aCallback) {
+    let value = undefined;
+    if (this._cache.has(null, aName)) {
+      value = this._cache.get(null, aName);
+      if (aCallback) {
+        this._scheduleCallback(function() { aCallback.onResult(value); });
+        return undefined;
+      }
+      return value;
+    }
+
+    try {
+      this._stmtSelectGlobalPref.params.name = aName;
+
+      if (aCallback) {
+        let cache = this._cache;
+        new AsyncStatement(this._stmtSelectGlobalPref).execute({onResult(aResult) {
+          cache.set(null, aName, aResult);
+          aCallback.onResult(aResult);
+        }});
+      } else {
+        if (this._stmtSelectGlobalPref.executeStep()) {
+          value = this._stmtSelectGlobalPref.row.value;
+        }
+        this._cache.set(null, aName, value);
+      }
+    } finally {
+      this._stmtSelectGlobalPref.reset();
+    }
+
+    return value;
+  },
+
+  __stmtSelectGroupID: null,
+  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.executeStep())
+        id = this._stmtSelectGroupID.row.id;
+    } finally {
+      this._stmtSelectGroupID.reset();
+    }
+
+    return id;
+  },
+
+  __stmtInsertGroup: null,
+  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() {
+    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.executeStep())
+        id = this._stmtSelectSettingID.row.id;
+    } finally {
+      this._stmtSelectSettingID.reset();
+    }
+
+    return id;
+  },
+
+  __stmtInsertSetting: null,
+  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() {
+    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.executeStep())
+        id = this._stmtSelectPrefID.row.id;
+    } finally {
+      this._stmtSelectPrefID.reset();
+    }
+
+    return id;
+  },
+
+  __stmtSelectGlobalPrefID: null,
+  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.executeStep())
+        id = this._stmtSelectGlobalPrefID.row.id;
+    } finally {
+      this._stmtSelectGlobalPrefID.reset();
+    }
+
+    return id;
+  },
+
+  __stmtInsertPref: null,
+  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() {
+    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() {
+    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() {
+    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() {
+    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() {
+    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.executeStep())
+        prefs.setProperty(this._stmtSelectPrefs.row.name,
+                          this._stmtSelectPrefs.row.value);
+    } finally {
+      this._stmtSelectPrefs.reset();
+    }
+
+    return prefs;
+  },
+
+  __stmtSelectGlobalPrefs: null,
+  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.executeStep())
+        prefs.setProperty(this._stmtSelectGlobalPrefs.row.name,
+                          this._stmtSelectGlobalPrefs.row.value);
+    } finally {
+      this._stmtSelectGlobalPrefs.reset();
+    }
+
+    return prefs;
+  },
+
+  __stmtSelectPrefsByName: null,
+  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.executeStep())
+        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: 4,
+
+  _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, \
+                   timestamp    INTEGER NOT NULL DEFAULT 0" // Storage in seconds, API in ms. 0 for migrated values.
+    },
+    indices: {
+      groups_idx: {
+        table: "groups",
+        columns: ["name"]
+      },
+      settings_idx: {
+        table: "settings",
+        columns: ["name"]
+      },
+      prefs_idx: {
+        table: "prefs",
+        columns: ["timestamp", "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;
+    }
+
+    return statement;
+  },
+
+  // _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);
+      } catch (e) {
+        // 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.
+        if (e.result != Cr.NS_ERROR_FILE_CORRUPTED)
+          throw e;
+        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) {
+    /**
+     * Migrations should follow the template rules in bug 1074817 comment 3 which are:
+     * 1. Migration should be incremental and non-breaking.
+     * 2. It should be idempotent because one can downgrade an upgrade again.
+     * On downgrade:
+     * 1. Decrement schema version so that upgrade runs the migrations again.
+     */
+    aDBConnection.beginTransaction();
+
+    try {
+       /**
+       * 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.
+       */
+      if (aOldVersion == 0) {
+        this._dbCreateSchema(aDBConnection);
+      } else {
+        for (let i = aOldVersion; i < aNewVersion; i++) {
+          let migrationName = "_dbMigrate" + i + "To" + (i + 1);
+          if (typeof this[migrationName] != "function") {
+            throw ("no migrator function from version " + aOldVersion + " to version " + aNewVersion);
+          }
+          this[migrationName](aDBConnection);
+        }
+      }
+      aDBConnection.schemaVersion = aNewVersion;
+      aDBConnection.commitTransaction();
+    } catch (ex) {
+      aDBConnection.rollbackTransaction();
+      throw ex;
+    }
+  },
+
+  _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(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");
+  },
+
+  _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) {
+    this._dbCreateIndices(aDBConnection);
+  },
+
+  _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) {
+    // Add timestamp column if it does not exist yet. This operation is idempotent.
+    try {
+      let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs");
+      stmt.finalize();
+    } catch (e) {
+      aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
+    }
+
+    // To modify prefs_idx drop it and create again.
+    aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx");
+    this._dbCreateIndices(aDBConnection);
+  },
+
+  _parseGroupParam: function ContentPrefService__parseGroupParam(aGroup) {
+    if (aGroup == null)
+      return null;
+    if (aGroup.constructor.name == "String")
+      return aGroup.toString();
+    if (aGroup instanceof Ci.nsIURI)
+      return this.grouper.group(aGroup);
+
+    throw Components.Exception("aGroup is not a string, nsIURI or null",
+                               Cr.NS_ERROR_ILLEGAL_VALUE);
+  },
+};
+
+function warnDeprecated() {
+  let Deprecated = Cu.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+  Deprecated.warning("nsIContentPrefService is deprecated. Please use nsIContentPrefService2 instead.",
+                     "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIContentPrefService2",
+                     Components.stack.caller);
+}
+
+
+function HostnameGrouper() {}
+
+HostnameGrouper.prototype = {
+  // XPCOM Plumbing
+
+  classID:          Components.ID("{8df290ae-dcaa-4c11-98a5-2429a4dc97bb}"),
+  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;
+  }
+};
+
+function AsyncStatement(aStatement) {
+  this.stmt = aStatement;
+}
+
+AsyncStatement.prototype = {
+  execute: function AsyncStmt_execute(aCallback) {
+    let stmt = this.stmt;
+    stmt.executeAsync({
+      _callback: aCallback,
+      _hadResult: false,
+      handleResult(aResult) {
+        this._hadResult = true;
+        if (this._callback) {
+          let row = aResult.getNextRow();
+          this._callback.onResult(row.getResultByName("value"));
+        }
+      },
+      handleCompletion(aReason) {
+        if (!this._hadResult && this._callback &&
+            aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED)
+          this._callback.onResult(undefined);
+      },
+      handleError(aError) {}
+    });
+  }
+};
+
+// XPCOM Plumbing
+
+var components = [ContentPrefService, HostnameGrouper];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
rename from toolkit/components/contentprefs/ContentPrefService2.manifest
rename to toolkit/components/contentprefs/nsContentPrefService.manifest
--- a/toolkit/components/contentprefs/ContentPrefService2.manifest
+++ b/toolkit/components/contentprefs/nsContentPrefService.manifest
@@ -1,5 +1,5 @@
-component {e3f772f3-023f-4b32-b074-36cf0fd5d414} ContentPrefService2.js
+component {e3f772f3-023f-4b32-b074-36cf0fd5d414} nsContentPrefService.js
 contract @mozilla.org/content-pref/service;1 {e3f772f3-023f-4b32-b074-36cf0fd5d414}
-component {8df290ae-dcaa-4c11-98a5-2429a4dc97bb} ContentPrefService2.js
+component {8df290ae-dcaa-4c11-98a5-2429a4dc97bb} nsContentPrefService.js
 contract @mozilla.org/content-pref/hostname-grouper;1 {8df290ae-dcaa-4c11-98a5-2429a4dc97bb}
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/xpcshell-test"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Inspired by the Places infrastructure in head_bookmarks.js
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/ContentPrefInstance.jsm");
+Cu.import("resource://testing-common/TestUtils.jsm");
+
+const CONTENT_PREFS_DB_FILENAME = "content-prefs.sqlite";
+const CONTENT_PREFS_BACKUP_DB_FILENAME = "content-prefs.sqlite.corrupt";
+
+var ContentPrefTest = {
+  // Convenience Getters
+
+  __dirSvc: null,
+  get _dirSvc() {
+    if (!this.__dirSvc)
+      this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+                      getService(Ci.nsIProperties);
+    return this.__dirSvc;
+  },
+
+  __consoleSvc: null,
+  get _consoleSvc() {
+    if (!this.__consoleSvc)
+      this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"].
+                          getService(Ci.nsIConsoleService);
+    return this.__consoleSvc;
+  },
+
+  __ioSvc: null,
+  get _ioSvc() {
+    if (!this.__ioSvc)
+      this.__ioSvc = Cc["@mozilla.org/network/io-service;1"].
+                     getService(Ci.nsIIOService);
+    return this.__ioSvc;
+  },
+
+
+  // nsISupports
+
+  interfaces: [Ci.nsIDirectoryServiceProvider, Ci.nsISupports],
+
+  QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+    if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+      throw Cr.NS_ERROR_NO_INTERFACE;
+    return this;
+  },
+
+
+  // nsIDirectoryServiceProvider
+
+  getFile: function ContentPrefTest_getFile(property, persistent) {
+    persistent.value = true;
+
+    if (property == "ProfD")
+      return this._dirSvc.get("CurProcD", Ci.nsIFile);
+
+    // This causes extraneous errors to show up in the log when the directory
+    // service asks us first for CurProcD and MozBinD.  I wish there was a way
+    // to suppress those errors.
+    throw Cr.NS_ERROR_FAILURE;
+  },
+
+
+  // Utilities
+
+  getURI: function ContentPrefTest_getURI(spec) {
+    return this._ioSvc.newURI(spec);
+  },
+
+  /**
+   * Get the profile directory.
+   */
+  getProfileDir: function ContentPrefTest_getProfileDir() {
+    // do_get_profile can be only called from a parent process
+    if (runningInParent) {
+      return do_get_profile();
+    }
+    // if running in a content process, this just returns the path
+    // profile was initialized in the ipc head file
+    let env = Components.classes["@mozilla.org/process/environment;1"]
+                        .getService(Components.interfaces.nsIEnvironment);
+    // the python harness sets this in the environment for us
+    let profd = env.get("XPCSHELL_TEST_PROFILE_DIR");
+    let file = Components.classes["@mozilla.org/file/local;1"]
+                         .createInstance(Components.interfaces.nsIFile);
+    file.initWithPath(profd);
+    return file;
+  },
+
+  /**
+   * Delete the content pref service's persistent datastore.  We do this before
+   * and after running tests to make sure we start from scratch each time. We
+   * also do it during the database creation, schema migration, and backup tests.
+   */
+  deleteDatabase: function ContentPrefTest_deleteDatabase() {
+    var file = this.getProfileDir();
+    file.append(CONTENT_PREFS_DB_FILENAME);
+    if (file.exists())
+      try { file.remove(false); } catch (e) { /* stupid windows box */ }
+    return file;
+  },
+
+  /**
+   * Delete the backup of the content pref service's persistent datastore.
+   * We do this during the database creation, schema migration, and backup tests.
+   */
+  deleteBackupDatabase: function ContentPrefTest_deleteBackupDatabase() {
+    var file = this.getProfileDir();
+    file.append(CONTENT_PREFS_BACKUP_DB_FILENAME);
+    if (file.exists())
+      file.remove(false);
+    return file;
+  },
+
+  /**
+   * Log a message to the console and the test log.
+   */
+  log: function ContentPrefTest_log(message) {
+    message = "*** ContentPrefTest: " + message;
+    this._consoleSvc.logStringMessage(message);
+    print(message);
+  }
+
+};
+
+let loadContext = Cc["@mozilla.org/loadcontext;1"].
+                    createInstance(Ci.nsILoadContext);
+let privateLoadContext = Cc["@mozilla.org/privateloadcontext;1"].
+                           createInstance(Ci.nsILoadContext);
+function enterPBMode(cps) {
+  cps.loadContext = privateLoadContext;
+}
+function exitPBMode(cps) {
+  cps.loadContext = loadContext;
+  Services.obs.notifyObservers(null, "last-pb-context-exited");
+}
+
+ContentPrefTest.deleteDatabase();
+
+do_register_cleanup(function() {
+  ContentPrefTest.deleteDatabase();
+  ContentPrefTest.__dirSvc = null;
+});
+
+function inChildProcess() {
+  var appInfo = Cc["@mozilla.org/xre/app-info;1"];
+  if (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType ==
+      Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+    return false;
+  }
+  return true;
+}
+
+// Turn on logging for the content preferences service so we can troubleshoot
+// problems with the tests. Note that we cannot do this in a child process
+// without crashing (but we don't need it anyhow)
+if (!inChildProcess()) {
+  var prefBranch = Cc["@mozilla.org/preferences-service;1"].
+                   getService(Ci.nsIPrefBranch);
+  prefBranch.setBoolPref("browser.preferences.content.log", true);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug248970.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function run_test() {
+    ContentPrefTest.deleteDatabase();
+    var cp = new ContentPrefInstance(loadContext);
+    do_check_neq(cp, null, "Retrieving the content prefs service failed");
+
+    try {
+      const uri1 = ContentPrefTest.getURI("http://www.example.com/");
+      const uri2 = ContentPrefTest.getURI("http://www.anotherexample.com/");
+      const pref_name = "browser.content.full-zoom";
+      const zoomA = 1.5, zoomA_new = 0.8, zoomB = 1.3;
+      // save Zoom-A
+      cp.setPref(uri1, pref_name, zoomA);
+      // make sure Zoom-A is retrievable
+      do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+      // enter private browsing mode
+      enterPBMode(cp);
+      // make sure Zoom-A is retrievable
+      do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+      // save Zoom-B
+      cp.setPref(uri2, pref_name, zoomB);
+      // make sure Zoom-B is retrievable
+      do_check_eq(cp.getPref(uri2, pref_name), zoomB);
+      // update Zoom-A
+      cp.setPref(uri1, pref_name, zoomA_new);
+      // make sure Zoom-A has changed
+      do_check_eq(cp.getPref(uri1, pref_name), zoomA_new);
+      // exit private browsing mode
+      exitPBMode(cp);
+      // make sure Zoom-A change has not persisted
+      do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+      // make sure Zoom-B change has not persisted
+      do_check_eq(cp.hasPref(uri2, pref_name), false);
+    } catch (e) {
+      do_throw("Unexpected exception: " + e);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug503971.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function run_test() {
+  var cps = new ContentPrefInstance(null);
+
+  var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+  do_check_thrown(function() { cps.setPref(uri, null, 8); });
+  do_check_thrown(function() { cps.hasPref(uri, null); });
+  do_check_thrown(function() { cps.getPref(uri, null); });
+  do_check_thrown(function() { cps.removePref(uri, null); });
+  do_check_thrown(function() { cps.getPrefsByName(null); });
+  do_check_thrown(function() { cps.removePrefsByName(null); });
+
+  do_check_thrown(function() { cps.setPref(uri, "", 21); });
+  do_check_thrown(function() { cps.hasPref(uri, ""); });
+  do_check_thrown(function() { cps.getPref(uri, ""); });
+  do_check_thrown(function() { cps.removePref(uri, ""); });
+  do_check_thrown(function() { cps.getPrefsByName(""); });
+  do_check_thrown(function() { cps.removePrefsByName(""); });
+}
+
+function do_check_thrown(aCallback) {
+  var exThrown = false;
+  try {
+    aCallback();
+    do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here");
+  } catch (e) {
+    do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+    exThrown = true;
+  }
+  do_check_true(exThrown);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug679784.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+var prefObserver = {
+    setCalledNum: 0,
+    onContentPrefSet(aGroup, aName, aValue) {
+        this.setCalledNum++;
+    },
+    removedCalledNum: 0,
+    onContentPrefRemoved(aGroup, aName) {
+        this.removedCalledNum++;
+    }
+};
+
+function run_test() {
+  var cps = new ContentPrefInstance(loadContext);
+  cps.removeGroupedPrefs();
+
+  var uri = ContentPrefTest.getURI("http://www.example.com/");
+  var group = cps.grouper.group(uri);
+
+  // first, set a pref in normal mode
+  cps.setPref(uri, "value", "foo");
+  cps.setPref(null, "value-global", "foo-global");
+
+  var num;
+  cps.addObserver("value", prefObserver);
+  cps.addObserver("value-global", prefObserver);
+
+  enterPBMode(cps);
+
+  // test setPref
+  num = prefObserver.setCalledNum;
+  cps.setPref(uri, "value", "foo-private-browsing");
+  do_check_eq(cps.hasPref(uri, "value"), true);
+  do_check_eq(cps.getPref(uri, "value"), "foo-private-browsing");
+  do_check_eq(prefObserver.setCalledNum, num + 1);
+
+  num = prefObserver.setCalledNum;
+  cps.setPref(null, "value-global", "foo-private-browsing-global");
+  do_check_eq(cps.hasPref(null, "value-global"), true);
+  do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global");
+  do_check_eq(prefObserver.setCalledNum, num + 1);
+
+  // test removePref
+  num = prefObserver.removedCalledNum;
+  cps.removePref(uri, "value");
+  do_check_eq(cps.hasPref(uri, "value"), true);
+  // fallback to non private mode value
+  do_check_eq(cps.getPref(uri, "value"), "foo");
+  do_check_eq(prefObserver.removedCalledNum, num + 1);
+
+  num = prefObserver.removedCalledNum;
+  cps.removePref(null, "value-global");
+  do_check_eq(cps.hasPref(null, "value-global"), true);
+  // fallback to non private mode value
+  do_check_eq(cps.getPref(null, "value-global"), "foo-global") ;
+  do_check_eq(prefObserver.removedCalledNum, num + 1);
+
+  // test removeGroupedPrefs
+  cps.setPref(uri, "value", "foo-private-browsing");
+  cps.removeGroupedPrefs();
+  do_check_eq(cps.hasPref(uri, "value"), false);
+  do_check_eq(cps.getPref(uri, "value"), undefined);
+
+  cps.setPref(null, "value-global", "foo-private-browsing-global");
+  cps.removeGroupedPrefs();
+  do_check_eq(cps.hasPref(null, "value-global"), true);
+  do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global");
+
+  // test removePrefsByName
+  num = prefObserver.removedCalledNum;
+  cps.setPref(uri, "value", "foo-private-browsing");
+  cps.removePrefsByName("value");
+  do_check_eq(cps.hasPref(uri, "value"), false);
+  do_check_eq(cps.getPref(uri, "value"), undefined);
+  do_check_true(prefObserver.removedCalledNum > num);
+
+  num = prefObserver.removedCalledNum;
+  cps.setPref(null, "value-global", "foo-private-browsing");
+  cps.removePrefsByName("value-global");
+  do_check_eq(cps.hasPref(null, "value-global"), false);
+  do_check_eq(cps.getPref(null, "value-global"), undefined);
+  do_check_true(prefObserver.removedCalledNum > num);
+
+  // test getPrefs
+  cps.setPref(uri, "value", "foo-private-browsing");
+  do_check_eq(cps.getPrefs(uri).getProperty("value"), "foo-private-browsing");
+
+  cps.setPref(null, "value-global", "foo-private-browsing-global");
+  do_check_eq(cps.getPrefs(null).getProperty("value-global"), "foo-private-browsing-global");
+
+  // test getPrefsByName
+  do_check_eq(cps.getPrefsByName("value").getProperty(group), "foo-private-browsing");
+  do_check_eq(cps.getPrefsByName("value-global").getProperty(null), "foo-private-browsing-global");
+
+  cps.removeObserver("value", prefObserver);
+  cps.removeObserver("value-global", prefObserver);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js
@@ -0,0 +1,462 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+add_task(async function() {
+  // Database Creation, Schema Migration, and Backup
+
+  // Note: in these tests we use createInstance instead of getService
+  // so we can instantiate the service multiple times and make it run
+  // its database initialization code each time.
+
+  function with_cps_instance(testFn) {
+    let cps = Cc["@mozilla.org/content-pref/service;1"]
+                .createInstance(Ci.nsIContentPrefService)
+                .QueryInterface(Ci.nsIObserver);
+    testFn(cps);
+    let promiseClosed = TestUtils.topicObserved("content-prefs-db-closed");
+    cps.observe(null, "xpcom-shutdown", "");
+    return promiseClosed;
+  }
+
+  // Create a new database.
+  ContentPrefTest.deleteDatabase();
+  await with_cps_instance(cps => {
+    do_check_true(cps.DBConnection.connectionReady);
+  });
+
+  // Open an existing database.
+
+  ContentPrefTest.deleteDatabase();
+  await with_cps_instance(cps => {});
+
+  // Get the service and make sure it has a ready database connection.
+  await with_cps_instance(cps => {
+    do_check_true(cps.DBConnection.connectionReady);
+  });
+
+  // Open an empty database.
+  {
+    let dbFile = ContentPrefTest.deleteDatabase();
+
+    // Create an empty database.
+    let dbService = Cc["@mozilla.org/storage/service;1"].
+                    getService(Ci.mozIStorageService);
+    let dbConnection = dbService.openDatabase(dbFile);
+    do_check_eq(dbConnection.schemaVersion, 0);
+    dbConnection.close();
+    do_check_true(dbFile.exists());
+
+    // Get the service and make sure it has created the schema.
+    await with_cps_instance(cps => {
+      do_check_neq(cps.DBConnection.schemaVersion, 0);
+    });
+  }
+
+  // Open a corrupted database.
+  {
+    let dbFile = ContentPrefTest.deleteDatabase();
+    let backupDBFile = ContentPrefTest.deleteBackupDatabase();
+
+    // Create a corrupted database.
+    let foStream = Cc["@mozilla.org/network/file-output-stream;1"].
+                   createInstance(Ci.nsIFileOutputStream);
+    foStream.init(dbFile, 0x02 | 0x08 | 0x20, 0o666, 0);
+    let garbageData = "garbage that makes SQLite think the file is corrupted";
+    foStream.write(garbageData, garbageData.length);
+    foStream.close();
+
+    // Get the service and make sure it backs up and recreates the database.
+    await with_cps_instance(cps => {
+      do_check_true(backupDBFile.exists());
+      do_check_true(cps.DBConnection.connectionReady);
+    });
+  }
+
+  // Open a database with a corrupted schema.
+  {
+    let dbFile = ContentPrefTest.deleteDatabase();
+    let backupDBFile = ContentPrefTest.deleteBackupDatabase();
+
+    // Create an empty database and set the schema version to a number
+    // that will trigger a schema migration that will fail.
+    let dbService = Cc["@mozilla.org/storage/service;1"].
+                    getService(Ci.mozIStorageService);
+    let dbConnection = dbService.openDatabase(dbFile);
+    dbConnection.schemaVersion = -1;
+    dbConnection.close();
+    do_check_true(dbFile.exists());
+
+    // Get the service and make sure it backs up and recreates the database.
+    await with_cps_instance(cps => {
+      do_check_true(backupDBFile.exists());
+      do_check_true(cps.DBConnection.connectionReady);
+    });
+  }
+
+
+  // Now get the content pref service for real for use by the rest of the tests.
+  let cps = new ContentPrefInstance(null);
+
+  var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+  // Make sure disk synchronization checking is turned off by default.
+  var statement = cps.DBConnection.createStatement("PRAGMA synchronous");
+  try {
+    statement.executeStep();
+    do_check_eq(0, statement.getInt32(0));
+  } finally {
+    statement.finalize();
+  }
+
+  // Nonexistent Pref
+
+  do_check_eq(cps.getPref(uri, "test.nonexistent.getPref"), undefined);
+  do_check_eq(cps.setPref(uri, "test.nonexistent.setPref", 5), undefined);
+  do_check_false(cps.hasPref(uri, "test.nonexistent.hasPref"));
+  do_check_eq(cps.removePref(uri, "test.nonexistent.removePref"), undefined);
+
+
+  // Existing Pref
+
+  cps.setPref(uri, "test.existing", 5);
+
+  // getPref should return the pref value
+  do_check_eq(cps.getPref(uri, "test.existing"), 5);
+
+  // setPref should return undefined and change the value of the pref
+  do_check_eq(cps.setPref(uri, "test.existing", 6), undefined);
+  do_check_eq(cps.getPref(uri, "test.existing"), 6);
+
+  // hasPref should return true
+  do_check_true(cps.hasPref(uri, "test.existing"));
+
+  // removePref should return undefined and remove the pref
+  do_check_eq(cps.removePref(uri, "test.existing"), undefined);
+  do_check_false(cps.hasPref(uri, "test.existing"));
+
+
+  // Round-Trip Data Integrity
+
+  // Make sure pref values remain the same from setPref to getPref.
+
+  cps.setPref(uri, "test.data-integrity.integer", 5);
+  do_check_eq(cps.getPref(uri, "test.data-integrity.integer"), 5);
+
+  cps.setPref(uri, "test.data-integrity.float", 5.5);
+  do_check_eq(cps.getPref(uri, "test.data-integrity.float"), 5.5);
+
+  cps.setPref(uri, "test.data-integrity.boolean", true);
+  do_check_eq(cps.getPref(uri, "test.data-integrity.boolean"), true);
+
+  cps.setPref(uri, "test.data-integrity.string", "test");
+  do_check_eq(cps.getPref(uri, "test.data-integrity.string"), "test");
+
+  cps.setPref(uri, "test.data-integrity.null", null);
+  do_check_eq(cps.getPref(uri, "test.data-integrity.null"), null);
+
+  // XXX Test arbitrary binary data.
+
+  // Make sure hasPref and removePref work on all data types.
+
+  do_check_true(cps.hasPref(uri, "test.data-integrity.integer"));
+  do_check_true(cps.hasPref(uri, "test.data-integrity.float"));
+  do_check_true(cps.hasPref(uri, "test.data-integrity.boolean"));
+  do_check_true(cps.hasPref(uri, "test.data-integrity.string"));
+  do_check_true(cps.hasPref(uri, "test.data-integrity.null"));
+
+  do_check_eq(cps.removePref(uri, "test.data-integrity.integer"), undefined);
+  do_check_eq(cps.removePref(uri, "test.data-integrity.float"), undefined);
+  do_check_eq(cps.removePref(uri, "test.data-integrity.boolean"), undefined);
+  do_check_eq(cps.removePref(uri, "test.data-integrity.string"), undefined);
+  do_check_eq(cps.removePref(uri, "test.data-integrity.null"), undefined);
+
+  do_check_false(cps.hasPref(uri, "test.data-integrity.integer"));
+  do_check_false(cps.hasPref(uri, "test.data-integrity.float"));
+  do_check_false(cps.hasPref(uri, "test.data-integrity.boolean"));
+  do_check_false(cps.hasPref(uri, "test.data-integrity.string"));
+  do_check_false(cps.hasPref(uri, "test.data-integrity.null"));
+
+
+  // getPrefs
+
+  cps.setPref(uri, "test.getPrefs.a", 1);
+  cps.setPref(uri, "test.getPrefs.b", 2);
+  cps.setPref(uri, "test.getPrefs.c", 3);
+
+  var prefs = cps.getPrefs(uri);
+  do_check_true(prefs.hasKey("test.getPrefs.a"));
+  do_check_eq(prefs.get("test.getPrefs.a"), 1);
+  do_check_true(prefs.hasKey("test.getPrefs.b"));
+  do_check_eq(prefs.get("test.getPrefs.b"), 2);
+  do_check_true(prefs.hasKey("test.getPrefs.c"));
+  do_check_eq(prefs.get("test.getPrefs.c"), 3);
+
+
+  // Site-Specificity
+
+  {
+    // These are all different sites, and setting a pref for one of them
+    // shouldn't set it for the others.
+    let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+    let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+    let uri3 = ContentPrefTest.getURI("http://domain1.com/");
+    let uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+    cps.setPref(uri1, "test.site-specificity.uri1", 5);
+    do_check_false(cps.hasPref(uri2, "test.site-specificity.uri1"));
+    do_check_false(cps.hasPref(uri3, "test.site-specificity.uri1"));
+    do_check_false(cps.hasPref(uri4, "test.site-specificity.uri1"));
+
+    cps.setPref(uri2, "test.site-specificity.uri2", 5);
+    do_check_false(cps.hasPref(uri1, "test.site-specificity.uri2"));
+    do_check_false(cps.hasPref(uri3, "test.site-specificity.uri2"));
+    do_check_false(cps.hasPref(uri4, "test.site-specificity.uri2"));
+
+    cps.setPref(uri3, "test.site-specificity.uri3", 5);
+    do_check_false(cps.hasPref(uri1, "test.site-specificity.uri3"));
+    do_check_false(cps.hasPref(uri2, "test.site-specificity.uri3"));
+    do_check_false(cps.hasPref(uri4, "test.site-specificity.uri3"));
+
+    cps.setPref(uri4, "test.site-specificity.uri4", 5);
+    do_check_false(cps.hasPref(uri1, "test.site-specificity.uri4"));
+    do_check_false(cps.hasPref(uri2, "test.site-specificity.uri4"));
+    do_check_false(cps.hasPref(uri3, "test.site-specificity.uri4"));
+  }
+
+  // Observers
+
+  var specificObserver = {
+    interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+    QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+      if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+        throw Cr.NS_ERROR_NO_INTERFACE;
+      return this;
+    },
+
+    numTimesSetCalled: 0,
+    onContentPrefSet: function specificObserver_onContentPrefSet(group, name, value) {
+      ++this.numTimesSetCalled;
+      do_check_eq(group, "www.example.com");
+      do_check_eq(name, "test.observer.1");
+      do_check_eq(value, "test value");
+    },
+
+    numTimesRemovedCalled: 0,
+    onContentPrefRemoved: function specificObserver_onContentPrefRemoved(group, name) {
+      ++this.numTimesRemovedCalled;
+      do_check_eq(group, "www.example.com");
+      do_check_eq(name, "test.observer.1");
+    }
+
+  };
+
+  var genericObserver = {
+    interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+    QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+      if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+        throw Cr.NS_ERROR_NO_INTERFACE;
+      return this;
+    },
+
+    numTimesSetCalled: 0,
+    onContentPrefSet: function genericObserver_onContentPrefSet(group, name, value, isPrivate) {
+      ++this.numTimesSetCalled;
+      do_check_eq(group, "www.example.com");
+      if (name == "test.observer.private")
+        do_check_true(isPrivate);
+      else if (name == "test.observer.normal")
+        do_check_false(isPrivate);
+      else if (name != "test.observer.1" && name != "test.observer.2")
+        do_throw("genericObserver.onContentPrefSet: " +
+                 "name not in (test.observer.(1|2|normal|private))");
+      do_check_eq(value, "test value");
+    },
+
+    numTimesRemovedCalled: 0,
+    onContentPrefRemoved: function genericObserver_onContentPrefRemoved(group, name, isPrivate) {
+      ++this.numTimesRemovedCalled;
+      do_check_eq(group, "www.example.com");
+      if (name == "test.observer.private")
+        do_check_true(isPrivate);
+      else if (name == "test.observer.normal")
+        do_check_false(isPrivate);
+      if (name != "test.observer.1" && name != "test.observer.2" &&
+          name != "test.observer.normal" && name != "test.observer.private") {
+        do_throw("genericObserver.onContentPrefSet: " +
+                 "name not in (test.observer.(1|2|normal|private))");
+      }
+    }
+
+  };
+
+  // Make sure we can add observers, observers get notified about changes,
+  // specific observers only get notified about changes to the specific setting,
+  // and generic observers get notified about changes to all settings.
+  cps.addObserver("test.observer.1", specificObserver);
+  cps.addObserver(null, genericObserver);
+  cps.setPref(uri, "test.observer.1", "test value");
+  cps.setPref(uri, "test.observer.2", "test value");
+  cps.removePref(uri, "test.observer.1");
+  cps.removePref(uri, "test.observer.2");
+  do_check_eq(specificObserver.numTimesSetCalled, 1);
+  do_check_eq(genericObserver.numTimesSetCalled, 2);
+  do_check_eq(specificObserver.numTimesRemovedCalled, 1);
+  do_check_eq(genericObserver.numTimesRemovedCalled, 2);
+
+  // Make sure information about private context is properly
+  // retrieved by the observer.
+  cps.setPref(uri, "test.observer.private", "test value", privateLoadContext);
+  cps.setPref(uri, "test.observer.normal", "test value", loadContext);
+  cps.removePref(uri, "test.observer.private");
+  cps.removePref(uri, "test.observer.normal");
+
+  // Make sure we can remove observers and they don't get notified
+  // about changes anymore.
+  cps.removeObserver("test.observer.1", specificObserver);
+  cps.removeObserver(null, genericObserver);
+  cps.setPref(uri, "test.observer.1", "test value");
+  cps.removePref(uri, "test.observer.1", "test value");
+  do_check_eq(specificObserver.numTimesSetCalled, 1);
+  do_check_eq(genericObserver.numTimesSetCalled, 4);
+  do_check_eq(specificObserver.numTimesRemovedCalled, 1);
+  do_check_eq(genericObserver.numTimesRemovedCalled, 3);
+
+
+  // Get/Remove Prefs By Name
+
+  {
+    var anObserver = {
+      interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+      QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+        if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+          throw Cr.NS_ERROR_NO_INTERFACE;
+        return this;
+      },
+
+      onContentPrefSet: function anObserver_onContentPrefSet(group, name, value) {
+      },
+
+      expectedDomains: [],
+      numTimesRemovedCalled: 0,
+      onContentPrefRemoved: function anObserver_onContentPrefRemoved(group, name) {
+        ++this.numTimesRemovedCalled;
+
+        // remove the domain from the list of expected domains
+        var index = this.expectedDomains.indexOf(group);
+        do_check_true(index >= 0);
+        this.expectedDomains.splice(index, 1);
+      }
+    };
+
+    let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+    let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+    let uri3 = ContentPrefTest.getURI("http://domain1.com/");
+    let uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+    cps.setPref(uri1, "test.byname.1", 1);
+    cps.setPref(uri1, "test.byname.2", 2);
+    cps.setPref(uri2, "test.byname.1", 4);
+    cps.setPref(uri3, "test.byname.3", 8);
+    cps.setPref(uri4, "test.byname.1", 16);
+    cps.setPref(null, "test.byname.1", 32);
+    cps.setPref(null, "test.byname.2", false);
+
+    function enumerateAndCheck(testName, expectedSum, expectedDomains) {
+      var prefsByName = cps.getPrefsByName(testName);
+      var enumerator = prefsByName.enumerator;
+      var sum = 0;
+      while (enumerator.hasMoreElements()) {
+        var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty);
+        sum += parseInt(property.value);
+
+        // remove the domain from the list of expected domains
+        var index = expectedDomains.indexOf(property.name);
+        do_check_true(index >= 0);
+        expectedDomains.splice(index, 1);
+      }
+      do_check_eq(sum, expectedSum);
+      // check all domains have been removed from the array
+      do_check_eq(expectedDomains.length, 0);
+    }
+
+    enumerateAndCheck("test.byname.1", 53,
+      ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"]);
+    enumerateAndCheck("test.byname.2", 2, ["www.domain1.com", null]);
+    enumerateAndCheck("test.byname.3", 8, ["domain1.com"]);
+
+    cps.addObserver("test.byname.1", anObserver);
+    anObserver.expectedDomains = ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"];
+
+    cps.removePrefsByName("test.byname.1");
+    do_check_false(cps.hasPref(uri1, "test.byname.1"));
+    do_check_false(cps.hasPref(uri2, "test.byname.1"));
+    do_check_false(cps.hasPref(uri3, "test.byname.1"));
+    do_check_false(cps.hasPref(uri4, "test.byname.1"));
+    do_check_false(cps.hasPref(null, "test.byname.1"));
+    do_check_true(cps.hasPref(uri1, "test.byname.2"));
+    do_check_true(cps.hasPref(uri3, "test.byname.3"));
+
+    do_check_eq(anObserver.numTimesRemovedCalled, 4);
+    do_check_eq(anObserver.expectedDomains.length, 0);
+
+    cps.removeObserver("test.byname.1", anObserver);
+
+    // Clean up after ourselves
+    cps.removePref(uri1, "test.byname.2");
+    cps.removePref(uri3, "test.byname.3");
+    cps.removePref(null, "test.byname.2");
+  }
+
+
+  // Clear Private Data Pref Removal
+
+  {
+    let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+    let uri2 = ContentPrefTest.getURI("http://www.domain2.com/");
+    let uri3 = ContentPrefTest.getURI("http://www.domain3.com/");
+
+    let dbConnection = cps.DBConnection;
+
+    let prefCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM prefs");
+
+    let groupCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM groups");
+
+    // Add some prefs for multiple domains.
+    cps.setPref(uri1, "test.removeAllGroups", 1);
+    cps.setPref(uri2, "test.removeAllGroups", 2);
+    cps.setPref(uri3, "test.removeAllGroups", 3);
+
+    // Add a global pref.
+    cps.setPref(null, "test.removeAllGroups", 1);
+
+    // Make sure there are some prefs and groups in the database.
+    prefCount.executeStep();
+    do_check_true(prefCount.row.count > 0);
+    prefCount.reset();
+    groupCount.executeStep();
+    do_check_true(groupCount.row.count > 0);
+    groupCount.reset();
+
+    // Remove all prefs and groups from the database using the same routine
+    // the Clear Private Data dialog uses.
+    cps.removeGroupedPrefs();
+
+    // Make sure there are no longer any groups in the database and the only pref
+    // is the global one.
+    prefCount.executeStep();
+    do_check_true(prefCount.row.count == 1);
+    prefCount.reset();
+    groupCount.executeStep();
+    do_check_true(groupCount.row.count == 0);
+    groupCount.reset();
+    let globalPref = dbConnection.createStatement("SELECT groupID FROM prefs");
+    globalPref.executeStep();
+    do_check_true(globalPref.row.groupID == null);
+    globalPref.reset();
+  }
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
@@ -0,0 +1,244 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var cps = new ContentPrefInstance(null);
+
+function run_test() {
+  testCacheWorks("test1.example.com", "test-pref1");
+  testHasCachedPrefFunction("test2.example.com", "test-pref2");
+  testSetCaches("test3.example.com", "test-pref3");
+  testGetCaches("test4.example.com", "test-pref4");
+  testRemovePrefs("test5.example.com", "test-pref5");
+  testTypeConversions("test6.example.com", "test-pref6");
+  testNonExistingPrefCachesAsUndefined("test7.example.com", "test-pref7");
+  testCacheEviction("test8.example.com", "test-pref8");
+}
+
+function testCacheWorks(uri, prefName) {
+  const CACHED_VALUE = 3;
+  const NEW_VALUE = 5;
+
+  cps.setPref(uri, prefName, CACHED_VALUE);
+  do_check_eq(cps.getPref(uri, prefName), CACHED_VALUE);
+
+  // Now change the value directly through the DB and check
+  // that the cached value is different
+
+  let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+  let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+  let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+                           "id", groupId, settingId);
+
+  let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+  stmt.params.value = NEW_VALUE;
+  stmt.params.id = prefId;
+  stmt.execute();
+
+  let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+  let cacheValue = cps.getPref(uri, prefName);
+
+  do_check_eq(dbValue, NEW_VALUE);
+  do_check_eq(cacheValue, CACHED_VALUE);
+  do_check_neq(cacheValue, dbValue);
+
+  do_test_pending();
+  cps.getPref(uri, prefName, function(value) {
+    do_check_eq(dbValue, NEW_VALUE);
+    do_check_eq(value, CACHED_VALUE);
+    do_check_neq(value, dbValue);
+    do_test_finished();
+  });
+}
+
+function testHasCachedPrefFunction(uri, prefName) {
+  const STARTING_VALUE = 3;
+  const NEW_VALUE = 5;
+
+  do_check_false(isCached(uri, prefName));
+
+  cps.setPref(uri, prefName, STARTING_VALUE);
+
+  let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+  let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+  let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+                       "id", groupId, settingId);
+
+  do_check_neq(prefId, undefined);
+
+  let originalValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+  do_check_eq(originalValue, STARTING_VALUE);
+
+  let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+  stmt.params.value = NEW_VALUE;
+  stmt.params.id = prefId;
+  stmt.execute();
+
+  let newValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+  do_check_eq(newValue, NEW_VALUE);
+
+  let cachedValue = cps.getPref(uri, prefName);
+  do_check_eq(cachedValue, STARTING_VALUE);
+  do_check_true(isCached(uri, prefName));
+}
+
+function testSetCaches(uri, prefName) {
+  cps.setPref(uri, prefName, 0);
+  do_check_true(isCached(uri, prefName));
+}
+
+function testRemovePrefs(uri, prefName) {
+
+  /* removePref */
+  cps.setPref("www1." + uri, prefName, 1);
+
+  do_check_eq(cps.getPref("www1." + uri, prefName), 1);
+
+  cps.removePref("www1." + uri, prefName);
+
+  do_check_false(isCached("www1." + uri, prefName));
+  do_check_false(cps.hasPref("www1." + uri, prefName));
+  do_check_neq(cps.getPref("www1." + uri, prefName), 1);
+
+  /* removeGroupedPrefs */
+  cps.setPref("www2." + uri, prefName, 2);
+  cps.setPref("www3." + uri, prefName, 3);
+
+  do_check_eq(cps.getPref("www2." + uri, prefName), 2);
+  do_check_eq(cps.getPref("www3." + uri, prefName), 3);
+
+  cps.removeGroupedPrefs();
+
+  do_check_false(isCached("www2." + uri, prefName));
+  do_check_false(isCached("www3." + uri, prefName));
+  do_check_false(cps.hasPref("www2." + uri, prefName));
+  do_check_false(cps.hasPref("www3." + uri, prefName));
+  do_check_neq(cps.getPref("www2." + uri, prefName), 2);
+  do_check_neq(cps.getPref("www3." + uri, prefName), 3);
+
+  /* removePrefsByName */
+  cps.setPref("www4." + uri, prefName, 4);
+  cps.setPref("www5." + uri, prefName, 5);
+
+  do_check_eq(cps.getPref("www4." + uri, prefName), 4);
+  do_check_eq(cps.getPref("www5." + uri, prefName), 5);
+
+  cps.removePrefsByName(prefName);
+
+  do_check_false(isCached("www4." + uri, prefName));
+  do_check_false(isCached("www5." + uri, prefName));
+  do_check_false(cps.hasPref("www4." + uri, prefName));
+  do_check_false(cps.hasPref("www5." + uri, prefName));
+  do_check_neq(cps.getPref("www4." + uri, prefName), 4);
+  do_check_neq(cps.getPref("www5." + uri, prefName), 5);
+}
+
+function testGetCaches(uri, prefName) {
+  const VALUE = 4;
+
+  let insertGroup = cps.DBConnection.createStatement("INSERT INTO groups (name) VALUES (:name)");
+  insertGroup.params.name = uri;
+  insertGroup.execute();
+  let groupId = cps.DBConnection.lastInsertRowID;
+
+  let insertSetting = cps.DBConnection.createStatement("INSERT INTO settings (name) VALUES (:name)");
+  insertSetting.params.name = prefName;
+  insertSetting.execute();
+  let settingId = cps.DBConnection.lastInsertRowID;
+
+  let insertPref = cps.DBConnection.createStatement(`
+    INSERT INTO prefs (groupID, settingID, value)
+    VALUES (:groupId, :settingId, :value)
+  `);
+  insertPref.params.groupId = groupId;
+  insertPref.params.settingId = settingId;
+  insertPref.params.value = VALUE;
+  insertPref.execute();
+  let prefId = cps.DBConnection.lastInsertRowID;
+
+  let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+
+  // First access from service should hit the DB
+  let svcValue = cps.getPref(uri, prefName);
+
+  // Second time should get the value from cache
+  let cacheValue = cps.getPref(uri, prefName);
+
+  do_check_eq(VALUE, dbValue);
+  do_check_eq(VALUE, svcValue);
+  do_check_eq(VALUE, cacheValue);
+
+  do_check_true(isCached(uri, prefName));
+}
+
+function testTypeConversions(uri, prefName) {
+  let value;
+
+  cps.setPref(uri, prefName, true);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === 1);
+
+  cps.setPref(uri, prefName, false);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === 0);
+
+  cps.setPref(uri, prefName, null);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === null);
+
+  cps.setPref(uri, prefName, undefined);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === null);
+}
+
+function testNonExistingPrefCachesAsUndefined(uri, prefName) {
+
+  do_check_false(isCached(uri, prefName));
+
+  // Cache the pref
+  let value = cps.getPref(uri, prefName);
+  do_check_true(value === undefined);
+
+  do_check_true(isCached(uri, prefName));
+
+  // Cached pref
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === undefined);
+}
+
+function testCacheEviction(uri, prefName) {
+
+  cps.setPref(uri, prefName, 5);
+  do_check_eq(cps.getPref(uri, prefName), 5);
+  do_check_true(isCached(uri, prefName));
+
+  // try to evict value from cache by adding various other entries
+  const ENTRIES_TO_ADD = 200;
+  for (let i = 0; i < ENTRIES_TO_ADD; i++) {
+    let uriToAdd = "www" + i + uri;
+    cps.setPref(uriToAdd, prefName, 0);
+  }
+
+  do_check_false(isCached(uri, prefName));
+
+}
+
+function selectValue(stmt, columnName, param1, param2) {
+  stmt = cps.DBConnection.createStatement(stmt);
+  if (param1)
+    stmt.params.param1 = param1;
+
+  if (param2)
+    stmt.params.param2 = param2;
+
+  stmt.executeStep();
+  let val = stmt.row[columnName];
+  stmt.reset();
+  stmt.finalize();
+  return val;
+}
+
+function isCached(uri, prefName) {
+  return cps.hasCachedPref(uri, prefName);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+var cps = new ContentPrefInstance(null);
+var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+function run_test() {
+  do_test_pending();
+
+  cps.setPref(uri, "asynctest", "pie");
+  do_check_eq(cps.getPref(uri, "asynctest"), "pie");
+
+  cps.getPref(uri, "asynctest", function(aValue) {
+    do_check_eq(aValue, "pie");
+    testCallbackObj();
+  });
+}
+
+function testCallbackObj() {
+  cps.getPref(uri, "asynctest", {
+    onResult(aValue) {
+      do_check_eq(aValue, "pie");
+      cps.removePref(uri, "asynctest");
+      testNoResult();
+    }
+  });
+}
+
+function testNoResult() {
+  cps.getPref(uri, "asynctest", function(aValue) {
+    do_check_eq(aValue, undefined);
+    do_test_finished();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_stringGroups.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function run_test() {
+
+  var cps = new ContentPrefInstance(null);
+
+  // Make sure disk synchronization checking is turned off by default.
+  var statement = cps.DBConnection.createStatement("PRAGMA synchronous");
+  statement.executeStep();
+  do_check_eq(0, statement.getInt32(0));
+
+  // These are the different types of aGroup arguments we'll test.
+  var anObject = {"foo": "bar"};                               // a simple object
+  var uri = ContentPrefTest.getURI("http://www.example.com/"); // nsIURI
+  var stringURI = "www.example.com";                           // typeof = "string"
+
+  // Test wants to check for a String object
+  // eslint-disable-next-line no-new-wrappers
+  var stringObjectURI = new String("www.example.com");         // typeof = "object"
+
+  // First check that all the methods work or don't work.
+  function simple_test_methods(aGroup, shouldThrow) {
+    var prefName = "test.pref.0";
+    var prefValue = Math.floor(Math.random() * 100);
+
+    if (shouldThrow) {
+      do_check_thrown(function() { cps.getPref(aGroup, prefName); });
+      do_check_thrown(function() { cps.setPref(aGroup, prefName, prefValue); });
+      do_check_thrown(function() { cps.hasPref(aGroup, prefName); });
+      do_check_thrown(function() { cps.removePref(aGroup, prefName); });
+      do_check_thrown(function() { cps.getPrefs(aGroup); });
+    } else {
+      do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined);
+      do_check_true(cps.hasPref(aGroup, prefName));
+      do_check_eq(cps.getPref(aGroup, prefName), prefValue);
+      do_check_eq(cps.removePref(aGroup, prefName), undefined);
+      do_check_false(cps.hasPref(aGroup, prefName));
+    }
+  }
+
+  simple_test_methods(cps, true); // arbitrary nsISupports object, should throw too
+  simple_test_methods(anObject, true);
+  simple_test_methods(uri, false);
+  simple_test_methods(stringURI, false);
+  simple_test_methods(stringObjectURI, false);
+
+  // Now we'll check that each argument produces the same result.
+  function complex_test_methods(aGroup) {
+    var prefName = "test.pref.1";
+    var prefValue = Math.floor(Math.random() * 100);
+
+    do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined);
+
+    do_check_true(cps.hasPref(uri, prefName));
+    do_check_true(cps.hasPref(stringURI, prefName));
+    do_check_true(cps.hasPref(stringObjectURI, prefName));
+
+    do_check_eq(cps.getPref(uri, prefName), prefValue);
+    do_check_eq(cps.getPref(stringURI, prefName), prefValue);
+    do_check_eq(cps.getPref(stringObjectURI, prefName), prefValue);
+
+    do_check_eq(cps.removePref(aGroup, prefName), undefined);
+
+    do_check_false(cps.hasPref(uri, prefName));
+    do_check_false(cps.hasPref(stringURI, prefName));
+    do_check_false(cps.hasPref(stringObjectURI, prefName));
+  }
+
+  complex_test_methods(uri);
+  complex_test_methods(stringURI);
+  complex_test_methods(stringObjectURI);
+
+  // test getPrefs returns the same prefs
+  do_check_eq(cps.setPref(stringObjectURI, "test.5", 5), undefined);
+  do_check_eq(cps.setPref(stringURI, "test.2", 2), undefined);
+  do_check_eq(cps.setPref(uri, "test.1", 1), undefined);
+
+  enumerateAndCheck(cps.getPrefs(uri), 8, ["test.1", "test.2", "test.5"]);
+  enumerateAndCheck(cps.getPrefs(stringURI), 8, ["test.1", "test.2", "test.5"]);
+  enumerateAndCheck(cps.getPrefs(stringObjectURI), 8, ["test.1", "test.2", "test.5"]);
+
+  do_check_eq(cps.setPref(uri, "test.4", 4), undefined);
+  do_check_eq(cps.setPref(stringObjectURI, "test.0", 0), undefined);
+
+  enumerateAndCheck(cps.getPrefs(uri), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+  enumerateAndCheck(cps.getPrefs(stringURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+  enumerateAndCheck(cps.getPrefs(stringObjectURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+
+  do_check_eq(cps.setPref(stringURI, "test.3", 3), undefined);
+
+  enumerateAndCheck(cps.getPrefs(uri), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+  enumerateAndCheck(cps.getPrefs(stringURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+  enumerateAndCheck(cps.getPrefs(stringObjectURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+}
+
+function do_check_thrown(aCallback) {
+  var exThrown = false;
+  try {
+    aCallback();
+    do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here");
+  } catch (e) {
+    do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+    exThrown = true;
+  }
+  do_check_true(exThrown);
+}
+
+function enumerateAndCheck(prefs, expectedSum, expectedNames) {
+  var enumerator = prefs.enumerator;
+  var sum = 0;
+  while (enumerator.hasMoreElements()) {
+    var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty);
+    sum += parseInt(property.value);
+
+    // remove the pref name from the list of expected names
+    var index = expectedNames.indexOf(property.name);
+    do_check_true(index >= 0);
+    expectedNames.splice(index, 1);
+  }
+  do_check_eq(sum, expectedSum);
+  // check all pref names have been removed from the array
+  do_check_eq(expectedNames.length, 0);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var cps = new ContentPrefInstance(null);
+
+function run_test() {
+  var uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+  var uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+  var uri3 = ContentPrefTest.getURI("http://domain1.com/");
+  var uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+  cps.setPref(uri1, "one", 1);
+  cps.setPref(uri1, "two", 2);
+  cps.setPref(uri2, "one", 4);
+  cps.setPref(uri3, "three", 8);
+  cps.setPref(uri4, "two", 16);
+
+  cps.removePref(uri3, "three"); // uri3 should be removed now
+  checkForUnusedGroups();
+  checkForUnusedSettings();
+
+  cps.removePrefsByName("two"); // uri4 should be removed now
+  checkForUnusedGroups();
+  checkForUnusedSettings();
+
+  cps.removeGroupedPrefs();
+  checkForUnusedGroups();
+  checkForUnusedSettings();
+}
+
+function checkForUnusedGroups() {
+  var stmt = cps.DBConnection.createStatement(`
+               SELECT COUNT(*) AS count FROM groups
+               WHERE id NOT IN (SELECT DISTINCT groupID FROM prefs)
+             `);
+  stmt.executeStep();
+  do_check_eq(0, stmt.row.count);
+  stmt.reset();
+  stmt.finalize();
+}
+
+function checkForUnusedSettings() {
+  var stmt = cps.DBConnection.createStatement(`
+               SELECT COUNT(*) AS count FROM settings
+               WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+             `);
+  stmt.executeStep();
+  do_check_eq(0, stmt.row.count);
+  stmt.reset();
+  stmt.finalize();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head = head_contentPrefs.js
+
+[test_bug248970.js]
+[test_bug503971.js]
+[test_bug679784.js]
+[test_contentPrefs.js]
+[test_contentPrefsCache.js]
+[test_getPrefAsync.js]
+[test_stringGroups.js]
+[test_unusedGroupsAndSettings.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function run_test() {
+  let serv = Cc["@mozilla.org/content-pref/service;1"].
+             getService(Ci.nsIContentPrefService2);
+  do_check_eq(serv.QueryInterface(Ci.nsIContentPrefService2), serv);
+  do_check_eq(serv.QueryInterface(Ci.nsISupports), serv);
+  let val = serv.QueryInterface(Ci.nsIContentPrefService);
+  do_check_true(val instanceof Ci.nsIContentPrefService);
+}
--- a/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
+++ b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 head = head.js
 skip-if = toolkit == 'android'
 support-files = AsyncRunner.jsm
 
+[test_service.js]
 [test_setGet.js]
 [test_getSubdomains.js]
 [test_remove.js]
 [test_removeByDomain.js]
 [test_removeAllDomains.js]
 [test_removeByName.js]
 [test_getCached.js]
 [test_getCachedSubdomains.js]
--- a/toolkit/modules/Services.jsm
+++ b/toolkit/modules/Services.jsm
@@ -67,16 +67,17 @@ XPCOMUtils.defineLazyGetter(Services, "i
 });
 
 var initTable = {
   appShell: ["@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
   cache: ["@mozilla.org/network/cache-service;1", "nsICacheService"],
   cache2: ["@mozilla.org/netwerk/cache-storage-service;1", "nsICacheStorageService"],
   cpmm: ["@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"],
   console: ["@mozilla.org/consoleservice;1", "nsIConsoleService"],
+  contentPrefs: ["@mozilla.org/content-pref/service;1", "nsIContentPrefService"],
   cookies: ["@mozilla.org/cookiemanager;1", "nsICookieManager2"],
   downloads: ["@mozilla.org/download-manager;1", "nsIDownloadManager"],
   droppedLinkHandler: ["@mozilla.org/content/dropped-link-handler;1", "nsIDroppedLinkHandler"],
   els: ["@mozilla.org/eventlistenerservice;1", "nsIEventListenerService"],
   eTLD: ["@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"],
   intl: ["@mozilla.org/mozintl;1", "mozIMozIntl"],
   locale: ["@mozilla.org/intl/localeservice;1", "mozILocaleService"],
   logins: ["@mozilla.org/login-manager;1", "nsILoginManager"],
--- a/toolkit/modules/tests/xpcshell/test_Services.js
+++ b/toolkit/modules/tests/xpcshell/test_Services.js
@@ -28,16 +28,17 @@ function run_test() {
 
   checkService("appShell", Ci.nsIAppShellService);
   checkService("appinfo", Ci.nsIXULRuntime);
   checkService("blocklist", Ci.nsIBlocklistService);
   checkService("cache", Ci.nsICacheService);
   checkService("cache2", Ci.nsICacheStorageService);
   checkService("clipboard", Ci.nsIClipboard);
   checkService("console", Ci.nsIConsoleService);
+  checkService("contentPrefs", Ci.nsIContentPrefService);
   checkService("cookies", Ci.nsICookieManager2);
   checkService("dirsvc", Ci.nsIDirectoryService);
   checkService("dirsvc", Ci.nsIProperties);
   checkService("DOMRequest", Ci.nsIDOMRequestService);
   checkService("domStorageManager", Ci.nsIDOMStorageManager);
   checkService("downloads", Ci.nsIDownloadManager);
   checkService("droppedLinkHandler", Ci.nsIDroppedLinkHandler);
   checkService("eTLD", Ci.nsIEffectiveTLDService);
--- a/toolkit/mozapps/downloads/DownloadLastDir.jsm
+++ b/toolkit/mozapps/downloads/DownloadLastDir.jsm
@@ -108,16 +108,35 @@ DownloadLastDir.prototype = {
     return this.fakeContext.usePrivateBrowsing;
   },
   // compat shims
   get file() { return this._getLastFile(); },
   set file(val) { this.setFile(null, val); },
   cleanupPrivateFile() {
     gDownloadLastDirFile = null;
   },
+  // This function is now deprecated as it uses the sync nsIContentPrefService
+  // interface. New consumers should use the getFileAsync function.
+  getFile(aURI) {
+    let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+    Deprecated.warning("DownloadLastDir.getFile is deprecated. Please use getFileAsync instead.",
+                       "https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/DownloadLastDir.jsm",
+                       Components.stack.caller);
+
+    if (aURI && isContentPrefEnabled()) {
+      let lastDir = Services.contentPrefs.getPref(aURI, LAST_DIR_PREF, this.fakeContext);
+      if (lastDir) {
+        var lastDirFile = Components.classes["@mozilla.org/file/local;1"]
+                                    .createInstance(Components.interfaces.nsIFile);
+        lastDirFile.initWithPath(lastDir);
+        return lastDirFile;
+      }
+    }
+    return this._getLastFile();
+  },
 
   _getLastFile() {
     if (gDownloadLastDirFile && !gDownloadLastDirFile.exists())
       gDownloadLastDirFile = null;
 
     if (this.isPrivate()) {
       if (!gDownloadLastDirFile)
         gDownloadLastDirFile = readLastDirPref();