Bug 1203168 - ask the user for confirmation when searching with a default search engine of unknown origin, data-review=bsmedberg, r=adw.
☠☠ backed out by df3281895c93 ☠ ☠
authorFlorian Quèze <florian@queze.net>
Fri, 27 May 2016 13:41:29 +0200
changeset 338297 08053d1e2cc1f3b6e6901e4aedaf122c4e1de27b
parent 338296 4a7a28982402ba18d512b8d5cefa52b504d4ad9c
child 338298 6cfd5ff960f774afa9b946b93142e7dcc5869558
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1203168
milestone49.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 1203168 - ask the user for confirmation when searching with a default search engine of unknown origin, data-review=bsmedberg, r=adw.
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/components/about/AboutRedirector.cpp
browser/components/build/nsModule.cpp
browser/components/search/content/searchReset.js
browser/components/search/content/searchReset.xhtml
browser/components/search/jar.mn
browser/components/search/test/browser.ini
browser/components/search/test/browser_aboutSearchReset.js
browser/components/search/test/head.js
browser/locales/en-US/chrome/browser/aboutSearchReset.dtd
browser/locales/jar.mn
browser/themes/shared/favicon-search-16.svg
browser/themes/shared/incontent-icons/icon-search-64.svg
browser/themes/shared/jar.inc.mn
browser/themes/shared/searchReset.css
modules/libpref/init/all.js
netwerk/base/nsIBrowserSearchService.idl
toolkit/components/search/nsSearchService.js
toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js
toolkit/components/search/tests/xpcshell/test_searchReset.js
toolkit/components/search/tests/xpcshell/xpcshell.ini
toolkit/components/telemetry/Histograms.json
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
toolkit/themes/shared/in-content/common.inc.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -364,16 +364,18 @@ pref("browser.search.context.loadInBackg
 pref("browser.search.hiddenOneOffs", "");
 
 #ifdef XP_WIN
 pref("browser.search.redirectWindowsSearch", true);
 #else
 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");
 
 pref("browser.sessionhistory.max_entries", 50);
 
 // Built-in default permissions.
 pref("permissions.manager.defaultsUrl", "resource://app/defaults/permissions");
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7047,17 +7047,17 @@ var gIdentityHandler = {
 
     try {
       this._uri.host;
       this._uriHasHost = true;
     } catch (ex) {
       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
     // of the request to determine if it's loaded from the file system.
     this._isURILoadedFromFile = false;
     let chanOptions = {uri: this._uri, loadUsingSystemPrincipal: true};
     let resolvedURI;
     try {
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -68,16 +68,19 @@ static RedirEntry kRedirMap[] = {
     nsIAboutModule::ALLOW_SCRIPT },
   { "rights",
     "chrome://global/content/aboutRights.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT },
   { "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",
     nsIAboutModule::ALLOW_SCRIPT },
   { "sync-tabs", "chrome://browser/content/sync/aboutSyncTabs.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   // Linkable because of indexeddb use (bug 1228118)
   { "home", "chrome://browser/content/abouthome/aboutHome.xhtml",
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -91,16 +91,17 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "certerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "socialerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "providerdirectory", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "tabcrashed", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "feeds", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { 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 },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "home", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
new file mode 100644
--- /dev/null
+++ b/browser/components/search/content/searchReset.js
@@ -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);
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/search/content/searchReset.xhtml
@@ -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>
--- a/browser/components/search/jar.mn
+++ b/browser/components/search/jar.mn
@@ -1,7 +1,9 @@
 # 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/.
 
 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)
--- a/browser/components/search/test/browser.ini
+++ b/browser/components/search/test/browser.ini
@@ -32,13 +32,14 @@ skip-if = os == "mac" # bug 967013
 [browser_hiddenOneOffs_cleanup.js]
 [browser_hiddenOneOffs_diacritics.js]
 [browser_oneOffHeader.js]
 [browser_private_search_perwindowpb.js]
 [browser_yahoo.js]
 [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]
 [browser_searchbar_smallpanel_keyboard_navigation.js]
 [browser_webapi.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser_aboutSearchReset.js
@@ -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();
+  });
+}
--- a/browser/components/search/test/head.js
+++ b/browser/components/search/test/head.js
@@ -1,11 +1,13 @@
 /* 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.
  */
 function isSubObjectOf(expectedObj, actualObj, name) {
   for (let prop in expectedObj) {
     if (typeof expectedObj[prop] == 'function')
       continue;
@@ -80,8 +82,57 @@ function promiseNewEngine(basename, opti
             ok(false, "addEngine failed with error code " + errCode);
             reject();
           }
         });
       }
     });
   });
 }
+
+/**
+ * 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]);
+}
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/aboutSearchReset.dtd
@@ -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        "Don’t Change">
+<!ENTITY searchreset.noChangeButton.access "D">
+
+<!ENTITY searchreset.changeEngineButton        "Change Search Engine">
+<!ENTITY searchreset.changeEngineButton.access "C">
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -12,16 +12,17 @@
     locale/browser/aboutPrivateBrowsing.dtd        (%chrome/browser/aboutPrivateBrowsing.dtd)
     locale/browser/aboutPrivateBrowsing.properties (%chrome/browser/aboutPrivateBrowsing.properties)
     locale/browser/aboutRobots.dtd                 (%chrome/browser/aboutRobots.dtd)
     locale/browser/aboutHome.dtd                   (%chrome/browser/aboutHome.dtd)
     locale/browser/accounts.properties             (%chrome/browser/accounts.properties)
 #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)
     locale/browser/aboutSyncTabs.dtd               (%chrome/browser/aboutSyncTabs.dtd)
     locale/browser/browser.dtd                     (%chrome/browser/browser.dtd)
     locale/browser/baseMenuOverlay.dtd             (%chrome/browser/baseMenuOverlay.dtd)
     locale/browser/browser.properties              (%chrome/browser/browser.properties)
     locale/browser/customizableui/customizableWidgets.properties (%chrome/browser/customizableui/customizableWidgets.properties)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/favicon-search-16.svg
@@ -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>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/incontent-icons/icon-search-64.svg
@@ -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>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -83,16 +83,17 @@
   skin/classic/browser/fxa/android@2x.png                      (../shared/fxa/android@2x.png)
   skin/classic/browser/fxa/ios.png                             (../shared/fxa/ios.png)
   skin/classic/browser/fxa/ios@2x.png                          (../shared/fxa/ios@2x.png)
   skin/classic/browser/search-pref.png                         (../shared/search/search-pref.png)
   skin/classic/browser/search-indicator.png                    (../shared/search/search-indicator.png)
   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)
   skin/classic/browser/search-indicator-badge-add@2x.png       (../shared/search/search-indicator-badge-add@2x.png)
   skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
   skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
   skin/classic/browser/search-arrow-go.svg                     (../shared/search/search-arrow-go.svg)
   skin/classic/browser/social/chat-icons.svg                   (../shared/social/chat-icons.svg)
@@ -114,16 +115,18 @@
   skin/classic/browser/update-badge.svg                        (../shared/update-badge.svg)
   skin/classic/browser/update-badge-failed.svg                 (../shared/update-badge-failed.svg)
   skin/classic/browser/urlbar-arrow.png                        (../shared/urlbar-arrow.png)
   skin/classic/browser/urlbar-arrow@2x.png                     (../shared/urlbar-arrow@2x.png)
   skin/classic/browser/warning.svg                             (../shared/warning.svg)
   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)
   skin/classic/browser/readerMode.svg                          (../shared/reader/readerMode.svg)
   skin/classic/browser/notification-pluginNormal.png           (../shared/plugins/notification-pluginNormal.png)
   skin/classic/browser/notification-pluginNormal@2x.png        (../shared/plugins/notification-pluginNormal@2x.png)
   skin/classic/browser/notification-pluginAlert.png            (../shared/plugins/notification-pluginAlert.png)
   skin/classic/browser/notification-pluginAlert@2x.png         (../shared/plugins/notification-pluginAlert@2x.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/searchReset.css
@@ -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);
+}
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5221,16 +5221,18 @@ pref("browser.addon-watch.ignore", "[\"m
 pref("browser.addon-watch.percentage-limit", 5);
 
 // Search service settings
 pref("browser.search.log", false);
 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
 // SEARCH_SERVICE_COUNTRY_FETCH_MS - if you change this also change that probe.
 pref("browser.search.geoip.timeout", 2000);
 
 #ifdef MOZ_OFFICIAL_BRANDING
 // {moz:official} expands to "official"
--- a/netwerk/base/nsIBrowserSearchService.idl
+++ b/netwerk/base/nsIBrowserSearchService.idl
@@ -432,16 +432,22 @@ interface nsIBrowserSearchService : nsIS
    * profile directory, it will be removed from disk.
    *
    * @param  engine
    *         The engine to remove.
    */
   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.
    */
   attribute nsISearchEngine defaultEngine;
 
   /**
    * The currently active search engine.
    * Unless the application doesn't ship any search plugin, this should never
    * be null. If the currently active engine is removed, this attribute will
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -2403,35 +2403,68 @@ Engine.prototype = {
       type = "application/x-moz-phonesearch";
     }
 
     delete this._defaultMobileResponseType;
     return this._defaultMobileResponseType = type;
   },
 #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
     if (!aResponseType) {
       aResponseType = this._defaultMobileResponseType;
     }
 #endif
     if (!aResponseType) {
       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)
       return null;
 
     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 + "\"");
     var data = "";
     try {
       data = gTextToSubURI.ConvertAndEscape(this.queryCharset, aData);
     } catch (ex) {
       LOG("getSubmission: Falling back to default queryCharset!");
@@ -2817,17 +2850,17 @@ SearchService.prototype = {
   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.
-  get _originalDefaultEngine() {
+  get originalDefaultEngine() {
     let defaultEngine = this.getVerifiedGlobalAttr("searchDefault");
     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;
@@ -2836,17 +2869,17 @@ SearchService.prototype = {
         // getEngineByName will just return null, which is the best we can do.
       }
     }
 
     return this.getEngineByName(defaultEngine);
   },
 
   resetToOriginalDefaultEngine: function SRCH_SVC__resetToOriginalDefaultEngine() {
-    this.currentEngine = this._originalDefaultEngine;
+    this.currentEngine = this.originalDefaultEngine;
   },
 
   _buildCache: function SRCH_SVC__buildCache() {
     if (this._batchTask)
       this._batchTask.disarm();
 
     TelemetryStopwatch.start("SEARCH_SERVICE_BUILD_CACHE_MS");
     let cache = {};
@@ -3943,16 +3976,17 @@ SearchService.prototype = {
     if (!aTemplate)
       FAIL("Invalid template passed to addEngineWithDetails!");
     if (this._engines[aName])
       FAIL("An engine with that name already exists!", Cr.NS_ERROR_FILE_ALREADY_EXISTS);
 
     var engine = new Engine(sanitizeName(aName), false);
     engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
                              aMethod, aTemplate, aExtensionID);
+    engine._loadPath = "[other]addEngineWithDetails";
     this._addEngineToStore(engine);
   },
 
   addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL,
                                          aConfirm, aCallback) {
     LOG("addEngine: Adding \"" + aEngineURL + "\".");
     this._ensureInitialized();
     try {
@@ -4106,23 +4140,23 @@ SearchService.prototype = {
       if (engine && (this.getGlobalAttr("hash") == getVerificationHash(name) ||
                      engine._isDefault)) {
         // If the current engine is a default one, we can relax the
         // verification hash check to reduce the annoyance for users who
         // backup/sync their profile in custom ways.
         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];
         if (firstVisible && !firstVisible.hidden) {
           this.currentEngine = firstVisible;
           return firstVisible;
         }
         // and finally as a last resort we unhide the original default engine.
@@ -4149,19 +4183,21 @@ SearchService.prototype = {
     // handle both.
     if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine))
       FAIL("Invalid argument passed to currentEngine setter");
 
     var newCurrentEngine = this.getEngineByName(val.name);
     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) {
         newCurrentEngine.setAttr("loadPathHash", loadPathHash);
         notifyAction(newCurrentEngine, SEARCH_ENGINE_CHANGED);
       }
     }
 
@@ -4171,17 +4207,17 @@ SearchService.prototype = {
     this._currentEngine = newCurrentEngine;
 
     // If we change the default engine in the future, that change should impact
     // users who have switched away from and then back to the build's "default"
     // engine. So clear the user pref when the currentEngine is set to the
     // 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 = "";
     }
 
     this.setGlobalAttr("current", newName);
     this.setGlobalAttr("hash", getVerificationHash(newName));
 
     notifyAction(this._currentEngine, SEARCH_ENGINE_DEFAULT);
     notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js
@@ -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);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchReset.js
@@ -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);
+});
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -89,8 +89,10 @@ tags = addons
 [test_remove_profile_engine.js]
 [test_selectedEngine.js]
 [test_geodefaults.js]
 [test_hidden.js]
 [test_currentEngine_fallback.js]
 [test_require_engines_in_cache.js]
 [test_update_telemetry.js]
 [test_svg_icon.js]
+[test_searchReset.js]
+[test_addEngineWithDetails.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5513,16 +5513,25 @@
   },
   "SEARCH_COUNTS": {
     "expires_in_version": "never",
     "kind": "count",
     "keyed": true,
     "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",
     "high": 1000,
     "n_buckets": 15,
     "description": "Time (ms) it takes to initialize the search service"
   },
   "SEARCH_SERVICE_INIT_SYNC": {
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -1384,18 +1384,18 @@ add_task(function* test_defaultSearchEng
   data = TelemetryEnvironment.currentEnvironment;
   checkEnvironmentData(data);
 
   const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
   Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
 
   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");
 
   // Cleanly install an engine from an xml file, and check if origin is
   // recorded as "verified".
   gNow = fakeNow(futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE));
   let promise = new Promise(resolve => {
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -170,45 +170,49 @@ html|button {
   padding: 3px;
   /* override forms.css */
   font: inherit;
 }
 
 /* xul buttons and menulists */
 
 *|button,
+html|select,
 xul|colorpicker[type="button"],
 xul|menulist {
   -moz-appearance: none;
   min-height: 30px;
   color: var(--in-content-text-color);
   border: 1px solid var(--in-content-box-border-color);
   -moz-border-top-colors: none !important;
   -moz-border-right-colors: none !important;
   -moz-border-bottom-colors: none !important;
   -moz-border-left-colors: none !important;
   border-radius: 2px;
   background-color: var(--in-content-page-background);
 }
 
 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 {
   background-color: var(--in-content-box-background-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"]) {
   background-color: var(--in-content-box-background-active);
 }
 
 html|button:disabled,
+html|select:disabled,
 xul|button[disabled="true"],
 xul|colorpicker[type="button"][disabled="true"],
 xul|menulist[disabled="true"] {
   opacity: 0.5;
 }
 
 *|button.primary {
   background-color: var(--in-content-primary-button-background);