Bug 1690750 - Simplify OpenSearchEngine to only allow loading engines from protocols where users can load them from. r=mak
authorMark Banner <standard8@mozilla.com>
Wed, 10 Feb 2021 18:12:08 +0000
changeset 566865 63167d041bb4f68e77e46ed6e941003e131ecc8e
parent 566864 eb2907b5ef3e545fa9c53893347eeffc476688c9
child 566866 ce88b75e5b6c6cca0a4adc919f2fafdb40c89389
push id38191
push userbtara@mozilla.com
push dateThu, 11 Feb 2021 05:02:45 +0000
treeherdermozilla-central@5cbcb80f72bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1690750
milestone87.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 1690750 - Simplify OpenSearchEngine to only allow loading engines from protocols where users can load them from. r=mak The urls where an OpenSearch engine can be loaded from are already limited in LinkHandlerChild. This is cleaning up and simplifying what the OpenSearchEngine allows, and as a result allows the load path handling to be greatly simplified. The test changes are due to no longer allowing chrome or file protocols. For future, we probably want to move away from OpenSearch for most of these, but the changes will make it easier to find the places to update. Differential Revision: https://phabricator.services.mozilla.com/D104010
browser/actors/LinkHandlerChild.jsm
browser/base/content/test/about/browser_aboutHome_search_composing.js
browser/base/content/test/about/browser_aboutHome_search_suggestion.js
browser/base/content/test/about/browser_aboutHome_search_telemetry.js
browser/base/content/test/about/head.js
browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
browser/components/search/test/browser/browser_contextSearchTabPosition.js
browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js
browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js
browser/components/search/test/browser/browser_oneOffContextMenu.js
browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
browser/components/search/test/browser/browser_private_search_perwindowpb.js
browser/components/search/test/browser/browser_search_telemetry_searchbar.js
browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
browser/components/search/test/browser/browser_searchbar_openpopup.js
browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
browser/components/search/test/browser/head.js
browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js
browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js
browser/modules/test/browser/browser_ContentSearch.js
toolkit/components/search/OpenSearchEngine.jsm
toolkit/components/search/SearchEngine.jsm
toolkit/components/search/SearchService.jsm
toolkit/components/search/tests/SearchTestUtils.jsm
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
--- a/browser/actors/LinkHandlerChild.jsm
+++ b/browser/actors/LinkHandlerChild.jsm
@@ -141,16 +141,18 @@ class LinkHandlerChild extends JSWindowA
           ) {
             break;
           }
 
           if (!searchAdded && event.type == "DOMLinkAdded") {
             let type = link.type && link.type.toLowerCase();
             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
+            // Note: This protocol list should be kept in sync with
+            // the one in OpenSearchEngine's install function.
             let re = /^(?:https?|ftp):/i;
             if (
               type == "application/opensearchdescription+xml" &&
               link.title &&
               re.test(link.href)
             ) {
               let engine = { title: link.title, href: link.href };
               this.sendAsyncMessage("Link:AddSearch", {
--- a/browser/base/content/test/about/browser_aboutHome_search_composing.js
+++ b/browser/base/content/test/about/browser_aboutHome_search_composing.js
@@ -10,17 +10,19 @@ add_task(async function() {
   await BrowserTestUtils.withNewTab(
     { gBrowser, url: "about:home" },
     async function(browser) {
       // Add a test engine that provides suggestions and switch to it.
       let currEngine = await Services.search.getDefault();
 
       let engine;
       await promiseContentSearchChange(browser, async () => {
-        engine = await promiseNewEngine("searchSuggestionEngine.xml");
+        engine = await SearchTestUtils.promiseNewSearchEngine(
+          getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+        );
         await Services.search.setDefault(engine);
         return engine.name;
       });
 
       // Clear any search history results
       await new Promise((resolve, reject) => {
         FormHistory.update(
           { op: "remove" },
--- a/browser/base/content/test/about/browser_aboutHome_search_suggestion.js
+++ b/browser/base/content/test/about/browser_aboutHome_search_suggestion.js
@@ -11,17 +11,19 @@ add_task(async function() {
   await BrowserTestUtils.withNewTab(
     { gBrowser, url: "about:home" },
     async function(browser) {
       // Add a test engine that provides suggestions and switch to it.
       let currEngine = await Services.search.getDefault();
 
       let engine;
       await promiseContentSearchChange(browser, async () => {
-        engine = await promiseNewEngine("searchSuggestionEngine.xml");
+        engine = await SearchTestUtils.promiseNewSearchEngine(
+          getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+        );
         await Services.search.setDefault(engine);
         return engine.name;
       });
 
       await SpecialPowers.spawn(browser, [], async function() {
         // Type an X in the search input.
         let input = content.document.querySelector([
           "#searchText",
--- a/browser/base/content/test/about/browser_aboutHome_search_telemetry.js
+++ b/browser/base/content/test/about/browser_aboutHome_search_telemetry.js
@@ -11,17 +11,19 @@ add_task(async function() {
 
   await BrowserTestUtils.withNewTab(
     { gBrowser, url: "about:home" },
     async function(browser) {
       let currEngine = await Services.search.getDefault();
 
       let engine;
       await promiseContentSearchChange(browser, async () => {
-        engine = await promiseNewEngine("searchSuggestionEngine.xml");
+        engine = await SearchTestUtils.promiseNewSearchEngine(
+          getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+        );
         await Services.search.setDefault(engine);
         return engine.name;
       });
 
       await SpecialPowers.spawn(
         browser,
         [{ expectedName: engine.name }],
         async function(args) {
--- a/browser/base/content/test/about/head.js
+++ b/browser/base/content/test/about/head.js
@@ -1,14 +1,17 @@
 /* eslint-env mozilla/frame-script */
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   FormHistory: "resource://gre/modules/FormHistory.jsm",
+  SearchTestUtils: "resource://testing-common/SearchTestUtils.jsm",
 });
 
+SearchTestUtils.init(this);
+
 function getSecurityInfo(securityInfoAsString) {
   const serhelper = Cc[
     "@mozilla.org/network/serialization-helper;1"
   ].getService(Ci.nsISerializationHelper);
   let securityInfo = serhelper.deserializeObject(securityInfoAsString);
   securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
   return securityInfo;
 }
@@ -205,42 +208,16 @@ async function promiseContentSearchChang
         content._searchDetails.listener,
         { mozSystemGroup: true }
       );
       delete content._searchDetails;
     }
   );
 }
 
-/**
- * Wait for the search engine to be added.
- */
-async function promiseNewEngine(basename) {
-  info("Waiting for engine to be added: " + basename);
-  let url = getRootDirectory(gTestPath) + basename;
-  let engine;
-  try {
-    engine = await Services.search.addOpenSearchEngine(url, "");
-  } catch (errCode) {
-    ok(false, "addEngine failed with error code " + errCode);
-    throw errCode;
-  }
-
-  info("Search engine added: " + basename);
-  registerCleanupFunction(async () => {
-    try {
-      await Services.search.removeEngine(engine);
-    } catch (ex) {
-      /* Can't remove the engine more than once */
-    }
-  });
-
-  return engine;
-}
-
 async function waitForBookmarksToolbarVisibility({
   win = window,
   visible,
   message,
 }) {
   let result = await TestUtils.waitForCondition(() => {
     let toolbar = win.document.getElementById("PersonalToolbar");
     return toolbar && (visible ? !toolbar.collapsed : toolbar.collapsed);
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
@@ -4,20 +4,27 @@
 
 ChromeUtils.defineModuleGetter(
   this,
   "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
+  "SearchTestUtils",
+  "resource://testing-common/SearchTestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
   "UrlbarTestUtils",
   "resource://testing-common/UrlbarTestUtils.jsm"
 );
 
+SearchTestUtils.init(this);
+
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
 function promiseAutocompleteResultPopup(value) {
   return UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
     waitForFocus,
     value,
@@ -38,31 +45,23 @@ async function addBookmark(bookmark) {
     title: bookmark.title,
   });
 
   registerCleanupFunction(async function() {
     await PlacesUtils.bookmarks.eraseEverything();
   });
 }
 
-async function addSearchEngine(basename) {
-  info("Waiting for engine to be added: " + basename);
-  let url = getRootDirectory(gTestPath) + basename;
-  let engine = await Services.search.addOpenSearchEngine(url, "");
-
-  info(`Search engine added: ${basename}`);
-  registerCleanupFunction(async () => Services.search.removeEngine(engine));
-  return engine;
-}
-
 async function prepareSearchEngine() {
   let oldDefaultEngine = await Services.search.getDefault();
   let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
-  let engine = await addSearchEngine(TEST_ENGINE_BASENAME);
+  let engine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+  );
   await Services.search.setDefault(engine);
 
   registerCleanupFunction(async function() {
     Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     await Services.search.setDefault(oldDefaultEngine);
 
     // Make sure the popup is closed for the next test.
     await UrlbarTestUtils.promisePopupClose(window);
--- a/browser/components/search/test/browser/browser_contextSearchTabPosition.js
+++ b/browser/components/search/test/browser/browser_contextSearchTabPosition.js
@@ -1,14 +1,26 @@
 /* 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/. */
 
+let engine;
+
+add_task(async function setup() {
+  engine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine.xml"
+  );
+  const current = await Services.search.getDefault();
+  await Services.search.setDefault(engine);
+  registerCleanupFunction(async () => {
+    await Services.search.setDefault(current);
+  });
+});
+
 add_task(async function test() {
-  let engine = await promiseNewEngine("testEngine.xml");
   let histogramKey = "other-" + engine.name + ".contextmenu";
   let numSearchesBefore = 0;
 
   try {
     let hs = Services.telemetry
       .getKeyedHistogramById("SEARCH_COUNTS")
       .snapshot();
     if (histogramKey in hs) {
--- a/browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js
+++ b/browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js
@@ -1,24 +1,24 @@
 /* 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/. */
 const testPref = "Foo,FooDupe";
 
-async function promiseNewEngine(basename) {
-  info("Waiting for engine to be added: " + basename);
-  let url = getRootDirectory(gTestPath) + basename;
-  let engine = await Services.search.addOpenSearchEngine(url, "");
-  info("Search engine added: " + basename);
-  return engine;
-}
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+});
 
 add_task(async function test_remove() {
-  await promiseNewEngine("testEngine_dupe.xml");
-  await promiseNewEngine("testEngine.xml");
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine_dupe.xml"
+  );
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine.xml"
+  );
   Services.prefs.setCharPref("browser.search.hiddenOneOffs", testPref);
 
   info("Removing testEngine_dupe.xml");
   await Services.search.removeEngine(
     Services.search.getEngineByName("FooDupe")
   );
 
   let hiddenOneOffs = Services.prefs
@@ -47,21 +47,24 @@ add_task(async function test_remove() {
   is(
     Services.prefs.getCharPref("browser.search.hiddenOneOffs"),
     "",
     "hiddenOneOffs is empty after removing all hidden engines."
   );
 });
 
 add_task(async function test_add() {
-  await promiseNewEngine("testEngine.xml");
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine.xml"
+  );
   info("setting prefs to " + testPref);
   Services.prefs.setCharPref("browser.search.hiddenOneOffs", testPref);
-  await promiseNewEngine("testEngine_dupe.xml");
-
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine_dupe.xml"
+  );
   let hiddenOneOffs = Services.prefs
     .getCharPref("browser.search.hiddenOneOffs")
     .split(",");
 
   is(
     hiddenOneOffs.length,
     1,
     "hiddenOneOffs has the correct number of hidden engines present post add."
@@ -81,17 +84,19 @@ add_task(async function test_add() {
 add_task(async function test_diacritics() {
   const diacritic_engine = "Foo \u2661";
   let Preferences = ChromeUtils.import(
     "resource://gre/modules/Preferences.jsm",
     {}
   ).Preferences;
 
   Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
-  await promiseNewEngine("testEngine_diacritics.xml");
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine_diacritics.xml"
+  );
 
   let hiddenOneOffs = Preferences.get("browser.search.hiddenOneOffs").split(
     ","
   );
   is(
     hiddenOneOffs.some(x => x == diacritic_engine),
     false,
     "Observer cleans up added hidden engines that include a diacritic."
@@ -106,18 +111,8 @@ add_task(async function test_diacritics(
 
   hiddenOneOffs = Preferences.get("browser.search.hiddenOneOffs").split(",");
   is(
     hiddenOneOffs.some(x => x == diacritic_engine),
     false,
     "Observer cleans up removed hidden engines that include a diacritic."
   );
 });
-
-registerCleanupFunction(async () => {
-  info("Removing testEngine.xml");
-  await Services.search.removeEngine(Services.search.getEngineByName("Foo"));
-  info("Removing testEngine_dupe.xml");
-  await Services.search.removeEngine(
-    Services.search.getEngineByName("FooDupe")
-  );
-  Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
-});
--- a/browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js
+++ b/browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js
@@ -17,17 +17,19 @@ let searchIcon;
 add_task(async function init() {
   let searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
   searchIcon = searchbar.querySelector(".searchbar-search-button");
 
   let defaultEngine = await Services.search.getDefault();
-  await promiseNewEngine("testEngine_diacritics.xml", { setAsCurrent: false });
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine_diacritics.xml"
+  );
   registerCleanupFunction(async () => {
     await Services.search.setDefault(defaultEngine);
     Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
   });
 });
 
 add_task(async function test_hidden() {
   Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
--- a/browser/components/search/test/browser/browser_oneOffContextMenu.js
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu.js
@@ -18,19 +18,19 @@ let searchIcon;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
   searchIcon = searchbar.querySelector(".searchbar-search-button");
 
-  await promiseNewEngine(TEST_ENGINE_BASENAME, {
-    setAsCurrent: false,
-  });
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+  );
 });
 
 add_task(async function telemetry() {
   // Open the popup.
   let shownPromise = promiseEvent(searchPopup, "popupshown");
   let builtPromise = promiseEvent(oneOffInstance, "rebuild");
   info("Opening search panel");
   EventUtils.synthesizeMouseAtCenter(searchIcon, {});
--- a/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
@@ -23,19 +23,19 @@ add_task(async function init() {
     ],
   });
   originalEngine = await Services.search.getDefault();
   originalPrivateEngine = await Services.search.getDefaultPrivate();
   registerCleanupFunction(async () => {
     await resetEngines();
   });
 
-  await promiseNewEngine(TEST_ENGINE_BASENAME, {
-    setAsCurrent: false,
-  });
+  await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+  );
 });
 
 async function testSearchBarChangeEngine(win, testPrivate, isPrivateWindow) {
   info(
     `Testing search bar with testPrivate: ${testPrivate} isPrivateWindow: ${isPrivateWindow}`
   );
 
   const searchPopup = win.document.getElementById("PopupSearchAutoComplete");
--- a/browser/components/search/test/browser/browser_private_search_perwindowpb.js
+++ b/browser/components/search/test/browser/browser_private_search_perwindowpb.js
@@ -1,15 +1,22 @@
 // This test performs a search in a public window, then a different
 // search in a private window, and then checks in the public window
 // whether there is an autocomplete entry for the private search.
 
 add_task(async function test_setup() {
   await gCUITestUtils.addSearchBar();
-  registerCleanupFunction(() => {
+  let engine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "426329.xml"
+  );
+
+  const current = await Services.search.getDefault();
+  await Services.search.setDefault(engine);
+  registerCleanupFunction(async () => {
+    await Services.search.setDefault(current);
     gCUITestUtils.removeSearchBar();
   });
 });
 
 add_task(async function() {
   let windowsToClose = [];
 
   function performSearch(aWin, aIsPrivate) {
@@ -31,18 +38,16 @@ add_task(async function() {
     let win = await BrowserTestUtils.openNewBrowserWindow({
       private: aIsPrivate,
     });
     await SimpleTest.promiseFocus(win);
     windowsToClose.push(win);
     return win;
   }
 
-  await promiseNewEngine("426329.xml", { iconURL: "data:image/x-icon,%00" });
-
   let newWindow = await testOnWindow(false);
   await performSearch(newWindow, false);
 
   newWindow = await testOnWindow(true);
   await performSearch(newWindow, true);
 
   newWindow = await testOnWindow(false);
 
--- a/browser/components/search/test/browser/browser_search_telemetry_searchbar.js
+++ b/browser/components/search/test/browser/browser_search_telemetry_searchbar.js
@@ -3,16 +3,18 @@
 const SCALAR_SEARCHBAR = "browser.engagement.navigation.searchbar";
 
 ChromeUtils.defineModuleGetter(
   this,
   "UrlbarTestUtils",
   "resource://testing-common/UrlbarTestUtils.jsm"
 );
 
+let suggestionEngine;
+
 function checkHistogramResults(resultIndexes, expected, histogram) {
   for (let [i, val] of Object.entries(resultIndexes.values)) {
     if (i == expected) {
       Assert.equal(
         val,
         1,
         `expected counts should match for ${histogram} index ${i}`
       );
@@ -62,16 +64,19 @@ function clickSearchbarSuggestion(entryN
 
   // Make sure the suggestion is visible and simulate the click.
   richlistbox.ensureElementIsVisible(richlistitem);
   EventUtils.synthesizeMouseAtCenter(richlistitem, clickOptions);
 }
 
 add_task(async function setup() {
   await gCUITestUtils.addSearchBar();
+  const url = getRootDirectory(gTestPath) + "telemetrySearchSuggestions.xml";
+  suggestionEngine = await SearchTestUtils.promiseNewSearchEngine(url, "");
+
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
 
   // Create two new search engines. Mark one as the default engine, so
   // the test don't crash. We need to engines for this test as the searchbar
   // doesn't display the default search engine among the one-off engines.
   await Services.search.addEngineWithDetails("MozSearch", {
@@ -254,20 +259,16 @@ add_task(async function test_oneOff_ente
 // histogram since test_oneOff_enter covers everything else.
 add_task(async function test_oneOff_enterSelection() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
   let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
   );
 
-  // Create an engine to generate search suggestions and add it as default
-  // for this test.
-  const url = getRootDirectory(gTestPath) + "telemetrySearchSuggestions.xml";
-  let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
   let previousEngine = await Services.search.getDefault();
   await Services.search.setDefault(suggestionEngine);
 
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     "about:blank"
   );
 
@@ -286,17 +287,16 @@ add_task(async function test_oneOff_ente
   let resultMethods = resultMethodHist.snapshot();
   checkHistogramResults(
     resultMethods,
     UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
     "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
   );
 
   await Services.search.setDefault(previousEngine);
-  await Services.search.removeEngine(suggestionEngine);
   BrowserTestUtils.removeTab(tab);
 });
 
 // Performs a search using a click on a one-off button.  This only tests the
 // FX_SEARCHBAR_SELECTED_RESULT_METHOD histogram since test_oneOff_enter covers
 // everything else.
 add_task(async function test_oneOff_click() {
   // Let's reset the counts.
@@ -335,20 +335,16 @@ async function checkSuggestionClick(clic
   Services.telemetry.clearEvents();
   let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
   );
   let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
     "SEARCH_COUNTS"
   );
 
-  // Create an engine to generate search suggestions and add it as default
-  // for this test.
-  const url = getRootDirectory(gTestPath) + "telemetrySearchSuggestions.xml";
-  let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
   let previousEngine = await Services.search.getDefault();
   await Services.search.setDefault(suggestionEngine);
 
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     "about:blank"
   );
 
@@ -397,17 +393,16 @@ async function checkSuggestionClick(clic
   let resultMethods = resultMethodHist.snapshot();
   checkHistogramResults(
     resultMethods,
     UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
     "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
   );
 
   await Services.search.setDefault(previousEngine);
-  await Services.search.removeEngine(suggestionEngine);
   BrowserTestUtils.removeTab(tab);
 }
 
 // Clicks the first suggestion offered by the test search engine.
 add_task(async function test_suggestion_click() {
   await checkSuggestionClick({}, tab => {
     return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
   });
@@ -429,20 +424,16 @@ add_task(async function test_suggestion_
 // covers everything else.
 add_task(async function test_suggestion_enterSelection() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
   let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
   );
 
-  // Create an engine to generate search suggestions and add it as default
-  // for this test.
-  const url = getRootDirectory(gTestPath) + "telemetrySearchSuggestions.xml";
-  let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
   let previousEngine = await Services.search.getDefault();
   await Services.search.setDefault(suggestionEngine);
 
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     "about:blank"
   );
 
@@ -457,11 +448,10 @@ add_task(async function test_suggestion_
   let resultMethods = resultMethodHist.snapshot();
   checkHistogramResults(
     resultMethods,
     UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
     "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
   );
 
   await Services.search.setDefault(previousEngine);
-  await Services.search.removeEngine(suggestionEngine);
   BrowserTestUtils.removeTab(tab);
 });
--- a/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
@@ -41,25 +41,25 @@ async function checkHeader(engine) {
     });
   }
   Assert.ok(
     header.getAttribute("value").includes(engine.name),
     "Should have the correct engine name displayed in the header"
   );
 }
 
-add_task(async function init() {
+add_task(async function setup() {
   searchbar = await gCUITestUtils.addSearchBar();
-  registerCleanupFunction(() => {
-    gCUITestUtils.removeSearchBar();
-  });
   textbox = searchbar.textbox;
 
-  await promiseNewEngine("testEngine.xml");
-
+  let defaultEngine = await Services.search.getDefault();
+  let engine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine.xml"
+  );
+  await Services.search.setDefault(engine);
   // First cleanup the form history in case other tests left things there.
   await new Promise((resolve, reject) => {
     info("cleanup the search history");
     searchbar.FormHistory.update(
       { op: "remove", fieldname: "searchbar-history" },
       { handleCompletion: resolve, handleError: reject }
     );
   });
@@ -71,16 +71,21 @@ add_task(async function init() {
     });
     searchbar.FormHistory.update(addOps, {
       handleCompletion: resolve,
       handleError: reject,
     });
   });
 
   textbox.value = kUserValue;
+
+  registerCleanupFunction(async () => {
+    await Services.search.setDefault(defaultEngine);
+    gCUITestUtils.removeSearchBar();
+  });
 });
 
 add_task(async function test_arrows() {
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
   searchbar.focus();
   await promise;
   is(
--- a/browser/components/search/test/browser/browser_searchbar_openpopup.js
+++ b/browser/components/search/test/browser/browser_searchbar_openpopup.js
@@ -42,26 +42,30 @@ async function startCustomizing(aWindow 
   return eventPromise;
 }
 
 let searchbar;
 let textbox;
 let searchIcon;
 let goButton;
 
-add_task(async function init() {
+add_task(async function setup() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
   textbox = searchbar.textbox;
   searchIcon = searchbar.querySelector(".searchbar-search-button");
   goButton = searchbar.querySelector(".search-go-button");
 
-  await promiseNewEngine("testEngine.xml");
+  let defaultEngine = await Services.search.getDefault();
+  let engine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine.xml"
+  );
+  await Services.search.setDefault(engine);
 
   // First cleanup the form history in case other tests left things there.
   await new Promise((resolve, reject) => {
     info("cleanup the search history");
     searchbar.FormHistory.update(
       { op: "remove", fieldname: "searchbar-history" },
       { handleCompletion: resolve, handleError: reject }
     );
@@ -72,16 +76,21 @@ add_task(async function init() {
     let addOps = kValues.map(value => {
       return { op: "add", fieldname: "searchbar-history", value };
     });
     searchbar.FormHistory.update(addOps, {
       handleCompletion: resolve,
       handleError: reject,
     });
   });
+
+  registerCleanupFunction(async () => {
+    await Services.search.setDefault(defaultEngine);
+    gCUITestUtils.removeSearchBar();
+  });
 });
 
 // Adds a task that shouldn't show the search suggestions popup.
 function add_no_popup_task(task) {
   add_task(async function() {
     let sawPopup = false;
     function listener() {
       sawPopup = true;
--- a/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
+++ b/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -19,25 +19,29 @@ function getOpenSearchItems() {
 
   return os;
 }
 
 let searchbar;
 let textbox;
 let searchIcon;
 
-add_task(async function init() {
+add_task(async function setup() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
   textbox = searchbar.textbox;
   searchIcon = searchbar.querySelector(".searchbar-search-button");
 
-  await promiseNewEngine("testEngine.xml");
+  let defaultEngine = await Services.search.getDefault();
+  let engine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "testEngine.xml"
+  );
+  await Services.search.setDefault(engine);
 
   // First cleanup the form history in case other tests left things there.
   await new Promise((resolve, reject) => {
     info("cleanup the search history");
     searchbar.FormHistory.update(
       { op: "remove", fieldname: "searchbar-history" },
       { handleCompletion: resolve, handleError: reject }
     );
@@ -48,16 +52,21 @@ add_task(async function init() {
     let addOps = kValues.map(value => {
       return { op: "add", fieldname: "searchbar-history", value };
     });
     searchbar.FormHistory.update(addOps, {
       handleCompletion: resolve,
       handleError: reject,
     });
   });
+
+  registerCleanupFunction(async () => {
+    await Services.search.setDefault(defaultEngine);
+    gCUITestUtils.removeSearchBar();
+  });
 });
 
 add_task(async function test_arrows() {
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
   EventUtils.synthesizeMouseAtCenter(searchIcon, {});
   await promise;
   info(
--- a/browser/components/search/test/browser/head.js
+++ b/browser/components/search/test/browser/head.js
@@ -63,65 +63,16 @@ function promiseEvent(aTarget, aEventNam
     }
 
     return true;
   }
 
   return BrowserTestUtils.waitForEvent(aTarget, aEventName, false, cancelEvent);
 }
 
-/**
- * Adds a new search engine to the search service and confirms it completes.
- *
- * @param {string} basename  The file to load that contains the search engine
- *                           details.
- * @param {object} [options] Options for the test:
- *   - {String} [iconURL]       The icon to use for the search engine.
- *   - {Boolean} [setAsCurrent] Whether to set the new engine to be the
- *                              current engine or not.
- *   - {Boolean} [setAsCurrentPrivate] Whether to set the new engine to be the
- *                              current private browsing mode engine or not.
- *                              Defaults to false.
- *   - {String} [testPath]      Used to override the current test path if this
- *                              file is used from a different directory.
- * @returns {Promise} The promise is resolved once the engine is added, or
- *                    rejected if the addition failed.
- */
-async function promiseNewEngine(basename, options = {}) {
-  // Default the setAsCurrent option to true.
-  let setAsCurrent =
-    options.setAsCurrent == undefined ? true : options.setAsCurrent;
-  info("Waiting for engine to be added: " + basename);
-  let url = getRootDirectory(options.testPath || gTestPath) + basename;
-  let engine = await Services.search.addOpenSearchEngine(
-    url,
-    options.iconURL || ""
-  );
-  info("Search engine added: " + basename);
-  const current = await Services.search.getDefault();
-  if (setAsCurrent) {
-    await Services.search.setDefault(engine);
-  }
-  const currentPrivate = await Services.search.getDefaultPrivate();
-  if (options.setAsCurrentPrivate) {
-    await Services.search.setDefaultPrivate(engine);
-  }
-  registerCleanupFunction(async () => {
-    if (setAsCurrent) {
-      await Services.search.setDefault(current);
-    }
-    if (options.setAsCurrentPrivate) {
-      await Services.search.setDefaultPrivate(currentPrivate);
-    }
-    await Services.search.removeEngine(engine);
-    info("Search engine removed: " + basename);
-  });
-  return engine;
-}
-
 // Get an array of the one-off buttons.
 function getOneOffs() {
   let oneOffs = [];
   let searchPopup = document.getElementById("PopupSearchAutoComplete");
   let oneOffsContainer = searchPopup.searchOneOffsContainer;
   let oneOff = oneOffsContainer.querySelector(".search-panel-one-offs");
   for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
     if (oneOff.nodeType == Node.ELEMENT_NODE) {
--- a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js
@@ -67,19 +67,19 @@ async function clickURLBarSuggestion(res
 /**
  * Create an engine to generate search suggestions and add it as default
  * for this test.
  *
  * @param {function} taskFn
  *   The function to run with the new search engine as default.
  */
 async function withNewSearchEngine(taskFn) {
-  const url =
-    getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml";
-  let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
+  let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml"
+  );
   let previousEngine = await Services.search.getDefault();
   await Services.search.setDefault(suggestionEngine);
 
   try {
     await taskFn(suggestionEngine);
   } finally {
     await Services.search.setDefault(previousEngine);
     await Services.search.removeEngine(suggestionEngine);
--- a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js
@@ -88,19 +88,19 @@ add_task(async function setup() {
       // Disable tab-to-search onboarding results for general tests. They are
       // enabled in tests that specifically address onboarding.
       ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
     ],
   });
 
   // Create an engine to generate search suggestions and add it as default
   // for this test.
-  const url =
-    getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml";
-  let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
+  let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml"
+  );
   suggestionEngine.alias = ENGINE_ALIAS;
   engineDomain = suggestionEngine.getResultDomain();
   engineName = suggestionEngine.name;
 
   // Make it the default search engine.
   let originalEngine = await Services.search.getDefault();
   await Services.search.setDefault(suggestionEngine);
 
@@ -126,17 +126,16 @@ add_task(async function setup() {
 
   // Allows UrlbarTestUtils to access this scope's test helpers, like Assert.
   UrlbarTestUtils.init(this);
 
   // Make sure to restore the engine once we're done.
   registerCleanupFunction(async function() {
     Services.telemetry.canRecordExtended = oldCanRecord;
     await Services.search.setDefault(originalEngine);
-    await Services.search.removeEngine(suggestionEngine);
     await PlacesUtils.history.clear();
     Services.telemetry.setEventRecordingEnabled("navigation", false);
     UrlbarTestUtils.uninit();
   });
 });
 
 // Clicks the first one off.
 add_task(async function test_oneoff_remote() {
--- a/browser/modules/test/browser/browser_ContentSearch.js
+++ b/browser/modules/test/browser/browser_ContentSearch.js
@@ -436,18 +436,19 @@ async function waitForTestMsg(browser, t
 async function waitForNewEngine(browser, basename) {
   info("Waiting for engine to be added: " + basename);
 
   // Wait for the search events triggered by adding the new engine.
   // There are two events triggerd by engine-added and engine-loaded
   let statePromise = await waitForTestMsg(browser, "CurrentState", 2);
 
   // Wait for addOpenSearchEngine().
-  let url = getRootDirectory(gTestPath) + basename;
-  let engine = await Services.search.addOpenSearchEngine(url, "");
+  let engine = await SearchTestUtils.promiseNewSearchEngine(
+    getRootDirectory(gTestPath) + basename
+  );
   let results = await statePromise.donePromise;
   return [engine, ...results];
 }
 
 async function addTab() {
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     "about:newtab"
--- a/toolkit/components/search/OpenSearchEngine.jsm
+++ b/toolkit/components/search/OpenSearchEngine.jsm
@@ -4,29 +4,22 @@
 
 /* eslint no-shadow: error, mozilla/no-aArgs: error */
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  AppConstants: "resource://gre/modules/AppConstants.jsm",
   EngineURL: "resource://gre/modules/SearchEngine.jsm",
-  OS: "resource://gre/modules/osfile.jsm",
   SearchEngine: "resource://gre/modules/SearchEngine.jsm",
   SearchUtils: "resource://gre/modules/SearchUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
-XPCOMUtils.defineLazyServiceGetters(this, {
-  gChromeReg: ["@mozilla.org/chrome/chrome-registry;1", "nsIChromeRegistry"],
-  gEnvironment: ["@mozilla.org/process/environment;1", "nsIEnvironment"],
-});
-
 XPCOMUtils.defineLazyGetter(this, "logConsole", () => {
   return console.createInstance({
     prefix: "OpenSearchEngine",
     maxLogLevel: SearchUtils.loggingEnabled ? "Debug" : "Warn",
   });
 });
 
 const OPENSEARCH_NS_10 = "http://a9.com/-/spec/opensearch/1.0/";
@@ -67,101 +60,50 @@ function ENSURE_WARN(assertion, message,
 
 /**
  * OpenSearchEngine represents an OpenSearch base search engine.
  */
 class OpenSearchEngine extends SearchEngine {
   // The data describing the engine, in the form of an XML document element.
   _data = null;
 
-  /**
-   * Constructor.
-   *
-   * @param {object} options
-   *   The options for this search engine. At least one of
-   *   options.fileURI or options.uri are required.
-   * @param {nsIFile} [options.fileURI]
-   *   The file URI that points to the search engine data.
-   * @param {nsIURI|string} [options.uri]
-   *   Represents the location of the search engine data file.
-   */
-  constructor(options = {}) {
-    let file;
-    let uri;
-    let shortName;
-    if ("fileURI" in options && options.fileURI instanceof Ci.nsIFile) {
-      file = options.fileURI;
-      shortName = file.leafName;
-    } else if ("uri" in options) {
-      let optionsURI = options.uri;
-      if (typeof optionsURI == "string") {
-        optionsURI = SearchUtils.makeURI(optionsURI);
-      }
-      // makeURI can return null if the URI is invalid.
-      if (!optionsURI || !(optionsURI instanceof Ci.nsIURI)) {
-        throw new Components.Exception(
-          "options.uri isn't a string nor an nsIURI",
-          Cr.NS_ERROR_INVALID_ARG
-        );
-      }
-      switch (optionsURI.scheme) {
-        case "https":
-        case "http":
-        case "ftp":
-        case "data":
-        case "file":
-        case "resource":
-        case "chrome":
-          uri = optionsURI;
-          break;
-        default:
-          throw Components.Exception(
-            "Invalid URI passed to SearchEngine constructor",
-            Cr.NS_ERROR_INVALID_ARG
-          );
-      }
-      if (
-        gEnvironment.get("XPCSHELL_TEST_PROFILE_DIR") &&
-        uri.scheme == "resource"
-      ) {
-        shortName = uri.fileName;
-      }
-    }
-
-    if (shortName && shortName.endsWith(".xml")) {
-      shortName = shortName.slice(0, -4);
-    }
-
+  constructor() {
     super({
       isAppProvided: false,
-      loadPath: OpenSearchEngine.getAnonymizedLoadPath(shortName, file, uri),
+      // We don't know what this is until after it has loaded, so add a placeholder.
+      loadPath: "[opensearch]loading",
     });
   }
 
   /**
    * Retrieves the engine data from a URI. Initializes the engine, flushes to
    * disk, and notifies the search service once initialization is complete.
    *
    * @param {string|nsIURI} uri
    *   The uri to load the search plugin from.
    * @param {function} [callback]
    *   A callback to receive any details of errors.
    */
-  _initFromURIAndLoad(uri, callback) {
+  _install(uri, callback) {
     let loadURI = uri instanceof Ci.nsIURI ? uri : SearchUtils.makeURI(uri);
-    ENSURE_WARN(
-      loadURI,
-      "Must have URI when calling _initFromURIAndLoad!",
-      Cr.NS_ERROR_UNEXPECTED
-    );
+    if (!loadURI) {
+      throw Components.Exception(
+        loadURI,
+        "Must have URI when calling _install!",
+        Cr.NS_ERROR_UNEXPECTED
+      );
+    }
+    if (!/^(?:https?|ftp)$/i.test(loadURI.scheme)) {
+      throw Components.Exception(
+        "Invalid URI passed to SearchEngine constructor",
+        Cr.NS_ERROR_INVALID_ARG
+      );
+    }
 
-    logConsole.debug(
-      "_initFromURIAndLoad: Downloading engine from:",
-      loadURI.spec
-    );
+    logConsole.debug("_install: Downloading engine from:", loadURI.spec);
 
     var chan = SearchUtils.makeChannel(loadURI);
 
     if (this._engineToUpdate && chan instanceof Ci.nsIHttpChannel) {
       var lastModified = this._engineToUpdate.getAttr("updatelastmodified");
       if (lastModified) {
         chan.setRequestHeader("If-Modified-Since", lastModified, false);
       }
@@ -172,63 +114,16 @@ class OpenSearchEngine extends SearchEng
       chan,
       this._onLoad.bind(this, callback)
     );
     chan.notificationCallbacks = listener;
     chan.asyncOpen(listener);
   }
 
   /**
-   * Retrieves the data from the engine's file asynchronously.
-   * The document element is placed in the engine's data field.
-   *
-   * @param {nsIFile} file
-   *   The file to load the search plugin from.
-   */
-  async _initFromFile(file) {
-    if (!file || !(await OS.File.exists(file.path))) {
-      throw Components.Exception(
-        "File must exist before calling initFromFile!",
-        Cr.NS_ERROR_UNEXPECTED
-      );
-    }
-
-    let fileURI = Services.io.newFileURI(file);
-    await this._retrieveSearchXMLData(fileURI.spec);
-
-    // Now that the data is loaded, initialize the engine object
-    this._initFromData();
-  }
-
-  /**
-   * Retrieves the engine data for a given URI asynchronously.
-   *
-   * @param {string} url
-   *   The URL to get engine data from.
-   * @returns {Promise}
-   *   A promise, resolved successfully if retrieveing data succeeds.
-   */
-  _retrieveSearchXMLData(url) {
-    return new Promise(resolve => {
-      let request = new XMLHttpRequest();
-      request.overrideMimeType("text/xml");
-      request.onload = event => {
-        let responseXML = event.target.responseXML;
-        this._data = responseXML.documentElement;
-        resolve();
-      };
-      request.onerror = function(event) {
-        resolve();
-      };
-      request.open("GET", url, true);
-      request.send();
-    });
-  }
-
-  /**
    * Handle the successful download of an engine. Initializes the engine and
    * triggers parsing of the data. The engine is then flushed to disk. Notifies
    * the search service once initialization is complete.
    *
    * @param {function} callback
    *   A callback to receive success or failure notifications. May be null.
    * @param {array} bytes
    *  The loaded search engine data.
@@ -286,17 +181,16 @@ class OpenSearchEngine extends SearchEng
       if (Services.search.getEngineByName(this.name)) {
         onError(Ci.nsISearchService.ERROR_DUPLICATE_ENGINE);
         logConsole.debug("_onLoad: duplicate engine found, bailing");
         return;
       }
 
       this._loadPath = OpenSearchEngine.getAnonymizedLoadPath(
         SearchUtils.sanitizeName(this.name),
-        null,
         this._uri
       );
       if (this._extensionID) {
         this._loadPath += ":" + this._extensionID;
       }
       this.setAttr(
         "loadPathHash",
         SearchUtils.getVerificationHash(this._loadPath)
@@ -493,131 +387,16 @@ class OpenSearchEngine extends SearchEng
   }
 
   get _hasUpdates() {
     // Whether or not the engine has an update URL
     let selfURL = this._getURLOfType(SearchUtils.URL_TYPE.OPENSEARCH, "self");
     return !!(this._updateURL || this._iconUpdateURL || selfURL);
   }
 
-  /**
-   * Gets a directory from the directory service.
-   * @param {string} key
-   *   The directory service key indicating the directory to get.
-   * @param {nsIIDRef} iface
-   *   The expected interface type of the directory information.
-   * @returns {object}
-   */
-  static getDir(key, iface) {
-    return Services.dirsvc.get(key, iface || Ci.nsIFile);
-  }
-
   // This indicates where we found the .xml file to load the engine,
   // and attempts to hide user-identifiable data (such as username).
-  static getAnonymizedLoadPath(shortName, file, uri) {
-    /* Examples of expected output:
-     *   jar:[app]/omni.ja!browser/engine.xml
-     *     'browser' here is the name of the chrome package, not a folder.
-     *   [profile]/searchplugins/engine.xml
-     *   [distribution]/searchplugins/common/engine.xml
-     *   [other]/engine.xml
-     */
-
-    const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
-    const NS_APP_USER_PROFILE_50_DIR = "ProfD";
-    const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
-
-    const knownDirs = {
-      app: NS_XPCOM_CURRENT_PROCESS_DIR,
-      profile: NS_APP_USER_PROFILE_50_DIR,
-      distribution: XRE_APP_DISTRIBUTION_DIR,
-    };
-
-    let leafName = shortName;
-    if (!leafName) {
-      return "null";
-    }
-    leafName += ".xml";
-
-    let prefix = "",
-      suffix = "";
-    if (!file) {
-      if (uri.schemeIs("resource")) {
-        uri = SearchUtils.makeURI(
-          Services.io
-            .getProtocolHandler("resource")
-            .QueryInterface(Ci.nsISubstitutingProtocolHandler)
-            .resolveURI(uri)
-        );
-      }
-      let scheme = uri.scheme;
-      let packageName = "";
-      if (scheme == "chrome") {
-        packageName = uri.hostPort;
-        uri = gChromeReg.convertChromeURL(uri);
-      }
-
-      if (AppConstants.platform == "android") {
-        // On Android the omni.ja file isn't at the same path as the binary
-        // used to start the process. We tweak the path here so that the code
-        // shared with Desktop will correctly identify files from the omni.ja
-        // file as coming from the [app] folder.
-        let appPath = Services.io
-          .getProtocolHandler("resource")
-          .QueryInterface(Ci.nsIResProtocolHandler)
-          .getSubstitution("android");
-        if (appPath) {
-          appPath = appPath.spec;
-          let spec = uri.spec;
-          if (spec.includes(appPath)) {
-            let appURI = Services.io.newFileURI(
-              OpenSearchEngine.getDir(knownDirs.app)
-            );
-            uri = Services.io.newURI(spec.replace(appPath, appURI.spec));
-          }
-        }
-      }
-
-      if (uri instanceof Ci.nsINestedURI) {
-        prefix = "jar:";
-        suffix = "!" + packageName + "/" + leafName;
-        uri = uri.innermostURI;
-      }
-      if (uri instanceof Ci.nsIFileURL) {
-        file = uri.file;
-      } else {
-        let path = "[" + scheme + "]";
-        if (/^(?:https?|ftp)$/.test(scheme)) {
-          path += uri.host;
-        }
-        return path + "/" + leafName;
-      }
-    }
-
-    let id;
-    let enginePath = file.path;
-
-    for (let key in knownDirs) {
-      let path;
-      try {
-        path = this.getDir(knownDirs[key]).path;
-      } catch (e) {
-        // Getting XRE_APP_DISTRIBUTION_DIR throws during unit tests.
-        continue;
-      }
-      if (enginePath.startsWith(path)) {
-        id =
-          "[" + key + "]" + enginePath.slice(path.length).replace(/\\/g, "/");
-        break;
-      }
-    }
-
-    // If the folder doesn't have a known ancestor, don't record its path to
-    // avoid leaking user identifiable data.
-    if (!id) {
-      id = "[other]/" + file.leafName;
-    }
-
-    return prefix + id + suffix;
+  static getAnonymizedLoadPath(shortName, uri) {
+    return `[${uri.scheme}]${uri.host}/${shortName}.xml`;
   }
 }
 
 var EXPORTED_SYMBOLS = ["OpenSearchEngine"];
--- a/toolkit/components/search/SearchEngine.jsm
+++ b/toolkit/components/search/SearchEngine.jsm
@@ -379,19 +379,16 @@ class EngineURL {
         "new EngineURL: template is not a valid URI!",
         Cr.NS_ERROR_FAILURE
       );
     }
 
     switch (templateURI.scheme) {
       case "http":
       case "https":
-        // Disable these for now, see bug 295018
-        // case "file":
-        // case "resource":
         this.template = template;
         break;
       default:
         throw Components.Exception(
           "new EngineURL: template uses invalid scheme!",
           Cr.NS_ERROR_FAILURE
         );
     }
--- a/toolkit/components/search/SearchService.jsm
+++ b/toolkit/components/search/SearchService.jsm
@@ -1843,23 +1843,20 @@ SearchService.prototype = {
     });
   },
 
   async addOpenSearchEngine(engineURL, iconURL) {
     logConsole.debug("addEngine: Adding", engineURL);
     await this.init();
     let errCode;
     try {
-      var engine = new OpenSearchEngine({
-        uri: engineURL,
-        isAppProvided: false,
-      });
+      var engine = new OpenSearchEngine();
       engine._setIcon(iconURL, false);
       errCode = await new Promise(resolve => {
-        engine._initFromURIAndLoad(engineURL, errorCode => {
+        engine._install(engineURL, errorCode => {
           resolve(errorCode);
         });
       });
       if (errCode) {
         throw errCode;
       }
     } catch (ex) {
       throw Components.Exception(
@@ -2770,22 +2767,23 @@ var engineUpdateService = {
         : SearchUtils.makeURI(engine._updateURL);
     if (updateURI) {
       if (engine.isAppProvided && !updateURI.schemeIs("https")) {
         logConsole.debug("Invalid scheme for default engine update");
         return;
       }
 
       logConsole.debug("updating", engine.name, updateURI.spec);
-      testEngine = new OpenSearchEngine({
-        uri: updateURI,
-        isAppProvided: false,
-      });
+      testEngine = new OpenSearchEngine();
       testEngine._engineToUpdate = engine;
-      testEngine._initFromURIAndLoad(updateURI);
+      try {
+        testEngine._install(updateURI);
+      } catch (ex) {
+        logConsole.error("Failed to update", engine.name, ex);
+      }
     } else {
       logConsole.debug("invalid updateURI");
     }
 
     if (engine._iconUpdateURL) {
       // If we're updating the engine too, use the new engine object,
       // otherwise use the existing engine object.
       (testEngine || engine)._setIcon(engine._iconUpdateURL, true);
--- a/toolkit/components/search/tests/SearchTestUtils.jsm
+++ b/toolkit/components/search/tests/SearchTestUtils.jsm
@@ -41,20 +41,26 @@ var SearchTestUtils = Object.freeze({
    *
    * @param {string}   url                     The URL of the engine to add.
    * @param {Function} registerCleanupFunction Pass the registerCleanupFunction
    *                                           from the test's scope.
    * @returns {Promise} Returns a promise that is resolved with the new engine
    *                    or rejected if it fails.
    */
   async promiseNewSearchEngine(url) {
+    // OpenSearch engines can only be added via http protocols.
+    url = url.replace("chrome://mochitests/content", "https://example.com");
     let engine = await Services.search.addOpenSearchEngine(url, "");
-    gTestScope.registerCleanupFunction(async () =>
-      Services.search.removeEngine(engine)
-    );
+    gTestScope.registerCleanupFunction(async () => {
+      try {
+        await Services.search.removeEngine(engine);
+      } catch (ex) {
+        // Don't throw if the test has already removed it.
+      }
+    });
     return engine;
   },
 
   /**
    * Returns a promise that is resolved when an observer notification from the
    * search service fires with the specified data.
    *
    * @param {*} expectedData
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -2065,29 +2065,26 @@ add_task(async function test_defaultSear
         }
 
         Services.obs.removeObserver(obs, "browser-search-engine-modified");
         resolve(searchEngine);
       } catch (ex) {
         reject(ex);
       }
     }, "browser-search-engine-modified");
-    Services.search.addOpenSearchEngine(
-      "file://" + do_get_cwd().path + "/engine.xml",
-      null
-    );
+    Services.search.addOpenSearchEngine(gDataRoot + "/engine.xml", null);
   });
   await Services.search.setDefault(engine);
   await promise;
   TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
   let data = TelemetryEnvironment.currentEnvironment;
   checkEnvironmentData(data);
   Assert.deepEqual(data.settings.defaultSearchEngineData, {
     name: "engine-telemetry",
-    loadPath: "[other]/engine.xml",
+    loadPath: "[http]localhost/engine-telemetry.xml",
     origin: "verified",
   });
 
   // Now break this engine's load path hash.
   promise = new Promise(resolve => {
     TelemetryEnvironment.registerChangeListener(
       "testWatch_SearchDefault",
       resolve