Bug 1194265 - Some default search engines should only be visible in specific countries, r=Mossop.

This commit is contained in:
Florian Quèze 2015-08-15 11:08:58 +02:00
parent 23084b60ca
commit e92b1c03b4
10 changed files with 257 additions and 72 deletions

View File

@ -69,9 +69,10 @@ STUB_HOOK = $(NSINSTALL) -D '$(_ABS_DIST)/$(PKG_INST_PATH)'; \
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

View File

@ -556,11 +556,16 @@ let ensureKnownCountryCode = Task.async(function* () {
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;
}
}
@ -812,6 +817,16 @@ let fetchRegionDefault = () => new Promise(resolve => {
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",
@ -3543,6 +3558,7 @@ SearchService.prototype = {
_engines: { },
__sortedEngines: null,
_visibleDefaultEngines: [],
get _sortedEngines() {
if (!this.__sortedEngines)
return this._buildSortedEngineList();
@ -3600,6 +3616,7 @@ SearchService.prototype = {
cache.locale = locale;
cache.directories = {};
cache.visibleDefaultEngines = this._visibleDefaultEngines;
function getParent(engine) {
if (engine._file)
@ -3721,6 +3738,8 @@ SearchService.prototype = {
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)];
@ -3731,6 +3750,8 @@ SearchService.prototype = {
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) {
@ -3858,6 +3879,8 @@ SearchService.prototype = {
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)];
@ -3868,6 +3891,8 @@ SearchService.prototype = {
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) {
@ -3913,6 +3938,7 @@ SearchService.prototype = {
this.__sortedEngines = null;
this._currentEngine = null;
this._defaultEngine = null;
this._visibleDefaultEngines = [];
// Clear the metadata service.
engineMetadataService._initialized = false;
@ -4224,7 +4250,7 @@ SearchService.prototype = {
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;
@ -4256,18 +4282,8 @@ SearchService.prototype = {
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);
@ -4340,21 +4356,73 @@ SearchService.prototype = {
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;
}
}
}
// 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;
},

View File

@ -209,6 +209,34 @@ function getSearchMetadata()
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();

View File

@ -25,6 +25,10 @@ function run_test() {
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();
});
}

View File

@ -62,34 +62,6 @@ function checkRequest(cohort = "") {
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);

View File

@ -0,0 +1,95 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const kUrlPref = "geoSpecificDefaults.url";
function run_test() {
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);
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);
// 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);
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();
// 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);
});

View File

@ -64,12 +64,32 @@ function run_test() {
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));
@ -82,23 +102,13 @@ function run_test() {
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);
}
}
@ -116,6 +126,8 @@ function run_test() {
cacheTemplate.directories[profPlugins].engines[0].filePath = engineFile.path;
cacheTemplate.directories[profPlugins].lastModifiedTime = engineFile.parent.lastModifiedTime;
cacheTemplate.visibleDefaultEngines = visibleDefaultEngines;
run_next_test();
}

View File

@ -20,4 +20,8 @@ function run_test() {
// 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);
}

View File

@ -77,3 +77,4 @@ support-files =
[test_rel_searchform.js]
[test_selectedEngine.js]
[test_geodefaults.js]
[test_hidden.js]