Bug 1194265 - Some default search engines should only be visible in specific countries, r=Mossop.
authorFlorian Quèze <florian@queze.net>
Sat, 15 Aug 2015 11:08:58 +0200
changeset 257918 462b94db3335
parent 257917 9a5f3dd7d1c0
child 257919 880bb943d653
push id29237
push userryanvm@gmail.com
push dateMon, 17 Aug 2015 12:55:19 +0000
treeherdermozilla-central@0a7e118ca369 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMossop
bugs1194265
milestone43.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 1194265 - Some default search engines should only be visible in specific countries, r=Mossop.
browser/locales/Makefile.in
toolkit/components/search/nsSearchService.js
toolkit/components/search/tests/xpcshell/data/searchTest.jar
toolkit/components/search/tests/xpcshell/head_search.js
toolkit/components/search/tests/xpcshell/test_async.js
toolkit/components/search/tests/xpcshell/test_geodefaults.js
toolkit/components/search/tests/xpcshell/test_hidden.js
toolkit/components/search/tests/xpcshell/test_json_cache.js
toolkit/components/search/tests/xpcshell/test_sync.js
toolkit/components/search/tests/xpcshell/xpcshell.ini
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -64,19 +64,20 @@ UNINSTALLER_PACKAGE_HOOK = $(RM) -r $(ST
 STUB_HOOK = $(NSINSTALL) -D '$(_ABS_DIST)/$(PKG_INST_PATH)'; \
     $(RM) '$(_ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe'; \
     cp ../installer/windows/l10ngen/stub.exe '$(_ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe'; \
     chmod 0755 '$(_ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe'; \
     $(NULL)
 endif
 
 SEARCHPLUGINS_NAMES = $(shell cat $(call MERGE_FILE,/searchplugins/list.txt)) ddg
+SEARCHPLUGINS_FILENAMES = $(subst :hidden,,$(SEARCHPLUGINS_NAMES))
 SEARCHPLUGINS_PATH := .deps/generated_$(AB_CD)
 SEARCHPLUGINS_TARGET := libs searchplugins
-SEARCHPLUGINS := $(foreach plugin,$(addsuffix .xml,$(SEARCHPLUGINS_NAMES)),$(or $(wildcard $(call EN_US_OR_L10N_FILE,searchplugins/$(plugin))),$(info Missing searchplugin: $(plugin))))
+SEARCHPLUGINS := $(foreach plugin,$(addsuffix .xml,$(SEARCHPLUGINS_FILENAMES)),$(or $(wildcard $(call EN_US_OR_L10N_FILE,searchplugins/$(plugin))),$(info Missing searchplugin: $(plugin))))
 # Some locale-specific search plugins may have preprocessor directives, but the
 # default en-US ones do not.
 SEARCHPLUGINS_FLAGS := --silence-missing-directive-warnings
 PP_TARGETS += SEARCHPLUGINS
 
 list-txt = $(SEARCHPLUGINS_PATH)/list.txt
 GARBAGE += $(list-txt)
 
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -551,21 +551,26 @@ let ensureKnownCountryCode = Task.async(
   } else {
     // if nothing to do, return early.
     if (!geoSpecificDefaultsEnabled())
       return;
 
     let expir = engineMetadataService.getGlobalAttr("searchDefaultExpir") || 0;
     if (expir > Date.now()) {
       // The territory default we have already fetched hasn't expired yet.
-      // If we have an engine saved, the hash should be valid, verify it now.
+      // If we have a default engine or a list of visible default engines
+      // saved, the hashes should be valid, verify them now so that we can
+      // refetch if they have been tampered with.
       let defaultEngine = engineMetadataService.getGlobalAttr("searchDefault");
-      if (!defaultEngine ||
-          engineMetadataService.getGlobalAttr("searchDefaultHash") == getVerificationHash(defaultEngine)) {
-        // No geo default, or valid hash; nothing to do.
+      let visibleDefaultEngines =
+        engineMetadataService.getGlobalAttr("visibleDefaultEngines");
+      if ((!defaultEngine || engineMetadataService.getGlobalAttr("searchDefaultHash") == getVerificationHash(defaultEngine)) &&
+          (!visibleDefaultEngines ||
+           engineMetadataService.getGlobalAttr("visibleDefaultEnginesHash") == getVerificationHash(visibleDefaultEngines))) {
+        // No geo defaults, or valid hashes; nothing to do.
         return;
       }
     }
 
     yield new Promise(resolve => {
       let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
       let timerId = setTimeout(() => {
         timerId = null;
@@ -807,16 +812,26 @@ let fetchRegionDefault = () => new Promi
       let defaultEngine = response.settings.searchDefault;
       engineMetadataService.setGlobalAttr("searchDefault", defaultEngine);
       let hash = getVerificationHash(defaultEngine);
       LOG("fetchRegionDefault saved searchDefault: " + defaultEngine +
           " with verification hash: " + hash);
       engineMetadataService.setGlobalAttr("searchDefaultHash", hash);
     }
 
+    if (response.settings && response.settings.visibleDefaultEngines) {
+      let visibleDefaultEngines = response.settings.visibleDefaultEngines;
+      let string = visibleDefaultEngines.join(",");
+      engineMetadataService.setGlobalAttr("visibleDefaultEngines", string);
+      let hash = getVerificationHash(string);
+      LOG("fetchRegionDefault saved visibleDefaultEngines: " + string +
+          " with verification hash: " + hash);
+      engineMetadataService.setGlobalAttr("visibleDefaultEnginesHash", hash);
+    }
+
     let interval = response.interval || SEARCH_GEO_DEFAULT_UPDATE_INTERVAL;
     let milliseconds = interval * 1000; // |interval| is in seconds.
     engineMetadataService.setGlobalAttr("searchDefaultExpir",
                                         Date.now() + milliseconds);
 
     LOG("fetchRegionDefault got success response in " + took + "ms");
     resolve();
   };
@@ -3538,16 +3553,17 @@ SearchService.prototype = {
 
       LOG("_asyncInit: Completed _asyncInit");
     }.bind(this));
   },
 
 
   _engines: { },
   __sortedEngines: null,
+  _visibleDefaultEngines: [],
   get _sortedEngines() {
     if (!this.__sortedEngines)
       return this._buildSortedEngineList();
     return this.__sortedEngines;
   },
 
   // Get the original Engine object that is the default for this region,
   // ignoring changes the user may have subsequently made.
@@ -3595,16 +3611,17 @@ SearchService.prototype = {
     // runtime (where we refresh for changes through the API) and app updates
     // (where the buildID is obviously going to change).
     // Extension-shipped plugins are the only exception to this, but their
     // directories are blown away during updates, so we'll detect their changes.
     cache.buildID = buildID;
     cache.locale = locale;
 
     cache.directories = {};
+    cache.visibleDefaultEngines = this._visibleDefaultEngines;
 
     function getParent(engine) {
       if (engine._file)
         return engine._file.parent;
 
       let uri = engine._uri;
       if (!uri.schemeIs("chrome")) {
         LOG("getParent: engine URI must be a chrome URI if it has no file");
@@ -3716,26 +3733,30 @@ SearchService.prototype = {
 
     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;
+    function notInCacheVisibleEngines(aEngineName)
+      cache.visibleDefaultEngines.indexOf(aEngineName) == -1;
 
     let buildID = Services.appinfo.platformBuildID;
     let cachePaths = [path for (path in cache.directories)];
 
     let rebuildCache = !cache.directories ||
                        cache.version != CACHE_VERSION ||
                        cache.locale != getLocale() ||
                        cache.buildID != buildID ||
                        cachePaths.length != toLoad.length ||
                        toLoad.some(notInCachePath) ||
+                       cache.visibleDefaultEngines.length != this._visibleDefaultEngines.length ||
+                       this._visibleDefaultEngines.some(notInCacheVisibleEngines) ||
                        toLoad.some(modifiedDir);
 
     if (!cacheEnabled || rebuildCache) {
       LOG("_loadEngines: Absent or outdated cache. Loading engines from disk.");
       distDirs.forEach(this._loadEnginesFromDir, this);
 
       this._loadFromChromeURLs(chromeURIs);
 
@@ -3853,26 +3874,30 @@ SearchService.prototype = {
             }
           }
           throw new Task.Result(modifiedDir);
         });
       }
 
       function notInCachePath(aPathToLoad)
         cachePaths.indexOf(aPathToLoad.path) == -1;
+      function notInCacheVisibleEngines(aEngineName)
+        cache.visibleDefaultEngines.indexOf(aEngineName) == -1;
 
       let buildID = Services.appinfo.platformBuildID;
       let cachePaths = [path for (path in cache.directories)];
 
       let rebuildCache = !cache.directories ||
                          cache.version != CACHE_VERSION ||
                          cache.locale != getLocale() ||
                          cache.buildID != buildID ||
                          cachePaths.length != toLoad.length ||
                          toLoad.some(notInCachePath) ||
+                         cache.visibleDefaultEngines.length != this._visibleDefaultEngines.length ||
+                         this._visibleDefaultEngines.some(notInCacheVisibleEngines) ||
                          (yield checkForSyncCompletion(hasModifiedDir(toLoad)));
 
       if (!cacheEnabled || rebuildCache) {
         LOG("_asyncLoadEngines: Absent or outdated cache. Loading engines from disk.");
         let engines = [];
         for (let loadDir of distDirs) {
           let enginesFromDir =
             yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
@@ -3908,16 +3933,17 @@ SearchService.prototype = {
     // Start by clearing the initialized state, so we don't abort early.
     gInitialized = false;
 
     // Clear the engines, too, so we don't stick with the stale ones.
     this._engines = {};
     this.__sortedEngines = null;
     this._currentEngine = null;
     this._defaultEngine = null;
+    this._visibleDefaultEngines = [];
 
     // Clear the metadata service.
     engineMetadataService._initialized = false;
     engineMetadataService._initializer = null;
 
     Task.spawn(function* () {
       try {
         LOG("Restarting engineMetadataService");
@@ -4219,17 +4245,17 @@ SearchService.prototype = {
 
       return [[], []];
     }
 
     let rootURIs = rootURIPref.split(",");
     let uris = [];
     let chromeFiles = [];
 
-    rootURIs.forEach(function (root) {
+    rootURIs.forEach(root => {
       // Find the underlying JAR file for this chrome package (_loadEngines uses
       // it to determine whether it needs to invalidate the cache)
       let jarPackaging = false;
       try {
         let chromeURI = gChromeReg.convertChromeURL(makeURI(root));
         if (chromeURI instanceof Ci.nsIJARURI) {
           let fileURI = chromeURI;
           while (fileURI instanceof Ci.nsIJARURI)
@@ -4251,28 +4277,18 @@ SearchService.prototype = {
                                                         null,      // aLoadingNode
                                                         Services.scriptSecurityManager.getSystemPrincipal(),
                                                         null,      // aTriggeringPrincipal
                                                         Ci.nsILoadInfo.SEC_NORMAL,
                                                         Ci.nsIContentPolicy.TYPE_OTHER);
         let sis = Cc["@mozilla.org/scriptableinputstream;1"].
                   createInstance(Ci.nsIScriptableInputStream);
         sis.init(chan.open());
-        let list = sis.read(sis.available());
-        let names = list.split("\n").filter(function (n) !!n);
-        for (let name of names) {
-          let uri = root + name + ".xml";
-          uris.push(uri);
-          if (!jarPackaging) {
-            // Flat packaging requires that _loadEngines checks the modification
-            // time of each engine file.
-            uri = gChromeReg.convertChromeURL(makeURI(uri));
-            chromeFiles.push(uri.QueryInterface(Ci.nsIFileURL).file);
-          }
-        }
+        this._parseListTxt(sis.read(sis.available()), root, jarPackaging,
+                           chromeFiles, uris);
       } catch (ex) {
         LOG("_findJAREngines: failed to retrieve list.txt from " + listURL + ": " + ex);
 
         return;
       }
     });
 
     return [chromeFiles, uris];
@@ -4335,31 +4351,83 @@ SearchService.prototype = {
         request.onerror = function(aEvent) {
           LOG("_asyncFindJAREngines: failed to retrieve list.txt from " + listURL);
           deferred.resolve("");
         };
         request.open("GET", NetUtil.newURI(listURL).spec, true);
         request.send();
         let list = yield deferred.promise;
 
-        let names = [];
-        names = list.split("\n").filter(function (n) !!n);
-        for (let name of names) {
-          let uri = root + name + ".xml";
-          uris.push(uri);
-          if (!jarPackaging) {
-            // Flat packaging requires that _loadEngines checks the modification
-            // time of each engine file.
-            uri = gChromeReg.convertChromeURL(makeURI(uri));
-            chromeFiles.push(uri.QueryInterface(Ci.nsIFileURL).file);
-          }
+        this._parseListTxt(list, root, jarPackaging, chromeFiles, uris);
+      }
+      throw new Task.Result([chromeFiles, uris]);
+    }.bind(this));
+  },
+
+  _parseListTxt: function SRCH_SVC_parseListTxt(list, root, jarPackaging,
+                                                chromeFiles, uris) {
+    let names = list.split("\n").filter(function (n) !!n);
+    // This maps the names of our built-in engines to a boolean
+    // indicating whether it should be hidden by default.
+    let jarNames = new Map();
+    for (let name of names) {
+      if (name.endsWith(":hidden")) {
+        name = name.split(":")[0];
+        jarNames.set(name, true);
+      } else {
+        jarNames.set(name, false);
+      }
+    }
+
+    // Check if we have a useable country specific list of visible default engines.
+    let engineNames;
+    let visibleDefaultEngines =
+      engineMetadataService.getGlobalAttr("visibleDefaultEngines");
+    if (visibleDefaultEngines &&
+        engineMetadataService.getGlobalAttr("visibleDefaultEnginesHash") == getVerificationHash(visibleDefaultEngines)) {
+      engineNames = visibleDefaultEngines.split(",");
+
+      for (let engineName of engineNames) {
+        // If all engineName values are part of jarNames,
+        // then we can use the country specific list, otherwise ignore it.
+        // The visibleDefaultEngines string containing the name of an engine we
+        // don't ship indicates the server is misconfigured to answer requests
+        // from the specific Firefox version we are running, so ignoring the
+        // value altogether is safer.
+        if (!jarNames.has(engineName)) {
+          LOG("_parseListTxt: ignoring visibleDefaultEngines value because " +
+              engineName + " is not in the jar engines we have found");
+          engineNames = null;
+          break;
         }
       }
-      throw new Task.Result([chromeFiles, uris]);
-    });
+    }
+
+    // Fallback to building a list based on the :hidden suffixes found in list.txt.
+    if (!engineNames) {
+      engineNames = [];
+      for (let [name, hidden] of jarNames) {
+        if (!hidden)
+          engineNames.push(name);
+      }
+    }
+
+    for (let name of engineNames) {
+      let uri = root + name + ".xml";
+      uris.push(uri);
+      if (!jarPackaging) {
+        // Flat packaging requires that _loadEngines checks the modification
+        // time of each engine file.
+        uri = gChromeReg.convertChromeURL(makeURI(uri));
+        chromeFiles.push(uri.QueryInterface(Ci.nsIFileURL).file);
+      }
+    }
+
+    // Store this so that it can be used while writing the cache file.
+    this._visibleDefaultEngines = engineNames;
   },
 
 
   _saveSortedEngineList: function SRCH_SVC_saveSortedEngineList() {
     LOG("SRCH_SVC_saveSortedEngineList: starting");
 
     // Set the useDB pref to indicate that from now on we should use the order
     // information stored in the database.
index 3882ae938f1df50ea037fbc17b1be2331a5b2da2..6cce53d1265d0da885a2f21344a22a7405e752a0
GIT binary patch
literal 2189
zc%1E4O=}b}7@qBR{TRWMN})x<K)pyeZRt0&3zmK;rDdVJCr{~Y*h!t4giOZWDjxJC
z6#s$V^ipqv2f?EV9x8|j@#w*mXF+c!lkV)aVGE@mJ44>tH=8`qJefSasjU?UZA)Em
zo?KpC8~M7sf8MfGyK0SCp3iBBVT=*?{5p<Bc^mOY{i0Q5fA<@zC7nxFVeogsvIeh6
zkXi<}E8*OaufGq;->O}ca2kgNNsB}=42Z-N9g*VQkXRcg-d56@nVO!T9k+ebY~pA^
zKkOdQuNTj%pFh3*`F_B%R2w#UGK_L33{tYaAFtNG{V~Yg&2(NZod@LLD6TlZ5NrX$
zcDr2;=>rl3s7!eaPL7Yyz%5SOG3E#B1D&evZu*pq4HV*PM(e^}r|mA`*yDr=LZkeU
ze7Ei1iC7|*Bab#o)T%b>D`RsmJltu!4ITio!*InBxFeu1!oUF`7CvoOoYm#J0|X9P
zfCSD|AYN{hdxYU8(K1v9N0;VCZ!M>BR>j5Xg*2H<<5C(Ir8ofG8saDfk(5$#v}y<3
zM?oUfhuvMm!}yT{&EYiR9wyCf!n$70u>%}A!-Z1QA!UfMfOtqP3+#-=NvkC{p-keN
z+cAw!E*Bz3%FJh`8ssV5?ByuKQSZf0sJyE~@23^Y!|LfjfZRqsIJ=wa&05VF;;A&=
zyT;y!FVYOE4H<^e9?IMRr0sxdz5nY5&Dq}yx6}UtjwBfdg36B>3m^>wCMM>mCTFJ4
zEOKU%(^{l3aJ|^CIt^Lf>*Zq=`_-zFu2n|e>tUM}DUa<N*hlJ9N!O=i2uz3P33iG{
w3G#j~X}=rjrE_X_QQpI{^Ld)(InC3-(FN;sRw(JSLLb6{yzk2A$OV=C1M%XK+5i9m
--- a/toolkit/components/search/tests/xpcshell/head_search.js
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -204,16 +204,44 @@ function getSearchMetadata()
   let metadata = gProfD.clone();
   metadata.append("search-metadata.json");
   do_check_true(metadata.exists());
 
   do_print("Parsing metadata");
   return readJSONFile(metadata);
 }
 
+function promiseGlobalMetadata() {
+  return new Promise(resolve => Task.spawn(function* () {
+    let path = OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json");
+    let bytes = yield OS.File.read(path);
+    resolve(JSON.parse(new TextDecoder().decode(bytes))["[global]"]);
+  }));
+}
+
+function promiseSaveGlobalMetadata(globalData) {
+  return new Promise(resolve => Task.spawn(function* () {
+    let path = OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json");
+    let bytes = yield OS.File.read(path);
+    let data = JSON.parse(new TextDecoder().decode(bytes));
+    data["[global]"] = globalData;
+    yield OS.File.writeAtomic(path,
+                              new TextEncoder().encode(JSON.stringify(data)));
+    resolve();
+  }));
+}
+
+let forceExpiration = Task.async(function* () {
+  let metadata = yield promiseGlobalMetadata();
+
+  // Make the current geodefaults expire 1s ago.
+  metadata.searchdefaultexpir = Date.now() - 1000;
+  yield promiseSaveGlobalMetadata(metadata);
+});
+
 function removeCacheFile()
 {
   let file = gProfD.clone();
   file.append("search.json");
   if (file.exists()) {
     file.remove(false);
   }
 }
--- a/toolkit/components/search/tests/xpcshell/test_async.js
+++ b/toolkit/components/search/tests/xpcshell/test_async.js
@@ -20,11 +20,15 @@ function run_test() {
     // 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);
 
+    // Check the hidden engine is not loaded.
+    engine = Services.search.getEngineByName("hidden");
+    do_check_eq(engine, null);
+
     do_test_finished();
   });
 }
--- a/toolkit/components/search/tests/xpcshell/test_geodefaults.js
+++ b/toolkit/components/search/tests/xpcshell/test_geodefaults.js
@@ -57,44 +57,16 @@ function checkNoRequest() {
 
 function checkRequest(cohort = "") {
   do_check_eq(requests.length, 1);
   let req = requests.pop();
   do_check_eq(req._method, "GET");
   do_check_eq(req._queryString, cohort ? "/" + cohort : "");
 }
 
-function promiseGlobalMetadata() {
-  return new Promise(resolve => Task.spawn(function* () {
-    let path = OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json");
-    let bytes = yield OS.File.read(path);
-    resolve(JSON.parse(new TextDecoder().decode(bytes))["[global]"]);
-  }));
-}
-
-function promiseSaveGlobalMetadata(globalData) {
-  return new Promise(resolve => Task.spawn(function* () {
-    let path = OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json");
-    let bytes = yield OS.File.read(path);
-    let data = JSON.parse(new TextDecoder().decode(bytes));
-    data["[global]"] = globalData;
-    yield OS.File.writeAtomic(path,
-                              new TextEncoder().encode(JSON.stringify(data)));
-    resolve();
-  }));
-}
-
-let forceExpiration = Task.async(function* () {
-  let metadata = yield promiseGlobalMetadata();
-
-  // Make the current geodefaults expire 1s ago.
-  metadata.searchdefaultexpir = Date.now() - 1000;
-  yield promiseSaveGlobalMetadata(metadata);
-});
-
 add_task(function* no_request_if_prefed_off() {
   // Disable geoSpecificDefaults and check no HTTP request is made.
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
   yield asyncInit();
   checkNoRequest();
 
   // The default engine should be set based on the prefs.
   do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
copy from toolkit/components/search/tests/xpcshell/test_async.js
copy to toolkit/components/search/tests/xpcshell/test_hidden.js
--- a/toolkit/components/search/tests/xpcshell/test_async.js
+++ b/toolkit/components/search/tests/xpcshell/test_hidden.js
@@ -1,30 +1,95 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+const kUrlPref = "geoSpecificDefaults.url";
+
 function run_test() {
-  do_test_pending();
-
   removeMetadata();
   removeCacheFile();
 
   do_load_manifest("data/chrome.manifest");
 
   configureToLoadJarEngines();
 
+  // Geo specific defaults won't be fetched if there's no country code.
+  Services.prefs.setCharPref("browser.search.geoip.url",
+                             'data:application/json,{"country_code": "US"}');
+
+  // Make 'hidden' the only visible engine.
+ let url = "data:application/json,{\"interval\": 31536000, \"settings\": {\"searchDefault\": \"hidden\", \"visibleDefaultEngines\": [\"hidden\"]}}";
+  Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+
   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);
+  run_next_test();
+}
+
+add_task(function* async_init() {
+  let commitPromise = promiseAfterCommit()
+  yield asyncInit();
+
+  let engines = Services.search.getEngines();
+  do_check_eq(engines.length, 1);
+
+  // The default test jar engine has been hidden.
+  let engine = Services.search.getEngineByName("bug645970");
+  do_check_eq(engine, null);
 
-    // test engines from dir are not loaded.
-    let engines = Services.search.getEngines();
-    do_check_eq(engines.length, 1);
+  // The hidden engine is visible.
+  engine = Services.search.getEngineByName("hidden");
+  do_check_neq(engine, null);
+
+  // The next test does a sync init, which won't do the geoSpecificDefaults XHR,
+  // so it depends on the metadata having been written to disk.
+  yield commitPromise;
+});
+
+add_task(function* sync_init() {
+  let reInitPromise = asyncReInit();
+  // Synchronously check the current default engine, to force a sync init.
+  // XXX For some reason forcing a sync init while already asynchronously
+  // reinitializing causes a shutdown warning related to engineMetadataService's
+  // finalize method having already been called. Seems harmless for the purpose
+  // of this test.
+  do_check_false(Services.search.isInitialized);
+  do_check_eq(Services.search.currentEngine.name, "hidden");
+  do_check_true(Services.search.isInitialized);
 
-    // test jar engine is loaded ok.
-    let engine = Services.search.getEngineByName("bug645970");
-    do_check_neq(engine, null);
+  let engines = Services.search.getEngines();
+  do_check_eq(engines.length, 1);
+
+  // The default test jar engine has been hidden.
+  let engine = Services.search.getEngineByName("bug645970");
+  do_check_eq(engine, null);
+
+  // The hidden engine is visible.
+  engine = Services.search.getEngineByName("hidden");
+  do_check_neq(engine, null);
+
+  yield reInitPromise;
+});
+
+add_task(function* invalid_engine() {
+  // Trigger a new request.
+  yield forceExpiration();
 
-    do_test_finished();
-  });
-}
+  // Set the visibleDefaultEngines list to something that contains a non-existent engine.
+  // This should cause the search service to ignore the list altogether and fallback to
+  // local defaults.
+  let url = "data:application/json,{\"interval\": 31536000, \"settings\": {\"searchDefault\": \"hidden\", \"visibleDefaultEngines\": [\"hidden\", \"bogus\"]}}";
+  Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+
+  let commitPromise = promiseAfterCommit();
+  yield asyncReInit();
+
+  let engines = Services.search.getEngines();
+  do_check_eq(engines.length, 1);
+
+  // The default test jar engine is visible.
+  let engine = Services.search.getEngineByName("bug645970");
+  do_check_neq(engine, null);
+
+  // The hidden engine is... hidden.
+  engine = Services.search.getEngineByName("hidden");
+  do_check_eq(engine, null);
+});
--- a/toolkit/components/search/tests/xpcshell/test_json_cache.js
+++ b/toolkit/components/search/tests/xpcshell/test_json_cache.js
@@ -59,51 +59,61 @@ function run_test() {
   let filesToIgnore = []
   let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
 
   let loadFromJARs = false;
   try {
     loadFromJARs = defaultBranch.getBoolPref("loadFromJars");
   } catch (ex) {}
 
+  let visibleDefaultEngines = [];
   if (!loadFromJARs) {
     filesToIgnore.push(getDir(NS_APP_SEARCH_DIR));
   } else {
     let rootURIPref = defaultBranch.getCharPref("jarURIs");
     let rootURIs = rootURIPref.split(",");
     for (let root of rootURIs) {
+      let visibleEnginesForRoot = [];
+      let listURL = root + "list.txt";
+      let chan = NetUtil.ioService.newChannelFromURI2(makeURI(listURL),
+                                                      null, // aLoadingNode
+                                                      Services.scriptSecurityManager.getSystemPrincipal(),
+                                                      null, // aTriggeringPrincipal
+                                                      Ci.nsILoadInfo.SEC_NORMAL,
+                                                      Ci.nsIContentPolicy.TYPE_OTHER);
+      let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+                createInstance(Ci.nsIScriptableInputStream);
+      sis.init(chan.open());
+      let list = sis.read(sis.available());
+      let names = list.split("\n").filter(n => !!n);
+      for (let name of names) {
+        if (name.endsWith(":hidden"))
+          continue;
+        visibleEnginesForRoot.push(name);
+      }
+
       let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
                         getService(Ci.nsIChromeRegistry);
       let chromeURI = chromeReg.convertChromeURL(makeURI(root));
       if (chromeURI instanceof Ci.nsIJARURI) {
         // JAR packaging, we only need the parent jar file.
         let fileURI = chromeURI; // flat packaging
         while (fileURI instanceof Ci.nsIJARURI)
           fileURI = fileURI.JARFile;
         fileURI.QueryInterface(Ci.nsIFileURL);
         filesToIgnore.push(fileURI.file);
       } else {
         // flat packaging, we need to find each .xml file.
-        let listURL = root + "list.txt";
-        let chan = NetUtil.ioService.newChannelFromURI2(makeURI(listURL),
-                                                        null, // aLoadingNode
-                                                        Services.scriptSecurityManager.getSystemPrincipal(),
-                                                        null, // aTriggeringPrincipal
-                                                        Ci.nsILoadInfo.SEC_NORMAL,
-                                                        Ci.nsIContentPolicy.TYPE_OTHER);
-        let sis = Cc["@mozilla.org/scriptableinputstream;1"].
-                  createInstance(Ci.nsIScriptableInputStream);
-        sis.init(chan.open());
-        let list = sis.read(sis.available());
-        let names = list.split("\n").filter(n => !!n);
-        for (let name of names) {
+        for (let name of visibleEnginesForRoot) {
           let uri = chromeReg.convertChromeURL(makeURI(root + name + ".xml"));
           filesToIgnore.push(uri.QueryInterface(Ci.nsIFileURL).file);
         }
       }
+
+      visibleDefaultEngines = visibleDefaultEngines.concat(visibleEnginesForRoot);
     }
   }
 
   for (let file of filesToIgnore) {
     cacheTemplate.directories[file.path] = {
       lastModifiedTime: file.lastModifiedTime,
       engines: []
     };
@@ -111,16 +121,18 @@ function run_test() {
 
   // Replace the profile placeholder with the correct path.
   profPlugins = engineFile.parent.path;
   cacheTemplate.directories[profPlugins] = cacheTemplate.directories["[profile]/searchplugins"];
   delete cacheTemplate.directories["[profile]/searchplugins"];
   cacheTemplate.directories[profPlugins].engines[0].filePath = engineFile.path;
   cacheTemplate.directories[profPlugins].lastModifiedTime = engineFile.parent.lastModifiedTime;
 
+  cacheTemplate.visibleDefaultEngines = visibleDefaultEngines;
+
   run_next_test();
 }
 
 add_test(function prepare_test_data() {
 
   let ostream = Cc["@mozilla.org/network/file-output-stream;1"].
                 createInstance(Ci.nsIFileOutputStream);
   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
--- a/toolkit/components/search/tests/xpcshell/test_sync.js
+++ b/toolkit/components/search/tests/xpcshell/test_sync.js
@@ -15,9 +15,13 @@ function run_test() {
   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);
+
+  // Check the hidden engine is not loaded.
+  engine = Services.search.getEngineByName("hidden");
+  do_check_eq(engine, null);
 }
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -72,8 +72,9 @@ support-files =
 [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]
 [test_geodefaults.js]
+[test_hidden.js]