Bug 1203168 - ask the user for confirmation when searching with a default search engine of unknown origin, data-review=bsmedberg, r=adw.

This commit is contained in:
Florian Quèze 2016-05-27 13:41:29 +02:00
parent b08e506896
commit e9ee2349cc
25 changed files with 743 additions and 10 deletions

View File

@ -369,6 +369,8 @@ pref("browser.search.redirectWindowsSearch", true);
pref("browser.search.redirectWindowsSearch", false);
#endif
pref("browser.search.reset.enabled", true);
pref("browser.usedOnWindows10", false);
pref("browser.usedOnWindows10.introURL", "https://www.mozilla.org/%LOCALE%/firefox/windows-10/welcome/?utm_source=firefox-browser&utm_medium=firefox-browser");

View File

@ -7052,7 +7052,7 @@ var gIdentityHandler = {
this._uriHasHost = false;
}
let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|searchreset|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path);
// Create a channel for the sole purpose of getting the resolved URI

View File

@ -73,6 +73,9 @@ static RedirEntry kRedirMap[] = {
{ "robots", "chrome://browser/content/aboutRobots.xhtml",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT },
{ "searchreset", "chrome://browser/content/search/searchReset.xhtml",
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
{ "sessionrestore", "chrome://browser/content/aboutSessionRestore.xhtml",
nsIAboutModule::ALLOW_SCRIPT },
{ "welcomeback", "chrome://browser/content/aboutWelcomeBack.xhtml",

View File

@ -96,6 +96,7 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "privatebrowsing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "rights", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "robots", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "searchreset", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "sessionrestore", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "welcomeback", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-tabs", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },

View File

@ -0,0 +1,111 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
const TELEMETRY_RESULT_ENUM = {
RESTORED_DEFAULT: 0,
KEPT_CURRENT: 1,
CHANGED_ENGINE: 2,
CLOSED_PAGE: 3
};
window.onload = function() {
let list = document.getElementById("defaultEngine");
let originalDefault = Services.search.originalDefaultEngine.name;
Services.search.getDefaultEngines().forEach(e => {
let opt = document.createElement("option");
opt.setAttribute("value", e.name);
opt.engine = e;
opt.textContent = e.name;
if (e.iconURI)
opt.style.backgroundImage = 'url("' + e.iconURI.spec + '")';
if (e.name == originalDefault)
opt.setAttribute("selected", "true");
list.appendChild(opt);
});
let updateIcon = () => {
list.style.setProperty("--engine-icon-url",
list.selectedOptions[0].style.backgroundImage);
};
list.addEventListener("change", updateIcon);
// When selecting using the keyboard, the 'change' event is only fired after
// the user presses <enter> or moves the focus elsewhere.
// keypress/keyup fire too late and cause flicker when updating the icon.
// keydown fires too early and the selected option isn't changed yet.
list.addEventListener("keydown", () => {
Services.tm.mainThread.dispatch(updateIcon, Ci.nsIThread.DISPATCH_NORMAL);
});
updateIcon();
document.getElementById("searchResetChangeEngine").focus();
window.addEventListener("unload", recordPageClosed);
};
function doSearch() {
let queryString = "";
let purpose = "";
let params = window.location.href.match(/^about:searchreset\?([^#]*)/);
if (params) {
params = params[1].split("&");
for (let param of params) {
if (param.startsWith("data="))
queryString = decodeURIComponent(param.slice(5));
else if (param.startsWith("purpose="))
purpose = param.slice(8);
}
}
let engine = Services.search.currentEngine;
let submission = engine.getSubmission(queryString, null, purpose);
window.removeEventListener("unload", recordPageClosed);
let win = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
win.openUILinkIn(submission.uri.spec, "current", false, submission.postData);
}
function record(result) {
Services.telemetry.getHistogramById("SEARCH_RESET_RESULT").add(result);
}
function keepCurrentEngine() {
// Calling the currentEngine setter will force a correct loadPathHash to be
// written for this engine, so that we don't prompt the user again.
Services.search.currentEngine = Services.search.currentEngine;
record(TELEMETRY_RESULT_ENUM.KEPT_CURRENT);
doSearch();
}
function changeSearchEngine() {
let list = document.getElementById("defaultEngine");
let engine = list.selectedOptions[0].engine;
if (engine.hidden)
engine.hidden = false;
Services.search.currentEngine = engine;
// Record if we restored the original default or changed to another engine.
let originalDefault = Services.search.originalDefaultEngine.name;
let code = TELEMETRY_RESULT_ENUM.CHANGED_ENGINE;
if (Services.search.originalDefaultEngine.name == engine.name)
code = TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT;
record(code);
doSearch();
}
function recordPageClosed() {
record(TELEMETRY_RESULT_ENUM.CLOSED_PAGE);
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html [
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
%htmlDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % searchresetDTD SYSTEM "chrome://browser/locale/aboutSearchReset.dtd">
%searchresetDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<head>
<title>&searchreset.tabtitle;</title>
<link rel="stylesheet" type="text/css" media="all"
href="chrome://global/skin/in-content/info-pages.css"/>
<link rel="stylesheet" type="text/css" media="all"
href="chrome://browser/skin/searchReset.css"/>
<link rel="icon" type="image/png"
href="chrome://browser/skin/favicon-search-16.svg"/>
<script type="application/javascript;version=1.8"
src="chrome://browser/content/search/searchReset.js"/>
</head>
<body dir="&locale.dir;">
<div class="container">
<div class="title">
<h1 class="title-text">&searchreset.pageTitle;</h1>
</div>
<div class="description">
<p>&searchreset.pageInfo1;</p>
<p>&searchreset.selector.label;
<select id="defaultEngine"></select>
</p>
<p>&searchreset.beforelink.pageInfo2;<a id="linkSettingsPage" href="about:preferences#search">&searchreset.link.pageInfo2;</a>&searchreset.afterlink.pageInfo2;</p>
</div>
<div class="button-container">
<xul:button id="searchResetKeepCurrent"
label="&searchreset.noChangeButton;"
accesskey="&searchreset.noChangeButton.access;"
oncommand="keepCurrentEngine();"/>
<xul:button class="primary"
id="searchResetChangeEngine"
label="&searchreset.changeEngineButton;"
accesskey="&searchreset.changeEngineButton.access;"
oncommand="changeSearchEngine();"/>
</div>
</div>
</body>
</html>

View File

@ -5,3 +5,5 @@
browser.jar:
content/browser/search/search.xml (content/search.xml)
content/browser/search/searchbarBindings.css (content/searchbarBindings.css)
content/browser/search/searchReset.xhtml (content/searchReset.xhtml)
content/browser/search/searchReset.js (content/searchReset.js)

View File

@ -37,6 +37,7 @@ skip-if = os == "mac" # bug 967013
[browser_yahoo_behavior.js]
[browser_abouthome_behavior.js]
skip-if = true # Bug ??????, Bug 1100301 - leaks windows until shutdown when --run-by-dir
[browser_aboutSearchReset.js]
[browser_searchbar_openpopup.js]
skip-if = os == "linux" # Linux has different focus behaviours.
[browser_searchbar_keyboard_navigation.js]

View File

@ -0,0 +1,181 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const TELEMETRY_RESULT_ENUM = {
RESTORED_DEFAULT: 0,
KEPT_CURRENT: 1,
CHANGED_ENGINE: 2,
CLOSED_PAGE: 3
};
const kSearchStr = "a search";
const kSearchPurpose = "searchbar";
const kTestEngine = "testEngine.xml";
function checkTelemetryRecords(expectedValue) {
let histogram = Services.telemetry.getHistogramById("SEARCH_RESET_RESULT");
let snapshot = histogram.snapshot();
// The probe is declared with 5 values, but we get 6 back from .counts
let expectedCounts = [0, 0, 0, 0, 0, 0];
if (expectedValue != null) {
expectedCounts[expectedValue] = 1;
}
Assert.deepEqual(snapshot.counts, expectedCounts,
"histogram has expected content");
histogram.clear();
}
function promiseStoppedLoad(expectedURL) {
return new Promise(resolve => {
let browser = gBrowser.selectedBrowser;
let original = browser.loadURIWithFlags;
browser.loadURIWithFlags = function(URI) {
if (URI == expectedURL) {
browser.loadURIWithFlags = original;
ok(true, "loaded expected url: " + URI);
resolve();
return;
}
original.apply(browser, arguments);
};
});
}
var gTests = [
{
desc: "Test the 'Keep Current Settings' button.",
run: function* () {
let engine = yield promiseNewEngine(kTestEngine, {setAsCurrent: true});
let expectedURL = engine.
getSubmission(kSearchStr, null, kSearchPurpose).
uri.spec;
let rawEngine = engine.wrappedJSObject;
let initialHash = rawEngine.getAttr("loadPathHash");
rawEngine.setAttr("loadPathHash", "broken");
let loadPromise = promiseStoppedLoad(expectedURL);
gBrowser.contentDocument.getElementById("searchResetKeepCurrent").click();
yield loadPromise;
is(engine, Services.search.currentEngine,
"the custom engine is still default");
is(rawEngine.getAttr("loadPathHash"), initialHash,
"the loadPathHash has been fixed");
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.KEPT_CURRENT);
}
},
{
desc: "Test the 'Restore Search Defaults' button.",
run: function* () {
let currentEngine = Services.search.currentEngine;
let originalEngine = Services.search.originalDefaultEngine;
let expectedURL = originalEngine.
getSubmission(kSearchStr, null, kSearchPurpose).
uri.spec;
let loadPromise = promiseStoppedLoad(expectedURL);
let doc = gBrowser.contentDocument;
let button = doc.getElementById("searchResetChangeEngine");
is(doc.activeElement, button,
"the 'Change Search Engine' button is focused");
button.click();
yield loadPromise;
is(originalEngine, Services.search.currentEngine,
"the default engine is back to the original one");
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT);
Services.search.currentEngine = currentEngine;
}
},
{
desc: "Test the engine selector drop down.",
run: function* () {
let originalEngineName = Services.search.originalDefaultEngine.name;
let doc = gBrowser.contentDocument;
let list = doc.getElementById("defaultEngine");
is(list.value, originalEngineName,
"the default selection of the dropdown is the original default engine");
let defaultEngines = Services.search.getDefaultEngines();
is(list.childNodes.length, defaultEngines.length,
"the dropdown has the correct count of engines");
// Select an engine that isn't the original default one.
let engine;
for (let i = 0; i < defaultEngines.length; ++i) {
if (defaultEngines[i].name != originalEngineName) {
engine = defaultEngines[i];
engine.hidden = true;
break;
}
}
list.value = engine.name;
let expectedURL = engine.getSubmission(kSearchStr, null, kSearchPurpose)
.uri.spec;
let loadPromise = promiseStoppedLoad(expectedURL);
doc.getElementById("searchResetChangeEngine").click();
yield loadPromise;
ok(!engine.hidden, "the selected engine has been unhidden");
is(engine, Services.search.currentEngine,
"the current engine is what was selected in the drop down");
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.CHANGED_ENGINE);
}
},
{
desc: "Load another page without clicking any of the buttons.",
run: function* () {
yield promiseTabLoadEvent(gBrowser.selectedTab, "about:mozilla");
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.CLOSED_PAGE);
}
},
];
function test()
{
waitForExplicitFinish();
Task.spawn(function* () {
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
checkTelemetryRecords();
for (let test of gTests) {
info(test.desc);
// Create a tab to run the test.
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
// Start loading about:searchreset and wait for it to complete.
let url = "about:searchreset?data=" + encodeURIComponent(kSearchStr) +
"&purpose=" + kSearchPurpose;
yield promiseTabLoadEvent(tab, url);
info("Running test");
yield test.run();
info("Cleanup");
gBrowser.removeCurrentTab();
}
Services.telemetry.canRecordExtended = oldCanRecord;
}).then(finish, ex => {
ok(false, "Unexpected Exception: " + ex);
finish();
});
}

View File

@ -1,6 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/Promise.jsm");
/**
* Recursively compare two objects and check that every property of expectedObj has the same value
* on actualObj.
@ -85,3 +87,52 @@ function promiseNewEngine(basename, options = {}) {
});
});
}
/**
* Waits for a load (or custom) event to finish in a given tab. If provided
* load an uri into the tab.
*
* @param tab
* The tab to load into.
* @param [optional] url
* The url to load, or the current url.
* @return {Promise} resolved when the event is handled.
* @resolves to the received event
* @rejects if a valid load event is not received within a meaningful interval
*/
function promiseTabLoadEvent(tab, url)
{
let deferred = Promise.defer();
info("Wait tab event: load");
function handle(loadedUrl) {
if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
info(`Skipping spurious load event for ${loadedUrl}`);
return false;
}
info("Tab event received: load");
return true;
}
// Create two promises: one resolved from the content process when the page
// loads and one that is rejected if we take too long to load the url.
let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
let timeout = setTimeout(() => {
deferred.reject(new Error("Timed out while waiting for a 'load' event"));
}, 30000);
loaded.then(() => {
clearTimeout(timeout);
deferred.resolve()
});
if (url)
BrowserTestUtils.loadURI(tab.linkedBrowser, url);
// Promise.all rejects if either promise rejects (i.e. if we time out) and
// if our loaded promise resolves before the timeout, then we resolve the
// timeout promise as well, causing the all promise to resolve.
return Promise.all([deferred.promise, loaded]);
}

View File

@ -0,0 +1,28 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!ENTITY searchreset.tabtitle "Restore Search Settings">
<!ENTITY searchreset.pageTitle "Restore your search settings?">
<!ENTITY searchreset.pageInfo1 "Your search settings might be out-of-date. Firefox can help you restore the default search settings.">
<!ENTITY searchreset.selector.label "This will set your default search engine to">
<!-- LOCALIZATION NOTE (searchreset.beforelink.pageInfo,
searchreset.afterlink.pageInfo): these two string are used respectively
before and after the the "Settings page" link (searchreset.link.pageInfo).
Localizers can use one of them, or both, to better adapt this sentence to
their language.
-->
<!ENTITY searchreset.beforelink.pageInfo2 "You can change these settings at any time from the ">
<!ENTITY searchreset.afterlink.pageInfo2 ".">
<!ENTITY searchreset.link.pageInfo2 "Settings page">
<!ENTITY searchreset.noChangeButton "Dont Change">
<!ENTITY searchreset.noChangeButton.access "D">
<!ENTITY searchreset.changeEngineButton "Change Search Engine">
<!ENTITY searchreset.changeEngineButton.access "C">

View File

@ -17,6 +17,7 @@
#ifdef MOZ_SERVICES_HEALTHREPORT
locale/browser/aboutHealthReport.dtd (%chrome/browser/aboutHealthReport.dtd)
#endif
locale/browser/aboutSearchReset.dtd (%chrome/browser/aboutSearchReset.dtd)
locale/browser/aboutSessionRestore.dtd (%chrome/browser/aboutSessionRestore.dtd)
locale/browser/aboutTabCrashed.dtd (%chrome/browser/aboutTabCrashed.dtd)
locale/browser/syncCustomize.dtd (%chrome/browser/syncCustomize.dtd)

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="8" fill="#58bf43"/>
<circle cx="8" cy="8" r="7.5" stroke="#41a833" stroke-width="1" fill="none"/>
<path d="M12.879,12L12,12.879,9.015,9.9A4.276,4.276,0,1,1,9.9,9.015ZM6.5,3.536A2.964,2.964,0,1,0,9.464,6.5,2.964,2.964,0,0,0,6.5,3.536Z" stroke="#41a833" stroke-width="2" fill="none"/>
<path d="M12.879,12L12,12.879,9.015,9.9A4.276,4.276,0,1,1,9.9,9.015ZM6.5,3.536A2.964,2.964,0,1,0,9.464,6.5,2.964,2.964,0,0,0,6.5,3.536Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<ellipse cx="32" cy="34" rx="29.5" ry="30" fill="#000" fill-opacity=".1"/>
<circle cx="32" cy="32" r="30" fill="#58bf43"/>
<circle cx="32" cy="32" r="29.5" stroke="#41a833" stroke-width="1" fill="none"/>
<path d="M50,47.131L47.131,50,36.776,39.647a16.038,16.038,0,1,1,2.871-2.871ZM27,15A12,12,0,1,0,39,27,12,12,0,0,0,27,15Z" stroke="#41a833" stroke-width="2" fill="none"/>
<path d="M50,47.131L47.131,50,36.776,39.647a16.038,16.038,0,1,1,2.871-2.871ZM27,15A12,12,0,1,0,39,27,12,12,0,0,0,27,15Z" fill="#fff"/>
<circle cx="27" cy="27" r="13" fill="#fff" fill-opacity=".2"/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

View File

@ -88,6 +88,7 @@
skin/classic/browser/search-indicator@2x.png (../shared/search/search-indicator@2x.png)
skin/classic/browser/search-engine-placeholder.png (../shared/search/search-engine-placeholder.png)
skin/classic/browser/search-engine-placeholder@2x.png (../shared/search/search-engine-placeholder@2x.png)
skin/classic/browser/searchReset.css (../shared/searchReset.css)
skin/classic/browser/badge-add-engine.png (../shared/search/badge-add-engine.png)
skin/classic/browser/badge-add-engine@2x.png (../shared/search/badge-add-engine@2x.png)
skin/classic/browser/search-indicator-badge-add.png (../shared/search/search-indicator-badge-add.png)
@ -119,6 +120,8 @@
skin/classic/browser/cert-error.svg (../shared/incontent-icons/cert-error.svg)
skin/classic/browser/session-restore.svg (../shared/incontent-icons/session-restore.svg)
skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
skin/classic/browser/favicon-search-16.svg (../shared/favicon-search-16.svg)
skin/classic/browser/icon-search-64.svg (../shared/incontent-icons/icon-search-64.svg)
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/browser/reader-tour.png (../shared/reader/reader-tour.png)
skin/classic/browser/reader-tour@2x.png (../shared/reader/reader-tour@2x.png)

View File

@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
body {
align-items: center;
}
.title {
background-image: url("chrome://browser/skin/icon-search-64.svg");
}
select {
font: inherit;
padding-inline-end: 24px;
padding-inline-start: 26px;
background-image: var(--engine-icon-url),
url("chrome://global/skin/in-content/dropdown.svg#dropdown");
background-repeat: no-repeat;
background-position: 8px center, calc(100% - 4px) center;
background-size: 16px, 16px;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 var(--in-content-text-color);
}
option {
padding: 4px;
padding-inline-start: 30px;
background-repeat: no-repeat;
background-position: 8px center;
background-size: 16px;
background-color: var(--in-content-page-background);
}

View File

@ -5226,6 +5226,8 @@ pref("browser.search.update", true);
pref("browser.search.update.log", false);
pref("browser.search.update.interval", 21600);
pref("browser.search.suggest.enabled", true);
pref("browser.search.reset.enabled", false);
pref("browser.search.reset.whitelist", "");
pref("browser.search.geoSpecificDefaults", false);
pref("browser.search.geoip.url", "https://location.services.mozilla.com/v1/country?key=%MOZILLA_API_KEY%");
// NOTE: this timeout figure is also the "high" value for the telemetry probe

View File

@ -436,6 +436,12 @@ interface nsIBrowserSearchService : nsISupports
*/
void removeEngine(in nsISearchEngine engine);
/**
* The original Engine object that is the default for this region,
* ignoring changes the user may have subsequently made.
*/
readonly attribute nsISearchEngine originalDefaultEngine;
/**
* Alias for the currentEngine attribute, kept for add-on compatibility.
*/

View File

@ -2408,6 +2408,21 @@ Engine.prototype = {
},
#endif
get _isWhiteListed() {
let url = this._getURLOfType(URLTYPE_SEARCH_HTML).template;
let hostname = makeURI(url).host;
let whitelist = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
.getCharPref("reset.whitelist")
.split(",");
if (whitelist.includes(hostname)) {
LOG("The hostname " + hostname + " is white listed, " +
"we won't show the search reset prompt");
return true;
}
return false;
},
// from nsISearchEngine
getSubmission: function SRCH_ENG_getSubmission(aData, aResponseType, aPurpose) {
#ifdef ANDROID
@ -2419,6 +2434,24 @@ Engine.prototype = {
aResponseType = URLTYPE_SEARCH_HTML;
}
if (aResponseType == URLTYPE_SEARCH_HTML &&
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).getBoolPref("reset.enabled") &&
this.name == Services.search.currentEngine.name &&
!this._isDefault &&
(!this.getAttr("loadPathHash") ||
this.getAttr("loadPathHash") != getVerificationHash(this._loadPath)) &&
!this._isWhiteListed) {
let url = "about:searchreset";
let data = [];
if (aData)
data.push("data=" + encodeURIComponent(aData));
if (aPurpose)
data.push("purpose=" + aPurpose);
if (data.length)
url += "?" + data.join("&");
return new Submission(makeURI(url));
}
var url = this._getURLOfType(aResponseType);
if (!url)
@ -2426,7 +2459,7 @@ Engine.prototype = {
if (!aData) {
// Return a dummy submission object with our searchForm attribute
return new Submission(makeURI(this._getSearchFormWithPurpose(aPurpose)), null);
return new Submission(makeURI(this._getSearchFormWithPurpose(aPurpose)));
}
LOG("getSubmission: In data: \"" + aData + "\"; Purpose: \"" + aPurpose + "\"");
@ -2822,7 +2855,7 @@ SearchService.prototype = {
// Get the original Engine object that is the default for this region,
// ignoring changes the user may have subsequently made.
get _originalDefaultEngine() {
get originalDefaultEngine() {
let defaultEngine = this.getVerifiedGlobalAttr("searchDefault");
if (!defaultEngine) {
let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
@ -2841,7 +2874,7 @@ SearchService.prototype = {
},
resetToOriginalDefaultEngine: function SRCH_SVC__resetToOriginalDefaultEngine() {
this.currentEngine = this._originalDefaultEngine;
this.currentEngine = this.originalDefaultEngine;
},
_buildCache: function SRCH_SVC__buildCache() {
@ -3948,6 +3981,7 @@ SearchService.prototype = {
var engine = new Engine(sanitizeName(aName), false);
engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
aMethod, aTemplate, aExtensionID);
engine._loadPath = "[other]addEngineWithDetails";
this._addEngineToStore(engine);
},
@ -4111,13 +4145,13 @@ SearchService.prototype = {
this._currentEngine = engine;
}
if (!name)
this._currentEngine = this._originalDefaultEngine;
this._currentEngine = this.originalDefaultEngine;
}
// If the current engine is not set or hidden, we fallback...
if (!this._currentEngine || this._currentEngine.hidden) {
// first to the original default engine
let originalDefault = this._originalDefaultEngine;
let originalDefault = this.originalDefaultEngine;
if (!originalDefault || originalDefault.hidden) {
// then to the first visible engine
let firstVisible = this._getSortedEngines(false)[0];
@ -4154,9 +4188,11 @@ SearchService.prototype = {
if (!newCurrentEngine)
FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
if (!newCurrentEngine._isDefault && newCurrentEngine._loadPath) {
if (!newCurrentEngine._isDefault) {
// If a non default engine is being set as the current engine, ensure
// its loadPath has a verification hash.
if (!newCurrentEngine._loadPath)
newCurrentEngine._loadPath = "[other]unknown";
let loadPathHash = getVerificationHash(newCurrentEngine._loadPath);
let currentHash = newCurrentEngine.getAttr("loadPathHash");
if (!currentHash || currentHash != loadPathHash) {
@ -4176,7 +4212,7 @@ SearchService.prototype = {
// build's default engine, so that the currentEngine getter falls back to
// whatever the default is.
let newName = this._currentEngine.name;
if (this._currentEngine == this._originalDefaultEngine) {
if (this._currentEngine == this.originalDefaultEngine) {
newName = "";
}

View File

@ -0,0 +1,34 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const kSearchEngineID = "addEngineWithDetails_test_engine";
const kSearchEngineURL = "http://example.com/?search={searchTerms}";
const kSearchTerm = "foo";
add_task(function* test_addEngineWithDetails() {
do_check_false(Services.search.isInitialized);
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
.setBoolPref("reset.enabled", true);
yield asyncInit();
Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
kSearchEngineURL);
// An engine added with addEngineWithDetails should have a load path, even
// though we can't point to a specific file.
let engine = Services.search.getEngineByName(kSearchEngineID);
do_check_eq(engine.wrappedJSObject._loadPath, "[other]addEngineWithDetails");
// Set the engine as default; this should set a loadPath verification hash,
// which should ensure we don't show the search reset prompt.
Services.search.currentEngine = engine;
let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
let submission =
Services.search.currentEngine.getSubmission(kSearchTerm, null, "searchbar");
do_check_eq(submission.uri.spec, expectedURL);
});

View File

@ -0,0 +1,137 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
const kTestEngineShortName = "engine";
const kWhiteListPrefName = "reset.whitelist";
function run_test() {
// Copy an engine to [profile]/searchplugin/
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.xml").copyTo(dir, kTestEngineShortName + ".xml");
let file = dir.clone();
file.append(kTestEngineShortName + ".xml");
do_check_true(file.exists());
do_check_false(Services.search.isInitialized);
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
.setBoolPref("reset.enabled", true);
run_next_test();
}
function* removeLoadPathHash() {
// Remove the loadPathHash and re-initialize the search service.
let cache = yield promiseCacheData();
for (let engine of cache.engines) {
if (engine._shortName == kTestEngineShortName) {
delete engine._metaData["loadPathHash"];
break;
}
}
yield promiseSaveCacheData(cache);
yield asyncReInit();
}
add_task(function* test_no_prompt_when_valid_loadPathHash() {
yield asyncInit();
// test the engine is loaded ok.
let engine = Services.search.getEngineByName(kTestEngineName);
do_check_neq(engine, null);
yield promiseAfterCache();
// The test engine has been found in the profile directory and imported,
// so it shouldn't have a loadPathHash.
let metadata = yield promiseEngineMetadata();
do_check_true(kTestEngineShortName in metadata);
do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
// After making it the currentEngine with the search service API,
// the test engine should have a valid loadPathHash.
Services.search.currentEngine = engine;
yield promiseAfterCache();
metadata = yield promiseEngineMetadata();
do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
do_check_eq(typeof loadPathHash, "string");
do_check_eq(loadPathHash.length, 44);
// A search should not cause the search reset prompt.
let submission =
Services.search.currentEngine.getSubmission("foo", null, "searchbar");
do_check_eq(submission.uri.spec,
"http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
});
add_task(function* test_promptURLs() {
yield removeLoadPathHash();
// The default should still be the test engine.
let currentEngine = Services.search.currentEngine;
do_check_eq(currentEngine.name, kTestEngineName);
// but the submission url should be about:searchreset
let url = (data, purpose) =>
currentEngine.getSubmission(data, null, purpose).uri.spec;
do_check_eq(url("foo", "searchbar"),
"about:searchreset?data=foo&purpose=searchbar");
do_check_eq(url("foo"), "about:searchreset?data=foo");
do_check_eq(url("", "searchbar"), "about:searchreset?purpose=searchbar");
do_check_eq(url(""), "about:searchreset");
do_check_eq(url("", ""), "about:searchreset");
// Calling the currentEngine setter for the same engine should
// prevent further prompts.
Services.search.currentEngine = Services.search.currentEngine;
do_check_eq(url("foo", "searchbar"),
"http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
// And the loadPathHash should be back.
yield promiseAfterCache();
let metadata = yield promiseEngineMetadata();
do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
do_check_eq(typeof loadPathHash, "string");
do_check_eq(loadPathHash.length, 44);
});
add_task(function* test_whitelist() {
yield removeLoadPathHash();
// The default should still be the test engine.
let currentEngine = Services.search.currentEngine;
do_check_eq(currentEngine.name, kTestEngineName);
let expectPrompt = shouldPrompt => {
let expectedURL =
shouldPrompt ? "about:searchreset?data=foo&purpose=searchbar"
: "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t";
let url = currentEngine.getSubmission("foo", null, "searchbar").uri.spec;
do_check_eq(url, expectedURL);
};
expectPrompt(true);
// Unless we whitelist our test engine.
let branch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let initialWhiteList = branch.getCharPref(kWhiteListPrefName);
branch.setCharPref(kWhiteListPrefName, "example.com,test.tld");
expectPrompt(true);
branch.setCharPref(kWhiteListPrefName, "www.google.com");
expectPrompt(false);
branch.setCharPref(kWhiteListPrefName, "example.com,www.google.com,test.tld");
expectPrompt(false);
// The loadPathHash should not be back after the prompt was skipped due to the
// whitelist.
yield asyncReInit();
let metadata = yield promiseEngineMetadata();
do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
branch.setCharPref(kWhiteListPrefName, initialWhiteList);
expectPrompt(true);
});

View File

@ -94,3 +94,5 @@ tags = addons
[test_require_engines_in_cache.js]
[test_update_telemetry.js]
[test_svg_icon.js]
[test_searchReset.js]
[test_addEngineWithDetails.js]

View File

@ -5518,6 +5518,15 @@
"releaseChannelCollection": "opt-out",
"description": "Record the search counts for search engines"
},
"SEARCH_RESET_RESULT": {
"alert_emails": ["fqueze@mozilla.com"],
"bug_numbers": [1203168],
"expires_in_version": "53",
"kind": "enumerated",
"n_values": 5,
"releaseChannelCollection": "opt-out",
"description": "Result of showing the search reset prompt to the user. 0=restored original default, 1=kept current engine, 2=changed engine, 3=closed the page"
},
"SEARCH_SERVICE_INIT_MS": {
"expires_in_version": "never",
"kind": "exponential",

View File

@ -1389,8 +1389,8 @@ add_task(function* test_defaultSearchEngine() {
const EXPECTED_SEARCH_ENGINE_DATA = {
name: "telemetry_default",
loadPath: null,
origin: "unverified"
loadPath: "[other]addEngineWithDetails",
origin: "verified"
};
Assert.deepEqual(data.settings.defaultSearchEngineData, EXPECTED_SEARCH_ENGINE_DATA);
TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");

View File

@ -175,6 +175,7 @@ html|button {
/* xul buttons and menulists */
*|button,
html|select,
xul|colorpicker[type="button"],
xul|menulist {
-moz-appearance: none;
@ -190,6 +191,7 @@ xul|menulist {
}
html|button:enabled:hover,
html|select:enabled:hover,
xul|button:not([disabled="true"]):hover,
xul|colorpicker[type="button"]:not([disabled="true"]):hover,
xul|menulist:not([disabled="true"]):hover {
@ -197,6 +199,7 @@ xul|menulist:not([disabled="true"]):hover {
}
html|button:enabled:hover:active,
html|select:enabled:hover:active,
xul|button:not([disabled="true"]):hover:active,
xul|colorpicker[type="button"]:not([disabled="true"]):hover:active,
xul|menulist[open="true"]:not([disabled="true"]) {
@ -204,6 +207,7 @@ xul|menulist[open="true"]:not([disabled="true"]) {
}
html|button:disabled,
html|select:disabled,
xul|button[disabled="true"],
xul|colorpicker[type="button"][disabled="true"],
xul|menulist[disabled="true"] {