Bug 1175218 - The original default engine should be set per region rather than per locale, r=markh.

--HG--
extra : rebase_source : 98eed0fe583dfaef7b83eccba4534b6e46c5accf
This commit is contained in:
Florian Quèze 2015-07-10 21:06:24 +02:00
parent 6938d4eca7
commit af833ef045
14 changed files with 596 additions and 136 deletions

View File

@ -404,8 +404,13 @@ pref("browser.search.order.1", "chrome://browser-region/locale/re
pref("browser.search.order.2", "chrome://browser-region/locale/region.properties");
pref("browser.search.order.3", "chrome://browser-region/locale/region.properties");
// Market-specific search defaults (US market only)
pref("browser.search.geoSpecificDefaults", true);
// Market-specific search defaults
// This is disabled globally, and then enabled for individual locales
// in firefox-l10n.js (eg. it's enabled for en-US).
pref("browser.search.geoSpecificDefaults", false);
pref("browser.search.geoSpecificDefaults.url", "https://search.services.mozilla.com/1/%APP%/%VERSION%/%CHANNEL%/%LOCALE%/%REGION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%");
// US specific default (used as a fallback if the geoSpecificDefaults request fails).
pref("browser.search.defaultenginename.US", "data:text/plain,browser.search.defaultenginename.US=Yahoo");
pref("browser.search.order.US.1", "data:text/plain,browser.search.order.US.1=Yahoo");
pref("browser.search.order.US.2", "data:text/plain,browser.search.order.US.2=Google");

View File

@ -53,8 +53,10 @@ function test() {
ok(data.days.hasDay(now), "Have data for today.");
let day = data.days.getDay(now);
// Will need to be changed if Yahoo isn't the default search engine.
let defaultProviderID = "yahoo";
// Will need to be changed if Google isn't the default search engine.
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
// non-US en-US default.
let defaultProviderID = "google";
let field = defaultProviderID + ".contextmenu";
ok(day.has(field), "Have search recorded for context menu.");
@ -66,4 +68,3 @@ function test() {
});
});
}

View File

@ -27,8 +27,10 @@ add_task(function* test_healthreport_search_recording() {
let now = new Date();
let oldCount = 0;
// This will to be need changed if default search engine is not Yahoo.
let defaultEngineID = "yahoo";
// This will to be need changed if default search engine is not Google.
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
// non-US en-US default.
let defaultEngineID = "google";
let field = defaultEngineID + ".urlbar";

View File

@ -4,4 +4,8 @@
#filter substitution
# LOCALIZATION NOTE: this preference is set to true for en-US specifically,
# locales without this line have the setting set to false by default.
pref("browser.search.geoSpecificDefaults", true);
pref("general.useragent.locale", "@AB_CD@");

View File

@ -265,8 +265,13 @@ pref("browser.search.order.1", "chrome://browser/locale/region.properties");
pref("browser.search.order.2", "chrome://browser/locale/region.properties");
pref("browser.search.order.3", "chrome://browser/locale/region.properties");
// Market-specific search defaults (US market only)
pref("browser.search.geoSpecificDefaults", true);
// Market-specific search defaults
// This is disabled globally, and then enabled for individual locales
// in firefox-l10n.js (eg. it's enabled for en-US).
pref("browser.search.geoSpecificDefaults", false);
pref("browser.search.geoSpecificDefaults.url", "https://search.services.mozilla.com/1/%APP%/%VERSION%/%CHANNEL%/%LOCALE%/%REGION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%");
// US specific default (used as a fallback if the geoSpecificDefaults request fails).
pref("browser.search.defaultenginename.US", "chrome://browser/locale/region.properties");
pref("browser.search.order.US.1", "chrome://browser/locale/region.properties");
pref("browser.search.order.US.2", "chrome://browser/locale/region.properties");

View File

@ -4,4 +4,8 @@
#filter substitution
# LOCALIZATION NOTE: this preference is set to true for en-US specifically,
# locales without this line have the setting set to false by default.
pref("browser.search.geoSpecificDefaults", true);
pref("general.useragent.locale", "@AB_CD@");

View File

@ -290,6 +290,8 @@ user_pref("browser.uitour.url", "http://%(server)s/uitour-dummy/tour");
// side-effect of preventing our geoip lookup.
user_pref("browser.search.isUS", true);
user_pref("browser.search.countryCode", "US");
// This will prevent HTTP requests for region defaults.
user_pref("browser.search.geoSpecificDefaults", false);
// Make sure the self support tab doesn't hit the network.
user_pref("browser.selfsupport.url", "https://%(server)s/selfsupport-dummy/");

View File

@ -9,6 +9,7 @@ function run_test() {
// desired side-effect of preventing our geoip lookup.
Services.prefs.setBoolPref("browser.search.isUS", true);
Services.prefs.setCharPref("browser.search.countryCode", "US");
Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
run_next_test();
}

View File

@ -210,6 +210,11 @@ var OS_UNSUPPORTED_PARAMS = [
// specifies an updateURL, but not an updateInterval.
const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
// The default interval before checking again for the name of the
// default engine for the region, in seconds. Only used if the response
// from the server doesn't specify an interval.
const SEARCH_GEO_DEFAULT_UPDATE_INTERVAL = 2592000; // 30 days.
// Returns false for whitespace-only or commented out lines in a
// Sherlock file, true otherwise.
function isUsefulLine(aLine) {
@ -408,20 +413,21 @@ loadListener.prototype = {
onStatus: function (aRequest, aContext, aStatus, aStatusArg) {}
}
// Method to determine if we should be using geo-specific defaults
function geoSpecificDefaultsEnabled() {
// check to see if this is a partner build. Partner builds should not use geo-specific defaults.
let distroID;
function isPartnerBuild() {
try {
distroID = Services.prefs.getCharPref("distribution.id");
let distroID = Services.prefs.getCharPref("distribution.id");
// Mozilla-provided builds (i.e. funnelcake) are not partner builds
if (distroID && !distroID.startsWith("mozilla")) {
return false;
return true;
}
} catch (e) {}
// if we make it here, the pref should dictate behaviour
return false;
}
// Method to determine if we should be using geo-specific defaults
function geoSpecificDefaultsEnabled() {
let geoSpecificDefaults = false;
try {
geoSpecificDefaults = Services.prefs.getBoolPref("browser.search.geoSpecificDefaults");
@ -500,7 +506,7 @@ function getIsUS() {
// Helper method to modify preference keys with geo-specific modifiers, if needed.
function getGeoSpecificPrefName(basepref) {
if (!geoSpecificDefaultsEnabled())
if (!geoSpecificDefaultsEnabled() || isPartnerBuild())
return basepref;
if (getIsUS())
return basepref + ".US";
@ -532,16 +538,53 @@ function isUSTimezone() {
// If it fails we don't touch that pref so isUS() does its normal thing.
let ensureKnownCountryCode = Task.async(function* () {
// If we have a country-code already stored in our prefs we trust it.
let countryCode;
try {
Services.prefs.getCharPref("browser.search.countryCode");
return; // pref exists, so we've done this before.
countryCode = Services.prefs.getCharPref("browser.search.countryCode");
} catch(e) {}
// We don't have it cached, so fetch it. fetchCountryCode() will call
// storeCountryCode if it gets a result (even if that happens after the
// promise resolves)
yield fetchCountryCode();
if (!countryCode) {
// We don't have it cached, so fetch it. fetchCountryCode() will call
// storeCountryCode if it gets a result (even if that happens after the
// promise resolves) and fetchRegionDefault.
yield fetchCountryCode();
} 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.
let defaultEngine = engineMetadataService.getGlobalAttr("searchDefault");
if (!defaultEngine ||
engineMetadataService.getGlobalAttr("searchDefaultHash") == getVerificationHash(defaultEngine)) {
// No geo default, or valid hash; nothing to do.
return;
}
}
yield new Promise(resolve => {
let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
let timerId = setTimeout(() => {
timerId = null;
resolve();
}, timeoutMS);
let callback = () => {
clearTimeout(timerId);
resolve();
};
fetchRegionDefault().then(callback).catch(err => {
Components.utils.reportError(err);
callback();
});
});
}
// If gInitialized is true then the search service was forced to perform
// a sync initialization during our XHR - capture this via telemetry.
// a sync initialization during our XHRs - capture this via telemetry.
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT").add(gInitialized);
});
@ -628,9 +671,11 @@ function fetchCountryCode() {
// large value just incase the request never completes - we don't want the
// XHR object to live forever)
let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
let geoipTimeoutPossible = true;
let timerId = setTimeout(() => {
LOG("_fetchCountryCode: timeout fetching country information");
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(1);
if (geoipTimeoutPossible)
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(1);
timerId = null;
resolve();
}, timeoutMS);
@ -646,14 +691,29 @@ function fetchCountryCode() {
// This notification is just for tests...
Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-complete");
// If we've already timed out then we've already resolved the promise,
// so there's nothing else to do.
if (timerId == null) {
return;
if (timerId) {
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(0);
geoipTimeoutPossible = false;
}
let callback = () => {
// If we've already timed out then we've already resolved the promise,
// so there's nothing else to do.
if (timerId == null) {
return;
}
clearTimeout(timerId);
resolve();
};
if (result && geoSpecificDefaultsEnabled()) {
fetchRegionDefault().then(callback).catch(err => {
Components.utils.reportError(err);
callback();
});
} else {
callback();
}
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(0);
clearTimeout(timerId);
resolve();
};
let request = new XMLHttpRequest();
@ -683,6 +743,123 @@ function fetchCountryCode() {
});
}
// This will make an HTTP request to a Mozilla server that will return
// JSON data telling us what engine should be set as the default for
// the current region, and how soon we should check again.
//
// The optional cohort value returned by the server is to be kept locally
// and sent to the server the next time we ping it. It lets the server
// identify profiles that have been part of a specific experiment.
//
// This promise may take up to 100s to resolve, it's the caller's
// responsibility to ensure with a timer that we are not going to
// block the async init for too long.
let fetchRegionDefault = () => new Promise(resolve => {
let urlTemplate = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
.getCharPref("geoSpecificDefaults.url");
let endpoint = Services.urlFormatter.formatURL(urlTemplate);
// As an escape hatch, no endpoint means no region specific defaults.
if (!endpoint) {
resolve();
return;
}
// Append the optional cohort value.
const cohortPref = "browser.search.cohort";
let cohort;
try {
cohort = Services.prefs.getCharPref(cohortPref);
} catch(e) {}
if (cohort)
endpoint += "/" + cohort;
LOG("fetchRegionDefault starting with endpoint " + endpoint);
let startTime = Date.now();
let request = new XMLHttpRequest();
request.timeout = 100000; // 100 seconds as the last-chance fallback
request.onload = function(event) {
let took = Date.now() - startTime;
let status = event.target.status;
if (status != 200) {
LOG("fetchRegionDefault failed with HTTP code " + status);
let retryAfter = request.getResponseHeader("retry-after");
if (retryAfter) {
engineMetadataService.setGlobalAttr("searchDefaultExpir",
Date.now() + retryAfter * 1000);
}
resolve();
return;
}
let response = event.target.response || {};
LOG("received " + response.toSource());
if (response.cohort) {
Services.prefs.setCharPref(cohortPref, response.cohort);
} else {
Services.prefs.clearUserPref(cohortPref);
}
if (response.settings && response.settings.searchDefault) {
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);
}
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();
};
request.ontimeout = function(event) {
LOG("fetchRegionDefault: XHR finally timed-out");
resolve();
};
request.onerror = function(event) {
LOG("fetchRegionDefault: failed to retrieve territory default information");
resolve();
};
request.open("GET", endpoint, true);
request.setRequestHeader("Content-Type", "application/json");
request.responseType = "json";
request.send();
});
function getVerificationHash(aName) {
let disclaimer = "By modifying this file, I agree that I am doing so " +
"only within $appName itself, using official, user-driven search " +
"engine selection processes, and in a way which does not circumvent " +
"user consent. I acknowledge that any attempt to change this file " +
"from outside of $appName is a malicious act, and will be responded " +
"to accordingly."
let salt = OS.Path.basename(OS.Constants.Path.profileDir) + aName +
disclaimer.replace(/\$appName/g, Services.appinfo.name);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Data is an array of bytes.
let data = converter.convertToByteArray(salt, {});
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
}
/**
* Used to verify a given DOM node's localName and namespaceURI.
* @param aElement
@ -3368,25 +3545,34 @@ SearchService.prototype = {
return this.__sortedEngines;
},
// Get the original Engine object that belongs to the defaultenginename pref
// of the default branch.
// Get the original Engine object that is the default for this region,
// ignoring changes the user may have subsequently made.
get _originalDefaultEngine() {
let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let nsIPLS = Ci.nsIPrefLocalizedString;
let defaultEngine;
let defPref = getGeoSpecificPrefName("defaultenginename");
try {
defaultEngine = defaultPrefB.getComplexValue(defPref, nsIPLS).data;
} catch (ex) {
// If the default pref is invalid (e.g. an add-on set it to a bogus value)
// getEngineByName will just return null, which is the best we can do.
let defaultEngine = engineMetadataService.getGlobalAttr("searchDefault");
if (defaultEngine &&
engineMetadataService.getGlobalAttr("searchDefaultHash") != getVerificationHash(defaultEngine)) {
LOG("get _originalDefaultEngine, invalid searchDefaultHash for: " + defaultEngine);
defaultEngine = "";
}
if (!defaultEngine) {
let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let nsIPLS = Ci.nsIPrefLocalizedString;
let defPref = getGeoSpecificPrefName("defaultenginename");
try {
defaultEngine = defaultPrefB.getComplexValue(defPref, nsIPLS).data;
} catch (ex) {
// If the default pref is invalid (e.g. an add-on set it to a bogus value)
// getEngineByName will just return null, which is the best we can do.
}
}
return this.getEngineByName(defaultEngine);
},
resetToOriginalDefaultEngine: function SRCH_SVC__resetToOriginalDefaultEngine() {
this.defaultEngine = this._originalDefaultEngine;
this.currentEngine = this._originalDefaultEngine;
},
_buildCache: function SRCH_SVC__buildCache() {
@ -3714,6 +3900,7 @@ SearchService.prototype = {
},
_asyncReInit: function () {
LOG("_asyncReInit");
// Start by clearing the initialized state, so we don't abort early.
gInitialized = false;
@ -3729,8 +3916,14 @@ SearchService.prototype = {
Task.spawn(function* () {
try {
LOG("Restarting engineMetadataService");
yield engineMetadataService.init();
yield this._asyncLoadEngines();
yield ensureKnownCountryCode();
// Due to the HTTP requests done by ensureKnownCountryCode, it's possible that
// at this point a synchronous init has been forced by other code.
if (!gInitialized)
yield this._asyncLoadEngines();
// Typically we'll re-init as a result of a pref observer,
// so signal to 'callers' that we're done.
@ -4295,31 +4488,6 @@ SearchService.prototype = {
});
},
_getVerificationHash: function SRCH_SVC__getVerificationHash(aName) {
let disclaimer = "By modifying this file, I agree that I am doing so " +
"only within $appName itself, using official, user-driven search " +
"engine selection processes, and in a way which does not circumvent " +
"user consent. I acknowledge that any attempt to change this file " +
"from outside of $appName is a malicious act, and will be responded " +
"to accordingly."
let salt = OS.Path.basename(OS.Constants.Path.profileDir) + aName +
disclaimer.replace(/\$appName/g, Services.appinfo.name);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Data is an array of bytes.
let data = converter.convertToByteArray(salt, {});
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
},
// nsIBrowserSearchService
init: function SRCH_SVC_init(observer) {
LOG("SearchService.init");
@ -4673,7 +4841,7 @@ SearchService.prototype = {
this._ensureInitialized();
if (!this._currentEngine) {
let name = engineMetadataService.getGlobalAttr("current");
if (engineMetadataService.getGlobalAttr("hash") == this._getVerificationHash(name)) {
if (engineMetadataService.getGlobalAttr("hash") == getVerificationHash(name)) {
this._currentEngine = this.getEngineByName(name);
}
}
@ -4713,7 +4881,7 @@ SearchService.prototype = {
}
engineMetadataService.setGlobalAttr("current", newName);
engineMetadataService.setGlobalAttr("hash", this._getVerificationHash(newName));
engineMetadataService.setGlobalAttr("hash", getVerificationHash(newName));
notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT);
},

View File

@ -251,6 +251,22 @@ function isUSTimezone() {
return UTCOffset >= 150 && UTCOffset <= 600;
}
const kDefaultenginenamePref = "browser.search.defaultenginename";
const kTestEngineName = "Test search engine";
const kLocalePref = "general.useragent.locale";
function getDefaultEngineName(isUS) {
const nsIPLS = Ci.nsIPrefLocalizedString;
// Copy the logic from nsSearchService
let pref = kDefaultenginenamePref;
if (isUS === undefined)
isUS = Services.prefs.getCharPref(kLocalePref) == "en-US" && isUSTimezone();
if (isUS) {
pref += ".US";
}
return Services.prefs.getComplexValue(pref, nsIPLS).data;
}
/**
* Waits for metadata being committed.
* @return {Promise} Resolved when the metadata is committed to disk.
@ -317,6 +333,8 @@ if (!isChild) {
Services.prefs.setIntPref("browser.search.geoip.timeout", 2000);
// But still disable geoip lookups - tests that need it will re-configure this.
Services.prefs.setCharPref("browser.search.geoip.url", "");
// Also disable region defaults - tests using it will also re-configure it.
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref("geoSpecificDefaults.url", "");
}
/**
@ -489,6 +507,24 @@ function waitForSearchNotification(aExpectedData) {
});
}
function asyncInit() {
return new Promise(resolve => {
Services.search.init(function() {
do_check_true(Services.search.isInitialized);
resolve();
});
});
}
function asyncReInit() {
let promise = waitForSearchNotification("reinit-complete");
Services.search.QueryInterface(Ci.nsIObserver)
.observe(null, "nsPref:changed", kLocalePref);
return promise;
}
// This "enum" from nsSearchService.js
const TELEMETRY_RESULT_ENUM = {
SUCCESS: 0,

View File

@ -0,0 +1,284 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var requests = [];
var gServerCohort = "";
const kUrlPref = "geoSpecificDefaults.url";
const kDayInSeconds = 86400;
const kYearInSeconds = kDayInSeconds * 365;
function run_test() {
updateAppInfo();
installTestEngine();
let srv = new HttpServer();
srv.registerPathHandler("/lookup_defaults", (metadata, response) => {
response.setStatusLine("1.1", 200, "OK");
let data = {interval: kYearInSeconds,
settings: {searchDefault: "Test search engine"}};
if (gServerCohort)
data.cohort = gServerCohort;
response.write(JSON.stringify(data));
requests.push(metadata);
});
srv.registerPathHandler("/lookup_fail", (metadata, response) => {
response.setStatusLine("1.1", 404, "Not Found");
requests.push(metadata);
});
srv.registerPathHandler("/lookup_unavailable", (metadata, response) => {
response.setStatusLine("1.1", 503, "Service Unavailable");
response.setHeader("Retry-After", kDayInSeconds.toString());
requests.push(metadata);
});
srv.start(-1);
do_register_cleanup(() => srv.stop(() => {}));
let url = "http://localhost:" + srv.identity.primaryPort + "/lookup_defaults?";
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
// Set a bogus user value so that running the test ensures we ignore it.
Services.prefs.setCharPref(BROWSER_SEARCH_PREF + kUrlPref, "about:blank");
Services.prefs.setCharPref("browser.search.geoip.url",
'data:application/json,{"country_code": "FR"}');
run_next_test();
}
function checkNoRequest() {
do_check_eq(requests.length, 0);
}
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));
// Change the default engine and then revert to default to ensure
// the metadata file exists.
let commitPromise = promiseAfterCommit();
Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
Services.search.resetToOriginalDefaultEngine();
yield commitPromise;
// Ensure nothing related to geoSpecificDefaults has been written in the metadata.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "undefined");
do_check_eq(typeof metadata.searchdefault, "undefined");
do_check_eq(typeof metadata.searchdefaulthash, "undefined");
Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
});
add_task(function* should_get_geo_defaults_only_once() {
// (Re)initializing the search service should trigger a request,
// and set the default engine based on it.
let commitPromise = promiseAfterCommit();
// Due to the previous initialization, we expect the countryCode to already be set.
do_check_true(Services.prefs.prefHasUserValue("browser.search.countryCode"));
do_check_eq(Services.prefs.getCharPref("browser.search.countryCode"), "FR");
yield asyncReInit();
checkRequest();
do_check_eq(Services.search.currentEngine.name, kTestEngineName);
yield commitPromise;
// Verify the metadata was written correctly.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "number");
do_check_true(metadata.searchdefaultexpir > Date.now());
do_check_eq(typeof metadata.searchdefault, "string");
do_check_eq(metadata.searchdefault, "Test search engine");
do_check_eq(typeof metadata.searchdefaulthash, "string");
do_check_eq(metadata.searchdefaulthash.length, 44);
// The next restart shouldn't trigger a request.
yield asyncReInit();
checkNoRequest();
do_check_eq(Services.search.currentEngine.name, kTestEngineName);
});
add_task(function* should_request_when_countryCode_not_set() {
Services.prefs.clearUserPref("browser.search.countryCode");
let commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest();
yield commitPromise;
});
add_task(function* should_recheck_if_interval_expired() {
yield forceExpiration();
let commitPromise = promiseAfterCommit();
let date = Date.now();
yield asyncReInit();
checkRequest();
yield commitPromise;
// Check that the expiration timestamp has been updated.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "number");
do_check_true(metadata.searchdefaultexpir >= date + kYearInSeconds * 1000);
do_check_true(metadata.searchdefaultexpir < date + (kYearInSeconds + 3600) * 1000);
});
add_task(function* should_recheck_when_broken_hash() {
// This test verifies both that we ignore saved geo-defaults if the
// hash is invalid, and that we keep the local preferences-based
// default for all of the session in case a synchronous
// initialization was triggered before our HTTP request completed.
let metadata = yield promiseGlobalMetadata();
// Break the hash.
let hash = metadata.searchdefaulthash;
metadata.searchdefaulthash = "broken";
yield promiseSaveGlobalMetadata(metadata);
let commitPromise = promiseAfterCommit();
let reInitPromise = asyncReInit();
// Synchronously check the current default engine, to force a sync init.
// The hash is wrong, so we should fallback to the default engine from prefs.
// 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, getDefaultEngineName(false));
do_check_true(Services.search.isInitialized)
yield reInitPromise;
checkRequest();
yield commitPromise;
// Check that the hash is back to its previous value.
metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaulthash, "string");
do_check_eq(metadata.searchdefaulthash, hash);
// The current default engine shouldn't change during a session.
do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
// After another restart, the current engine should be back to the geo default,
// without doing yet another request.
yield asyncReInit();
checkNoRequest();
do_check_eq(Services.search.currentEngine.name, kTestEngineName);
});
add_task(function* should_remember_cohort_id() {
// Check that initially the cohort pref doesn't exist.
const cohortPref = "browser.search.cohort";
do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
// Make the server send a cohort id.
let cohort = gServerCohort = "xpcshell";
// Trigger a new request.
yield forceExpiration();
let commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest();
yield commitPromise;
// Check that the cohort was saved.
do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_STRING);
do_check_eq(Services.prefs.getCharPref(cohortPref), cohort);
// Make the server stop sending the cohort.
gServerCohort = "";
// Check that the next request sends the previous cohort id, and
// will remove it from the prefs due to the server no longer sending it.
yield forceExpiration();
commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest(cohort);
yield commitPromise;
do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
});
add_task(function* should_retry_after_failure() {
let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let originalUrl = defaultBranch.getCharPref(kUrlPref);
defaultBranch.setCharPref(kUrlPref, originalUrl.replace("defaults", "fail"));
// Trigger a new request.
yield forceExpiration();
yield asyncReInit();
checkRequest();
// After another restart, a new request should be triggered automatically without
// the test having to call forceExpiration again.
yield asyncReInit();
checkRequest();
});
add_task(function* should_honor_retry_after_header() {
let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let originalUrl = defaultBranch.getCharPref(kUrlPref);
defaultBranch.setCharPref(kUrlPref, originalUrl.replace("fail", "unavailable"));
// Trigger a new request.
yield forceExpiration();
let date = Date.now();
let commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest();
yield commitPromise;
// Check that the expiration timestamp has been updated.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "number");
do_check_true(metadata.searchdefaultexpir >= date + kDayInSeconds * 1000);
do_check_true(metadata.searchdefaultexpir < date + (kDayInSeconds + 3600) * 1000);
// After another restart, a new request should not be triggered.
yield asyncReInit();
checkNoRequest();
});

View File

@ -3,73 +3,11 @@
Components.utils.import("resource://gre/modules/osfile.jsm");
const kDefaultenginenamePref = "browser.search.defaultenginename";
const kSelectedEnginePref = "browser.search.selectedEngine";
const kTestEngineName = "Test search engine";
// These two functions (getLocale and getIsUS) are copied from nsSearchService.js
function getLocale() {
let LOCALE_PREF = "general.useragent.locale";
return Services.prefs.getCharPref(LOCALE_PREF);
}
function getIsUS() {
if (getLocale() != "en-US") {
return false;
}
// Timezone assumptions! We assume that if the system clock's timezone is
// between Newfoundland and Hawaii, that the user is in North America.
// This includes all of South America as well, but we have relatively few
// en-US users there, so that's OK.
// 150 minutes = 2.5 hours (UTC-2.5), which is
// Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt)
// 600 minutes = 10 hours (UTC-10), which is
// Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast)
let UTCOffset = (new Date()).getTimezoneOffset();
let isNA = UTCOffset >= 150 && UTCOffset <= 600;
return isNA;
}
function getDefaultEngineName() {
const nsIPLS = Ci.nsIPrefLocalizedString;
// Copy the logic from nsSearchService
let pref = kDefaultenginenamePref;
if (getIsUS()) {
pref += ".US";
}
return Services.prefs.getComplexValue(pref, nsIPLS).data;
}
// waitForSearchNotification is in head_search.js
let waitForNotification = waitForSearchNotification;
function asyncInit() {
let deferred = Promise.defer();
Services.search.init(function() {
do_check_true(Services.search.isInitialized);
deferred.resolve();
});
return deferred.promise;
}
function asyncReInit() {
let promise = waitForNotification("reinit-complete");
Services.search.QueryInterface(Ci.nsIObserver)
.observe(null, "nsPref:changed", "general.useragent.locale");
return promise;
}
// Check that the default engine matches the defaultenginename pref
add_task(function* test_defaultEngine() {
yield asyncInit();
@ -138,7 +76,7 @@ add_task(function* test_ignoreInvalidHash() {
json["[global]"].hash = "invalid";
let data = new TextEncoder().encode(JSON.stringify(json));
let promise = OS.File.writeAtomic(path, data);//, { tmpPath: path + ".tmp" });
yield OS.File.writeAtomic(path, data);//, { tmpPath: path + ".tmp" });
// Re-init the search service, and check that the json file is ignored.
yield asyncReInit();

View File

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

View File

@ -90,6 +90,15 @@ nsURLFormatterService.prototype = {
LOCALE: function() Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry).
getSelectedLocale('global'),
REGION: function() {
try {
// When the geoip lookup failed to identify the region, we fallback to
// the 'ZZ' region code to mean 'unknown'.
return Services.prefs.getCharPref("browser.search.region") || "ZZ";
} catch(e) {
return "ZZ";
}
},
VENDOR: function() this.appInfo.vendor,
NAME: function() this.appInfo.name,
ID: function() this.appInfo.ID,