Bug 1109354 - Prefer Firefox default engines over profile-installed plugins with the same name. r=markh, a=sledru
authorFlorian Quèze <florian@queze.net>
Mon, 15 Jun 2015 18:32:09 +0200
changeset 275201 52f8ab7477a5eb597c5f797985bcaee3ea93bca2
parent 275200 0d5c1eb75f3e2deb1c16b4970155d49b1d2aa24f
child 275202 2ba1d89f9bb89e33e9fe0be6f906af5ede43bd66
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh, sledru
bugs1109354
milestone40.0a2
Bug 1109354 - Prefer Firefox default engines over profile-installed plugins with the same name. r=markh, a=sledru
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