Bug 1550599 - Move duplicated code out of SearchService/SearchEngine.jsm into SearchUtils.jsm. r=daleharvey
authorMark Banner <standard8@mozilla.com>
Fri, 10 May 2019 14:53:20 +0000
changeset 532237 77a70aed5f5577bddf31845e14453ad4b268bf75
parent 532236 292fec00f80015bbfabdf07c15a3b35184b8b1c0
child 532238 ee952bc0bc518f3976227e25051eb5f1ae70a00d
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdaleharvey
bugs1550599
milestone68.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 1550599 - Move duplicated code out of SearchService/SearchEngine.jsm into SearchUtils.jsm. r=daleharvey Differential Revision: https://phabricator.services.mozilla.com/D30635
toolkit/components/search/SearchEngine.jsm
toolkit/components/search/SearchService.jsm
toolkit/components/search/SearchUtils.jsm
toolkit/components/search/moz.build
--- a/toolkit/components/search/SearchEngine.jsm
+++ b/toolkit/components/search/SearchEngine.jsm
@@ -5,52 +5,29 @@
 
 /* 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",
   OS: "resource://gre/modules/osfile.jsm",
+  SearchUtils: "resource://gre/modules/SearchUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   gEnvironment: ["@mozilla.org/process/environment;1", "nsIEnvironment"],
   gChromeReg: ["@mozilla.org/chrome/chrome-registry;1", "nsIChromeRegistry"],
 });
 
-const BROWSER_SEARCH_PREF = "browser.search.";
-
-XPCOMUtils.defineLazyPreferenceGetter(this, "loggingEnabled", BROWSER_SEARCH_PREF + "log", false);
-
 const BinaryInputStream = Components.Constructor(
   "@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
-const APP_SEARCH_PREFIX = "resource://search-plugins/";
-
-// See documentation in nsISearchService.idl.
-const SEARCH_ENGINE_TOPIC        = "browser-search-engine-modified";
-
-const SEARCH_ENGINE_CHANGED      = "engine-changed";
-const SEARCH_ENGINE_LOADED       = "engine-loaded";
-
-// The following constants are left undocumented in nsISearchService.idl
-// For the moment, they are meant for testing/debugging purposes only.
-
-// Set an arbitrary cap on the maximum icon size. Without this, large icons can
-// cause big delays when loading them at startup.
-const MAX_ICON_SIZE   = 20000;
-
-// Default charset to use for sending search parameters. ISO-8859-1 is used to
-// match previous nsInternetSearchService behavior as a URL parameter. Label
-// resolution causes windows-1252 to be actually used.
-const DEFAULT_QUERY_CHARSET = "ISO-8859-1";
-
 const SEARCH_BUNDLE = "chrome://global/locale/search/search.properties";
 const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
 
 const OPENSEARCH_NS_10  = "http://a9.com/-/spec/opensearch/1.0/";
 const OPENSEARCH_NS_11  = "http://a9.com/-/spec/opensearch/1.1/";
 
 // Although the specification at http://opensearch.a9.com/spec/1.1/description/
 // gives the namespace names defined above, many existing OpenSearch engines
@@ -61,20 +38,16 @@ const OPENSEARCH_NAMESPACES = [
   "http://a9.com/-/spec/opensearchdescription/1.0/",
 ];
 
 const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
 
 const MOZSEARCH_NS_10     = "http://www.mozilla.org/2006/browser/search/";
 const MOZSEARCH_LOCALNAME = "SearchPlugin";
 
-const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
-const URLTYPE_SEARCH_HTML  = "text/html";
-const URLTYPE_OPENSEARCH   = "application/opensearchdescription+xml";
-
 const USER_DEFINED = "searchTerms";
 
 // Custom search parameters
 const MOZ_PARAM_LOCALE         = "moz:locale";
 const MOZ_PARAM_DIST_ID        = "moz:distributionID";
 const MOZ_PARAM_OFFICIAL       = "moz:official";
 
 // Supported OpenSearch parameters
@@ -106,39 +79,16 @@ const OS_PARAM_START_PAGE_DEF   = "1"; /
 // give us the output we want.
 var OS_UNSUPPORTED_PARAMS = [
   [OS_PARAM_COUNT, OS_PARAM_COUNT_DEF],
   [OS_PARAM_START_INDEX, OS_PARAM_START_INDEX_DEF],
   [OS_PARAM_START_PAGE, OS_PARAM_START_PAGE_DEF],
 ];
 
 /**
- * Outputs text to the JavaScript console as well as to stdout.
- */
-function LOG(text) {
-  if (loggingEnabled) {
-    dump("*** Search: " + text + "\n");
-    Services.console.logStringMessage(text);
-  }
-}
-
-/**
- * Logs the failure message (if browser.search.log is enabled) and throws.
- * @param  message
- *         A message to display
- * @param  resultCode
- *         The NS_ERROR_* value to throw.
- * @throws resultCode or NS_ERROR_INVALID_ARG if resultCode isn't specified.
- */
-function FAIL(message, resultCode) {
-  LOG(message);
-  throw Components.Exception(message, resultCode || Cr.NS_ERROR_INVALID_ARG);
-}
-
-/**
  * Truncates big blobs of (data-)URIs to console-friendly sizes
  * @param str
  *        String to tone down
  * @param len
  *        Maximum length of the string to return. Defaults to the length of a tweet.
  */
 function limitURILength(str, len) {
   len = len || 140;
@@ -181,30 +131,30 @@ loadListener.prototype = {
     Ci.nsIStreamListener,
     Ci.nsIChannelEventSink,
     Ci.nsIInterfaceRequestor,
     Ci.nsIProgressEventSink,
   ]),
 
   // nsIRequestObserver
   onStartRequest(request) {
-    LOG("loadListener: Starting request: " + request.name);
+    SearchUtils.log("loadListener: Starting request: " + request.name);
     this._stream = Cc["@mozilla.org/binaryinputstream;1"].
                    createInstance(Ci.nsIBinaryInputStream);
   },
 
   onStopRequest(request, statusCode) {
-    LOG("loadListener: Stopping request: " + request.name);
+    SearchUtils.log("loadListener: Stopping request: " + request.name);
 
     var requestFailed = !Components.isSuccessCode(statusCode);
     if (!requestFailed && (request instanceof Ci.nsIHttpChannel))
       requestFailed = !request.requestSucceeded;
 
     if (requestFailed || this._countRead == 0) {
-      LOG("loadListener: request failed!");
+      SearchUtils.log("loadListener: request failed!");
       // send null so the callback can deal with the failure
       this._bytes = null;
     }
     this._callback(this._bytes, this._engine);
     this._channel = null;
     this._engine = null;
   },
 
@@ -251,17 +201,17 @@ function rescaleIcon(byteArray, contentT
   if (contentType == "image/svg+xml")
     throw new Error("Cannot rescale SVG image");
 
   let imgTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
   let arrayBuffer = (new Int8Array(byteArray)).buffer;
   let container = imgTools.decodeImageFromArrayBuffer(arrayBuffer, contentType);
   let stream = imgTools.encodeScaledImage(container, "image/png", size, size);
   let streamSize = stream.available();
-  if (streamSize > MAX_ICON_SIZE)
+  if (streamSize > SearchUtils.MAX_ICON_SIZE)
     throw new Error("Icon is too big");
   let bis = new BinaryInputStream(stream);
   return [bis.readByteArray(streamSize), "image/png"];
 }
 
 function getVerificationHash(name) {
   let disclaimer = "By modifying this file, I agree that I am doing so " +
     "only within $appName itself, using official, user-driven search " +
@@ -283,60 +233,26 @@ function getVerificationHash(name) {
                  .createInstance(Ci.nsICryptoHash);
   hasher.init(hasher.SHA256);
   hasher.update(data, data.length);
 
   return hasher.finish(true);
 }
 
 /**
- * Wrapper function for nsIIOService::newURI.
- * @param {string} urlSpec
- *        The URL string from which to create an nsIURI.
- * @returns {nsIURI} an nsIURI object, or null if the creation of the URI failed.
- */
-function makeURI(urlSpec) {
-  try {
-    return Services.io.newURI(urlSpec);
-  } catch (ex) { }
-
-  return null;
-}
-
-/**
- * Wrapper function for nsIIOService::newChannel.
- * @param url
- *        The URL string from which to create an nsIChannel.
- * @returns an nsIChannel object, or null if the url is invalid.
- */
-function makeChannel(url) {
-  try {
-    let uri = typeof url == "string" ? Services.io.newURI(url) : url;
-    return Services.io.newChannelFromURI(uri,
-                                         null, /* loadingNode */
-                                         Services.scriptSecurityManager.getSystemPrincipal(),
-                                         null, /* triggeringPrincipal */
-                                         Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                         Ci.nsIContentPolicy.TYPE_OTHER);
-  } catch (ex) { }
-
-  return null;
-}
-
-/**
  * 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}
  */
 function getDir(key, iface) {
   if (!key)
-    FAIL("getDir requires a directory key!");
+    SearchUtils.fail("getDir requires a directory key!");
 
   return Services.dirsvc.get(key, iface || Ci.nsIFile);
 }
 
 /**
  * Sanitizes a name so that it can be used as a filename. If it cannot be
  * sanitized (e.g. no valid characters), then it returns a random name.
  *
@@ -363,50 +279,32 @@ function sanitizeName(name) {
 /**
  * Retrieve a pref from the search param branch. Returns null if the
  * preference is not found.
  *
  * @param prefName
  *        The name of the pref.
  **/
 function getMozParamPref(prefName) {
-  let branch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF + "param.");
+  let branch = Services.prefs.getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF + "param.");
   let prefValue = branch.getCharPref(prefName, null);
   return prefValue ? encodeURIComponent(prefValue) : null;
 }
 
 /**
- * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to
- * the state of the search service.
- *
- * @param {nsISearchEngine} engine
- *   The engine to which the change applies.
- * @param {string} verb
- *   A verb describing the change.
- *
- * @see nsISearchService.idl
- */
-function notifyAction(engine, verb) {
-  if (Services.search.isInitialized) {
-    LOG("NOTIFY: Engine: \"" + engine.name + "\"; Verb: \"" + verb + "\"");
-    Services.obs.notifyObservers(engine, SEARCH_ENGINE_TOPIC, verb);
-  }
-}
-
-/**
  * Simple object representing a name/value pair.
  * @see nsISearchEngine::addParam
  *
  * @param {string} name
  * @param {string} value
  * @param {string} purpose
  */
 function QueryParameter(name, value, purpose) {
   if (!name || (value == null))
-    FAIL("missing name or value for QueryParameter!");
+    SearchUtils.fail("missing name or value for QueryParameter!");
 
   this.name = name;
   this.value = value;
   this.purpose = purpose;
 }
 
 /**
  * Perform OpenSearch parameter substitution on aParamValue.
@@ -435,22 +333,22 @@ function ParamSubstitution(paramValue, s
       return engine.queryCharset;
 
     // moz: parameters are only available for default search engines.
     if (name.startsWith("moz:") && engine._isDefault) {
       // {moz:locale} and {moz:distributionID} are common
       if (name == MOZ_PARAM_LOCALE)
         return Services.locale.requestedLocale;
       if (name == MOZ_PARAM_DIST_ID) {
-        return Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID",
+        return Services.prefs.getCharPref(SearchUtils.BROWSER_SEARCH_PREF + "distributionID",
                                           Services.appinfo.distributionID || "");
       }
       // {moz:official} seems to have little use.
       if (name == MOZ_PARAM_OFFICIAL) {
-        if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official",
+        if (Services.prefs.getBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "official",
                                        AppConstants.MOZ_OFFICIAL_BRANDING))
           return "official";
         return "unofficial";
       }
     }
 
     // Handle the less common OpenSearch parameters we're confident about.
     if (name == OS_PARAM_LANGUAGE)
@@ -518,45 +416,45 @@ function getInternalAliases(engine) {
  *   The root domain for this URL.  Defaults to the template's host.
  *
  * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
  *
  * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported.
  */
 function EngineURL(mimeType, requestMethod, template, resultDomain) {
   if (!mimeType || !requestMethod || !template)
-    FAIL("missing mimeType, method or template for EngineURL!");
+    SearchUtils.fail("missing mimeType, method or template for EngineURL!");
 
   var method = requestMethod.toUpperCase();
   var type   = mimeType.toLowerCase();
 
   if (method != "GET" && method != "POST")
-    FAIL("method passed to EngineURL must be \"GET\" or \"POST\"");
+    SearchUtils.fail("method passed to EngineURL must be \"GET\" or \"POST\"");
 
   this.type     = type;
   this.method   = method;
   this.params   = [];
   this.rels     = [];
   // Don't serialize expanded mozparams
   this.mozparams = {};
 
-  var templateURI = makeURI(template);
+  var templateURI = SearchUtils.makeURI(template);
   if (!templateURI)
-    FAIL("new EngineURL: template is not a valid URI!", Cr.NS_ERROR_FAILURE);
+    SearchUtils.fail("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:
-      FAIL("new EngineURL: template uses invalid scheme!", Cr.NS_ERROR_FAILURE);
+      SearchUtils.fail("new EngineURL: template uses invalid scheme!", Cr.NS_ERROR_FAILURE);
   }
 
   this.templateHost = templateURI.host;
   // If no resultDomain was specified in the engine definition file, use the
   // host from the template.
   this.resultDomain = resultDomain || this.templateHost;
 }
 EngineURL.prototype = {
@@ -661,17 +559,17 @@ EngineURL.prototype = {
    **/
   toJSON() {
     var json = {
       template: this.template,
       rels: this.rels,
       resultDomain: this.resultDomain,
     };
 
-    if (this.type != URLTYPE_SEARCH_HTML)
+    if (this.type != SearchUtils.URL_TYPE.SEARCH)
       json.type = this.type;
     if (this.method != "GET")
       json.method = this.method;
 
     function collapseMozParams(param) {
       return this.mozparams[param.name] || param;
     }
     json.params = this.params.map(collapseMozParams, this);
@@ -713,17 +611,17 @@ function SearchEngine(options = {}) {
     } else {
       this._shortName = options.name;
     }
   } else if ("fileURI" in options && options.fileURI instanceof Ci.nsIFile) {
     file = options.fileURI;
   } else if ("uri" in options) {
     let optionsURI = options.uri;
     if (typeof optionsURI == "string") {
-      optionsURI = makeURI(optionsURI);
+      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":
@@ -765,27 +663,27 @@ function SearchEngine(options = {}) {
       // We are in the process of downloading and installing the engine.
       // We'll have the shortName and id once we are done parsing it.
      return;
     }
 
     // Build the id used for the legacy metadata storage, so that we
     // can do a one-time import of data from old profiles.
     if (this._isDefault ||
-        (uri && uri.spec.startsWith(APP_SEARCH_PREFIX))) {
+        (uri && uri.spec.startsWith(SearchUtils.APP_SEARCH_PREFIX))) {
       // The second part of the check is to catch engines from language packs.
       // They aren't default engines (because they aren't app-shipped), but we
       // still need to give their id an [app] prefix for backward compat.
       this._id = "[app]/" + this._shortName + ".xml";
     } else if (!this._readOnly) {
       this._id = "[profile]/" + this._shortName + ".xml";
     } else {
       // If the engine is neither a default one, nor a user-installed one,
       // it must be extension-shipped, so use the full path as id.
-      LOG("Setting _id to full path for engine from " + this._loadPath);
+      SearchUtils.log("Setting _id to full path for engine from " + this._loadPath);
       this._id = file ? file.path : uri.spec;
     }
   }
 }
 
 SearchEngine.prototype = {
   // Data set by the user.
   _metaData: null,
@@ -813,17 +711,17 @@ SearchEngine.prototype = {
   __searchForm: null,
   get _searchForm() {
     return this.__searchForm;
   },
   set _searchForm(value) {
     if (/^https?:/i.test(value))
       this.__searchForm = value;
     else
-      LOG("_searchForm: Invalid URL dropped for " + this._name ||
+      SearchUtils.log("_searchForm: Invalid URL dropped for " + this._name ||
           "the current engine");
   },
   // Whether to obtain user confirmation before adding the engine. This is only
   // used when the engine is first added to the list.
   _confirm: false,
   // Whether to set this as the current engine as soon as it is loaded.  This
   // is only used when the engine is first added to the list.
   _useNow: false,
@@ -847,40 +745,40 @@ SearchEngine.prototype = {
    *
    * @param file The file to load the search plugin from.
    *
    * @returns {Promise} A promise, resolved successfully if initializing from
    * data succeeds, rejected if it fails.
    */
   async _initFromFile(file) {
     if (!file || !(await OS.File.exists(file.path)))
-      FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED);
+      SearchUtils.fail("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 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.
    */
   _initFromURIAndLoad(uri) {
-    let loadURI = uri instanceof Ci.nsIURI ? uri : makeURI(uri);
+    let loadURI = uri instanceof Ci.nsIURI ? uri : SearchUtils.makeURI(uri);
     ENSURE_WARN(loadURI,
                 "Must have URI when calling _initFromURIAndLoad!",
                 Cr.NS_ERROR_UNEXPECTED);
 
-    LOG("_initFromURIAndLoad: Downloading engine from: \"" + loadURI.spec + "\".");
+    SearchUtils.log("_initFromURIAndLoad: Downloading engine from: \"" + loadURI.spec + "\".");
 
-    var chan = makeChannel(loadURI);
+    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);
     }
     this._uri = loadURI;
     var listener = new loadListener(chan, this, this._onLoad);
@@ -892,17 +790,17 @@ SearchEngine.prototype = {
    * Retrieves the engine data from a URI asynchronously and initializes it.
    *
    * @param uri The uri to load the search plugin from.
    *
    * @returns {Promise} A promise, resolved successfully if retrieveing data
    * succeeds.
    */
   async _initFromURI(uri) {
-    LOG("_initFromURI: Loading engine from: \"" + uri.spec + "\".");
+    SearchUtils.log("_initFromURI: Loading engine from: \"" + uri.spec + "\".");
     await this._retrieveSearchXMLData(uri.spec);
     // Now that the data is loaded, initialize the engine object
     this._initFromData();
   },
 
   /**
    * Retrieves the engine data for a given URI asynchronously.
    *
@@ -953,17 +851,17 @@ SearchEngine.prototype = {
     var stringBundle = Services.strings.createBundle(SEARCH_BUNDLE);
     var titleMessage = stringBundle.GetStringFromName("addEngineConfirmTitle");
 
     // Display only the hostname portion of the URL.
     var dialogMessage =
         stringBundle.formatStringFromName("addEngineConfirmation",
                                           [this._name, this._uri.host], 2);
     var checkboxMessage = null;
-    if (!Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "noCurrentEngine", false))
+    if (!Services.prefs.getBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "noCurrentEngine", false))
       checkboxMessage = stringBundle.GetStringFromName("addEngineAsCurrentText");
 
     var addButtonLabel =
         stringBundle.GetStringFromName("addEngineAddButtonLabel");
 
     var ps = Services.prompt;
     var buttonFlags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
                       (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1) +
@@ -1006,17 +904,17 @@ SearchEngine.prototype = {
       }
     }
 
     function promptError(strings = {}, error = undefined) {
       onError(error);
 
       if (engine._engineToUpdate) {
         // We're in an update, so just fail quietly
-        LOG("updating " + engine._engineToUpdate.name + " failed");
+        SearchUtils.log("updating " + engine._engineToUpdate.name + " failed");
         return;
       }
       var brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
       var brandName = brandBundle.GetStringFromName("brandShortName");
 
       var searchBundle = Services.strings.createBundle(SEARCH_BUNDLE);
       var msgStringName = strings.error || "error_loading_engine_msg2";
       var titleStringName = strings.title || "error_loading_engine_title";
@@ -1036,17 +934,17 @@ SearchEngine.prototype = {
     var parser = new DOMParser();
     var doc = parser.parseFromBuffer(bytes, "text/xml");
     engine._data = doc.documentElement;
 
     try {
       // Initialize the engine from the obtained data
       engine._initFromData();
     } catch (ex) {
-      LOG("_onLoad: Failed to init engine!\n" + ex);
+      SearchUtils.log("_onLoad: Failed to init engine!\n" + ex);
       // Report an error to the user
       if (ex.result == Cr.NS_ERROR_FILE_CORRUPTED) {
         promptError({ error: "error_invalid_engine_msg2",
                       title: "error_invalid_format_title",
                     });
       } else {
         promptError();
       }
@@ -1079,26 +977,26 @@ SearchEngine.prototype = {
         // duplicate engine" prompt; otherwise, fail silently.
         if (engine._confirm) {
           promptError({ error: "error_duplicate_engine_msg",
                         title: "error_invalid_engine_title",
                       }, Ci.nsISearchService.ERROR_DUPLICATE_ENGINE);
         } else {
           onError(Ci.nsISearchService.ERROR_DUPLICATE_ENGINE);
         }
-        LOG("_onLoad: duplicate engine found, bailing");
+        SearchUtils.log("_onLoad: duplicate engine found, bailing");
         return;
       }
 
       // If requested, confirm the addition now that we have the title.
       // This property is only ever true for engines added via
       // nsISearchService::addEngine.
       if (engine._confirm) {
         var confirmation = engine._confirmAddEngine();
-        LOG("_onLoad: confirm is " + confirmation.confirmed +
+        SearchUtils.log("_onLoad: confirm is " + confirmation.confirmed +
             "; useNow is " + confirmation.useNow);
         if (!confirmation.confirmed) {
           onError();
           return;
         }
         engine._useNow = confirmation.useNow;
       }
 
@@ -1107,17 +1005,17 @@ SearchEngine.prototype = {
       if (engine._extensionID) {
         engine._loadPath += ":" + engine._extensionID;
       }
       engine.setAttr("loadPathHash", getVerificationHash(engine._loadPath));
     }
 
     // Notify the search service of the successful load. It will deal with
     // updates by checking aEngine._engineToUpdate.
-    notifyAction(engine, SEARCH_ENGINE_LOADED);
+    SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.LOADED);
 
     // Notify the callback if needed
     if (engine._installCallback) {
       engine._installCallback();
     }
   },
 
   /**
@@ -1176,87 +1074,87 @@ SearchEngine.prototype = {
    *   Whether or not this icon is to be preferred. Preferred icons can
    *   override non-preferred icons.
    * @param {number} [width]
    *   Width of the icon.
    * @param {number} [height]
    *   Height of the icon.
    */
   _setIcon(iconURL, isPreferred, width, height) {
-    var uri = makeURI(iconURL);
+    var uri = SearchUtils.makeURI(iconURL);
 
     // Ignore bad URIs
     if (!uri)
       return;
 
-    LOG("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \""
+    SearchUtils.log("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \""
         + this.name + "\".");
     // Only accept remote icons from http[s] or ftp
     switch (uri.scheme) {
       case "resource":
       case "chrome":
         // We only allow chrome and resource icon URLs for built-in search engines
         if (!this._isDefault) {
           return;
         }
         // Fall through to the data case
       case "moz-extension":
       case "data":
         if (!this._hasPreferredIcon || isPreferred) {
           this._iconURI = uri;
-          notifyAction(this, SEARCH_ENGINE_CHANGED);
+          SearchUtils.notifyAction(this, SearchUtils.MODIFIED_TYPE.CHANGED);
           this._hasPreferredIcon = isPreferred;
         }
 
         if (width && height) {
           this._addIconToMap(width, height, iconURL);
         }
         break;
       case "http":
       case "https":
       case "ftp":
-        LOG("_setIcon: Downloading icon: \"" + uri.spec +
+        SearchUtils.log("_setIcon: Downloading icon: \"" + uri.spec +
             "\" for engine: \"" + this.name + "\"");
-        var chan = makeChannel(uri);
+        var chan = SearchUtils.makeChannel(uri);
 
         let iconLoadCallback = function(byteArray, engine) {
           // This callback may run after we've already set a preferred icon,
           // so check again.
           if (engine._hasPreferredIcon && !isPreferred)
             return;
 
           if (!byteArray) {
-            LOG("iconLoadCallback: load failed");
+            SearchUtils.log("iconLoadCallback: load failed");
             return;
           }
 
           let contentType = chan.contentType;
-          if (byteArray.length > MAX_ICON_SIZE) {
+          if (byteArray.length > SearchUtils.MAX_ICON_SIZE) {
             try {
-              LOG("iconLoadCallback: rescaling icon");
+              SearchUtils.log("iconLoadCallback: rescaling icon");
               [byteArray, contentType] = rescaleIcon(byteArray, contentType);
             } catch (ex) {
-              LOG("iconLoadCallback: got exception: " + ex);
+              SearchUtils.log("iconLoadCallback: got exception: " + ex);
               Cu.reportError("Unable to set an icon for the search engine because: " + ex);
               return;
             }
           }
 
           if (!contentType.startsWith("image/"))
             contentType = "image/x-icon";
           let dataURL = "data:" + contentType + ";base64," +
             btoa(String.fromCharCode.apply(null, byteArray));
 
-          engine._iconURI = makeURI(dataURL);
+          engine._iconURI = SearchUtils.makeURI(dataURL);
 
           if (width && height) {
             engine._addIconToMap(width, height, dataURL);
           }
 
-          notifyAction(engine, SEARCH_ENGINE_CHANGED);
+          SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
           engine._hasPreferredIcon = isPreferred;
         };
 
         // If we're currently acting as an "update engine", then the callback
         // should set the icon on the engine we're updating and not us, since
         // |this| might be gone by the time the callback runs.
         var engineToSet = this._engineToUpdate || this;
 
@@ -1275,22 +1173,22 @@ SearchEngine.prototype = {
                 Cr.NS_ERROR_UNEXPECTED);
 
     // Ensure we have a supported engine type before attempting to parse it.
     let element = this._data;
     if ((element.localName == MOZSEARCH_LOCALNAME &&
          element.namespaceURI == MOZSEARCH_NS_10) ||
         (element.localName == OPENSEARCH_LOCALNAME &&
          OPENSEARCH_NAMESPACES.includes(element.namespaceURI))) {
-      LOG("_init: Initing search plugin from " + this._location);
+      SearchUtils.log("_init: Initing search plugin from " + this._location);
 
       this._parse();
     } else {
       Cu.reportError("Invalid search plugin due to namespace not matching.");
-      FAIL(this._location + " is not a valid search plugin.", Cr.NS_ERROR_FILE_CORRUPTED);
+      SearchUtils.fail(this._location + " is not a valid search plugin.", Cr.NS_ERROR_FILE_CORRUPTED);
     }
     // No need to keep a ref to our data (which in some cases can be a document
     // element) past this point
     this._data = null;
   },
 
   /**
    * Initialize an EngineURL object from metadata.
@@ -1364,26 +1262,26 @@ SearchEngine.prototype = {
    *   Any parameters for a POST method.
    * @param {string} params.template
    *   The url template.
    */
   _initFromMetadata(engineName, params) {
     this._extensionID = params.extensionID;
     this._isBuiltin = !!params.isBuiltin;
 
-    this._initEngineURLFromMetaData(URLTYPE_SEARCH_HTML, {
+    this._initEngineURLFromMetaData(SearchUtils.URL_TYPE.SEARCH, {
       method: (params.searchPostParams && "POST") || params.method || "GET",
       template: params.template,
       getParams: params.searchGetParams,
       postParams: params.searchPostParams,
       mozParams: params.mozParams,
     });
 
     if (params.suggestURL) {
-      this._initEngineURLFromMetaData(URLTYPE_SUGGEST_JSON, {
+      this._initEngineURLFromMetaData(SearchUtils.URL_TYPE.SUGGEST_JSON, {
         method: (params.suggestPostParams && "POST") || params.method || "GET",
         template: params.suggestURL,
         getParams: params.suggestGetParams,
         postParams: params.suggestPostParams,
       });
     }
 
     if (params.queryCharset) {
@@ -1435,50 +1333,50 @@ SearchEngine.prototype = {
 
     let rels = [];
     if (element.hasAttribute("rel")) {
       rels = element.getAttribute("rel").toLowerCase().split(/\s+/);
     }
 
     // Support an alternate suggestion type, see bug 1425827 for details.
     if (type == "application/json" && rels.includes("suggestions")) {
-      type = URLTYPE_SUGGEST_JSON;
+      type = SearchUtils.URL_TYPE.SUGGEST_JSON;
     }
 
     try {
       var url = new EngineURL(type, method, template, resultDomain);
     } catch (ex) {
-      FAIL("_parseURL: failed to add " + template + " as a URL",
+      SearchUtils.fail("_parseURL: failed to add " + template + " as a URL",
            Cr.NS_ERROR_FAILURE);
     }
 
     if (rels.length) {
       url.rels = rels;
     }
 
     for (var i = 0; i < element.children.length; ++i) {
       var param = element.children[i];
       if (param.localName == "Param") {
         try {
           url.addParam(param.getAttribute("name"), param.getAttribute("value"));
         } catch (ex) {
           // Ignore failure
-          LOG("_parseURL: Url element has an invalid param");
+          SearchUtils.log("_parseURL: Url element has an invalid param");
         }
       } else if (param.localName == "MozParam" &&
                  // We only support MozParams for default search engines
                  this._isDefault) {
         var value;
         let condition = param.getAttribute("condition");
 
         // MozParams must have a condition to be valid
         if (!condition) {
           let engineLoc = this._location;
           let paramName = param.getAttribute("name");
-          LOG("_parseURL: MozParam (" + paramName +
+          SearchUtils.log("_parseURL: MozParam (" + paramName +
             ") without a condition attribute found parsing engine: " + engineLoc);
           continue;
         }
 
         switch (condition) {
           case "purpose":
             url.addParam(param.getAttribute("name"),
                          param.getAttribute("value"),
@@ -1493,17 +1391,17 @@ SearchEngine.prototype = {
             }
             url._addMozParam({"pref": param.getAttribute("pref"),
                               "name": param.getAttribute("name"),
                               "condition": "pref"});
             break;
           default:
             let engineLoc = this._location;
             let paramName = param.getAttribute("name");
-            LOG("_parseURL: MozParam (" + paramName + ") has an unknown condition: " +
+            SearchUtils.log("_parseURL: MozParam (" + paramName + ") has an unknown condition: " +
               condition + ". Found parsing engine: " + engineLoc);
           break;
         }
       }
     }
 
     this._urls.push(url);
   },
@@ -1511,24 +1409,24 @@ SearchEngine.prototype = {
   /**
    * Get the icon from an OpenSearch Image element.
    *
    * @param {HTMLLinkElement} element
    *   The OpenSearch URL element.
    * @see http://opensearch.a9.com/spec/1.1/description/#image
    */
   _parseImage(element) {
-    LOG("_parseImage: Image textContent: \"" + limitURILength(element.textContent) + "\"");
+    SearchUtils.log("_parseImage: Image textContent: \"" + limitURILength(element.textContent) + "\"");
 
     let width = parseInt(element.getAttribute("width"), 10);
     let height = parseInt(element.getAttribute("height"), 10);
     let isPrefered = width == 16 && height == 16;
 
     if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
-      LOG("OpenSearch image element must have positive width and height.");
+      SearchUtils.log("OpenSearch image element must have positive width and height.");
       return;
     }
 
     this._setIcon(element.textContent, isPrefered, width, height);
   },
 
   /**
    * Extract search engine information from the collected data to initialize
@@ -1549,17 +1447,17 @@ SearchEngine.prototype = {
         case "Description":
           this._description = child.textContent;
           break;
         case "Url":
           try {
             this._parseURL(child);
           } catch (ex) {
             // Parsing of the element failed, just skip it.
-            LOG("_parse: failed to parse URL child: " + ex);
+            SearchUtils.log("_parse: failed to parse URL child: " + ex);
           }
           break;
         case "Image":
           this._parseImage(child);
           break;
         case "InputEncoding":
           this._queryCharset = child.textContent.toUpperCase();
           break;
@@ -1578,52 +1476,52 @@ SearchEngine.prototype = {
           this._iconUpdateURL = child.textContent;
           break;
         case "ExtensionID":
           this._extensionID = child.textContent;
           break;
       }
     }
     if (!this.name || (this._urls.length == 0))
-      FAIL("_parse: No name, or missing URL!", Cr.NS_ERROR_FAILURE);
-    if (!this.supportsResponseType(URLTYPE_SEARCH_HTML))
-      FAIL("_parse: No text/html result type!", Cr.NS_ERROR_FAILURE);
+      SearchUtils.fail("_parse: No name, or missing URL!", Cr.NS_ERROR_FAILURE);
+    if (!this.supportsResponseType(SearchUtils.URL_TYPE.SEARCH))
+      SearchUtils.fail("_parse: No text/html result type!", Cr.NS_ERROR_FAILURE);
   },
 
   /**
    * Init from a JSON record.
    *
    * @param {object} json
    *   The json record to use.
    */
   _initWithJSON(json) {
     this._name = json._name;
     this._shortName = json._shortName;
     this._loadPath = json._loadPath;
     this._description = json.description;
     this._hasPreferredIcon = json._hasPreferredIcon == undefined;
-    this._queryCharset = json.queryCharset || DEFAULT_QUERY_CHARSET;
+    this._queryCharset = json.queryCharset || SearchUtils.DEFAULT_QUERY_CHARSET;
     this.__searchForm = json.__searchForm;
     this._updateInterval = json._updateInterval || null;
     this._updateURL = json._updateURL || null;
     this._iconUpdateURL = json._iconUpdateURL || null;
     this._readOnly = json._readOnly == undefined;
-    this._iconURI = makeURI(json._iconURL);
+    this._iconURI = SearchUtils.makeURI(json._iconURL);
     this._iconMapObj = json._iconMapObj;
     this._metaData = json._metaData || {};
     this._isBuiltin = json._isBuiltin;
     if (json.filePath) {
       this._filePath = json.filePath;
     }
     if (json.extensionID) {
       this._extensionID = json.extensionID;
     }
     for (let i = 0; i < json._urls.length; ++i) {
       let url = json._urls[i];
-      let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML,
+      let engineURL = new EngineURL(url.type || SearchUtils.URL_TYPE.SEARCH,
                                     url.method || "GET", url.template,
                                     url.resultDomain || undefined);
       engineURL._initWithJSON(url);
       this._urls.push(engineURL);
     }
   },
 
   /**
@@ -1648,17 +1546,17 @@ SearchEngine.prototype = {
     if (this._updateInterval)
       json._updateInterval = this._updateInterval;
     if (this._updateURL)
       json._updateURL = this._updateURL;
     if (this._iconUpdateURL)
       json._iconUpdateURL = this._iconUpdateURL;
     if (!this._hasPreferredIcon)
       json._hasPreferredIcon = this._hasPreferredIcon;
-    if (this.queryCharset != DEFAULT_QUERY_CHARSET)
+    if (this.queryCharset != SearchUtils.DEFAULT_QUERY_CHARSET)
       json.queryCharset = this.queryCharset;
     if (!this._readOnly)
       json._readOnly = this._readOnly;
     if (this._filePath) {
       // File path is stored so that we can remove legacy xml files
       // from the profile if the user removes the engine.
       json.filePath = this._filePath;
     }
@@ -1679,17 +1577,17 @@ SearchEngine.prototype = {
 
   // nsISearchEngine
   get alias() {
     return this.getAttr("alias");
   },
   set alias(val) {
     var value = val ? val.trim() : null;
     this.setAttr("alias", value);
-    notifyAction(this, SEARCH_ENGINE_CHANGED);
+    SearchUtils.notifyAction(this, SearchUtils.MODIFIED_TYPE.CHANGED);
   },
 
   /**
    * Return the built-in identifier of app-provided engines.
    *
    * Note that this identifier is substantially similar to _id, with the
    * following exceptions:
    *
@@ -1709,17 +1607,17 @@ SearchEngine.prototype = {
 
   get hidden() {
     return this.getAttr("hidden") || false;
   },
   set hidden(val) {
     var value = !!val;
     if (value != this.hidden) {
       this.setAttr("hidden", value);
-      notifyAction(this, SEARCH_ENGINE_CHANGED);
+      SearchUtils.notifyAction(this, SearchUtils.MODIFIED_TYPE.CHANGED);
     }
   },
 
   get iconURI() {
     if (this._iconURI)
       return this._iconURI;
     return null;
   },
@@ -1764,19 +1662,19 @@ SearchEngine.prototype = {
     let leafName = this._shortName;
     if (!leafName)
       return "null";
     leafName += ".xml";
 
     let prefix = "", suffix = "";
     if (!file) {
       if (uri.schemeIs("resource")) {
-        uri = makeURI(Services.io.getProtocolHandler("resource")
-                              .QueryInterface(Ci.nsISubstitutingProtocolHandler)
-                              .resolveURI(uri));
+        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);
       }
 
@@ -1858,17 +1756,17 @@ SearchEngine.prototype = {
     if (/^(?:jar:)?(?:\[app\]|\[distribution\])/.test(this._loadPath))
       return true;
 
     return false;
   },
 
   get _hasUpdates() {
     // Whether or not the engine has an update URL
-    let selfURL = this._getURLOfType(URLTYPE_OPENSEARCH, "self");
+    let selfURL = this._getURLOfType(SearchUtils.URL_TYPE.OPENSEARCH, "self");
     return !!(this._updateURL || this._iconUpdateURL || selfURL);
   },
 
   get name() {
     return this._name;
   },
 
   get searchForm() {
@@ -1881,63 +1779,63 @@ SearchEngine.prototype = {
     if (!this.__internalAliases) {
       this.__internalAliases = getInternalAliases(this);
     }
     return this.__internalAliases;
   },
 
   _getSearchFormWithPurpose(aPurpose = "") {
     // First look for a <Url rel="searchform">
-    var searchFormURL = this._getURLOfType(URLTYPE_SEARCH_HTML, "searchform");
+    var searchFormURL = this._getURLOfType(SearchUtils.URL_TYPE.SEARCH, "searchform");
     if (searchFormURL) {
       let submission = searchFormURL.getSubmission("", this, aPurpose);
 
       // If the rel=searchform URL is not type="get" (i.e. has postData),
       // ignore it, since we can only return a URL.
       if (!submission.postData)
         return submission.uri.spec;
     }
 
     if (!this._searchForm) {
       // No SearchForm specified in the engine definition file, use the prePath
       // (e.g. https://foo.com for https://foo.com/search.php?q=bar).
-      var htmlUrl = this._getURLOfType(URLTYPE_SEARCH_HTML);
+      var htmlUrl = this._getURLOfType(SearchUtils.URL_TYPE.SEARCH);
       ENSURE_WARN(htmlUrl, "Engine has no HTML URL!", Cr.NS_ERROR_UNEXPECTED);
-      this._searchForm = makeURI(htmlUrl.template).prePath;
+      this._searchForm = SearchUtils.makeURI(htmlUrl.template).prePath;
     }
 
     return ParamSubstitution(this._searchForm, "", this);
   },
 
   get queryCharset() {
     if (this._queryCharset)
       return this._queryCharset;
     return this._queryCharset = "windows-1252"; // the default
   },
 
   // from nsISearchEngine
   addParam(name, value, responseType) {
     if (!name || (value == null))
-      FAIL("missing name or value for nsISearchEngine::addParam!");
+      SearchUtils.fail("missing name or value for nsISearchEngine::addParam!");
     ENSURE_WARN(!this._readOnly,
                 "called nsISearchEngine::addParam on a read-only engine!",
                 Cr.NS_ERROR_FAILURE);
     if (!responseType)
-      responseType = URLTYPE_SEARCH_HTML;
+      responseType = SearchUtils.URL_TYPE.SEARCH;
 
     var url = this._getURLOfType(responseType);
     if (!url)
-      FAIL("Engine object has no URL for response type " + responseType,
+      SearchUtils.fail("Engine object has no URL for response type " + responseType,
            Cr.NS_ERROR_FAILURE);
 
     url.addParam(name, value);
   },
 
   get _defaultMobileResponseType() {
-    let type = URLTYPE_SEARCH_HTML;
+    let type = SearchUtils.URL_TYPE.SEARCH;
 
     let isTablet = Services.sysinfo.get("tablet");
     if (isTablet && this.supportsResponseType("application/x-moz-tabletsearch")) {
       // Check for a tablet-specific search URL override
       type = "application/x-moz-tabletsearch";
     } else if (!isTablet && this.supportsResponseType("application/x-moz-phonesearch")) {
       // Check for a phone-specific search URL override
       type = "application/x-moz-phonesearch";
@@ -1947,85 +1845,85 @@ SearchEngine.prototype = {
       value: type,
       configurable: true,
     });
 
     return type;
   },
 
   get _isWhiteListed() {
-    let url = this._getURLOfType(URLTYPE_SEARCH_HTML).template;
-    let hostname = makeURI(url).host;
-    let whitelist = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+    let url = this._getURLOfType(SearchUtils.URL_TYPE.SEARCH).template;
+    let hostname = SearchUtils.makeURI(url).host;
+    let whitelist = Services.prefs.getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF)
                             .getCharPref("reset.whitelist")
                             .split(",");
     if (whitelist.includes(hostname)) {
-      LOG("The hostname " + hostname + " is white listed, " +
+      SearchUtils.log("The hostname " + hostname + " is white listed, " +
           "we won't show the search reset prompt");
       return true;
     }
 
     return false;
   },
 
   // from nsISearchEngine
   getSubmission(data, responseType, purpose) {
     if (!responseType) {
       responseType = AppConstants.platform == "android" ? this._defaultMobileResponseType :
-                                                          URLTYPE_SEARCH_HTML;
+                                                          SearchUtils.URL_TYPE.SEARCH;
     }
 
     var url = this._getURLOfType(responseType);
 
     if (!url)
       return null;
 
     if (!data) {
       // Return a dummy submission object with our searchForm attribute
-      return new Submission(makeURI(this._getSearchFormWithPurpose(purpose)));
+      return new Submission(SearchUtils.makeURI(this._getSearchFormWithPurpose(purpose)));
     }
 
-    LOG("getSubmission: In data: \"" + data + "\"; Purpose: \"" + purpose + "\"");
+    SearchUtils.log("getSubmission: In data: \"" + data + "\"; Purpose: \"" + purpose + "\"");
     var submissionData = "";
     try {
       submissionData = Services.textToSubURI.ConvertAndEscape(this.queryCharset, data);
     } catch (ex) {
-      LOG("getSubmission: Falling back to default queryCharset!");
-      submissionData = Services.textToSubURI.ConvertAndEscape(DEFAULT_QUERY_CHARSET, data);
+      SearchUtils.log("getSubmission: Falling back to default queryCharset!");
+      submissionData = Services.textToSubURI.ConvertAndEscape(SearchUtils.DEFAULT_QUERY_CHARSET, data);
     }
-    LOG("getSubmission: Out data: \"" + submissionData + "\"");
+    SearchUtils.log("getSubmission: Out data: \"" + submissionData + "\"");
     return url.getSubmission(submissionData, this, purpose);
   },
 
   // from nsISearchEngine
   supportsResponseType(type) {
     return (this._getURLOfType(type) != null);
   },
 
   // from nsISearchEngine
   getResultDomain(responseType) {
     if (!responseType) {
       responseType = AppConstants.platform == "android" ? this._defaultMobileResponseType :
-                                                          URLTYPE_SEARCH_HTML;
+                                                          SearchUtils.URL_TYPE.SEARCH;
     }
 
-    LOG("getResultDomain: responseType: \"" + responseType + "\"");
+    SearchUtils.log("getResultDomain: responseType: \"" + responseType + "\"");
 
     let url = this._getURLOfType(responseType);
     if (url)
       return url.resultDomain;
     return "";
   },
 
   /**
    * Returns URL parsing properties used by _buildParseSubmissionMap.
    */
   getURLParsingInfo() {
     let responseType = AppConstants.platform == "android" ? this._defaultMobileResponseType :
-                                                            URLTYPE_SEARCH_HTML;
+                                                            SearchUtils.URL_TYPE.SEARCH;
 
     let url = this._getURLOfType(responseType);
     if (!url || url.method != "GET") {
       return null;
     }
 
     let termsParameterName = url._getTermsParameterName();
     if (!termsParameterName) {
@@ -2136,18 +2034,18 @@ SearchEngine.prototype = {
 
     try {
       connector.speculativeConnect(searchURI, principal, callbacks);
     } catch (e) {
       // Can't setup speculative connection for this url, just ignore it.
       Cu.reportError(e);
     }
 
-    if (this.supportsResponseType(URLTYPE_SUGGEST_JSON)) {
-      let suggestURI = this.getSubmission("dummy", URLTYPE_SUGGEST_JSON).uri;
+    if (this.supportsResponseType(SearchUtils.URL_TYPE.SUGGEST_JSON)) {
+      let suggestURI = this.getSubmission("dummy", SearchUtils.URL_TYPE.SUGGEST_JSON).uri;
       if (suggestURI.prePath != searchURI.prePath)
         try {
           connector.speculativeConnect(suggestURI, principal, callbacks);
         } catch (e) {
           // Can't setup speculative connection for this url, just ignore it.
           Cu.reportError(e);
         }
     }
--- a/toolkit/components/search/SearchService.jsm
+++ b/toolkit/components/search/SearchService.jsm
@@ -14,26 +14,23 @@ XPCOMUtils.defineLazyModuleGetters(this,
   clearTimeout: "resource://gre/modules/Timer.jsm",
   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
   ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
   getVerificationHash: "resource://gre/modules/SearchEngine.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   RemoteSettings: "resource://services-settings/remote-settings.js",
   SearchEngine: "resource://gre/modules/SearchEngine.jsm",
   SearchStaticData: "resource://gre/modules/SearchStaticData.jsm",
+  SearchUtils: "resource://gre/modules/SearchUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
 });
 
-const BROWSER_SEARCH_PREF = "browser.search.";
-
-XPCOMUtils.defineLazyPreferenceGetter(this, "loggingEnabled",
-  BROWSER_SEARCH_PREF + "log", false);
 XPCOMUtils.defineLazyPreferenceGetter(this, "gGeoSpecificDefaultsEnabled",
-  BROWSER_SEARCH_PREF + "geoSpecificDefaults", false);
+  SearchUtils.BROWSER_SEARCH_PREF + "geoSpecificDefaults", false);
 
 // Can't use defineLazyPreferenceGetter because we want the value
 // from the default branch
 XPCOMUtils.defineLazyGetter(this, "distroID", () => {
   return Services.prefs.getDefaultBranch("distribution.").getCharPref("id", "");
 });
 
 // A text encoder to UTF8, used whenever we commit the cache to disk.
@@ -49,52 +46,31 @@ const NS_APP_DISTRIBUTION_SEARCH_DIR_LIS
 // We load plugins from EXT_SEARCH_PREFIX, where a list.json
 // file needs to exist to list available engines.
 const EXT_SEARCH_PREFIX = "resource://search-extensions/";
 const APP_SEARCH_PREFIX = "resource://search-plugins/";
 
 // The address we use to sign the built in search extensions with.
 const EXT_SIGNING_ADDRESS = "search.mozilla.org";
 
-// See documentation in nsISearchService.idl.
-const SEARCH_ENGINE_TOPIC        = "browser-search-engine-modified";
 const TOPIC_LOCALES_CHANGE       = "intl:app-locales-changed";
 const QUIT_APPLICATION_TOPIC     = "quit-application";
 
-const SEARCH_ENGINE_REMOVED      = "engine-removed";
-const SEARCH_ENGINE_ADDED        = "engine-added";
-const SEARCH_ENGINE_CHANGED      = "engine-changed";
-const SEARCH_ENGINE_LOADED       = "engine-loaded";
-const SEARCH_ENGINE_DEFAULT      = "engine-default";
-
 // The following constants are left undocumented in nsISearchService.idl
 // For the moment, they are meant for testing/debugging purposes only.
 
-/**
- * Topic used for events involving the service itself.
- */
-const SEARCH_SERVICE_TOPIC       = "browser-search-service";
-
-/**
- * Sent whenever the cache is fully written to disk.
- */
-const SEARCH_SERVICE_CACHE_WRITTEN  = "write-cache-to-disk-complete";
-
 // Delay for batching invalidation of the JSON cache (ms)
 const CACHE_INVALIDATION_DELAY = 1000;
 
 // Current cache version. This should be incremented if the format of the cache
 // file is modified.
 const CACHE_VERSION = 1;
 
 const CACHE_FILENAME = "search.json.mozlz4";
 
-const URLTYPE_SEARCH_HTML  = "text/html";
-const URLTYPE_OPENSEARCH   = "application/opensearchdescription+xml";
-
 // The default engine update interval, in days. This is only used if an engine
 // specifies an updateURL, but not an updateInterval.
 const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
 
 // The default interval before checking again for the name of the
 // default engine for the region, in seconds. Only used if the response
 // from the server doesn't specify an interval.
 const SEARCH_GEO_DEFAULT_UPDATE_INTERVAL = 2592000; // 30 days.
@@ -105,115 +81,16 @@ const SEARCH_GEO_DEFAULT_UPDATE_INTERVAL
 const MULTI_LOCALE_ENGINES = [
   "amazon", "amazondotcom", "bolcom", "ebay", "google", "marktplaats",
   "mercadolibre", "twitter", "wikipedia", "wiktionary", "yandex", "multilocale",
 ];
 
 // A tag to denote when we are using the "default_locale" of an engine
 const DEFAULT_TAG = "default";
 
-/**
- * This is the Remote Settings key that we use to get the ignore lists for
- * engines.
- */
-const SETTINGS_IGNORELIST_KEY = "hijack-blocklists";
-
-/**
- * Outputs text to the JavaScript console as well as to stdout.
- */
-function LOG(text) {
-  if (loggingEnabled) {
-    dump("*** Search: " + text + "\n");
-    Services.console.logStringMessage(text);
-  }
-}
-
-/**
- * Logs the failure message (if browser.search.log is enabled) and throws.
- * @param  message
- *         A message to display
- * @param  resultCode
- *         The NS_ERROR_* value to throw.
- * @throws resultCode or NS_ERROR_INVALID_ARG if resultCode isn't specified.
- */
-function FAIL(message, resultCode) {
-  LOG(message);
-  throw Components.Exception(message, resultCode || Cr.NS_ERROR_INVALID_ARG);
-}
-
-function loadListener(channel, engine, callback) {
-  this._channel = channel;
-  this._bytes = [];
-  this._engine = engine;
-  this._callback = callback;
-}
-loadListener.prototype = {
-  _callback: null,
-  _channel: null,
-  _countRead: 0,
-  _engine: null,
-  _stream: null,
-
-  QueryInterface: ChromeUtils.generateQI([
-    Ci.nsIRequestObserver,
-    Ci.nsIStreamListener,
-    Ci.nsIChannelEventSink,
-    Ci.nsIInterfaceRequestor,
-    Ci.nsIProgressEventSink,
-  ]),
-
-  // nsIRequestObserver
-  onStartRequest(request) {
-    LOG("loadListener: Starting request: " + request.name);
-    this._stream = Cc["@mozilla.org/binaryinputstream;1"].
-                   createInstance(Ci.nsIBinaryInputStream);
-  },
-
-  onStopRequest(request, statusCode) {
-    LOG("loadListener: Stopping request: " + request.name);
-
-    var requestFailed = !Components.isSuccessCode(statusCode);
-    if (!requestFailed && (request instanceof Ci.nsIHttpChannel))
-      requestFailed = !request.requestSucceeded;
-
-    if (requestFailed || this._countRead == 0) {
-      LOG("loadListener: request failed!");
-      // send null so the callback can deal with the failure
-      this._bytes = null;
-    }
-    this._callback(this._bytes, this._engine);
-    this._channel = null;
-    this._engine = null;
-  },
-
-  // nsIStreamListener
-  onDataAvailable(request, inputStream, offset, count) {
-    this._stream.setInputStream(inputStream);
-
-    // Get a byte array of the data
-    this._bytes = this._bytes.concat(this._stream.readByteArray(count));
-    this._countRead += count;
-  },
-
-  // nsIChannelEventSink
-  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
-    this._channel = newChannel;
-    callback.onRedirectVerifyCallback(Cr.NS_OK);
-  },
-
-  // nsIInterfaceRequestor
-  getInterface(iid) {
-    return this.QueryInterface(iid);
-  },
-
-  // nsIProgressEventSink
-  onProgress(request, context, progress, progressMax) {},
-  onStatus(request, context, status, statusArg) {},
-};
-
 function isPartnerBuild() {
   // Mozilla-provided builds (i.e. funnelcakes) are not partner builds
   return distroID && !distroID.startsWith("mozilla");
 }
 
 // A method that tries to determine if this user is in a US geography.
 function isUSTimezone() {
   // Timezone assumptions! We assume that if the system clock's timezone is
@@ -276,37 +153,37 @@ var ensureKnownRegion = async function(s
     // a sync initialization during our XHRs - capture this via telemetry.
     Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT").add(gInitialized);
   } catch (ex) {
     Cu.reportError(ex);
   } finally {
     // Since bug 1492475, we don't block our init flows on the region fetch as
     // performed here. But we'd still like to unit-test its implementation, thus
     // we fire this observer notification.
-    Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "ensure-known-region-done");
+    Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "ensure-known-region-done");
   }
 };
 
 // Store the result of the geoip request as well as any other values and
 // telemetry which depend on it.
 function storeRegion(region) {
   let isTimezoneUS = isUSTimezone();
   // If it's a US region, but not a US timezone, we don't store the value.
   // This works because no region defaults to ZZ (unknown) in nsURLFormatter
   if (region != "US" || isTimezoneUS) {
     Services.prefs.setCharPref("browser.search.region", region);
   }
 
   // and telemetry...
   if (region == "US" && !isTimezoneUS) {
-    LOG("storeRegion mismatch - US Region, non-US timezone");
+    SearchUtils.log("storeRegion mismatch - US Region, non-US timezone");
     Services.telemetry.getHistogramById("SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE").add(1);
   }
   if (region != "US" && isTimezoneUS) {
-    LOG("storeRegion mismatch - non-US Region, US timezone");
+    SearchUtils.log("storeRegion mismatch - non-US Region, US timezone");
     Services.telemetry.getHistogramById("SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY").add(1);
   }
   // telemetry to compare our geoip response with platform-specific country data.
   // On Mac and Windows, we can get a country code via sysinfo
   let platformCC = Services.sysinfo.get("countryCode");
   if (platformCC) {
     let probeUSMismatched, probeNonUSMismatched;
     switch (Services.appinfo.OS) {
@@ -343,17 +220,17 @@ function fetchRegion(ss) {
     SUCCESS_WITHOUT_DATA: 1,
     XHRTIMEOUT: 2,
     ERROR: 3,
     // Note that we expect to add finer-grained error types here later (eg,
     // dns error, network error, ssl error, etc) with .ERROR remaining as the
     // generic catch-all that doesn't fit into other categories.
   };
   let endpoint = Services.urlFormatter.formatURLPref("browser.search.geoip.url");
-  LOG("_fetchRegion starting with endpoint " + endpoint);
+  SearchUtils.log("_fetchRegion starting with endpoint " + endpoint);
   // As an escape hatch, no endpoint means no geoip.
   if (!endpoint) {
     return Promise.resolve();
   }
   let startTime = Date.now();
   return new Promise(resolve => {
     // Instead of using a timeout on the xhr object itself, we simulate one
     // using a timer and let the XHR request complete.  This allows us to
@@ -362,33 +239,33 @@ function fetchRegion(ss) {
     // that many users end up doing a sync init of the search service and thus
     // would see the jank that implies.
     // (Note we do actually use a timeout on the XHR, but that's set to be a
     // large value just incase the request never completes - we don't want the
     // XHR object to live forever)
     let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
     let geoipTimeoutPossible = true;
     let timerId = setTimeout(() => {
-      LOG("_fetchRegion: timeout fetching region information");
+      SearchUtils.log("_fetchRegion: timeout fetching region information");
       if (geoipTimeoutPossible)
         Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(1);
       timerId = null;
       resolve();
     }, timeoutMS);
 
     let resolveAndReportSuccess = (result, reason) => {
       // Even if we timed out, we want to save the region and everything
       // related so next startup sees the value and doesn't retry this dance.
       if (result) {
         storeRegion(result);
       }
       Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_RESULT").add(reason);
 
       // This notification is just for tests...
-      Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-complete");
+      Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "geoip-lookup-xhr-complete");
 
       if (timerId) {
         Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(0);
         geoipTimeoutPossible = false;
       }
 
       let callback = () => {
         // If we've already timed out then we've already resolved the promise,
@@ -407,32 +284,32 @@ function fetchRegion(ss) {
         });
       } else {
         callback();
       }
     };
 
     let request = new XMLHttpRequest();
     // This notification is just for tests...
-    Services.obs.notifyObservers(request, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-starting");
+    Services.obs.notifyObservers(request, SearchUtils.TOPIC_SEARCH_SERVICE, "geoip-lookup-xhr-starting");
     request.timeout = 100000; // 100 seconds as the last-chance fallback
     request.onload = function(event) {
       let took = Date.now() - startTime;
       let region = event.target.response && event.target.response.country_code;
-      LOG("_fetchRegion got success response in " + took + "ms: " + region);
+      SearchUtils.log("_fetchRegion got success response in " + took + "ms: " + region);
       Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS").add(took);
       let reason = region ? TELEMETRY_RESULT_ENUM.SUCCESS : TELEMETRY_RESULT_ENUM.SUCCESS_WITHOUT_DATA;
       resolveAndReportSuccess(region, reason);
     };
     request.ontimeout = function(event) {
-      LOG("_fetchRegion: XHR finally timed-out fetching region information");
+      SearchUtils.log("_fetchRegion: XHR finally timed-out fetching region information");
       resolveAndReportSuccess(null, TELEMETRY_RESULT_ENUM.XHRTIMEOUT);
     };
     request.onerror = function(event) {
-      LOG("_fetchRegion: failed to retrieve region information");
+      SearchUtils.log("_fetchRegion: failed to retrieve region information");
       resolveAndReportSuccess(null, TELEMETRY_RESULT_ENUM.ERROR);
     };
     request.open("POST", endpoint, true);
     request.setRequestHeader("Content-Type", "application/json");
     request.responseType = "json";
     request.send("{}");
   });
 }
@@ -470,88 +347,88 @@ function convertGoogleEngines(engineName
 // The optional cohort value returned by the server is to be kept locally
 // and sent to the server the next time we ping it. It lets the server
 // identify profiles that have been part of a specific experiment.
 //
 // This promise may take up to 100s to resolve, it's the caller's
 // responsibility to ensure with a timer that we are not going to
 // block the async init for too long.
 var fetchRegionDefault = (ss) => new Promise(resolve => {
-  let urlTemplate = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+  let urlTemplate = Services.prefs.getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF)
                             .getCharPref("geoSpecificDefaults.url");
   let endpoint = Services.urlFormatter.formatURL(urlTemplate);
 
   // As an escape hatch, no endpoint means no region specific defaults.
   if (!endpoint) {
     resolve();
     return;
   }
 
   // Append the optional cohort value.
   const cohortPref = "browser.search.cohort";
   let cohort = Services.prefs.getCharPref(cohortPref, "");
   if (cohort)
     endpoint += "/" + cohort;
 
-  LOG("fetchRegionDefault starting with endpoint " + endpoint);
+  SearchUtils.log("fetchRegionDefault starting with endpoint " + endpoint);
 
   let startTime = Date.now();
   let request = new XMLHttpRequest();
   request.timeout = 100000; // 100 seconds as the last-chance fallback
   request.onload = function(event) {
     let took = Date.now() - startTime;
 
     let status = event.target.status;
     if (status != 200) {
-      LOG("fetchRegionDefault failed with HTTP code " + status);
+      SearchUtils.log("fetchRegionDefault failed with HTTP code " + status);
       let retryAfter = request.getResponseHeader("retry-after");
       if (retryAfter) {
         ss.setGlobalAttr("searchDefaultExpir", Date.now() + retryAfter * 1000);
       }
       resolve();
       return;
     }
 
     let response = event.target.response || {};
-    LOG("received " + response.toSource());
+    SearchUtils.log("received " + response.toSource());
 
     if (response.cohort) {
       Services.prefs.setCharPref(cohortPref, response.cohort);
     } else {
       Services.prefs.clearUserPref(cohortPref);
     }
 
     if (response.settings && response.settings.searchDefault) {
       let defaultEngine = response.settings.searchDefault;
       ss.setVerifiedGlobalAttr("searchDefault", defaultEngine);
-      LOG("fetchRegionDefault saved searchDefault: " + defaultEngine);
+      SearchUtils.log("fetchRegionDefault saved searchDefault: " + defaultEngine);
     }
 
     if (response.settings && response.settings.visibleDefaultEngines) {
       let visibleDefaultEngines = response.settings.visibleDefaultEngines;
       let string = visibleDefaultEngines.join(",");
       ss.setVerifiedGlobalAttr("visibleDefaultEngines", string);
-      LOG("fetchRegionDefault saved visibleDefaultEngines: " + string);
+      SearchUtils.log("fetchRegionDefault saved visibleDefaultEngines: " + string);
     }
 
     let interval = response.interval || SEARCH_GEO_DEFAULT_UPDATE_INTERVAL;
     let milliseconds = interval * 1000; // |interval| is in seconds.
     ss.setGlobalAttr("searchDefaultExpir", Date.now() + milliseconds);
 
-    LOG("fetchRegionDefault got success response in " + took + "ms");
+    SearchUtils.log("fetchRegionDefault got success response in " + took + "ms");
     // If we're doing this somewhere during the app's lifetime, reload the list
     // of engines in order to pick up any geo-specific changes.
     ss._maybeReloadEngines().finally(resolve);
   };
   request.ontimeout = function(event) {
-    LOG("fetchRegionDefault: XHR finally timed-out");
+    SearchUtils.log("fetchRegionDefault: XHR finally timed-out");
     resolve();
   };
   request.onerror = function(event) {
-    LOG("fetchRegionDefault: failed to retrieve territory default information");
+    SearchUtils.log("fetchRegionDefault: failed to retrieve territory default information");
     resolve();
   };
   request.open("GET", endpoint, true);
   request.setRequestHeader("Content-Type", "application/json");
   request.responseType = "json";
   request.send();
 });
 
@@ -602,35 +479,18 @@ function getLocalizedPref(prefName, defa
   try {
     return Services.prefs.getComplexValue(prefName,
       Ci.nsIPrefLocalizedString).data;
   } catch (ex) {}
 
   return defaultValue;
 }
 
-/**
- * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to
- * the state of the search service.
- *
- * @param {nsISearchEngine} engine
- *   The engine to which the change applies.
- * @param {string} verb
- *   A verb describing the change.
- *
- * @see nsISearchService.idl
- */
 var gInitialized = false;
 var gReinitializing = false;
-function notifyAction(engine, verb) {
-  if (gInitialized) {
-    LOG("NOTIFY: Engine: \"" + engine.name + "\"; Verb: \"" + verb + "\"");
-    Services.obs.notifyObservers(engine, SEARCH_ENGINE_TOPIC, verb);
-  }
-}
 
 // nsISearchParseSubmissionResult
 function ParseSubmissionResult(engine, terms, termsOffset, termsLength) {
   this._engine = engine;
   this._terms = terms;
   this._termsOffset = termsOffset;
   this._termsLength = termsLength;
 }
@@ -695,17 +555,17 @@ SearchService.prototype = {
   _loadPathIgnoreList: [],
 
   // If initialization has not been completed yet, perform synchronous
   // initialization.
   // Throws in case of initialization error.
   _ensureInitialized() {
     if (gInitialized) {
       if (!Components.isSuccessCode(this._initRV)) {
-        LOG("_ensureInitialized: failure");
+        SearchUtils.log("_ensureInitialized: failure");
         throw this._initRV;
       }
       return;
     }
 
     let err = new Error("Something tried to use the search service before it's been " +
       "properly intialized. Please examine the stack trace to figure out what and " +
       "where to fix it:\n");
@@ -720,87 +580,87 @@ SearchService.prototype = {
    *          A boolean value indicating whether we should explicitly await the
    *          the region check process to complete, which may be fetched remotely.
    *          Pass in `false` if the caller needs to be absolutely certain of the
    *          correct default engine and/ or ordering of visible engines.
    * @returns {Promise} A promise, resolved successfully if the initialization
    * succeeds.
    */
   async _init(skipRegionCheck) {
-    LOG("_init start");
+    SearchUtils.log("_init start");
 
     try {
       // See if we have a cache file so we don't have to parse a bunch of XML.
       let cache = await this._readCacheFile();
 
       // The init flow is not going to block on a fetch from an external service,
       // but we're kicking it off as soon as possible to prevent UI flickering as
       // much as possible.
       this._ensureKnownRegionPromise = ensureKnownRegion(this)
-        .catch(ex => LOG("_init: failure determining region: " + ex))
+        .catch(ex => SearchUtils.log("_init: failure determining region: " + ex))
         .finally(() => this._ensureKnownRegionPromise = null);
       if (!skipRegionCheck) {
         await this._ensureKnownRegionPromise;
       }
 
       this._setupRemoteSettings().catch(Cu.reportError);
 
       await this._loadEngines(cache);
 
       // Make sure the current list of engines is persisted, without the need to wait.
-      LOG("_init: engines loaded, writing cache");
+      SearchUtils.log("_init: engines loaded, writing cache");
       this._buildCache();
       this._addObservers();
     } catch (ex) {
       this._initRV = (ex.result !== undefined ? ex.result : Cr.NS_ERROR_FAILURE);
-      LOG("_init: failure initializng search: " + ex + "\n" + ex.stack);
+      SearchUtils.log("_init: failure initializng search: " + ex + "\n" + ex.stack);
     }
     gInitialized = true;
     if (Components.isSuccessCode(this._initRV)) {
       this._initObservers.resolve(this._initRV);
     } else {
       this._initObservers.reject(this._initRV);
     }
-    Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
+    Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "init-complete");
 
-    LOG("_init: Completed _init");
+    SearchUtils.log("_init: Completed _init");
     return this._initRV;
   },
 
   /**
    * Obtains the remote settings for the search service. This should only be
    * called from init(). Any subsequent updates to the remote settings are
    * handled via a sync listener.
    *
    * For desktop, the initial remote settings are obtained from dumps in
    * `services/settings/dumps/main/`. These are not shipped with Android, and
    * hence the `get` may take a while to return.
    */
   async _setupRemoteSettings() {
-    const ignoreListSettings = RemoteSettings(SETTINGS_IGNORELIST_KEY);
+    const ignoreListSettings = RemoteSettings(SearchUtils.SETTINGS_IGNORELIST_KEY);
     // Trigger a get of the initial value.
     const current = await ignoreListSettings.get();
 
     // Now we have the values, listen for future updates.
     this._ignoreListListener = this._handleIgnoreListUpdated.bind(this);
     ignoreListSettings.on("sync", this._ignoreListListener);
 
     await this._handleIgnoreListUpdated({data: {current}});
-    Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "settings-update-complete");
+    Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "settings-update-complete");
   },
 
   /**
    * This handles updating of the ignore list settings, and removing any ignored
    * engines.
    *
    * @param {object} eventData
    *   The event in the format received from RemoteSettings.
    */
   async _handleIgnoreListUpdated(eventData) {
-    LOG("_handleIgnoreListUpdated");
+    SearchUtils.log("_handleIgnoreListUpdated");
     const {data: {current}} = eventData;
 
     for (const entry of current) {
       if (entry.id == "load-paths") {
         this._loadPathIgnoreList = [...entry.matches];
       } else if (entry.id == "submission-urls") {
         this._submissionURLIgnoreList = [...entry.matches];
       }
@@ -861,17 +721,17 @@ SearchService.prototype = {
   },
 
   getGlobalAttr(name) {
     return this._metaData[name] || undefined;
   },
   getVerifiedGlobalAttr(name) {
     let val = this.getGlobalAttr(name);
     if (val && this.getGlobalAttr(name + "Hash") != getVerificationHash(val)) {
-      LOG("getVerifiedGlobalAttr, invalid hash for " + name);
+      SearchUtils.log("getVerifiedGlobalAttr, invalid hash for " + name);
       return "";
     }
     return val;
   },
 
   _listJSONURL: ((AppConstants.platform == "android") ? APP_SEARCH_PREFIX : EXT_SEARCH_PREFIX) + "list.json",
 
   _engines: { },
@@ -893,17 +753,17 @@ SearchService.prototype = {
   // ignoring changes the user may have subsequently made.
   get originalDefaultEngine() {
     let defaultEngineName = this.getVerifiedGlobalAttr("searchDefault");
     if (!defaultEngineName) {
       // We only allow the old defaultenginename pref for distributions
       // We can't use isPartnerBuild because we need to allow reading
       // of the defaultengine name pref for funnelcakes.
       if (distroID) {
-        let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+        let defaultPrefB = Services.prefs.getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF);
         let nsIPLS = Ci.nsIPrefLocalizedString;
 
         try {
           defaultEngineName = defaultPrefB.getComplexValue("defaultenginename", nsIPLS).data;
         } catch (ex) {
           // If the default pref is invalid (e.g. an add-on set it to a bogus value)
           // use the default engine from the list.json.
           // This should eventually be the common case. We should only have the
@@ -960,37 +820,38 @@ SearchService.prototype = {
     for (let name in this._engines) {
       cache.engines.push(this._engines[name]);
     }
 
     try {
       if (!cache.engines.length)
         throw new Error("cannot write without any engine.");
 
-      LOG("_buildCache: Writing to cache file.");
+      SearchUtils.log("_buildCache: Writing to cache file.");
       let path = OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME);
       let data = gEncoder.encode(JSON.stringify(cache));
       await OS.File.writeAtomic(path, data, {compression: "lz4",
                                              tmpPath: path + ".tmp"});
-      LOG("_buildCache: cache file written to disk.");
-      Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, SEARCH_SERVICE_CACHE_WRITTEN);
+      SearchUtils.log("_buildCache: cache file written to disk.");
+      Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE,
+        "write-cache-to-disk-complete");
     } catch (ex) {
-      LOG("_buildCache: Could not write to cache file: " + ex);
+      SearchUtils.log("_buildCache: Could not write to cache file: " + ex);
     }
   },
 
   /**
    * Loads engines asynchronously.
    *
    * @returns {Promise} A promise, resolved successfully if loading data
    * succeeds.
    */
   async _loadEngines(cache, isReload) {
-    LOG("_loadEngines: start");
-    Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "find-jar-engines");
+    SearchUtils.log("_loadEngines: start");
+    Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "find-jar-engines");
     let engines = await this._findEngines();
 
     // Get the non-empty distribution directories into distDirs...
     let distDirs = [];
     let locations;
     try {
       locations = Services.dirsvc.get(NS_APP_DISTRIBUTION_SEARCH_DIR_LIST,
                                       Ci.nsISimpleEnumerator);
@@ -1071,65 +932,65 @@ SearchService.prototype = {
 
       if (!params) {
         localeMap.set(locale, !rebuildCache);
         this._extensions.set(id, localeMap);
       }
     }
 
     if (!rebuildCache) {
-      LOG("_loadEngines: loading from cache directories");
+      SearchUtils.log("_loadEngines: loading from cache directories");
       this._loadEnginesFromCache(cache);
       if (Object.keys(this._engines).length) {
-        LOG("_loadEngines: done using existing cache");
+        SearchUtils.log("_loadEngines: done using existing cache");
         return;
       }
-      LOG("_loadEngines: No valid engines found in cache. Loading engines from disk.");
+      SearchUtils.log("_loadEngines: No valid engines found in cache. Loading engines from disk.");
     }
 
-    LOG("_loadEngines: Absent or outdated cache. Loading engines from disk.");
+    SearchUtils.log("_loadEngines: Absent or outdated cache. Loading engines from disk.");
     for (let loadDir of distDirs) {
       let enginesFromDir = await this._loadEnginesFromDir(loadDir);
       enginesFromDir.forEach(this._addEngineToStore, this);
     }
     if (AppConstants.platform == "android") {
       let enginesFromURLs = await this._loadFromChromeURLs(engines, isReload);
       enginesFromURLs.forEach(this._addEngineToStore, this);
     } else {
       for (let [id, localeMap] of this._extensions) {
         let policy = WebExtensionPolicy.getByID(id);
         if (policy) {
-          LOG("_loadEngines: Found previously installed extension");
+          SearchUtils.log("_loadEngines: Found previously installed extension");
           await this.addEnginesFromExtension(policy.extension);
         } else {
-          LOG("_loadEngines: Installing " + id);
+          SearchUtils.log("_loadEngines: Installing " + id);
           // We may have marked these as loading from the cache previously
           // but if there wasnt an valid cache, mark as installing.
           for (let [locale, installed] of localeMap) {
             if (installed === true) {
               localeMap.set(locale, null);
             }
           }
           this._extensions.set(id, localeMap);
           let path = EXT_SEARCH_PREFIX + id.split("@")[0] + "/";
           await AddonManager.installBuiltinAddon(path);
           // The AddonManager will install the engine asynchronously
           // We can manually wait on that happening here.
           await ExtensionParent.apiManager.global.pendingSearchSetupTasks.get(id);
-          LOG("_loadEngines: " + id + " installed");
+          SearchUtils.log("_loadEngines: " + id + " installed");
         }
       }
     }
 
-    LOG("_loadEngines: loading user-installed engines from the obsolete cache");
+    SearchUtils.log("_loadEngines: loading user-installed engines from the obsolete cache");
     this._loadEnginesFromCache(cache, true);
 
     this._loadEnginesMetadataFromCache(cache);
 
-    LOG("_loadEngines: done using rebuilt cache");
+    SearchUtils.log("_loadEngines: done using rebuilt cache");
   },
 
   /**
    * Reloads engines asynchronously, but only when the service has already been
    * initialized.
    *
    * @return {Promise} A promise, resolved successfully if loading data succeeds.
    */
@@ -1152,38 +1013,39 @@ SearchService.prototype = {
 
     await this._loadEngines(await this._readCacheFile(), true);
     // Make sure the current list of engines is persisted.
     await this._buildCache();
 
     // If the defaultEngine has changed between the previous load and this one,
     // dispatch the appropriate notifications.
     if (prevCurrentEngine && this.defaultEngine !== prevCurrentEngine) {
-      notifyAction(this._currentEngine, SEARCH_ENGINE_DEFAULT);
+      SearchUtils.notifyAction(this._currentEngine, SearchUtils.MODIFIED_TYPE.DEFAULT);
     }
-    Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "engines-reloaded");
+    Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE,
+      "engines-reloaded");
   },
 
   _reInit(origin, skipRegionCheck = true) {
-    LOG("_reInit");
+    SearchUtils.log("_reInit");
     // Re-entrance guard, because we're using an async lambda below.
     if (gReinitializing) {
-      LOG("_reInit: already re-initializing, bailing out.");
+      SearchUtils.log("_reInit: already re-initializing, bailing out.");
       return;
     }
     gReinitializing = true;
 
     // Start by clearing the initialized state, so we don't abort early.
     gInitialized = false;
 
     (async () => {
       try {
         this._initObservers = PromiseUtils.defer();
         if (this._batchTask) {
-          LOG("finalizing batch task");
+          SearchUtils.log("finalizing batch task");
           let task = this._batchTask;
           this._batchTask = null;
           // Tests manipulate the cache directly, so let's not double-write with
           // stale cache data here.
           if (origin == "test") {
             task.disarm();
           } else {
             await task.finalize();
@@ -1196,47 +1058,47 @@ SearchService.prototype = {
         this._currentEngine = null;
         this._visibleDefaultEngines = [];
         this._searchDefault = null;
         this._searchOrder = [];
         this._metaData = {};
 
         // Tests that want to force a synchronous re-initialization need to
         // be notified when we are done uninitializing.
-        Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC,
+        Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE,
                                      "uninit-complete");
 
         let cache = await this._readCacheFile();
         // The init flow is not going to block on a fetch from an external service,
         // but we're kicking it off as soon as possible to prevent UI flickering as
         // much as possible.
         this._ensureKnownRegionPromise = ensureKnownRegion(this)
-          .catch(ex => LOG("_reInit: failure determining region: " + ex))
+          .catch(ex => SearchUtils.log("_reInit: failure determining region: " + ex))
           .finally(() => this._ensureKnownRegionPromise = null);
 
         if (!skipRegionCheck) {
           await this._ensureKnownRegionPromise;
         }
 
         await this._loadEngines(cache);
         // Make sure the current list of engines is persisted.
         await this._buildCache();
 
         // Typically we'll re-init as a result of a pref observer,
         // so signal to 'callers' that we're done.
         gInitialized = true;
         this._initObservers.resolve();
-        Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
+        Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "init-complete");
       } catch (err) {
-        LOG("Reinit failed: " + err);
-        LOG(err.stack);
-        Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "reinit-failed");
+        SearchUtils.log("Reinit failed: " + err);
+        SearchUtils.log(err.stack);
+        Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "reinit-failed");
       } finally {
         gReinitializing = false;
-        Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "reinit-complete");
+        Services.obs.notifyObservers(null, SearchUtils.TOPIC_SEARCH_SERVICE, "reinit-complete");
       }
     })();
   },
 
   /**
    * Reset SearchService data.
    */
   reset() {
@@ -1261,51 +1123,51 @@ SearchService.prototype = {
         throw new Error("no engine in the file");
       // Reset search default expiration on major releases
       if (json.appVersion != Services.appinfo.version &&
           gGeoSpecificDefaultsEnabled &&
           json.metaData) {
         json.metaData.searchDefaultExpir = 0;
       }
     } catch (ex) {
-      LOG("_readCacheFile: Error reading cache file: " + ex);
+      SearchUtils.log("_readCacheFile: Error reading cache file: " + ex);
       json = {};
     }
     if (!gInitialized && json.metaData)
       this._metaData = json.metaData;
 
     return json;
   },
 
   _batchTask: null,
   get batchTask() {
     if (!this._batchTask) {
       let task = async () => {
-        LOG("batchTask: Invalidating engine cache");
+        SearchUtils.log("batchTask: Invalidating engine cache");
         await this._buildCache();
       };
       this._batchTask = new DeferredTask(task, CACHE_INVALIDATION_DELAY);
     }
     return this._batchTask;
   },
 
   _addEngineToStore(engine) {
     if (this._engineMatchesIgnoreLists(engine)) {
-      LOG("_addEngineToStore: Ignoring engine");
+      SearchUtils.log("_addEngineToStore: Ignoring engine");
       return;
     }
 
-    LOG("_addEngineToStore: Adding engine: \"" + engine.name + "\"");
+    SearchUtils.log("_addEngineToStore: Adding engine: \"" + engine.name + "\"");
 
     // See if there is an existing engine with the same name. However, if this
     // engine is updating another engine, it's allowed to have the same name.
     var hasSameNameAsUpdate = (engine._engineToUpdate &&
                                engine.name == engine._engineToUpdate.name);
     if (engine.name in this._engines && !hasSameNameAsUpdate) {
-      LOG("_addEngineToStore: Duplicate engine found, aborting!");
+      SearchUtils.log("_addEngineToStore: Duplicate engine found, aborting!");
       return;
     }
 
     if (engine._engineToUpdate) {
       // We need to replace engineToUpdate with the engine that just loaded.
       var oldEngine = engine._engineToUpdate;
 
       // Remove the old engine from the hash, since it's keyed by name, and our
@@ -1320,97 +1182,97 @@ SearchService.prototype = {
         if (!(engine.__lookupGetter__(p) || engine.__lookupSetter__(p)))
           oldEngine[p] = engine[p];
       }
       engine = oldEngine;
       engine._engineToUpdate = null;
 
       // Add the engine back
       this._engines[engine.name] = engine;
-      notifyAction(engine, SEARCH_ENGINE_CHANGED);
+      SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
     } else {
       // Not an update, just add the new engine.
       this._engines[engine.name] = engine;
       // Only add the engine to the list of sorted engines if the initial list
       // has already been built (i.e. if this.__sortedEngines is non-null). If
       // it hasn't, we're loading engines from disk and the sorted engine list
       // will be built once we need it.
       if (this.__sortedEngines) {
         this.__sortedEngines.push(engine);
         this._saveSortedEngineList();
       }
-      notifyAction(engine, SEARCH_ENGINE_ADDED);
+      SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.ADDED);
     }
 
     if (engine._hasUpdates) {
       // Schedule the engine's next update, if it isn't already.
       if (!engine.getAttr("updateexpir"))
         engineUpdateService.scheduleNextUpdate(engine);
     }
   },
 
   _loadEnginesMetadataFromCache(cache) {
     if (!cache.engines)
       return;
 
     for (let engine of cache.engines) {
       let name = engine._name;
       if (name in this._engines) {
-        LOG("_loadEnginesMetadataFromCache, transfering metadata for " + name);
+        SearchUtils.log("_loadEnginesMetadataFromCache, transfering metadata for " + name);
         this._engines[name]._metaData = engine._metaData || {};
       }
     }
   },
 
   _loadEnginesFromCache(cache, skipReadOnly) {
     if (!cache.engines)
       return;
 
-    LOG("_loadEnginesFromCache: Loading " +
+    SearchUtils.log("_loadEnginesFromCache: Loading " +
         cache.engines.length + " engines from cache");
 
     let skippedEngines = 0;
     for (let engine of cache.engines) {
       if (skipReadOnly && engine._readOnly == undefined) {
         ++skippedEngines;
         continue;
       }
 
       this._loadEngineFromCache(engine);
     }
 
     if (skippedEngines) {
-      LOG("_loadEnginesFromCache: skipped " + skippedEngines + " read-only engines.");
+      SearchUtils.log("_loadEnginesFromCache: skipped " + skippedEngines + " read-only engines.");
     }
   },
 
   _loadEngineFromCache(json) {
     try {
       let engine = new SearchEngine({
         name: json._shortName,
         readOnly: json._readOnly == undefined,
       });
       engine._initWithJSON(json);
       this._addEngineToStore(engine);
     } catch (ex) {
-      LOG("Failed to load " + json._name + " from cache: " + ex);
-      LOG("Engine JSON: " + json.toSource());
+      SearchUtils.log("Failed to load " + json._name + " from cache: " + ex);
+      SearchUtils.log("Engine JSON: " + json.toSource());
     }
   },
 
   /**
    * Loads engines from a given directory asynchronously.
    *
    * @param {OS.File} dir the directory.
    *
    * @returns {Promise} A promise, resolved successfully if retrieveing data
    * succeeds.
    */
   async _loadEnginesFromDir(dir) {
-    LOG("_loadEnginesFromDir: Searching in " + dir.path + " for search engines.");
+    SearchUtils.log("_loadEnginesFromDir: Searching in " + dir.path + " for search engines.");
 
     let iterator = new OS.File.DirectoryIterator(dir.path);
 
     let osfiles = await iterator.nextBatch();
     iterator.close();
 
     let engines = [];
     for (let osfile of osfiles) {
@@ -1433,17 +1295,17 @@ SearchService.prototype = {
         file.initWithPath(osfile.path);
         addedEngine = new SearchEngine({
           fileURI: file,
           readOnly: false,
         });
         await addedEngine._initFromFile(file);
         engines.push(addedEngine);
       } catch (ex) {
-        LOG("_loadEnginesFromDir: Failed to load " + osfile.path + "!\n" + ex);
+        SearchUtils.log("_loadEnginesFromDir: Failed to load " + osfile.path + "!\n" + ex);
       }
     }
     return engines;
   },
 
   /**
    * Loads engines from Chrome URLs asynchronously.
    *
@@ -1453,63 +1315,63 @@ SearchService.prototype = {
    *   is being called from maybeReloadEngines.
    * @returns {Promise} A promise, resolved successfully if loading data
    * succeeds.
    */
   async _loadFromChromeURLs(urls, isReload = false) {
     let engines = [];
     for (let url of urls) {
       try {
-        LOG("_loadFromChromeURLs: loading engine from chrome url: " + url);
+        SearchUtils.log("_loadFromChromeURLs: loading engine from chrome url: " + url);
         let uri = Services.io.newURI(APP_SEARCH_PREFIX + url + ".xml");
         let engine = new SearchEngine({
           uri,
           readOnly: true,
         });
         await engine._initFromURI(uri);
         // If there is an existing engine with the same name then update that engine.
         // Only do this during reloads so it doesnt interfere with distribution
         // engines
         if (isReload && engine.name in this._engines) {
           engine._engineToUpdate = this._engines[engine.name];
         }
         engines.push(engine);
       } catch (ex) {
-        LOG("_loadFromChromeURLs: failed to load engine: " + ex);
+        SearchUtils.log("_loadFromChromeURLs: failed to load engine: " + ex);
       }
     }
     return engines;
   },
 
   /**
    * Loads jar engines asynchronously.
    *
    * @returns {Promise} A promise, resolved successfully if finding jar engines
    * succeeds.
    */
   async _findEngines() {
-    LOG("_findEngines: looking for engines in JARs");
+    SearchUtils.log("_findEngines: looking for engines in JARs");
 
     let chan = makeChannel(this._listJSONURL);
     if (!chan) {
-      LOG("_findEngines: " + this._listJSONURL + " isn't registered");
+      SearchUtils.log("_findEngines: " + this._listJSONURL + " isn't registered");
       return [];
     }
 
     let uris = [];
 
     // Read list.json to find the engines we need to load.
     let request = new XMLHttpRequest();
     request.overrideMimeType("text/plain");
     let list = await new Promise(resolve => {
       request.onload = function(event) {
         resolve(event.target.responseText);
       };
       request.onerror = function(event) {
-        LOG("_findEngines: failed to read " + this._listJSONURL);
+        SearchUtils.log("_findEngines: failed to read " + this._listJSONURL);
         resolve();
       };
       request.open("GET", Services.io.newURI(this._listJSONURL).spec, true);
       request.send();
     });
 
     this._parseListJSON(list, uris);
     return uris;
@@ -1575,17 +1437,17 @@ SearchService.prototype = {
       for (let engineName of engineNames) {
         // If all engineName values are part of jarNames,
         // then we can use the region specific list, otherwise ignore it.
         // The visibleDefaultEngines string containing the name of an engine we
         // don't ship indicates the server is misconfigured to answer requests
         // from the specific Firefox version we are running, so ignoring the
         // value altogether is safer.
         if (!jarNames.has(engineName)) {
-          LOG("_parseListJSON: ignoring visibleDefaultEngines value because " +
+          SearchUtils.log("_parseListJSON: ignoring visibleDefaultEngines value because " +
               engineName + " is not in the jar engines we have found");
           engineNames = null;
           break;
         }
       }
     }
 
     // Fallback to building a list based on the regions in the JSON
@@ -1595,17 +1457,17 @@ SearchService.prototype = {
         engineNames = searchSettings[searchRegion].visibleDefaultEngines;
       } else {
         engineNames = searchSettings.default.visibleDefaultEngines;
       }
     }
 
     // Remove any engine names that are supposed to be ignored.
     // This pref is only allowed in a partner distribution.
-    let branch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+    let branch = Services.prefs.getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF);
     if (isPartnerBuild() &&
         branch.getPrefType("ignoredJAREngines") == branch.PREF_STRING) {
       let ignoredJAREngines = branch.getCharPref("ignoredJAREngines")
                                     .split(",");
       let filteredEngineNames = engineNames.filter(e => !ignoredJAREngines.includes(e));
       // Don't allow all engines to be hidden
       if (filteredEngineNames.length > 0) {
         engineNames = filteredEngineNames;
@@ -1663,40 +1525,40 @@ SearchService.prototype = {
     } else if ("searchOrder" in searchSettings.default) {
       this._searchOrder = searchSettings.default.searchOrder;
     } else if ("searchOrder" in json.default) {
       this._searchOrder = json.default.searchOrder;
     }
   },
 
   _saveSortedEngineList() {
-    LOG("_saveSortedEngineList: starting");
+    SearchUtils.log("_saveSortedEngineList: starting");
 
     // Set the useDB pref to indicate that from now on we should use the order
     // information stored in the database.
-    Services.prefs.setBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", true);
+    Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder", true);
 
     var engines = this._getSortedEngines(true);
 
     for (var i = 0; i < engines.length; ++i) {
       engines[i].setAttr("order", i + 1);
     }
 
-    LOG("_saveSortedEngineList: done");
+    SearchUtils.log("_saveSortedEngineList: done");
   },
 
   _buildSortedEngineList() {
-    LOG("_buildSortedEngineList: building list");
+    SearchUtils.log("_buildSortedEngineList: building list");
     var addedEngines = { };
     this.__sortedEngines = [];
 
     // If the user has specified a custom engine order, read the order
     // information from the metadata instead of the default prefs.
-    if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", false)) {
-      LOG("_buildSortedEngineList: using db for order");
+    if (Services.prefs.getBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder", false)) {
+      SearchUtils.log("_buildSortedEngineList: using db for order");
 
       // Flag to keep track of whether or not we need to call _saveSortedEngineList.
       let needToSaveEngineList = false;
 
       for (let name in this._engines) {
         let engine = this._engines[name];
         var orderNumber = engine.getAttr("order");
 
@@ -1730,32 +1592,32 @@ SearchService.prototype = {
       if (this.originalDefaultEngine) {
         this.__sortedEngines.push(this.originalDefaultEngine);
         addedEngines[this.originalDefaultEngine.name] = this.originalDefaultEngine;
       }
 
       if (distroID) {
         try {
           var extras =
-            Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
+            Services.prefs.getChildList(SearchUtils.BROWSER_SEARCH_PREF + "order.extra.");
 
           for (prefName of extras) {
             let engineName = Services.prefs.getCharPref(prefName);
 
             let engine = this._engines[engineName];
             if (!engine || engine.name in addedEngines)
               continue;
 
             this.__sortedEngines.push(engine);
             addedEngines[engine.name] = engine;
           }
         } catch (e) { }
 
         while (true) {
-          prefName = `${BROWSER_SEARCH_PREF}order.${++i}`;
+          prefName = `${SearchUtils.BROWSER_SEARCH_PREF}order.${++i}`;
           let engineName = getLocalizedPref(prefName);
           if (!engineName)
             break;
 
           let engine = this._engines[engineName];
           if (!engine || engine.name in addedEngines)
             continue;
 
@@ -1803,17 +1665,17 @@ SearchService.prototype = {
 
     return this._sortedEngines.filter(function(engine) {
                                         return !engine.hidden;
                                       });
   },
 
   // nsISearchService
   async init(skipRegionCheck = false) {
-    LOG("SearchService.init");
+    SearchUtils.log("SearchService.init");
     if (this._initStarted) {
       if (!skipRegionCheck) {
         await this._ensureKnownRegionPromise;
       }
       return this._initObservers.promise;
     }
 
     TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
@@ -1846,24 +1708,24 @@ SearchService.prototype = {
 
   // reInit is currently only exposed for testing purposes
   async reInit(skipRegionCheck) {
     return this._reInit("test", skipRegionCheck);
   },
 
   async getEngines() {
     await this.init(true);
-    LOG("getEngines: getting all engines");
+    SearchUtils.log("getEngines: getting all engines");
     var engines = this._getSortedEngines(true);
     return engines;
   },
 
   async getVisibleEngines() {
     await this.init();
-    LOG("getVisibleEngines: getting all visible engines");
+    SearchUtils.log("getVisibleEngines: getting all visible engines");
     var engines = this._getSortedEngines(false);
     return engines;
   },
 
   async getDefaultEngines() {
     await this.init(true);
     function isDefault(engine) {
       return engine._isDefault;
@@ -1874,46 +1736,46 @@ SearchService.prototype = {
 
     // Build a list of engines which we have ordering information for.
     // We're rebuilding the list here because _sortedEngines contain the
     // current order, but we want the original order.
 
     if (distroID) {
       // First, look at the "browser.search.order.extra" branch.
       try {
-        var extras = Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
+        var extras = Services.prefs.getChildList(SearchUtils.BROWSER_SEARCH_PREF + "order.extra.");
 
         for (let prefName of extras) {
           let engineName = Services.prefs.getCharPref(prefName);
 
           if (!(engineName in engineOrder))
             engineOrder[engineName] = i++;
         }
       } catch (e) {
-        LOG("Getting extra order prefs failed: " + e);
+        SearchUtils.log("Getting extra order prefs failed: " + e);
       }
 
       // Now look through the "browser.search.order" branch.
       for (var j = 1; ; j++) {
-        let prefName = `${BROWSER_SEARCH_PREF}order.${j}`;
+        let prefName = `${SearchUtils.BROWSER_SEARCH_PREF}order.${j}`;
         let engineName = getLocalizedPref(prefName);
         if (!engineName)
           break;
 
         if (!(engineName in engineOrder))
           engineOrder[engineName] = i++;
       }
     }
 
     // Now look at list.json
     for (let engineName of this._searchOrder) {
       engineOrder[engineName] = i++;
     }
 
-    LOG("getDefaultEngines: engineOrder: " + engineOrder.toSource());
+    SearchUtils.log("getDefaultEngines: engineOrder: " + engineOrder.toSource());
 
     function compareEngines(a, b) {
       var aIdx = engineOrder[a.name];
       var bIdx = engineOrder[b.name];
 
       if (aIdx && bIdx)
         return aIdx - bIdx;
       if (aIdx)
@@ -1924,17 +1786,17 @@ SearchService.prototype = {
       return a.name.localeCompare(b.name);
     }
     engines.sort(compareEngines);
     return engines;
   },
 
   async getEnginesByExtensionID(extensionID) {
     await this.init(true);
-    LOG("getEngines: getting all engines for " + extensionID);
+    SearchUtils.log("getEngines: getting all engines for " + extensionID);
     var engines = this._getSortedEngines(true).filter(function(engine) {
       return engine._extensionID == extensionID;
     });
     return engines;
   },
 
   getEngineByName(engineName) {
     this._ensureInitialized();
@@ -1969,32 +1831,32 @@ SearchService.prototype = {
       };
     }
 
     let isBuiltin = !!params.isBuiltin;
     // We install search extensions during the init phase, both built in
     // web extensions freshly installed (via addEnginesFromExtension) or
     // user installed extensions being reenabled calling this directly.
     if (!gInitialized && !this._extensions.has(params.extensionID)) {
-      LOG("addEngineWithDetails: Not expecting " + params.extensionID);
+      SearchUtils.log("addEngineWithDetails: Not expecting " + params.extensionID);
       await this.init(true);
     }
     if (!name)
-      FAIL("Invalid name passed to addEngineWithDetails!");
+      SearchUtils.fail("Invalid name passed to addEngineWithDetails!");
     if (!params.template)
-      FAIL("Invalid template passed to addEngineWithDetails!");
+      SearchUtils.fail("Invalid template passed to addEngineWithDetails!");
     let existingEngine = this._engines[name];
     if (existingEngine) {
       if (params.extensionID &&
           existingEngine._loadPath.startsWith(`jar:[profile]/extensions/${params.extensionID}`)) {
         // This is a legacy extension engine that needs to be migrated to WebExtensions.
         isCurrent = this.defaultEngine == existingEngine;
         await this.removeEngine(existingEngine);
       } else {
-        FAIL("An engine with that name already exists!", Cr.NS_ERROR_FILE_ALREADY_EXISTS);
+        SearchUtils.fail("An engine with that name already exists!", Cr.NS_ERROR_FILE_ALREADY_EXISTS);
       }
     }
 
     let newEngine = new SearchEngine({
       name,
       readOnly: isBuiltin,
       sanitizeName: true,
     });
@@ -2007,50 +1869,50 @@ SearchService.prototype = {
     this._addEngineToStore(newEngine);
     if (isCurrent) {
       this.defaultEngine = newEngine;
     }
     return newEngine;
   },
 
   async addEnginesFromExtension(extension) {
-    LOG("addEnginesFromExtension: " + extension.id);
+    SearchUtils.log("addEnginesFromExtension: " + extension.id);
     try {
       // When Firefox starts, the AddonManager will report all the currently
       // installed search addons to us, keep the user installed engines
       // but the Builtin ones we will install during init().
       if (!gInitialized && extension.addonData.builtIn &&
           !this._extensions.has(extension.id)) {
         return [];
       }
 
       if (!this._extensions.has(extension.id)) {
-        LOG("addEnginesFromExtension: User installed extension " + extension.id);
+        SearchUtils.log("addEnginesFromExtension: User installed extension " + extension.id);
         this._extensions.set(extension.id, new Map([[DEFAULT_TAG, null]]));
       }
 
       let installLocale = async (locale) => {
         let manifest = (locale === DEFAULT_TAG) ? extension.manifest :
             (await extension.getLocalizedManifest(locale));
         return this._addEngineForManifest(extension, manifest, locale);
       };
 
       let engines = [];
       for (let [locale, installed] of this._extensions.get(extension.id)) {
         // If we have installed the engine from cache previously then
         // no need to reinstall.
         if (installed !== true) {
-          LOG("addEnginesFromExtension: installing locale: " +
+          SearchUtils.log("addEnginesFromExtension: installing locale: " +
               extension.id + ":" + locale);
           engines.push(await installLocale(locale));
         }
       }
       return engines;
     } catch (err) {
-      LOG("addEnginesFromExtension: Failed to install " + extension.id + ": \"" +
+      SearchUtils.log("addEnginesFromExtension: Failed to install " + extension.id + ": \"" +
           err.message + "\"\n" + err.stack);
       return [];
     }
   },
 
   async _addEngineForManifest(extension, manifest, locale = DEFAULT_TAG) {
     let {IconDetails} = ExtensionParent;
 
@@ -2101,17 +1963,17 @@ SearchService.prototype = {
     let localeMap = this._extensions.get(extension.id);
     localeMap.set(locale, params);
     this._extensions.set(extension.id, localeMap);
 
     return this.addEngineWithDetails(params.name, params);
   },
 
   async addEngine(engineURL, iconURL, confirm, extensionID) {
-    LOG("addEngine: Adding \"" + engineURL + "\".");
+    SearchUtils.log("addEngine: Adding \"" + engineURL + "\".");
     await this.init(true);
     let errCode;
     try {
       var engine = new SearchEngine({
         uri: engineURL,
         readOnly: false,
       });
       engine._setIcon(iconURL, false);
@@ -2129,48 +1991,48 @@ SearchService.prototype = {
       });
       if (errCode) {
         throw errCode;
       }
     } catch (ex) {
       // Drop the reference to the callback, if set
       if (engine)
         engine._installCallback = null;
-      FAIL("addEngine: Error adding engine:\n" + ex, errCode || Cr.NS_ERROR_FAILURE);
+      SearchUtils.fail("addEngine: Error adding engine:\n" + ex, errCode || Cr.NS_ERROR_FAILURE);
     }
     return engine;
   },
 
   async removeWebExtensionEngine(id) {
-    LOG("removeWebExtensionEngine: " + id);
+    SearchUtils.log("removeWebExtensionEngine: " + id);
     let localeMap = this._extensions.get(id);
     if (!localeMap) {
       Cu.reportError("Cannot find extension (" + id + ") to remove");
       return;
     }
     for (let params of localeMap.values()) {
       let engine = await this.getEngineByName(params.name);
       await this.removeEngine(engine);
     }
     this._extensions.delete(id);
   },
 
   async removeEngine(engine) {
     await this.init(true);
     if (!engine)
-      FAIL("no engine passed to removeEngine!");
+      SearchUtils.fail("no engine passed to removeEngine!");
 
     var engineToRemove = null;
     for (var e in this._engines) {
       if (engine.wrappedJSObject == this._engines[e])
         engineToRemove = this._engines[e];
     }
 
     if (!engineToRemove)
-      FAIL("removeEngine: Can't find engine to remove!", Cr.NS_ERROR_FILE_NOT_FOUND);
+      SearchUtils.fail("removeEngine: Can't find engine to remove!", Cr.NS_ERROR_FILE_NOT_FOUND);
 
     if (engineToRemove == this.defaultEngine) {
       this._currentEngine = null;
     }
 
     if (engineToRemove._readOnly || engineToRemove.isBuiltin) {
       // Just hide it (the "hidden" setter will notify) and remove its alias to
       // avoid future conflicts with other engines.
@@ -2185,73 +2047,73 @@ SearchService.prototype = {
           file.remove(false);
         }
         engineToRemove._filePath = null;
       }
 
       // Remove the engine from _sortedEngines
       var index = this._sortedEngines.indexOf(engineToRemove);
       if (index == -1)
-        FAIL("Can't find engine to remove in _sortedEngines!", Cr.NS_ERROR_FAILURE);
+        SearchUtils.fail("Can't find engine to remove in _sortedEngines!", Cr.NS_ERROR_FAILURE);
       this.__sortedEngines.splice(index, 1);
 
       // Remove the engine from the internal store
       delete this._engines[engineToRemove.name];
 
       // Since we removed an engine, we need to update the preferences.
       this._saveSortedEngineList();
     }
-    notifyAction(engineToRemove, SEARCH_ENGINE_REMOVED);
+    SearchUtils.notifyAction(engineToRemove, SearchUtils.MODIFIED_TYPE.REMOVED);
   },
 
   async moveEngine(engine, newIndex) {
     await this.init(true);
     if ((newIndex > this._sortedEngines.length) || (newIndex < 0))
-      FAIL("moveEngine: Index out of bounds!");
+      SearchUtils.fail("moveEngine: Index out of bounds!");
     if (!(engine instanceof Ci.nsISearchEngine) && !(engine instanceof SearchEngine))
-      FAIL("moveEngine: Invalid engine passed to moveEngine!");
+      SearchUtils.fail("moveEngine: Invalid engine passed to moveEngine!");
     if (engine.hidden)
-      FAIL("moveEngine: Can't move a hidden engine!", Cr.NS_ERROR_FAILURE);
+      SearchUtils.fail("moveEngine: Can't move a hidden engine!", Cr.NS_ERROR_FAILURE);
 
     engine = engine.wrappedJSObject;
 
     var currentIndex = this._sortedEngines.indexOf(engine);
     if (currentIndex == -1)
-      FAIL("moveEngine: Can't find engine to move!", Cr.NS_ERROR_UNEXPECTED);
+      SearchUtils.fail("moveEngine: Can't find engine to move!", Cr.NS_ERROR_UNEXPECTED);
 
     // Our callers only take into account non-hidden engines when calculating
     // newIndex, but we need to move it in the array of all engines, so we
     // need to adjust newIndex accordingly. To do this, we count the number
     // of hidden engines in the list before the engine that we're taking the
     // place of. We do this by first finding newIndexEngine (the engine that
     // we were supposed to replace) and then iterating through the complete
     // engine list until we reach it, increasing newIndex for each hidden
     // engine we find on our way there.
     //
     // This could be further simplified by having our caller pass in
     // newIndexEngine directly instead of newIndex.
     var newIndexEngine = this._getSortedEngines(false)[newIndex];
     if (!newIndexEngine)
-      FAIL("moveEngine: Can't find engine to replace!", Cr.NS_ERROR_UNEXPECTED);
+      SearchUtils.fail("moveEngine: Can't find engine to replace!", Cr.NS_ERROR_UNEXPECTED);
 
     for (var i = 0; i < this._sortedEngines.length; ++i) {
       if (newIndexEngine == this._sortedEngines[i])
         break;
       if (this._sortedEngines[i].hidden)
         newIndex++;
     }
 
     if (currentIndex == newIndex)
       return; // nothing to do!
 
     // Move the engine
     var movedEngine = this.__sortedEngines.splice(currentIndex, 1)[0];
     this.__sortedEngines.splice(newIndex, 0, movedEngine);
 
-    notifyAction(engine, SEARCH_ENGINE_CHANGED);
+    SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
 
     // Since we moved an engine, we need to update the preferences.
     this._saveSortedEngineList();
   },
 
   restoreDefaultEngines() {
     this._ensureInitialized();
     for (let name in this._engines) {
@@ -2307,32 +2169,32 @@ SearchService.prototype = {
   },
 
   set defaultEngine(val) {
     this._ensureInitialized();
     // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
     // and sometimes we get raw Engine JS objects (callers in this file), so
     // handle both.
     if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof SearchEngine))
-      FAIL("Invalid argument passed to defaultEngine setter");
+      SearchUtils.fail("Invalid argument passed to defaultEngine setter");
 
     var newCurrentEngine = this.getEngineByName(val.name);
     if (!newCurrentEngine)
-      FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
+      SearchUtils.fail("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
 
     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);
+        SearchUtils.notifyAction(newCurrentEngine, SearchUtils.MODIFIED_TYPE.CHANGED);
       }
     }
 
     if (newCurrentEngine == this._currentEngine)
       return;
 
     this._currentEngine = newCurrentEngine;
 
@@ -2344,17 +2206,17 @@ SearchService.prototype = {
     let newName = this._currentEngine.name;
     if (this._currentEngine == this.originalDefaultEngine) {
       newName = "";
     }
 
     this.setGlobalAttr("current", newName);
     this.setGlobalAttr("hash", getVerificationHash(newName));
 
-    notifyAction(this._currentEngine, SEARCH_ENGINE_DEFAULT);
+    SearchUtils.notifyAction(this._currentEngine, SearchUtils.MODIFIED_TYPE.DEFAULT);
   },
 
   async getDefault() {
     await this.init(true);
     return this.defaultEngine;
   },
 
   async setDefault(engine) {
@@ -2398,30 +2260,30 @@ SearchService.prototype = {
       result.origin = origin;
 
       // For privacy, we only collect the submission URL for default engines...
       let sendSubmissionURL = engine._isDefault;
 
       // ... or engines sorted by default near the top of the list.
       if (!sendSubmissionURL) {
         let extras =
-          Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
+          Services.prefs.getChildList(SearchUtils.BROWSER_SEARCH_PREF + "order.extra.");
 
         for (let prefName of extras) {
           try {
             if (result.name == Services.prefs.getCharPref(prefName)) {
               sendSubmissionURL = true;
               break;
             }
           } catch (e) {}
         }
 
         let i = 0;
         while (!sendSubmissionURL) {
-          let prefName = `${BROWSER_SEARCH_PREF}order.${++i}`;
+          let prefName = `${SearchUtils.BROWSER_SEARCH_PREF}order.${++i}`;
           let engineName = getLocalizedPref(prefName);
           if (!engineName)
             break;
           if (result.name == engineName) {
             sendSubmissionURL = true;
             break;
           }
         }
@@ -2431,24 +2293,24 @@ SearchService.prototype = {
             sendSubmissionURL = true;
             break;
           }
         }
       }
 
       if (!sendSubmissionURL) {
         // ... or engines that are the same domain as a default engine.
-        let engineHost = engine._getURLOfType(URLTYPE_SEARCH_HTML).templateHost;
+        let engineHost = engine._getURLOfType(SearchUtils.URL_TYPE.SEARCH).templateHost;
         for (let name in this._engines) {
           let innerEngine = this._engines[name];
           if (!innerEngine._isDefault) {
             continue;
           }
 
-          let innerEngineURL = innerEngine._getURLOfType(URLTYPE_SEARCH_HTML);
+          let innerEngineURL = innerEngine._getURLOfType(SearchUtils.URL_TYPE.SEARCH);
           if (innerEngineURL.templateHost == engineHost) {
             sendSubmissionURL = true;
             break;
           }
         }
 
         if (!sendSubmissionURL) {
           // ... or well known search domains.
@@ -2537,17 +2399,17 @@ SearchService.prototype = {
 
       processDomain(urlParsingInfo.mainDomain, false);
       SearchStaticData.getAlternateDomains(urlParsingInfo.mainDomain)
                       .forEach(d => processDomain(d, true));
     }
   },
 
   /**
-   * Checks to see if any engine has an EngineURL of type URLTYPE_SEARCH_HTML
+   * Checks to see if any engine has an EngineURL of type SearchUtils.URL_TYPE.SEARCH
    * for this request-method, template URL, and query params.
    */
   hasEngineWithURL(method, template, formData) {
     this._ensureInitialized();
 
     // Quick helper method to ensure formData filtered/sorted for compares.
     let getSortedFormData = data => {
       return data.filter(a => a.name && a.value).sort((a, b) => {
@@ -2565,17 +2427,17 @@ SearchService.prototype = {
     // Sanitize method, ensure formData is pre-sorted.
     let methodUpper = method.toUpperCase();
     let sortedFormData = getSortedFormData(formData);
     let sortedFormLength = sortedFormData.length;
 
     return this._getSortedEngines(false).some(engine => {
       return engine._urls.some(url => {
         // Not an engineURL match if type, method, url, #params don't match.
-        if (url.type != URLTYPE_SEARCH_HTML ||
+        if (url.type != SearchUtils.URL_TYPE.SEARCH ||
             url.method != methodUpper ||
             url.template != template ||
             url.params.length != sortedFormLength) {
           return false;
         }
 
         // Ensure engineURL formData is pre-sorted. Then, we're
         // not an engineURL match if any queryParam doesn't compare.
@@ -2676,33 +2538,33 @@ SearchService.prototype = {
     }
 
     return new ParseSubmissionResult(mapEntry.engine, terms, offset, length);
   },
 
   // nsIObserver
   observe(engine, topic, verb) {
     switch (topic) {
-      case SEARCH_ENGINE_TOPIC:
+      case SearchUtils.TOPIC_ENGINE_MODIFIED:
         switch (verb) {
-          case SEARCH_ENGINE_LOADED:
+          case SearchUtils.MODIFIED_TYPE.LOADED:
             engine = engine.QueryInterface(Ci.nsISearchEngine);
-            LOG("nsSearchService::observe: Done installation of " + engine.name
+            SearchUtils.log("nsSearchService::observe: Done installation of " + engine.name
                 + ".");
             this._addEngineToStore(engine.wrappedJSObject);
             if (engine.wrappedJSObject._useNow) {
-              LOG("nsSearchService::observe: setting current");
+              SearchUtils.log("nsSearchService::observe: setting current");
               this.defaultEngine = engine;
             }
             // The addition of the engine to the store always triggers an ADDED
             // or a CHANGED notification, that will trigger the task below.
             break;
-          case SEARCH_ENGINE_ADDED:
-          case SEARCH_ENGINE_CHANGED:
-          case SEARCH_ENGINE_REMOVED:
+          case SearchUtils.MODIFIED_TYPE.ADDED:
+          case SearchUtils.MODIFIED_TYPE.CHANGED:
+          case SearchUtils.MODIFIED_TYPE.REMOVED:
             this.batchTask.disarm();
             this.batchTask.arm();
             // Invalidate the map used to parse URLs to search engines.
             this._parseSubmissionMap = null;
             break;
         }
         break;
 
@@ -2719,61 +2581,61 @@ SearchService.prototype = {
           this._reInit(verb);
         }
         break;
     }
   },
 
   // nsITimerCallback
   notify(timer) {
-    LOG("_notify: checking for updates");
+    SearchUtils.log("_notify: checking for updates");
 
-    if (!Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "update", true))
+    if (!Services.prefs.getBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "update", true))
       return;
 
     // Our timer has expired, but unfortunately, we can't get any data from it.
     // Therefore, we need to walk our engine-list, looking for expired engines
     var currentTime = Date.now();
-    LOG("currentTime: " + currentTime);
+    SearchUtils.log("currentTime: " + currentTime);
     for (let name in this._engines) {
       let engine = this._engines[name].wrappedJSObject;
       if (!engine._hasUpdates)
         continue;
 
-      LOG("checking " + engine.name);
+      SearchUtils.log("checking " + engine.name);
 
       var expirTime = engine.getAttr("updateexpir");
-      LOG("expirTime: " + expirTime + "\nupdateURL: " + engine._updateURL +
+      SearchUtils.log("expirTime: " + expirTime + "\nupdateURL: " + engine._updateURL +
           "\niconUpdateURL: " + engine._iconUpdateURL);
 
       var engineExpired = expirTime <= currentTime;
 
       if (!expirTime || !engineExpired) {
-        LOG("skipping engine");
+        SearchUtils.log("skipping engine");
         continue;
       }
 
-      LOG(engine.name + " has expired");
+      SearchUtils.log(engine.name + " has expired");
 
       engineUpdateService.update(engine);
 
       // Schedule the next update
       engineUpdateService.scheduleNextUpdate(engine);
     } // end engine iteration
   },
 
   _addObservers() {
     if (this._observersAdded) {
       // There might be a race between synchronous and asynchronous
       // initialization for which we try to register the observers twice.
       return;
     }
     this._observersAdded = true;
 
-    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
+    Services.obs.addObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED);
     Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC);
     Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
 
     // The current stage of shutdown. Used to help analyze crash
     // signatures in case of shutdown timeout.
     let shutdownState = {
       step: "Not started",
       latestError: {
@@ -2806,83 +2668,83 @@ SearchService.prototype = {
 
       () => shutdownState
     );
   },
   _observersAdded: false,
 
   _removeObservers() {
     if (this._ignoreListListener) {
-      RemoteSettings(SETTINGS_IGNORELIST_KEY).off("sync", this._ignoreListListener);
+      RemoteSettings(SearchUtils.SETTINGS_IGNORELIST_KEY).off("sync", this._ignoreListListener);
       delete this._ignoreListListener;
     }
 
-    Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
+    Services.obs.removeObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED);
     Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC);
     Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE);
   },
 
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsISearchService,
     Ci.nsIObserver,
     Ci.nsITimerCallback,
   ]),
 };
 
-
-const SEARCH_UPDATE_LOG_PREFIX = "*** Search update: ";
-
-/**
- * Outputs text to the JavaScript console as well as to stdout, if the search
- * logging pref (browser.search.update.log) is set to true.
- */
-function ULOG(text) {
-  if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "update.log", false)) {
-    dump(SEARCH_UPDATE_LOG_PREFIX + text + "\n");
-    Services.console.logStringMessage(text);
-  }
-}
-
 var engineUpdateService = {
   scheduleNextUpdate(engine) {
     var interval = engine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL;
     var milliseconds = interval * 86400000; // |interval| is in days
     engine.setAttr("updateexpir", Date.now() + milliseconds);
   },
 
   update(engine) {
     engine = engine.wrappedJSObject;
-    ULOG("update called for " + engine._name);
-    if (!Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "update", true) ||
+    this._log("update called for " + engine._name);
+    if (!Services.prefs.getBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "update", true) ||
         !engine._hasUpdates)
       return;
 
     let testEngine = null;
-    let updateURL = engine._getURLOfType(URLTYPE_OPENSEARCH);
+    let updateURL = engine._getURLOfType(SearchUtils.URL_TYPE.OPENSEARCH);
     let updateURI = (updateURL && updateURL._hasRelation("self")) ?
                      updateURL.getSubmission("", engine).uri :
                      makeURI(engine._updateURL);
     if (updateURI) {
       if (engine._isDefault && !updateURI.schemeIs("https")) {
-        ULOG("Invalid scheme for default engine update");
+        this._log("Invalid scheme for default engine update");
         return;
       }
 
-      ULOG("updating " + engine.name + " from " + updateURI.spec);
+      this._log("updating " + engine.name + " from " + updateURI.spec);
       testEngine = new SearchEngine({
         uri: updateURI,
         readOnly: false,
       });
       testEngine._engineToUpdate = engine;
       testEngine._initFromURIAndLoad(updateURI);
     } else {
-      ULOG("invalid updateURI");
+      this._log("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);
     }
   },
+
+  /**
+   * Outputs text to the JavaScript console as well as to stdout, if the search
+   * logging pref (browser.search.update.log) is set to true.
+   *
+   * @param {string} text
+   *   The message to log.
+   */
+  _log(text) {
+    if (Services.prefs.getBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "update.log", false)) {
+      dump("*** Search update: " + text + "\n");
+      Services.console.logStringMessage(text);
+    }
+  },
 };
 
 var EXPORTED_SYMBOLS = ["SearchService"];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/SearchUtils.jsm
@@ -0,0 +1,143 @@
+/* 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/. */
+
+/* eslint no-shadow: error, mozilla/no-aArgs: error */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["SearchUtils"];
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "loggingEnabled",
+  BROWSER_SEARCH_PREF + "log", false);
+
+var SearchUtils = {
+  APP_SEARCH_PREFIX: "resource://search-plugins/",
+
+  BROWSER_SEARCH_PREF,
+
+  /**
+   * Topic used for events involving the service itself.
+   */
+  TOPIC_SEARCH_SERVICE: "browser-search-service",
+
+  // See documentation in nsISearchService.idl.
+  TOPIC_ENGINE_MODIFIED: "browser-search-engine-modified",
+  MODIFIED_TYPE: {
+    CHANGED: "engine-changed",
+    LOADED: "engine-loaded",
+    REMOVED: "engine-removed",
+    ADDED: "engine-added",
+    DEFAULT: "engine-default",
+  },
+
+  URL_TYPE: {
+    SUGGEST_JSON: "application/x-suggestions+json",
+    SEARCH: "text/html",
+    OPENSEARCH: "application/opensearchdescription+xml",
+  },
+
+  // The following constants are left undocumented in nsISearchService.idl
+  // For the moment, they are meant for testing/debugging purposes only.
+
+  // Set an arbitrary cap on the maximum icon size. Without this, large icons can
+  // cause big delays when loading them at startup.
+  MAX_ICON_SIZE: 20000,
+
+  // Default charset to use for sending search parameters. ISO-8859-1 is used to
+  // match previous nsInternetSearchService behavior as a URL parameter. Label
+  // resolution causes windows-1252 to be actually used.
+  DEFAULT_QUERY_CHARSET: "ISO-8859-1",
+
+  /**
+   * This is the Remote Settings key that we use to get the ignore lists for
+   * engines.
+   */
+  SETTINGS_IGNORELIST_KEY: "hijack-blocklists",
+
+  /**
+   * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to
+   * the state of the search service.
+   *
+   * @param {nsISearchEngine} engine
+   *   The engine to which the change applies.
+   * @param {string} verb
+   *   A verb describing the change.
+   *
+   * @see nsISearchService.idl
+   */
+  notifyAction(engine, verb) {
+    if (Services.search.isInitialized) {
+      this.log("NOTIFY: Engine: \"" + engine.name + "\"; Verb: \"" + verb + "\"");
+      Services.obs.notifyObservers(engine, this.TOPIC_ENGINE_MODIFIED, verb);
+    }
+  },
+
+  /**
+   * Outputs text to the JavaScript console as well as to stdout.
+   *
+   * @param {string} text
+   *   The message to log.
+   */
+  log(text) {
+    if (loggingEnabled) {
+      dump("*** Search: " + text + "\n");
+      Services.console.logStringMessage(text);
+    }
+  },
+
+  /**
+   * Logs the failure message (if browser.search.log is enabled) and throws.
+   * @param {string} message
+   *   A message to display
+   * @param {number} resultCode
+   *   The NS_ERROR_* value to throw.
+   * @throws resultCode or NS_ERROR_INVALID_ARG if resultCode isn't specified.
+   */
+  fail(message, resultCode) {
+    this.log(message);
+    throw Components.Exception(message, resultCode || Cr.NS_ERROR_INVALID_ARG);
+  },
+
+  /**
+   * Wrapper function for nsIIOService::newURI.
+   * @param {string} urlSpec
+   *        The URL string from which to create an nsIURI.
+   * @returns {nsIURI} an nsIURI object, or null if the creation of the URI failed.
+   */
+  makeURI(urlSpec) {
+    try {
+      return Services.io.newURI(urlSpec);
+    } catch (ex) { }
+
+    return null;
+  },
+
+  /**
+   * Wrapper function for nsIIOService::newChannel.
+   * @param {string|nsIURI} url
+   *        The URL string from which to create an nsIChannel.
+   * @returns an nsIChannel object, or null if the url is invalid.
+   */
+  makeChannel(url) {
+    try {
+      let uri = typeof url == "string" ? Services.io.newURI(url) : url;
+      return Services.io.newChannelFromURI(uri,
+                                           null, /* loadingNode */
+                                           Services.scriptSecurityManager.getSystemPrincipal(),
+                                           null, /* triggeringPrincipal */
+                                           Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                                           Ci.nsIContentPolicy.TYPE_OTHER);
+    } catch (ex) { }
+
+    return null;
+  },
+};
--- a/toolkit/components/search/moz.build
+++ b/toolkit/components/search/moz.build
@@ -10,30 +10,28 @@ XPCSHELL_TESTS_MANIFESTS += [
 ]
 
 XPIDL_SOURCES += [
     'nsISearchService.idl',
 ]
 
 XPIDL_MODULE = 'toolkit_search'
 
-EXTRA_COMPONENTS += [
-]
-
 if CONFIG['MOZ_BUILD_APP'] in ['browser', 'mobile/android', 'xulrunner']:
     EXTRA_JS_MODULES += [
         'Sidebar.jsm',
     ]
 
 EXTRA_JS_MODULES += [
     'SearchEngine.jsm',
     'SearchService.jsm',
     'SearchStaticData.jsm',
     'SearchSuggestionController.jsm',
     'SearchSuggestions.jsm',
+    'SearchUtils.jsm',
 ]
 
 EXTRA_COMPONENTS += [
     'toolkitsearch.manifest',
 ]
 
 XPCOM_MANIFESTS += [
     'components.conf',