Bug 1109354 - prefer Firefox default engines over profile-installed plugins with the same name, r=markh.
authorFlorian Quèze <florian@queze.net>
Mon, 15 Jun 2015 18:32:09 +0200
changeset 248905 3dff06c8adbcfd8cb86681879f080dd0f9407f8c
parent 248904 99f99d774fa3cea30621c7337b55624448498b94
child 248906 d6e9eea07a71e0707bb68146ec05dea25a5db479
push id28910
push userkwierso@gmail.com
push dateTue, 16 Jun 2015 00:49:24 +0000
treeherdermozilla-central@5a1e229e8bff [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1109354
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1109354 - prefer Firefox default engines over profile-installed plugins with the same name, r=markh.
browser/components/dirprovider/DirectoryProvider.cpp
mobile/android/components/DirectoryProvider.js
toolkit/components/search/nsSearchService.js
toolkit/components/search/tests/xpcshell/data/engine-addon.xml
toolkit/components/search/tests/xpcshell/data/engine-override.xml
toolkit/components/search/tests/xpcshell/data/install.rdf
toolkit/components/search/tests/xpcshell/head_search.js
toolkit/components/search/tests/xpcshell/test_async_addon.js
toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js
toolkit/components/search/tests/xpcshell/test_async_distribution.js
toolkit/components/search/tests/xpcshell/test_async_profile_engine.js
toolkit/components/search/tests/xpcshell/test_sync_addon.js
toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js
toolkit/components/search/tests/xpcshell/test_sync_distribution.js
toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js
toolkit/components/search/tests/xpcshell/xpcshell.ini
xpcom/io/nsAppDirectoryServiceDefs.h
--- a/browser/components/dirprovider/DirectoryProvider.cpp
+++ b/browser/components/dirprovider/DirectoryProvider.cpp
@@ -201,36 +201,54 @@ AppendDistroSearchDirs(nsIProperties* aD
       }
     }
   }
 }
 
 NS_IMETHODIMP
 DirectoryProvider::GetFiles(const char *aKey, nsISimpleEnumerator* *aResult)
 {
+  /**
+   * We want to preserve the following order, since the search service loads
+   * engines in first-loaded-wins order.
+   *   - distro search plugin locations (Loaded by the search service using
+   *     NS_APP_DISTRIBUTION_SEARCH_DIR_LIST)
+   *
+   *   - engines shipped in chrome (Loaded from jar files by the search
+   *     service)
+   *
+   *   Then other locations, from NS_APP_SEARCH_DIR_LIST:
+   *   - extension search plugin locations (prepended below using
+   *     NS_NewUnionEnumerator)
+   *   - user search plugin locations (profile)
+   *   - app search plugin location (shipped engines)
+   */
+
   nsresult rv;
 
+  if (!strcmp(aKey, NS_APP_DISTRIBUTION_SEARCH_DIR_LIST)) {
+    nsCOMPtr<nsIProperties> dirSvc
+      (do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID));
+    if (!dirSvc)
+      return NS_ERROR_FAILURE;
+
+    nsCOMArray<nsIFile> distroFiles;
+    AppendDistroSearchDirs(dirSvc, distroFiles);
+
+    return NS_NewArrayEnumerator(aResult, distroFiles);
+  }
+
   if (!strcmp(aKey, NS_APP_SEARCH_DIR_LIST)) {
     nsCOMPtr<nsIProperties> dirSvc
       (do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID));
     if (!dirSvc)
       return NS_ERROR_FAILURE;
 
     nsCOMArray<nsIFile> baseFiles;
 
-    /**
-     * We want to preserve the following order, since the search service loads
-     * engines in first-loaded-wins order.
-     *   - extension search plugin locations (prepended below using
-     *     NS_NewUnionEnumerator)
-     *   - distro search plugin locations
-     *   - user search plugin locations (profile)
-     *   - app search plugin location (shipped engines)
-     */
-    AppendDistroSearchDirs(dirSvc, baseFiles);
     AppendFileKey(NS_APP_USER_SEARCH_DIR, dirSvc, baseFiles);
     AppendFileKey(NS_APP_SEARCH_DIR, dirSvc, baseFiles);
 
     nsCOMPtr<nsISimpleEnumerator> baseEnum;
     rv = NS_NewArrayEnumerator(getter_AddRefs(baseEnum), baseFiles);
     if (NS_FAILED(rv))
       return rv;
 
--- a/mobile/android/components/DirectoryProvider.js
+++ b/mobile/android/components/DirectoryProvider.js
@@ -13,16 +13,17 @@ Cu.import("resource://gre/modules/XPCOMU
 
 // -----------------------------------------------------------------------
 // Directory Provider for special browser folders and files
 // -----------------------------------------------------------------------
 
 const NS_APP_CACHE_PARENT_DIR = "cachePDir";
 const NS_APP_SEARCH_DIR       = "SrchPlugns";
 const NS_APP_SEARCH_DIR_LIST  = "SrchPluginsDL";
+const NS_APP_DISTRIBUTION_SEARCH_DIR_LIST = "SrchPluginsDistDL";
 const NS_APP_USER_SEARCH_DIR  = "UsrSrchPlugns";
 const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
 const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
 const XRE_UPDATE_ROOT_DIR     = "UpdRootD";
 const ENVVAR_UPDATE_DIR       = "UPDATES_DIRECTORY";
 const WEBAPPS_DIR             = "webappsDir";
 const DOWNLOAD_DIR            = "DfltDwnld";
 
@@ -142,37 +143,42 @@ DirectoryProvider.prototype = {
     // We didn't append the locale dir - try the default one.
     let defLocale = Services.prefs.getCharPref("distribution.searchplugins.defaultLocale");
     let defLocalePlugins = localePlugins.clone();
     if (defLocalePlugins.exists())
       array.push(defLocalePlugins);
   },
 
   getFiles: function(prop) {
-    if (prop != NS_APP_SEARCH_DIR_LIST)
-      return;
+    if (prop != NS_APP_SEARCH_DIR_LIST &&
+        prop != NS_APP_DISTRIBUTION_SEARCH_DIR_LIST)
+      return null;
 
     let result = [];
 
-    /**
-     * We want to preserve the following order, since the search service loads
-     * engines in first-loaded-wins order.
-     *   - distro search plugin locations
-     *   - user search plugin locations (profile)
-     *   - app search plugin location (shipped engines)
-     */
-    this._appendDistroSearchDirs(result);
+    if (prop == NS_APP_DISTRIBUTION_SEARCH_DIR_LIST) {
+      this._appendDistroSearchDirs(result);
+    }
+    else {
+      /**
+       * We want to preserve the following order, since the search service
+       * loads engines in first-loaded-wins order.
+       *   - distro search plugin locations (loaded separately by the search
+       *     service)
+       *   - user search plugin locations (profile)
+       *   - app search plugin location (shipped engines)
+       */
+      let appUserSearchDir = FileUtils.getDir(NS_APP_USER_SEARCH_DIR, [], false);
+      if (appUserSearchDir.exists())
+        result.push(appUserSearchDir);
 
-    let appUserSearchDir = FileUtils.getDir(NS_APP_USER_SEARCH_DIR, [], false);
-    if (appUserSearchDir.exists())
-      result.push(appUserSearchDir);
-
-    let appSearchDir = FileUtils.getDir(NS_APP_SEARCH_DIR, [], false);
-    if (appSearchDir.exists())
-      result.push(appSearchDir);
+      let appSearchDir = FileUtils.getDir(NS_APP_SEARCH_DIR, [], false);
+      if (appSearchDir.exists())
+        result.push(appSearchDir);
+    }
 
     return {
       QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
       hasMoreElements: function() {
         return result.length > 0;
       },
       getNext: function() {
         return result.shift();
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -46,16 +46,17 @@ XPCOMUtils.defineLazyGetter(this, "gEnco
 const MODE_RDONLY   = 0x01;
 const MODE_WRONLY   = 0x02;
 const MODE_CREATE   = 0x08;
 const MODE_APPEND   = 0x10;
 const MODE_TRUNCATE = 0x20;
 
 // Directory service keys
 const NS_APP_SEARCH_DIR_LIST  = "SrchPluginsDL";
+const NS_APP_DISTRIBUTION_SEARCH_DIR_LIST = "SrchPluginsDistDL";
 const NS_APP_USER_SEARCH_DIR  = "UsrSrchPlugns";
 const NS_APP_SEARCH_DIR       = "SrchPlugns";
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 
 // Search engine "locations". If this list is changed, be sure to update
 // the engine's _isDefault function accordingly.
 const SEARCH_APP_DIR = 1;
 const SEARCH_PROFILE_DIR = 2;
@@ -3480,37 +3481,53 @@ SearchService.prototype = {
     let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
     if (cacheEnabled) {
       let cacheFile = getDir(NS_APP_USER_PROFILE_50_DIR);
       cacheFile.append("search.json");
       if (cacheFile.exists())
         cache = this._readCacheFile(cacheFile);
     }
 
-    let loadDirs = [], chromeURIs = [], chromeFiles = [];
-
+    let chromeURIs = [], chromeFiles = [];
     let loadFromJARs = false;
     try {
       loadFromJARs = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
                              .getBoolPref("loadFromJars");
     } catch (ex) {}
 
     if (loadFromJARs)
       [chromeFiles, chromeURIs] = this._findJAREngines();
 
-    let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
+    let distDirs = [];
+    let locations;
+    try {
+      locations = getDir(NS_APP_DISTRIBUTION_SEARCH_DIR_LIST,
+                         Ci.nsISimpleEnumerator);
+    } catch (e) {
+      // NS_APP_DISTRIBUTION_SEARCH_DIR_LIST is defined by each app
+      // so this throws during unit tests (but not xpcshell tests).
+      locations = {hasMoreElements: () => false};
+    }
+    while (locations.hasMoreElements()) {
+      let dir = locations.getNext().QueryInterface(Ci.nsIFile);
+      if (dir.directoryEntries.hasMoreElements())
+        distDirs.push(dir);
+    }
+
+    let otherDirs = [];
+    locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
     while (locations.hasMoreElements()) {
       let dir = locations.getNext().QueryInterface(Ci.nsIFile);
       if (loadFromJARs && dir.equals(getDir(NS_APP_SEARCH_DIR)))
         continue;
       if (dir.directoryEntries.hasMoreElements())
-        loadDirs.push(dir);
+        otherDirs.push(dir);
     }
 
-    let toLoad = chromeFiles.concat(loadDirs);
+    let toLoad = chromeFiles.concat(distDirs, otherDirs);
 
     function modifiedDir(aDir) {
       return (!cache.directories || !cache.directories[aDir.path] ||
               cache.directories[aDir.path].lastModifiedTime != aDir.lastModifiedTime);
     }
 
     function notInCachePath(aPathToLoad)
       cachePaths.indexOf(aPathToLoad.path) == -1;
@@ -3523,20 +3540,22 @@ SearchService.prototype = {
                        cache.locale != getLocale() ||
                        cache.buildID != buildID ||
                        cachePaths.length != toLoad.length ||
                        toLoad.some(notInCachePath) ||
                        toLoad.some(modifiedDir);
 
     if (!cacheEnabled || rebuildCache) {
       LOG("_loadEngines: Absent or outdated cache. Loading engines from disk.");
-      loadDirs.forEach(this._loadEnginesFromDir, this);
+      distDirs.forEach(this._loadEnginesFromDir, this);
 
       this._loadFromChromeURLs(chromeURIs);
 
+      otherDirs.forEach(this._loadEnginesFromDir, this);
+
       if (cacheEnabled)
         this._buildCache();
       return;
     }
 
     LOG("_loadEngines: loading from cache directories");
     for each (let dir in cache.directories)
       this._loadEnginesFromCache(dir);
@@ -3556,53 +3575,81 @@ SearchService.prototype = {
       // See if we have a cache file so we don't have to parse a bunch of XML.
       let cache = {};
       let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
       if (cacheEnabled) {
         let cacheFilePath = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
         cache = yield checkForSyncCompletion(this._asyncReadCacheFile(cacheFilePath));
       }
 
-      let loadDirs = [], chromeURIs = [], chromeFiles = [];
-
+      let chromeURIs = [], chromeFiles = [];
       let loadFromJARs = false;
       try {
         loadFromJARs = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
                                .getBoolPref("loadFromJars");
       } catch (ex) {}
 
       if (loadFromJARs) {
         Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "find-jar-engines");
         [chromeFiles, chromeURIs] =
           yield checkForSyncCompletion(this._asyncFindJAREngines());
       }
 
+      // Get the non-empty distribution directories into distDirs...
+      let distDirs = [];
+      let locations;
+      try {
+        locations = getDir(NS_APP_DISTRIBUTION_SEARCH_DIR_LIST,
+                           Ci.nsISimpleEnumerator);
+      } catch (e) {
+        // NS_APP_DISTRIBUTION_SEARCH_DIR_LIST is defined by each app
+        // so this throws during unit tests (but not xpcshell tests).
+        locations = {hasMoreElements: () => false};
+      }
+      while (locations.hasMoreElements()) {
+        let dir = locations.getNext().QueryInterface(Ci.nsIFile);
+        let iterator = new OS.File.DirectoryIterator(dir.path,
+                                                     { winPattern: "*.xml" });
+        try {
+          // Add dir to distDirs if it contains any files.
+          yield checkForSyncCompletion(iterator.next());
+          distDirs.push(dir);
+        } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
+          // Catch for StopIteration exception.
+        } finally {
+          iterator.close();
+        }
+      }
+
       // Add the non-empty directories of NS_APP_SEARCH_DIR_LIST to
-      // loadDirs...
-      let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
+      // otherDirs...
+      let otherDirs = [];
+      locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
       while (locations.hasMoreElements()) {
         let dir = locations.getNext().QueryInterface(Ci.nsIFile);
         // ... but skip the application directory if we are loading from JAR.
+        // Applications shipping JAR engines don't ship plain text
+        // engine files anymore.
         if (loadFromJARs && dir.equals(getDir(NS_APP_SEARCH_DIR)))
           continue;
 
         let iterator = new OS.File.DirectoryIterator(dir.path,
                                                      { winPattern: "*.xml" });
         try {
-          // Add dir to loadDirs if it contains any files.
+          // Add dir to otherDirs if it contains any files.
           yield checkForSyncCompletion(iterator.next());
-          loadDirs.push(dir);
+          otherDirs.push(dir);
         } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
           // Catch for StopIteration exception.
         } finally {
           iterator.close();
         }
       }
 
-      let toLoad = chromeFiles.concat(loadDirs);
+      let toLoad = chromeFiles.concat(distDirs, otherDirs);
       function hasModifiedDir(aList) {
         return Task.spawn(function() {
           let modifiedDir = false;
 
           for (let dir of aList) {
             if (!cache.directories || !cache.directories[dir.path]) {
               modifiedDir = true;
               break;
@@ -3631,24 +3678,29 @@ SearchService.prototype = {
                          cache.buildID != buildID ||
                          cachePaths.length != toLoad.length ||
                          toLoad.some(notInCachePath) ||
                          (yield checkForSyncCompletion(hasModifiedDir(toLoad)));
 
       if (!cacheEnabled || rebuildCache) {
         LOG("_asyncLoadEngines: Absent or outdated cache. Loading engines from disk.");
         let engines = [];
-        for (let loadDir of loadDirs) {
+        for (let loadDir of distDirs) {
           let enginesFromDir =
             yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
           engines = engines.concat(enginesFromDir);
         }
         let enginesFromURLs =
            yield checkForSyncCompletion(this._asyncLoadFromChromeURLs(chromeURIs));
         engines = engines.concat(enginesFromURLs);
+        for (let loadDir of otherDirs) {
+          let enginesFromDir =
+            yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
+          engines = engines.concat(enginesFromDir);
+        }
 
         for (let engine of engines) {
           this._addEngineToStore(engine);
         }
         if (cacheEnabled)
           this._buildCache();
         return;
       }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-addon.xml
@@ -0,0 +1,8 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>addon</ShortName>
+<Description>addon</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+  <Param name="search" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-override.xml
@@ -0,0 +1,8 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>bug645970</ShortName>
+<Description>override</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+  <Param name="search" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>search-engine@tests.mozilla.org</em:id>
+    <em:unpack>true</em:unpack>
+    <em:version>1.0</em:version>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>toolkit@mozilla.org</em:id>
+        <em:minVersion>0</em:minVersion>
+        <em:maxVersion>*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Search Engine</em:name>
+
+  </Description>
+</RDF>
--- a/toolkit/components/search/tests/xpcshell/head_search.js
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -92,16 +92,100 @@ function configureToLoadJarEngines(loadF
   // Ensure a test engine exists in the app dir anyway.
   let dir = Services.dirsvc.get(NS_APP_SEARCH_DIR, Ci.nsIFile);
   if (!dir.exists())
     dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
   do_get_file("data/engine-app.xml").copyTo(dir, "app.xml");
 }
 
 /**
+ * Fake the installation of an add-on in the profile, by creating the
+ * directory and registering it with the directory service.
+ */
+function installAddonEngine(name = "engine-addon")
+{
+  const XRE_EXTENSIONS_DIR_LIST = "XREExtDL";
+  const gProfD = do_get_profile().QueryInterface(Ci.nsILocalFile);
+
+  let dir = gProfD.clone();
+  dir.append("extensions");
+  if (!dir.exists())
+    dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+  dir.append("search-engine@tests.mozilla.org");
+  dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+  do_get_file("data/install.rdf").copyTo(dir, "install.rdf");
+  let addonDir = dir.clone();
+  dir.append("searchplugins");
+  dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+  do_get_file("data/" + name + ".xml").copyTo(dir, "bug645970.xml");
+
+  Services.dirsvc.registerProvider({
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
+                                           Ci.nsIDirectoryServiceProvider2]),
+
+    getFile: function (prop, persistant) {
+      throw Cr.NS_ERROR_FAILURE;
+    },
+
+    getFiles: function (prop) {
+      let result = [];
+
+      switch (prop) {
+      case XRE_EXTENSIONS_DIR_LIST:
+        result.push(addonDir);
+        break;
+      default:
+        throw Cr.NS_ERROR_FAILURE;
+      }
+
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+        hasMoreElements: () => result.length > 0,
+        getNext: () => result.shift()
+      };
+    }
+  });
+}
+
+/**
+ * Copy the engine-distribution.xml engine to a fake distribution
+ * created in the profile, and registered with the directory service.
+ */
+function installDistributionEngine()
+{
+  const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
+
+  const gProfD = do_get_profile().QueryInterface(Ci.nsILocalFile);
+
+  let dir = gProfD.clone();
+  dir.append("distribution");
+  dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+  let distDir = dir.clone();
+
+  dir.append("searchplugins");
+  dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+  dir.append("common");
+  dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+  do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+  Services.dirsvc.registerProvider({
+    getFile: function(aProp, aPersistent) {
+      aPersistent.value = true;
+      if (aProp == XRE_APP_DISTRIBUTION_DIR)
+        return distDir.clone();
+      return null;
+    }
+  });
+}
+
+/**
  * Clean the profile of any metadata files left from a previous run.
  */
 function removeMetadata()
 {
   let file = gProfD.clone();
   file.append("search-metadata.json");
   if (file.exists()) {
     file.remove(false);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_addon.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+  do_test_pending();
+
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+  installAddonEngine();
+
+  do_check_false(Services.search.isInitialized);
+
+  Services.search.init(function search_initialized(aStatus) {
+    do_check_true(Components.isSuccessCode(aStatus));
+    do_check_true(Services.search.isInitialized);
+
+    // test the add-on engine is loaded in addition to our jar engine
+    let engines = Services.search.getEngines();
+    do_check_eq(engines.length, 2);
+
+    // test jar engine is loaded ok.
+    let engine = Services.search.getEngineByName("addon");
+    do_check_neq(engine, null);
+
+    do_check_eq(engine.description, "addon");
+
+    do_test_finished();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+  do_test_pending();
+
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+  installAddonEngine("engine-override");
+
+  do_check_false(Services.search.isInitialized);
+
+  Services.search.init(function search_initialized(aStatus) {
+    do_check_true(Components.isSuccessCode(aStatus));
+    do_check_true(Services.search.isInitialized);
+
+    // test the add-on engine isn't overriding our jar engine
+    let engines = Services.search.getEngines();
+    do_check_eq(engines.length, 1);
+
+    // test jar engine is loaded ok.
+    let engine = Services.search.getEngineByName("bug645970");
+    do_check_neq(engine, null);
+
+    do_check_eq(engine.description, "bug645970");
+
+    do_test_finished();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_distribution.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+  do_test_pending();
+
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+  installDistributionEngine();
+
+  do_check_false(Services.search.isInitialized);
+
+  Services.search.init(function search_initialized(aStatus) {
+    do_check_true(Components.isSuccessCode(aStatus));
+    do_check_true(Services.search.isInitialized);
+
+    // test that the engine from the distribution overrides our jar engine
+    let engines = Services.search.getEngines();
+    do_check_eq(engines.length, 1);
+
+    let engine = Services.search.getEngineByName("bug645970");
+    do_check_neq(engine, null);
+
+    // check the engine we have is actually the one from the distribution
+    do_check_eq(engine.description, "override");
+
+    do_test_finished();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_profile_engine.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR  = "UsrSrchPlugns";
+
+function run_test() {
+  do_test_pending();
+
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+
+  // Copy an engine in [profile]/searchplugin/ and ensure it's not
+  // overriding the same file from a jar.
+  // The description in the file we are copying is 'profile'.
+  let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+  if (!dir.exists())
+    dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+  do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+  do_check_false(Services.search.isInitialized);
+
+  Services.search.init(function search_initialized(aStatus) {
+    do_check_true(Components.isSuccessCode(aStatus));
+    do_check_true(Services.search.isInitialized);
+
+    // test engines from dir are not loaded.
+    let engines = Services.search.getEngines();
+    do_check_eq(engines.length, 1);
+
+    // test jar engine is loaded ok.
+    let engine = Services.search.getEngineByName("bug645970");
+    do_check_neq(engine, null);
+
+    do_check_eq(engine.description, "bug645970");
+
+    do_test_finished();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_addon.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+  installAddonEngine();
+
+  do_check_false(Services.search.isInitialized);
+
+  // test the add-on engine is loaded in addition to our jar engine
+  let engines = Services.search.getEngines();
+  do_check_eq(engines.length, 2);
+
+  do_check_true(Services.search.isInitialized);
+
+  // test jar engine is loaded ok.
+  let engine = Services.search.getEngineByName("addon");
+  do_check_neq(engine, null);
+
+  do_check_eq(engine.description, "addon");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+  installAddonEngine("engine-override");
+
+  do_check_false(Services.search.isInitialized);
+
+  // test the add-on engine isn't overriding our jar engine
+  let engines = Services.search.getEngines();
+  do_check_eq(engines.length, 1);
+
+  do_check_true(Services.search.isInitialized);
+
+  // test jar engine is loaded ok.
+  let engine = Services.search.getEngineByName("bug645970");
+  do_check_neq(engine, null);
+
+  do_check_eq(engine.description, "bug645970");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_distribution.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+  installDistributionEngine();
+
+  do_check_false(Services.search.isInitialized);
+
+  // test that the engine from the distribution overrides our jar engine
+  let engines = Services.search.getEngines();
+  do_check_eq(engines.length, 1);
+
+  do_check_true(Services.search.isInitialized);
+
+  let engine = Services.search.getEngineByName("bug645970");
+  do_check_neq(engine, null);
+
+  // check the engine we have is actually the one from the distribution
+  do_check_eq(engine.description, "override");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR  = "UsrSrchPlugns";
+
+function run_test() {
+  removeMetadata();
+  removeCacheFile();
+
+  do_load_manifest("data/chrome.manifest");
+
+  configureToLoadJarEngines();
+
+  // Copy an engine in [profile]/searchplugin/ and ensure it's not
+  // overriding the same file from a jar.
+  // The description in the file we are copying is 'profile'.
+  let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+  if (!dir.exists())
+    dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+  do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+  do_check_false(Services.search.isInitialized);
+
+  // test engines from dir are not loaded.
+  let engines = Services.search.getEngines();
+  do_check_eq(engines.length, 1);
+
+  do_check_true(Services.search.isInitialized);
+
+  // test jar engine is loaded ok.
+  let engine = Services.search.getEngineByName("bug645970");
+  do_check_neq(engine, null);
+
+  do_check_eq(engine.description, "bug645970");
+}
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -3,25 +3,28 @@ head = head_search.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   data/chrome.manifest
   data/engine.src
   data/engine.xml
   data/engine2.xml
+  data/engine-addon.xml
+  data/engine-override.xml
   data/engine-app.xml
   data/engine-fr.xml
   data/engineMaker.sjs
   data/engine-rel-searchform.xml
   data/engine-rel-searchform-post.xml
   data/engine-rel-searchform-purpose.xml
   data/engineImages.xml
   data/ico-size-16x16-png.ico
   data/invalid-engine.xml
+  data/install.rdf
   data/search-metadata.json
   data/search.json
   data/search.sqlite
   data/searchSuggestions.sjs
   data/searchTest.jar
 
 [test_nocache.js]
 [test_645970.js]
@@ -53,14 +56,22 @@ support-files =
 [test_SearchStaticData.js]
 [test_addEngine_callback.js]
 [test_multipleIcons.js]
 [test_resultDomain.js]
 [test_serialize_file.js]
 [test_searchSuggest.js]
 [test_async.js]
 [test_async_app.js]
+[test_async_addon.js]
+[test_async_addon_no_override.js]
+[test_async_distribution.js]
+[test_async_profile_engine.js]
 [test_sync.js]
 [test_sync_app.js]
+[test_sync_addon.js]
+[test_sync_addon_no_override.js]
+[test_sync_distribution.js]
 [test_sync_fallback.js]
 [test_sync_delay_fallback.js]
+[test_sync_profile_engine.js]
 [test_rel_searchform.js]
 [test_selectedEngine.js]
--- a/xpcom/io/nsAppDirectoryServiceDefs.h
+++ b/xpcom/io/nsAppDirectoryServiceDefs.h
@@ -43,16 +43,17 @@
 #define NS_APP_RES_DIR                          "ARes"
 #define NS_APP_CHROME_DIR                       "AChrom"
 #define NS_APP_PLUGINS_DIR                      "APlugns"       // Deprecated - use NS_APP_PLUGINS_DIR_LIST
 #define NS_APP_SEARCH_DIR                       "SrchPlugns"
 
 #define NS_APP_CHROME_DIR_LIST                  "AChromDL"
 #define NS_APP_PLUGINS_DIR_LIST                 "APluginsDL"
 #define NS_APP_SEARCH_DIR_LIST                  "SrchPluginsDL"
+#define NS_APP_DISTRIBUTION_SEARCH_DIR_LIST     "SrchPluginsDistDL"
 
 // --------------------------------------------------------------------------------------
 // Files and directories which exist on a per-profile basis
 // These locations are typically provided by the profile mgr
 // --------------------------------------------------------------------------------------
 
 // In a shared profile environment, prefixing a profile-relative
 // key with NS_SHARED returns a location that is shared by