toolkit/components/search/nsSearchService.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/search/nsSearchService.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,4615 @@
     1.4 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +const Ci = Components.interfaces;
     1.9 +const Cc = Components.classes;
    1.10 +const Cr = Components.results;
    1.11 +const Cu = Components.utils;
    1.12 +
    1.13 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.14 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.15 +Components.utils.import("resource://gre/modules/Promise.jsm");
    1.16 +
    1.17 +XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
    1.18 +  "resource://gre/modules/AsyncShutdown.jsm");
    1.19 +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
    1.20 +  "resource://gre/modules/DeferredTask.jsm");
    1.21 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
    1.22 +  "resource://gre/modules/osfile.jsm");
    1.23 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
    1.24 +  "resource://gre/modules/Task.jsm");
    1.25 +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
    1.26 +  "resource://gre/modules/TelemetryStopwatch.jsm");
    1.27 +
    1.28 +// A text encoder to UTF8, used whenever we commit the
    1.29 +// engine metadata to disk.
    1.30 +XPCOMUtils.defineLazyGetter(this, "gEncoder",
    1.31 +                            function() {
    1.32 +                              return new TextEncoder();
    1.33 +                            });
    1.34 +
    1.35 +const PERMS_FILE      = 0644;
    1.36 +const PERMS_DIRECTORY = 0755;
    1.37 +
    1.38 +const MODE_RDONLY   = 0x01;
    1.39 +const MODE_WRONLY   = 0x02;
    1.40 +const MODE_CREATE   = 0x08;
    1.41 +const MODE_APPEND   = 0x10;
    1.42 +const MODE_TRUNCATE = 0x20;
    1.43 +
    1.44 +// Directory service keys
    1.45 +const NS_APP_SEARCH_DIR_LIST  = "SrchPluginsDL";
    1.46 +const NS_APP_USER_SEARCH_DIR  = "UsrSrchPlugns";
    1.47 +const NS_APP_SEARCH_DIR       = "SrchPlugns";
    1.48 +const NS_APP_USER_PROFILE_50_DIR = "ProfD";
    1.49 +
    1.50 +// Search engine "locations". If this list is changed, be sure to update
    1.51 +// the engine's _isDefault function accordingly.
    1.52 +const SEARCH_APP_DIR = 1;
    1.53 +const SEARCH_PROFILE_DIR = 2;
    1.54 +const SEARCH_IN_EXTENSION = 3;
    1.55 +const SEARCH_JAR = 4;
    1.56 +
    1.57 +// See documentation in nsIBrowserSearchService.idl.
    1.58 +const SEARCH_ENGINE_TOPIC        = "browser-search-engine-modified";
    1.59 +const QUIT_APPLICATION_TOPIC     = "quit-application";
    1.60 +
    1.61 +const SEARCH_ENGINE_REMOVED      = "engine-removed";
    1.62 +const SEARCH_ENGINE_ADDED        = "engine-added";
    1.63 +const SEARCH_ENGINE_CHANGED      = "engine-changed";
    1.64 +const SEARCH_ENGINE_LOADED       = "engine-loaded";
    1.65 +const SEARCH_ENGINE_CURRENT      = "engine-current";
    1.66 +const SEARCH_ENGINE_DEFAULT      = "engine-default";
    1.67 +
    1.68 +// The following constants are left undocumented in nsIBrowserSearchService.idl
    1.69 +// For the moment, they are meant for testing/debugging purposes only.
    1.70 +
    1.71 +/**
    1.72 + * Topic used for events involving the service itself.
    1.73 + */
    1.74 +const SEARCH_SERVICE_TOPIC       = "browser-search-service";
    1.75 +
    1.76 +/**
    1.77 + * Sent whenever metadata is fully written to disk.
    1.78 + */
    1.79 +const SEARCH_SERVICE_METADATA_WRITTEN  = "write-metadata-to-disk-complete";
    1.80 +
    1.81 +/**
    1.82 + * Sent whenever the cache is fully written to disk.
    1.83 + */
    1.84 +const SEARCH_SERVICE_CACHE_WRITTEN  = "write-cache-to-disk-complete";
    1.85 +
    1.86 +const SEARCH_TYPE_MOZSEARCH      = Ci.nsISearchEngine.TYPE_MOZSEARCH;
    1.87 +const SEARCH_TYPE_OPENSEARCH     = Ci.nsISearchEngine.TYPE_OPENSEARCH;
    1.88 +const SEARCH_TYPE_SHERLOCK       = Ci.nsISearchEngine.TYPE_SHERLOCK;
    1.89 +
    1.90 +const SEARCH_DATA_XML            = Ci.nsISearchEngine.DATA_XML;
    1.91 +const SEARCH_DATA_TEXT           = Ci.nsISearchEngine.DATA_TEXT;
    1.92 +
    1.93 +// Delay for lazy serialization (ms)
    1.94 +const LAZY_SERIALIZE_DELAY = 100;
    1.95 +
    1.96 +// Delay for batching invalidation of the JSON cache (ms)
    1.97 +const CACHE_INVALIDATION_DELAY = 1000;
    1.98 +
    1.99 +// Current cache version. This should be incremented if the format of the cache
   1.100 +// file is modified.
   1.101 +const CACHE_VERSION = 7;
   1.102 +
   1.103 +const ICON_DATAURL_PREFIX = "data:image/x-icon;base64,";
   1.104 +
   1.105 +const NEW_LINES = /(\r\n|\r|\n)/;
   1.106 +
   1.107 +// Set an arbitrary cap on the maximum icon size. Without this, large icons can
   1.108 +// cause big delays when loading them at startup.
   1.109 +const MAX_ICON_SIZE   = 10000;
   1.110 +
   1.111 +// Default charset to use for sending search parameters. ISO-8859-1 is used to
   1.112 +// match previous nsInternetSearchService behavior.
   1.113 +const DEFAULT_QUERY_CHARSET = "ISO-8859-1";
   1.114 +
   1.115 +const SEARCH_BUNDLE = "chrome://global/locale/search/search.properties";
   1.116 +const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
   1.117 +
   1.118 +const OPENSEARCH_NS_10  = "http://a9.com/-/spec/opensearch/1.0/";
   1.119 +const OPENSEARCH_NS_11  = "http://a9.com/-/spec/opensearch/1.1/";
   1.120 +
   1.121 +// Although the specification at http://opensearch.a9.com/spec/1.1/description/
   1.122 +// gives the namespace names defined above, many existing OpenSearch engines
   1.123 +// are using the following versions.  We therefore allow either.
   1.124 +const OPENSEARCH_NAMESPACES = [
   1.125 +  OPENSEARCH_NS_11, OPENSEARCH_NS_10,
   1.126 +  "http://a9.com/-/spec/opensearchdescription/1.1/",
   1.127 +  "http://a9.com/-/spec/opensearchdescription/1.0/"
   1.128 +];
   1.129 +
   1.130 +const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
   1.131 +
   1.132 +const MOZSEARCH_NS_10     = "http://www.mozilla.org/2006/browser/search/";
   1.133 +const MOZSEARCH_LOCALNAME = "SearchPlugin";
   1.134 +
   1.135 +const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
   1.136 +const URLTYPE_SEARCH_HTML  = "text/html";
   1.137 +const URLTYPE_OPENSEARCH   = "application/opensearchdescription+xml";
   1.138 +
   1.139 +// Empty base document used to serialize engines to file.
   1.140 +const EMPTY_DOC = "<?xml version=\"1.0\"?>\n" +
   1.141 +                  "<" + MOZSEARCH_LOCALNAME +
   1.142 +                  " xmlns=\"" + MOZSEARCH_NS_10 + "\"" +
   1.143 +                  " xmlns:os=\"" + OPENSEARCH_NS_11 + "\"" +
   1.144 +                  "/>";
   1.145 +
   1.146 +const BROWSER_SEARCH_PREF = "browser.search.";
   1.147 +
   1.148 +const USER_DEFINED = "{searchTerms}";
   1.149 +
   1.150 +// Custom search parameters
   1.151 +#ifdef MOZ_OFFICIAL_BRANDING
   1.152 +const MOZ_OFFICIAL = "official";
   1.153 +#else
   1.154 +const MOZ_OFFICIAL = "unofficial";
   1.155 +#endif
   1.156 +#expand const MOZ_DISTRIBUTION_ID = __MOZ_DISTRIBUTION_ID__;
   1.157 +
   1.158 +const MOZ_PARAM_LOCALE         = /\{moz:locale\}/g;
   1.159 +const MOZ_PARAM_DIST_ID        = /\{moz:distributionID\}/g;
   1.160 +const MOZ_PARAM_OFFICIAL       = /\{moz:official\}/g;
   1.161 +
   1.162 +// Supported OpenSearch parameters
   1.163 +// See http://opensearch.a9.com/spec/1.1/querysyntax/#core
   1.164 +const OS_PARAM_USER_DEFINED    = /\{searchTerms\??\}/g;
   1.165 +const OS_PARAM_INPUT_ENCODING  = /\{inputEncoding\??\}/g;
   1.166 +const OS_PARAM_LANGUAGE        = /\{language\??\}/g;
   1.167 +const OS_PARAM_OUTPUT_ENCODING = /\{outputEncoding\??\}/g;
   1.168 +
   1.169 +// Default values
   1.170 +const OS_PARAM_LANGUAGE_DEF         = "*";
   1.171 +const OS_PARAM_OUTPUT_ENCODING_DEF  = "UTF-8";
   1.172 +const OS_PARAM_INPUT_ENCODING_DEF   = "UTF-8";
   1.173 +
   1.174 +// "Unsupported" OpenSearch parameters. For example, we don't support
   1.175 +// page-based results, so if the engine requires that we send the "page index"
   1.176 +// parameter, we'll always send "1".
   1.177 +const OS_PARAM_COUNT        = /\{count\??\}/g;
   1.178 +const OS_PARAM_START_INDEX  = /\{startIndex\??\}/g;
   1.179 +const OS_PARAM_START_PAGE   = /\{startPage\??\}/g;
   1.180 +
   1.181 +// Default values
   1.182 +const OS_PARAM_COUNT_DEF        = "20"; // 20 results
   1.183 +const OS_PARAM_START_INDEX_DEF  = "1";  // start at 1st result
   1.184 +const OS_PARAM_START_PAGE_DEF   = "1";  // 1st page
   1.185 +
   1.186 +// Optional parameter
   1.187 +const OS_PARAM_OPTIONAL     = /\{(?:\w+:)?\w+\?\}/g;
   1.188 +
   1.189 +// A array of arrays containing parameters that we don't fully support, and
   1.190 +// their default values. We will only send values for these parameters if
   1.191 +// required, since our values are just really arbitrary "guesses" that should
   1.192 +// give us the output we want.
   1.193 +var OS_UNSUPPORTED_PARAMS = [
   1.194 +  [OS_PARAM_COUNT, OS_PARAM_COUNT_DEF],
   1.195 +  [OS_PARAM_START_INDEX, OS_PARAM_START_INDEX_DEF],
   1.196 +  [OS_PARAM_START_PAGE, OS_PARAM_START_PAGE_DEF],
   1.197 +];
   1.198 +
   1.199 +// The default engine update interval, in days. This is only used if an engine
   1.200 +// specifies an updateURL, but not an updateInterval.
   1.201 +const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
   1.202 +
   1.203 +// Returns false for whitespace-only or commented out lines in a
   1.204 +// Sherlock file, true otherwise.
   1.205 +function isUsefulLine(aLine) {
   1.206 +  return !(/^\s*($|#)/i.test(aLine));
   1.207 +}
   1.208 +
   1.209 +this.__defineGetter__("FileUtils", function() {
   1.210 +  delete this.FileUtils;
   1.211 +  Components.utils.import("resource://gre/modules/FileUtils.jsm");
   1.212 +  return FileUtils;
   1.213 +});
   1.214 +
   1.215 +this.__defineGetter__("NetUtil", function() {
   1.216 +  delete this.NetUtil;
   1.217 +  Components.utils.import("resource://gre/modules/NetUtil.jsm");
   1.218 +  return NetUtil;
   1.219 +});
   1.220 +
   1.221 +this.__defineGetter__("gChromeReg", function() {
   1.222 +  delete this.gChromeReg;
   1.223 +  return this.gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
   1.224 +                           getService(Ci.nsIChromeRegistry);
   1.225 +});
   1.226 +
   1.227 +/**
   1.228 + * Prefixed to all search debug output.
   1.229 + */
   1.230 +const SEARCH_LOG_PREFIX = "*** Search: ";
   1.231 +
   1.232 +/**
   1.233 + * Outputs aText to the JavaScript console as well as to stdout.
   1.234 + */
   1.235 +function DO_LOG(aText) {
   1.236 +  dump(SEARCH_LOG_PREFIX + aText + "\n");
   1.237 +  Services.console.logStringMessage(aText);
   1.238 +}
   1.239 +
   1.240 +#ifdef DEBUG
   1.241 +/**
   1.242 + * In debug builds, use a live, pref-based (browser.search.log) LOG function
   1.243 + * to allow enabling/disabling without a restart.
   1.244 + */
   1.245 +function PREF_LOG(aText) {
   1.246 +  if (getBoolPref(BROWSER_SEARCH_PREF + "log", false))
   1.247 +    DO_LOG(aText);
   1.248 +}
   1.249 +var LOG = PREF_LOG;
   1.250 +
   1.251 +#else
   1.252 +
   1.253 +/**
   1.254 + * Otherwise, don't log at all by default. This can be overridden at startup
   1.255 + * by the pref, see SearchService's _init method.
   1.256 + */
   1.257 +var LOG = function(){};
   1.258 +
   1.259 +#endif
   1.260 +
   1.261 +/**
   1.262 + * Presents an assertion dialog in non-release builds and throws.
   1.263 + * @param  message
   1.264 + *         A message to display
   1.265 + * @param  resultCode
   1.266 + *         The NS_ERROR_* value to throw.
   1.267 + * @throws resultCode
   1.268 + */
   1.269 +function ERROR(message, resultCode) {
   1.270 +  NS_ASSERT(false, SEARCH_LOG_PREFIX + message);
   1.271 +  throw Components.Exception(message, resultCode);
   1.272 +}
   1.273 +
   1.274 +/**
   1.275 + * Logs the failure message (if browser.search.log is enabled) and throws.
   1.276 + * @param  message
   1.277 + *         A message to display
   1.278 + * @param  resultCode
   1.279 + *         The NS_ERROR_* value to throw.
   1.280 + * @throws resultCode or NS_ERROR_INVALID_ARG if resultCode isn't specified.
   1.281 + */
   1.282 +function FAIL(message, resultCode) {
   1.283 +  LOG(message);
   1.284 +  throw Components.Exception(message, resultCode || Cr.NS_ERROR_INVALID_ARG);
   1.285 +}
   1.286 +
   1.287 +/**
   1.288 + * Truncates big blobs of (data-)URIs to console-friendly sizes
   1.289 + * @param str
   1.290 + *        String to tone down
   1.291 + * @param len
   1.292 + *        Maximum length of the string to return. Defaults to the length of a tweet.
   1.293 + */
   1.294 +function limitURILength(str, len) {
   1.295 +  len = len || 140;
   1.296 +  if (str.length > len)
   1.297 +    return str.slice(0, len) + "...";
   1.298 +  return str;
   1.299 +}
   1.300 +
   1.301 +/**
   1.302 + * Utilities for dealing with promises and Task.jsm
   1.303 + */
   1.304 +const TaskUtils = {
   1.305 +  /**
   1.306 +   * Add logging to a promise.
   1.307 +   *
   1.308 +   * @param {Promise} promise
   1.309 +   * @return {Promise} A promise behaving as |promise|, but with additional
   1.310 +   * logging in case of uncaught error.
   1.311 +   */
   1.312 +  captureErrors: function captureErrors(promise) {
   1.313 +    return promise.then(
   1.314 +      null,
   1.315 +      function onError(reason) {
   1.316 +        LOG("Uncaught asynchronous error: " + reason + " at\n" + reason.stack);
   1.317 +        throw reason;
   1.318 +      }
   1.319 +    );
   1.320 +  },
   1.321 +  /**
   1.322 +   * Spawn a new Task from a generator.
   1.323 +   *
   1.324 +   * This function behaves as |Task.spawn|, with the exception that it
   1.325 +   * adds logging in case of uncaught error. For more information, see
   1.326 +   * the documentation of |Task.jsm|.
   1.327 +   *
   1.328 +   * @param {generator} gen Some generator.
   1.329 +   * @return {Promise} A promise built from |gen|, with the same semantics
   1.330 +   * as |Task.spawn(gen)|.
   1.331 +   */
   1.332 +  spawn: function spawn(gen) {
   1.333 +    return this.captureErrors(Task.spawn(gen));
   1.334 +  },
   1.335 +  /**
   1.336 +   * Execute a mozIStorage statement asynchronously, wrapping the
   1.337 +   * result in a promise.
   1.338 +   *
   1.339 +   * @param {mozIStorageStaement} statement A statement to be executed
   1.340 +   * asynchronously. The semantics are the same as these of |statement.execute|.
   1.341 +   * @param {function*} onResult A callback, called for each successive result.
   1.342 +   *
   1.343 +   * @return {Promise} A promise, resolved successfully if |statement.execute|
   1.344 +   * succeeds, rejected if it fails.
   1.345 +   */
   1.346 +  executeStatement: function executeStatement(statement, onResult) {
   1.347 +    let deferred = Promise.defer();
   1.348 +    onResult = onResult || function() {};
   1.349 +    statement.executeAsync({
   1.350 +      handleResult: onResult,
   1.351 +      handleError: function handleError(aError) {
   1.352 +        deferred.reject(aError);
   1.353 +      },
   1.354 +      handleCompletion: function handleCompletion(aReason) {
   1.355 +        statement.finalize();
   1.356 +        // Note that, in case of error, deferred.reject(aError)
   1.357 +        // has already been called by this point, so the call to
   1.358 +        // |deferred.resolve| is simply ignored.
   1.359 +        deferred.resolve(aReason);
   1.360 +      }
   1.361 +    });
   1.362 +    return deferred.promise;
   1.363 +  }
   1.364 +};
   1.365 +
   1.366 +/**
   1.367 + * Ensures an assertion is met before continuing. Should be used to indicate
   1.368 + * fatal errors.
   1.369 + * @param  assertion
   1.370 + *         An assertion that must be met
   1.371 + * @param  message
   1.372 + *         A message to display if the assertion is not met
   1.373 + * @param  resultCode
   1.374 + *         The NS_ERROR_* value to throw if the assertion is not met
   1.375 + * @throws resultCode
   1.376 + */
   1.377 +function ENSURE_WARN(assertion, message, resultCode) {
   1.378 +  NS_ASSERT(assertion, SEARCH_LOG_PREFIX + message);
   1.379 +  if (!assertion)
   1.380 +    throw Components.Exception(message, resultCode);
   1.381 +}
   1.382 +
   1.383 +function loadListener(aChannel, aEngine, aCallback) {
   1.384 +  this._channel = aChannel;
   1.385 +  this._bytes = [];
   1.386 +  this._engine = aEngine;
   1.387 +  this._callback = aCallback;
   1.388 +}
   1.389 +loadListener.prototype = {
   1.390 +  _callback: null,
   1.391 +  _channel: null,
   1.392 +  _countRead: 0,
   1.393 +  _engine: null,
   1.394 +  _stream: null,
   1.395 +
   1.396 +  QueryInterface: function SRCH_loadQI(aIID) {
   1.397 +    if (aIID.equals(Ci.nsISupports)           ||
   1.398 +        aIID.equals(Ci.nsIRequestObserver)    ||
   1.399 +        aIID.equals(Ci.nsIStreamListener)     ||
   1.400 +        aIID.equals(Ci.nsIChannelEventSink)   ||
   1.401 +        aIID.equals(Ci.nsIInterfaceRequestor) ||
   1.402 +        // See FIXME comment below
   1.403 +        aIID.equals(Ci.nsIHttpEventSink)      ||
   1.404 +        aIID.equals(Ci.nsIProgressEventSink)  ||
   1.405 +        false)
   1.406 +      return this;
   1.407 +
   1.408 +    throw Cr.NS_ERROR_NO_INTERFACE;
   1.409 +  },
   1.410 +
   1.411 +  // nsIRequestObserver
   1.412 +  onStartRequest: function SRCH_loadStartR(aRequest, aContext) {
   1.413 +    LOG("loadListener: Starting request: " + aRequest.name);
   1.414 +    this._stream = Cc["@mozilla.org/binaryinputstream;1"].
   1.415 +                   createInstance(Ci.nsIBinaryInputStream);
   1.416 +  },
   1.417 +
   1.418 +  onStopRequest: function SRCH_loadStopR(aRequest, aContext, aStatusCode) {
   1.419 +    LOG("loadListener: Stopping request: " + aRequest.name);
   1.420 +
   1.421 +    var requestFailed = !Components.isSuccessCode(aStatusCode);
   1.422 +    if (!requestFailed && (aRequest instanceof Ci.nsIHttpChannel))
   1.423 +      requestFailed = !aRequest.requestSucceeded;
   1.424 +
   1.425 +    if (requestFailed || this._countRead == 0) {
   1.426 +      LOG("loadListener: request failed!");
   1.427 +      // send null so the callback can deal with the failure
   1.428 +      this._callback(null, this._engine);
   1.429 +    } else
   1.430 +      this._callback(this._bytes, this._engine);
   1.431 +    this._channel = null;
   1.432 +    this._engine  = null;
   1.433 +  },
   1.434 +
   1.435 +  // nsIStreamListener
   1.436 +  onDataAvailable: function SRCH_loadDAvailable(aRequest, aContext,
   1.437 +                                                aInputStream, aOffset,
   1.438 +                                                aCount) {
   1.439 +    this._stream.setInputStream(aInputStream);
   1.440 +
   1.441 +    // Get a byte array of the data
   1.442 +    this._bytes = this._bytes.concat(this._stream.readByteArray(aCount));
   1.443 +    this._countRead += aCount;
   1.444 +  },
   1.445 +
   1.446 +  // nsIChannelEventSink
   1.447 +  asyncOnChannelRedirect: function SRCH_loadCRedirect(aOldChannel, aNewChannel,
   1.448 +                                                      aFlags, callback) {
   1.449 +    this._channel = aNewChannel;
   1.450 +    callback.onRedirectVerifyCallback(Components.results.NS_OK);
   1.451 +  },
   1.452 +
   1.453 +  // nsIInterfaceRequestor
   1.454 +  getInterface: function SRCH_load_GI(aIID) {
   1.455 +    return this.QueryInterface(aIID);
   1.456 +  },
   1.457 +
   1.458 +  // FIXME: bug 253127
   1.459 +  // nsIHttpEventSink
   1.460 +  onRedirect: function (aChannel, aNewChannel) {},
   1.461 +  // nsIProgressEventSink
   1.462 +  onProgress: function (aRequest, aContext, aProgress, aProgressMax) {},
   1.463 +  onStatus: function (aRequest, aContext, aStatus, aStatusArg) {}
   1.464 +}
   1.465 +
   1.466 +
   1.467 +/**
   1.468 + * Used to verify a given DOM node's localName and namespaceURI.
   1.469 + * @param aElement
   1.470 + *        The element to verify.
   1.471 + * @param aLocalNameArray
   1.472 + *        An array of strings to compare against aElement's localName.
   1.473 + * @param aNameSpaceArray
   1.474 + *        An array of strings to compare against aElement's namespaceURI.
   1.475 + *
   1.476 + * @returns false if aElement is null, or if its localName or namespaceURI
   1.477 + *          does not match one of the elements in the aLocalNameArray or
   1.478 + *          aNameSpaceArray arrays, respectively.
   1.479 + * @throws NS_ERROR_INVALID_ARG if aLocalNameArray or aNameSpaceArray are null.
   1.480 + */
   1.481 +function checkNameSpace(aElement, aLocalNameArray, aNameSpaceArray) {
   1.482 +  if (!aLocalNameArray || !aNameSpaceArray)
   1.483 +    FAIL("missing aLocalNameArray or aNameSpaceArray for checkNameSpace");
   1.484 +  return (aElement                                                &&
   1.485 +          (aLocalNameArray.indexOf(aElement.localName)    != -1)  &&
   1.486 +          (aNameSpaceArray.indexOf(aElement.namespaceURI) != -1));
   1.487 +}
   1.488 +
   1.489 +/**
   1.490 + * Safely close a nsISafeOutputStream.
   1.491 + * @param aFOS
   1.492 + *        The file output stream to close.
   1.493 + */
   1.494 +function closeSafeOutputStream(aFOS) {
   1.495 +  if (aFOS instanceof Ci.nsISafeOutputStream) {
   1.496 +    try {
   1.497 +      aFOS.finish();
   1.498 +      return;
   1.499 +    } catch (e) { }
   1.500 +  }
   1.501 +  aFOS.close();
   1.502 +}
   1.503 +
   1.504 +/**
   1.505 + * Wrapper function for nsIIOService::newURI.
   1.506 + * @param aURLSpec
   1.507 + *        The URL string from which to create an nsIURI.
   1.508 + * @returns an nsIURI object, or null if the creation of the URI failed.
   1.509 + */
   1.510 +function makeURI(aURLSpec, aCharset) {
   1.511 +  try {
   1.512 +    return NetUtil.newURI(aURLSpec, aCharset);
   1.513 +  } catch (ex) { }
   1.514 +
   1.515 +  return null;
   1.516 +}
   1.517 +
   1.518 +/**
   1.519 + * Gets a directory from the directory service.
   1.520 + * @param aKey
   1.521 + *        The directory service key indicating the directory to get.
   1.522 + */
   1.523 +function getDir(aKey, aIFace) {
   1.524 +  if (!aKey)
   1.525 +    FAIL("getDir requires a directory key!");
   1.526 +
   1.527 +  return Services.dirsvc.get(aKey, aIFace || Ci.nsIFile);
   1.528 +}
   1.529 +
   1.530 +/**
   1.531 + * The following two functions are essentially copied from
   1.532 + * nsInternetSearchService. They are required for backwards compatibility.
   1.533 + */
   1.534 +function queryCharsetFromCode(aCode) {
   1.535 +  const codes = [];
   1.536 +  codes[0] = "macintosh";
   1.537 +  codes[6] = "x-mac-greek";
   1.538 +  codes[35] = "x-mac-turkish";
   1.539 +  codes[513] = "ISO-8859-1";
   1.540 +  codes[514] = "ISO-8859-2";
   1.541 +  codes[517] = "ISO-8859-5";
   1.542 +  codes[518] = "ISO-8859-6";
   1.543 +  codes[519] = "ISO-8859-7";
   1.544 +  codes[520] = "ISO-8859-8";
   1.545 +  codes[521] = "ISO-8859-9";
   1.546 +  codes[1280] = "windows-1252";
   1.547 +  codes[1281] = "windows-1250";
   1.548 +  codes[1282] = "windows-1251";
   1.549 +  codes[1283] = "windows-1253";
   1.550 +  codes[1284] = "windows-1254";
   1.551 +  codes[1285] = "windows-1255";
   1.552 +  codes[1286] = "windows-1256";
   1.553 +  codes[1536] = "us-ascii";
   1.554 +  codes[1584] = "GB2312";
   1.555 +  codes[1585] = "gbk";
   1.556 +  codes[1600] = "EUC-KR";
   1.557 +  codes[2080] = "ISO-2022-JP";
   1.558 +  codes[2096] = "ISO-2022-CN";
   1.559 +  codes[2112] = "ISO-2022-KR";
   1.560 +  codes[2336] = "EUC-JP";
   1.561 +  codes[2352] = "GB2312";
   1.562 +  codes[2353] = "x-euc-tw";
   1.563 +  codes[2368] = "EUC-KR";
   1.564 +  codes[2561] = "Shift_JIS";
   1.565 +  codes[2562] = "KOI8-R";
   1.566 +  codes[2563] = "Big5";
   1.567 +  codes[2565] = "HZ-GB-2312";
   1.568 +
   1.569 +  if (codes[aCode])
   1.570 +    return codes[aCode];
   1.571 +
   1.572 +  // Don't bother being fancy about what to return in the failure case.
   1.573 +  return "windows-1252";
   1.574 +}
   1.575 +function fileCharsetFromCode(aCode) {
   1.576 +  const codes = [
   1.577 +    "macintosh",             // 0
   1.578 +    "Shift_JIS",             // 1
   1.579 +    "Big5",                  // 2
   1.580 +    "EUC-KR",                // 3
   1.581 +    "X-MAC-ARABIC",          // 4
   1.582 +    "X-MAC-HEBREW",          // 5
   1.583 +    "X-MAC-GREEK",           // 6
   1.584 +    "X-MAC-CYRILLIC",        // 7
   1.585 +    "X-MAC-DEVANAGARI" ,     // 9
   1.586 +    "X-MAC-GURMUKHI",        // 10
   1.587 +    "X-MAC-GUJARATI",        // 11
   1.588 +    "X-MAC-ORIYA",           // 12
   1.589 +    "X-MAC-BENGALI",         // 13
   1.590 +    "X-MAC-TAMIL",           // 14
   1.591 +    "X-MAC-TELUGU",          // 15
   1.592 +    "X-MAC-KANNADA",         // 16
   1.593 +    "X-MAC-MALAYALAM",       // 17
   1.594 +    "X-MAC-SINHALESE",       // 18
   1.595 +    "X-MAC-BURMESE",         // 19
   1.596 +    "X-MAC-KHMER",           // 20
   1.597 +    "X-MAC-THAI",            // 21
   1.598 +    "X-MAC-LAOTIAN",         // 22
   1.599 +    "X-MAC-GEORGIAN",        // 23
   1.600 +    "X-MAC-ARMENIAN",        // 24
   1.601 +    "GB2312",                // 25
   1.602 +    "X-MAC-TIBETAN",         // 26
   1.603 +    "X-MAC-MONGOLIAN",       // 27
   1.604 +    "X-MAC-ETHIOPIC",        // 28
   1.605 +    "X-MAC-CENTRALEURROMAN", // 29
   1.606 +    "X-MAC-VIETNAMESE",      // 30
   1.607 +    "X-MAC-EXTARABIC"        // 31
   1.608 +  ];
   1.609 +  // Sherlock files have always defaulted to macintosh, so do that here too
   1.610 +  return codes[aCode] || codes[0];
   1.611 +}
   1.612 +
   1.613 +/**
   1.614 + * Returns a string interpretation of aBytes using aCharset, or null on
   1.615 + * failure.
   1.616 + */
   1.617 +function bytesToString(aBytes, aCharset) {
   1.618 +  var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
   1.619 +                  createInstance(Ci.nsIScriptableUnicodeConverter);
   1.620 +  LOG("bytesToString: converting using charset: " + aCharset);
   1.621 +
   1.622 +  try {
   1.623 +    converter.charset = aCharset;
   1.624 +    return converter.convertFromByteArray(aBytes, aBytes.length);
   1.625 +  } catch (ex) {}
   1.626 +
   1.627 +  return null;
   1.628 +}
   1.629 +
   1.630 +/**
   1.631 + * Converts an array of bytes representing a Sherlock file into an array of
   1.632 + * lines representing the useful data from the file.
   1.633 + *
   1.634 + * @param aBytes
   1.635 + *        The array of bytes representing the Sherlock file.
   1.636 + * @param aCharsetCode
   1.637 + *        An integer value representing a character set code to be passed to
   1.638 + *        fileCharsetFromCode, or null for the default Sherlock encoding.
   1.639 + */
   1.640 +function sherlockBytesToLines(aBytes, aCharsetCode) {
   1.641 +  // fileCharsetFromCode returns the default encoding if aCharsetCode is null
   1.642 +  var charset = fileCharsetFromCode(aCharsetCode);
   1.643 +
   1.644 +  var dataString = bytesToString(aBytes, charset);
   1.645 +  if (!dataString)
   1.646 +    FAIL("sherlockBytesToLines: Couldn't convert byte array!", Cr.NS_ERROR_FAILURE);
   1.647 +
   1.648 +  // Split the string into lines, and filter out comments and
   1.649 +  // whitespace-only lines
   1.650 +  return dataString.split(NEW_LINES).filter(isUsefulLine);
   1.651 +}
   1.652 +
   1.653 +/**
   1.654 + * Gets the current value of the locale.  It's possible for this preference to
   1.655 + * be localized, so we have to do a little extra work here.  Similar code
   1.656 + * exists in nsHttpHandler.cpp when building the UA string.
   1.657 + */
   1.658 +function getLocale() {
   1.659 +  const localePref = "general.useragent.locale";
   1.660 +  var locale = getLocalizedPref(localePref);
   1.661 +  if (locale)
   1.662 +    return locale;
   1.663 +
   1.664 +  // Not localized
   1.665 +  return Services.prefs.getCharPref(localePref);
   1.666 +}
   1.667 +
   1.668 +/**
   1.669 + * Wrapper for nsIPrefBranch::getComplexValue.
   1.670 + * @param aPrefName
   1.671 + *        The name of the pref to get.
   1.672 + * @returns aDefault if the requested pref doesn't exist.
   1.673 + */
   1.674 +function getLocalizedPref(aPrefName, aDefault) {
   1.675 +  const nsIPLS = Ci.nsIPrefLocalizedString;
   1.676 +  try {
   1.677 +    return Services.prefs.getComplexValue(aPrefName, nsIPLS).data;
   1.678 +  } catch (ex) {}
   1.679 +
   1.680 +  return aDefault;
   1.681 +}
   1.682 +
   1.683 +/**
   1.684 + * Wrapper for nsIPrefBranch::setComplexValue.
   1.685 + * @param aPrefName
   1.686 + *        The name of the pref to set.
   1.687 + */
   1.688 +function setLocalizedPref(aPrefName, aValue) {
   1.689 +  const nsIPLS = Ci.nsIPrefLocalizedString;
   1.690 +  try {
   1.691 +    var pls = Components.classes["@mozilla.org/pref-localizedstring;1"]
   1.692 +                        .createInstance(Ci.nsIPrefLocalizedString);
   1.693 +    pls.data = aValue;
   1.694 +    Services.prefs.setComplexValue(aPrefName, nsIPLS, pls);
   1.695 +  } catch (ex) {}
   1.696 +}
   1.697 +
   1.698 +/**
   1.699 + * Wrapper for nsIPrefBranch::getBoolPref.
   1.700 + * @param aPrefName
   1.701 + *        The name of the pref to get.
   1.702 + * @returns aDefault if the requested pref doesn't exist.
   1.703 + */
   1.704 +function getBoolPref(aName, aDefault) {
   1.705 +  try {
   1.706 +    return Services.prefs.getBoolPref(aName);
   1.707 +  } catch (ex) {
   1.708 +    return aDefault;
   1.709 +  }
   1.710 +}
   1.711 +
   1.712 +/**
   1.713 + * Get a unique nsIFile object with a sanitized name, based on the engine name.
   1.714 + * @param aName
   1.715 + *        A name to "sanitize". Can be an empty string, in which case a random
   1.716 + *        8 character filename will be produced.
   1.717 + * @returns A nsIFile object in the user's search engines directory with a
   1.718 + *          unique sanitized name.
   1.719 + */
   1.720 +function getSanitizedFile(aName) {
   1.721 +  var fileName = sanitizeName(aName) + ".xml";
   1.722 +  var file = getDir(NS_APP_USER_SEARCH_DIR);
   1.723 +  file.append(fileName);
   1.724 +  file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
   1.725 +  return file;
   1.726 +}
   1.727 +
   1.728 +/**
   1.729 + * @return a sanitized name to be used as a filename, or a random name
   1.730 + *         if a sanitized name cannot be obtained (if aName contains
   1.731 + *         no valid characters).
   1.732 + */
   1.733 +function sanitizeName(aName) {
   1.734 +  const maxLength = 60;
   1.735 +  const minLength = 1;
   1.736 +  var name = aName.toLowerCase();
   1.737 +  name = name.replace(/\s+/g, "-");
   1.738 +  name = name.replace(/[^-a-z0-9]/g, "");
   1.739 +
   1.740 +  // Use a random name if our input had no valid characters.
   1.741 +  if (name.length < minLength)
   1.742 +    name = Math.random().toString(36).replace(/^.*\./, '');
   1.743 +
   1.744 +  // Force max length.
   1.745 +  return name.substring(0, maxLength);
   1.746 +}
   1.747 +
   1.748 +/**
   1.749 + * Retrieve a pref from the search param branch.
   1.750 + *
   1.751 + * @param prefName
   1.752 + *        The name of the pref.
   1.753 + **/
   1.754 +function getMozParamPref(prefName)
   1.755 +  Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "param." + prefName);
   1.756 +
   1.757 +/**
   1.758 + * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to
   1.759 + * the state of the search service.
   1.760 + *
   1.761 + * @param aEngine
   1.762 + *        The nsISearchEngine object to which the change applies.
   1.763 + * @param aVerb
   1.764 + *        A verb describing the change.
   1.765 + *
   1.766 + * @see nsIBrowserSearchService.idl
   1.767 + */
   1.768 +let gInitialized = false;
   1.769 +function notifyAction(aEngine, aVerb) {
   1.770 +  if (gInitialized) {
   1.771 +    LOG("NOTIFY: Engine: \"" + aEngine.name + "\"; Verb: \"" + aVerb + "\"");
   1.772 +    Services.obs.notifyObservers(aEngine, SEARCH_ENGINE_TOPIC, aVerb);
   1.773 +  }
   1.774 +}
   1.775 +
   1.776 +function  parseJsonFromStream(aInputStream) {
   1.777 +  const json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
   1.778 +  const data = json.decodeFromStream(aInputStream, aInputStream.available());
   1.779 +  return data;
   1.780 +}
   1.781 +
   1.782 +/**
   1.783 + * Simple object representing a name/value pair.
   1.784 + */
   1.785 +function QueryParameter(aName, aValue, aPurpose) {
   1.786 +  if (!aName || (aValue == null))
   1.787 +    FAIL("missing name or value for QueryParameter!");
   1.788 +
   1.789 +  this.name = aName;
   1.790 +  this.value = aValue;
   1.791 +  this.purpose = aPurpose;
   1.792 +}
   1.793 +
   1.794 +/**
   1.795 + * Perform OpenSearch parameter substitution on aParamValue.
   1.796 + *
   1.797 + * @param aParamValue
   1.798 + *        A string containing OpenSearch search parameters.
   1.799 + * @param aSearchTerms
   1.800 + *        The user-provided search terms. This string will inserted into
   1.801 + *        aParamValue as the value of the OS_PARAM_USER_DEFINED parameter.
   1.802 + *        This value must already be escaped appropriately - it is inserted
   1.803 + *        as-is.
   1.804 + * @param aEngine
   1.805 + *        The engine which owns the string being acted on.
   1.806 + *
   1.807 + * @see http://opensearch.a9.com/spec/1.1/querysyntax/#core
   1.808 + */
   1.809 +function ParamSubstitution(aParamValue, aSearchTerms, aEngine) {
   1.810 +  var value = aParamValue;
   1.811 +
   1.812 +  var distributionID = MOZ_DISTRIBUTION_ID;
   1.813 +  try {
   1.814 +    distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID");
   1.815 +  }
   1.816 +  catch (ex) { }
   1.817 +  var official = MOZ_OFFICIAL;
   1.818 +  try {
   1.819 +    if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official"))
   1.820 +      official = "official";
   1.821 +    else
   1.822 +      official = "unofficial";
   1.823 +  }
   1.824 +  catch (ex) { }
   1.825 +
   1.826 +  // Custom search parameters. These are only available to default search
   1.827 +  // engines.
   1.828 +  if (aEngine._isDefault) {
   1.829 +    value = value.replace(MOZ_PARAM_LOCALE, getLocale());
   1.830 +    value = value.replace(MOZ_PARAM_DIST_ID, distributionID);
   1.831 +    value = value.replace(MOZ_PARAM_OFFICIAL, official);
   1.832 +  }
   1.833 +
   1.834 +  // Insert the OpenSearch parameters we're confident about
   1.835 +  value = value.replace(OS_PARAM_USER_DEFINED, aSearchTerms);
   1.836 +  value = value.replace(OS_PARAM_INPUT_ENCODING, aEngine.queryCharset);
   1.837 +  value = value.replace(OS_PARAM_LANGUAGE,
   1.838 +                        getLocale() || OS_PARAM_LANGUAGE_DEF);
   1.839 +  value = value.replace(OS_PARAM_OUTPUT_ENCODING,
   1.840 +                        OS_PARAM_OUTPUT_ENCODING_DEF);
   1.841 +
   1.842 +  // Replace any optional parameters
   1.843 +  value = value.replace(OS_PARAM_OPTIONAL, "");
   1.844 +
   1.845 +  // Insert any remaining required params with our default values
   1.846 +  for (var i = 0; i < OS_UNSUPPORTED_PARAMS.length; ++i) {
   1.847 +    value = value.replace(OS_UNSUPPORTED_PARAMS[i][0],
   1.848 +                          OS_UNSUPPORTED_PARAMS[i][1]);
   1.849 +  }
   1.850 +
   1.851 +  return value;
   1.852 +}
   1.853 +
   1.854 +/**
   1.855 + * Creates an engineURL object, which holds the query URL and all parameters.
   1.856 + *
   1.857 + * @param aType
   1.858 + *        A string containing the name of the MIME type of the search results
   1.859 + *        returned by this URL.
   1.860 + * @param aMethod
   1.861 + *        The HTTP request method. Must be a case insensitive value of either
   1.862 + *        "GET" or "POST".
   1.863 + * @param aTemplate
   1.864 + *        The URL to which search queries should be sent. For GET requests,
   1.865 + *        must contain the string "{searchTerms}", to indicate where the user
   1.866 + *        entered search terms should be inserted.
   1.867 + * @param aResultDomain
   1.868 + *        The root domain for this URL.  Defaults to the template's host.
   1.869 + *
   1.870 + * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
   1.871 + *
   1.872 + * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported.
   1.873 + */
   1.874 +function EngineURL(aType, aMethod, aTemplate, aResultDomain) {
   1.875 +  if (!aType || !aMethod || !aTemplate)
   1.876 +    FAIL("missing type, method or template for EngineURL!");
   1.877 +
   1.878 +  var method = aMethod.toUpperCase();
   1.879 +  var type   = aType.toLowerCase();
   1.880 +
   1.881 +  if (method != "GET" && method != "POST")
   1.882 +    FAIL("method passed to EngineURL must be \"GET\" or \"POST\"");
   1.883 +
   1.884 +  this.type     = type;
   1.885 +  this.method   = method;
   1.886 +  this.params   = [];
   1.887 +  this.rels     = [];
   1.888 +  // Don't serialize expanded mozparams
   1.889 +  this.mozparams = {};
   1.890 +
   1.891 +  var templateURI = makeURI(aTemplate);
   1.892 +  if (!templateURI)
   1.893 +    FAIL("new EngineURL: template is not a valid URI!", Cr.NS_ERROR_FAILURE);
   1.894 +
   1.895 +  switch (templateURI.scheme) {
   1.896 +    case "http":
   1.897 +    case "https":
   1.898 +    // Disable these for now, see bug 295018
   1.899 +    // case "file":
   1.900 +    // case "resource":
   1.901 +      this.template = aTemplate;
   1.902 +      break;
   1.903 +    default:
   1.904 +      FAIL("new EngineURL: template uses invalid scheme!", Cr.NS_ERROR_FAILURE);
   1.905 +  }
   1.906 +
   1.907 +  // If no resultDomain was specified in the engine definition file, use the
   1.908 +  // host from the template.
   1.909 +  this.resultDomain = aResultDomain || templateURI.host;
   1.910 +  // We never want to return a "www." prefix, so eventually strip it.
   1.911 +  if (this.resultDomain.startsWith("www.")) {
   1.912 +    this.resultDomain = this.resultDomain.substr(4);
   1.913 +  }
   1.914 +}
   1.915 +EngineURL.prototype = {
   1.916 +
   1.917 +  addParam: function SRCH_EURL_addParam(aName, aValue, aPurpose) {
   1.918 +    this.params.push(new QueryParameter(aName, aValue, aPurpose));
   1.919 +  },
   1.920 +
   1.921 +  // Note: This method requires that aObj has a unique name or the previous MozParams entry with
   1.922 +  // that name will be overwritten.
   1.923 +  _addMozParam: function SRCH_EURL__addMozParam(aObj) {
   1.924 +    aObj.mozparam = true;
   1.925 +    this.mozparams[aObj.name] = aObj;
   1.926 +  },
   1.927 +
   1.928 +  reevalMozParams: function(engine) {
   1.929 +    for (let param of this.params) {
   1.930 +      let mozparam = this.mozparams[param.name];
   1.931 +      if (mozparam && mozparam.positionDependent) {
   1.932 +        // the condition is a string in the form of "topN", extract N as int
   1.933 +        let positionStr = mozparam.condition.slice("top".length);
   1.934 +        let position = parseInt(positionStr, 10);
   1.935 +        let engines;
   1.936 +        try {
   1.937 +          // This will throw if we're not initialized yet (which shouldn't happen), just 
   1.938 +          // ignore and move on with the false Value (checking isInitialized also throws)
   1.939 +          // XXX
   1.940 +          engines = Services.search.getVisibleEngines({});
   1.941 +        } catch (ex) {
   1.942 +          LOG("reevalMozParams called before search service initialization!?");
   1.943 +          break;
   1.944 +        }
   1.945 +        let index = engines.map((e) => e.wrappedJSObject).indexOf(engine.wrappedJSObject);
   1.946 +        let isTopN = index > -1 && (index + 1) <= position;
   1.947 +        param.value = isTopN ? mozparam.trueValue : mozparam.falseValue;
   1.948 +      }
   1.949 +    }
   1.950 +  },
   1.951 +
   1.952 +  getSubmission: function SRCH_EURL_getSubmission(aSearchTerms, aEngine, aPurpose) {
   1.953 +    this.reevalMozParams(aEngine);
   1.954 +
   1.955 +    var url = ParamSubstitution(this.template, aSearchTerms, aEngine);
   1.956 +    // Default to an empty string if the purpose is not provided so that default purpose params
   1.957 +    // (purpose="") work consistently rather than having to define "null" and "" purposes.
   1.958 +    var purpose = aPurpose || "";
   1.959 +
   1.960 +    // Create an application/x-www-form-urlencoded representation of our params
   1.961 +    // (name=value&name=value&name=value)
   1.962 +    var dataString = "";
   1.963 +    for (var i = 0; i < this.params.length; ++i) {
   1.964 +      var param = this.params[i];
   1.965 +
   1.966 +      // If this parameter has a purpose, only add it if the purpose matches
   1.967 +      if (param.purpose !== undefined && param.purpose != purpose)
   1.968 +        continue;
   1.969 +
   1.970 +      var value = ParamSubstitution(param.value, aSearchTerms, aEngine);
   1.971 +
   1.972 +      dataString += (i > 0 ? "&" : "") + param.name + "=" + value;
   1.973 +    }
   1.974 +
   1.975 +    var postData = null;
   1.976 +    if (this.method == "GET") {
   1.977 +      // GET method requests have no post data, and append the encoded
   1.978 +      // query string to the url...
   1.979 +      if (url.indexOf("?") == -1 && dataString)
   1.980 +        url += "?";
   1.981 +      url += dataString;
   1.982 +    } else if (this.method == "POST") {
   1.983 +      // POST method requests must wrap the encoded text in a MIME
   1.984 +      // stream and supply that as POSTDATA.
   1.985 +      var stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
   1.986 +                         createInstance(Ci.nsIStringInputStream);
   1.987 +      stringStream.data = dataString;
   1.988 +
   1.989 +      postData = Cc["@mozilla.org/network/mime-input-stream;1"].
   1.990 +                 createInstance(Ci.nsIMIMEInputStream);
   1.991 +      postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
   1.992 +      postData.addContentLength = true;
   1.993 +      postData.setData(stringStream);
   1.994 +    }
   1.995 +
   1.996 +    return new Submission(makeURI(url), postData);
   1.997 +  },
   1.998 +
   1.999 +  _hasRelation: function SRC_EURL__hasRelation(aRel)
  1.1000 +    this.rels.some(function(e) e == aRel.toLowerCase()),
  1.1001 +
  1.1002 +  _initWithJSON: function SRC_EURL__initWithJSON(aJson, aEngine) {
  1.1003 +    if (!aJson.params)
  1.1004 +      return;
  1.1005 +
  1.1006 +    this.rels = aJson.rels;
  1.1007 +
  1.1008 +    for (let i = 0; i < aJson.params.length; ++i) {
  1.1009 +      let param = aJson.params[i];
  1.1010 +      if (param.mozparam) {
  1.1011 +        if (param.condition == "defaultEngine") {
  1.1012 +          if (aEngine._isDefaultEngine())
  1.1013 +            this.addParam(param.name, param.trueValue);
  1.1014 +          else
  1.1015 +            this.addParam(param.name, param.falseValue);
  1.1016 +        } else if (param.condition == "pref") {
  1.1017 +          let value = getMozParamPref(param.pref);
  1.1018 +          this.addParam(param.name, value);
  1.1019 +        }
  1.1020 +        this._addMozParam(param);
  1.1021 +      }
  1.1022 +      else
  1.1023 +        this.addParam(param.name, param.value, param.purpose);
  1.1024 +    }
  1.1025 +  },
  1.1026 +
  1.1027 +  /**
  1.1028 +   * Creates a JavaScript object that represents this URL.
  1.1029 +   * @returns An object suitable for serialization as JSON.
  1.1030 +   **/
  1.1031 +  _serializeToJSON: function SRCH_EURL__serializeToJSON() {
  1.1032 +    var json = {
  1.1033 +      template: this.template,
  1.1034 +      rels: this.rels,
  1.1035 +      resultDomain: this.resultDomain
  1.1036 +    };
  1.1037 +
  1.1038 +    if (this.type != URLTYPE_SEARCH_HTML)
  1.1039 +      json.type = this.type;
  1.1040 +    if (this.method != "GET")
  1.1041 +      json.method = this.method;
  1.1042 +
  1.1043 +    function collapseMozParams(aParam)
  1.1044 +      this.mozparams[aParam.name] || aParam;
  1.1045 +    json.params = this.params.map(collapseMozParams, this);
  1.1046 +
  1.1047 +    return json;
  1.1048 +  },
  1.1049 +
  1.1050 +  /**
  1.1051 +   * Serializes the engine object to a OpenSearch Url element.
  1.1052 +   * @param aDoc
  1.1053 +   *        The document to use to create the Url element.
  1.1054 +   * @param aElement
  1.1055 +   *        The element to which the created Url element is appended.
  1.1056 +   *
  1.1057 +   * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
  1.1058 +   */
  1.1059 +  _serializeToElement: function SRCH_EURL_serializeToEl(aDoc, aElement) {
  1.1060 +    var url = aDoc.createElementNS(OPENSEARCH_NS_11, "Url");
  1.1061 +    url.setAttribute("type", this.type);
  1.1062 +    url.setAttribute("method", this.method);
  1.1063 +    url.setAttribute("template", this.template);
  1.1064 +    if (this.rels.length)
  1.1065 +      url.setAttribute("rel", this.rels.join(" "));
  1.1066 +    if (this.resultDomain)
  1.1067 +      url.setAttribute("resultDomain", this.resultDomain);
  1.1068 +
  1.1069 +    for (var i = 0; i < this.params.length; ++i) {
  1.1070 +      var param = aDoc.createElementNS(OPENSEARCH_NS_11, "Param");
  1.1071 +      param.setAttribute("name", this.params[i].name);
  1.1072 +      param.setAttribute("value", this.params[i].value);
  1.1073 +      url.appendChild(aDoc.createTextNode("\n  "));
  1.1074 +      url.appendChild(param);
  1.1075 +    }
  1.1076 +    url.appendChild(aDoc.createTextNode("\n"));
  1.1077 +    aElement.appendChild(url);
  1.1078 +  }
  1.1079 +};
  1.1080 +
  1.1081 +/**
  1.1082 + * nsISearchEngine constructor.
  1.1083 + * @param aLocation
  1.1084 + *        A nsILocalFile or nsIURI object representing the location of the
  1.1085 + *        search engine data file.
  1.1086 + * @param aSourceDataType
  1.1087 + *        The data type of the file used to describe the engine. Must be either
  1.1088 + *        DATA_XML or DATA_TEXT.
  1.1089 + * @param aIsReadOnly
  1.1090 + *        Boolean indicating whether the engine should be treated as read-only.
  1.1091 + *        Read only engines cannot be serialized to file.
  1.1092 + */
  1.1093 +function Engine(aLocation, aSourceDataType, aIsReadOnly) {
  1.1094 +  this._dataType = aSourceDataType;
  1.1095 +  this._readOnly = aIsReadOnly;
  1.1096 +  this._urls = [];
  1.1097 +
  1.1098 +  if (aLocation.type) {
  1.1099 +    if (aLocation.type == "filePath")
  1.1100 +      this._file = aLocation.value;
  1.1101 +    else if (aLocation.type == "uri")
  1.1102 +      this._uri = aLocation.value;
  1.1103 +  } else if (aLocation instanceof Ci.nsILocalFile) {
  1.1104 +    // we already have a file (e.g. loading engines from disk)
  1.1105 +    this._file = aLocation;
  1.1106 +  } else if (aLocation instanceof Ci.nsIURI) {
  1.1107 +    switch (aLocation.scheme) {
  1.1108 +      case "https":
  1.1109 +      case "http":
  1.1110 +      case "ftp":
  1.1111 +      case "data":
  1.1112 +      case "file":
  1.1113 +      case "resource":
  1.1114 +      case "chrome":
  1.1115 +        this._uri = aLocation;
  1.1116 +        break;
  1.1117 +      default:
  1.1118 +        ERROR("Invalid URI passed to the nsISearchEngine constructor",
  1.1119 +              Cr.NS_ERROR_INVALID_ARG);
  1.1120 +    }
  1.1121 +  } else
  1.1122 +    ERROR("Engine location is neither a File nor a URI object",
  1.1123 +          Cr.NS_ERROR_INVALID_ARG);
  1.1124 +}
  1.1125 +
  1.1126 +Engine.prototype = {
  1.1127 +  // The engine's alias (can be null). Initialized to |undefined| to indicate
  1.1128 +  // not-initialized-from-engineMetadataService.
  1.1129 +  _alias: undefined,
  1.1130 +  // A distribution-unique identifier for the engine. Either null or set
  1.1131 +  // when loaded. See getter.
  1.1132 +  _identifier: undefined,
  1.1133 +  // The data describing the engine. Is either an array of bytes, for Sherlock
  1.1134 +  // files, or an XML document element, for XML plugins.
  1.1135 +  _data: null,
  1.1136 +  // The engine's data type. See data types (DATA_) defined above.
  1.1137 +  _dataType: null,
  1.1138 +  // Whether or not the engine is readonly.
  1.1139 +  _readOnly: true,
  1.1140 +  // The engine's description
  1.1141 +  _description: "",
  1.1142 +  // Used to store the engine to replace, if we're an update to an existing
  1.1143 +  // engine.
  1.1144 +  _engineToUpdate: null,
  1.1145 +  // The file from which the plugin was loaded.
  1.1146 +  __file: null,
  1.1147 +  get _file() {
  1.1148 +    if (this.__file && !(this.__file instanceof Ci.nsILocalFile)) {
  1.1149 +      let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
  1.1150 +      file.persistentDescriptor = this.__file;
  1.1151 +      return this.__file = file;
  1.1152 +    }
  1.1153 +    return this.__file;
  1.1154 +  },
  1.1155 +  set _file(aValue) {
  1.1156 +    this.__file = aValue;
  1.1157 +  },
  1.1158 +  // Set to true if the engine has a preferred icon (an icon that should not be
  1.1159 +  // overridden by a non-preferred icon).
  1.1160 +  _hasPreferredIcon: null,
  1.1161 +  // Whether the engine is hidden from the user.
  1.1162 +  _hidden: null,
  1.1163 +  // The engine's name.
  1.1164 +  _name: null,
  1.1165 +  // The engine type. See engine types (TYPE_) defined above.
  1.1166 +  _type: null,
  1.1167 +  // The name of the charset used to submit the search terms.
  1.1168 +  _queryCharset: null,
  1.1169 +  // The engine's raw SearchForm value (URL string pointing to a search form).
  1.1170 +  __searchForm: null,
  1.1171 +  get _searchForm() {
  1.1172 +    return this.__searchForm;
  1.1173 +  },
  1.1174 +  set _searchForm(aValue) {
  1.1175 +    if (/^https?:/i.test(aValue))
  1.1176 +      this.__searchForm = aValue;
  1.1177 +    else
  1.1178 +      LOG("_searchForm: Invalid URL dropped for " + this._name ||
  1.1179 +          "the current engine");
  1.1180 +  },
  1.1181 +  // The URI object from which the engine was retrieved.
  1.1182 +  // This is null for engines loaded from disk, but present for engines loaded
  1.1183 +  // from chrome:// URIs.
  1.1184 +  __uri: null,
  1.1185 +  get _uri() {
  1.1186 +    if (this.__uri && !(this.__uri instanceof Ci.nsIURI))
  1.1187 +      this.__uri = makeURI(this.__uri);
  1.1188 +
  1.1189 +    return this.__uri;
  1.1190 +  },
  1.1191 +  set _uri(aValue) {
  1.1192 +    this.__uri = aValue;
  1.1193 +  },
  1.1194 +  // Whether to obtain user confirmation before adding the engine. This is only
  1.1195 +  // used when the engine is first added to the list.
  1.1196 +  _confirm: false,
  1.1197 +  // Whether to set this as the current engine as soon as it is loaded.  This
  1.1198 +  // is only used when the engine is first added to the list.
  1.1199 +  _useNow: false,
  1.1200 +  // A function to be invoked when this engine object's addition completes (or
  1.1201 +  // fails). Only used for installation via addEngine.
  1.1202 +  _installCallback: null,
  1.1203 +  // Where the engine was loaded from. Can be one of: SEARCH_APP_DIR,
  1.1204 +  // SEARCH_PROFILE_DIR, SEARCH_IN_EXTENSION.
  1.1205 +  __installLocation: null,
  1.1206 +  // The number of days between update checks for new versions
  1.1207 +  _updateInterval: null,
  1.1208 +  // The url to check at for a new update
  1.1209 +  _updateURL: null,
  1.1210 +  // The url to check for a new icon
  1.1211 +  _iconUpdateURL: null,
  1.1212 +  /* Deferred serialization task. */
  1.1213 +  _lazySerializeTask: null,
  1.1214 +
  1.1215 +  /**
  1.1216 +   * Retrieves the data from the engine's file. If the engine's dataType is
  1.1217 +   * XML, the document element is placed in the engine's data field. For text
  1.1218 +   * engines, the data is just read directly from file and placed as an array
  1.1219 +   * of lines in the engine's data field.
  1.1220 +   */
  1.1221 +  _initFromFile: function SRCH_ENG_initFromFile() {
  1.1222 +    if (!this._file || !this._file.exists())
  1.1223 +      FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED);
  1.1224 +
  1.1225 +    var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"].
  1.1226 +                       createInstance(Ci.nsIFileInputStream);
  1.1227 +
  1.1228 +    fileInStream.init(this._file, MODE_RDONLY, PERMS_FILE, false);
  1.1229 +
  1.1230 +    if (this._dataType == SEARCH_DATA_XML) {
  1.1231 +      var domParser = Cc["@mozilla.org/xmlextras/domparser;1"].
  1.1232 +                      createInstance(Ci.nsIDOMParser);
  1.1233 +      var doc = domParser.parseFromStream(fileInStream, "UTF-8",
  1.1234 +                                          this._file.fileSize,
  1.1235 +                                          "text/xml");
  1.1236 +
  1.1237 +      this._data = doc.documentElement;
  1.1238 +    } else {
  1.1239 +      ERROR("Unsuppored engine _dataType in _initFromFile: \"" +
  1.1240 +            this._dataType + "\"",
  1.1241 +            Cr.NS_ERROR_UNEXPECTED);
  1.1242 +    }
  1.1243 +    fileInStream.close();
  1.1244 +
  1.1245 +    // Now that the data is loaded, initialize the engine object
  1.1246 +    this._initFromData();
  1.1247 +  },
  1.1248 +
  1.1249 +  /**
  1.1250 +   * Retrieves the data from the engine's file asynchronously. If the engine's
  1.1251 +   * dataType is XML, the document element is placed in the engine's data field.
  1.1252 +   *
  1.1253 +   * @returns {Promise} A promise, resolved successfully if initializing from
  1.1254 +   * data succeeds, rejected if it fails.
  1.1255 +   */
  1.1256 +  _asyncInitFromFile: function SRCH_ENG__asyncInitFromFile() {
  1.1257 +    return TaskUtils.spawn(function() {
  1.1258 +      if (!this._file || !(yield OS.File.exists(this._file.path)))
  1.1259 +        FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED);
  1.1260 +
  1.1261 +      if (this._dataType == SEARCH_DATA_XML) {
  1.1262 +        let fileURI = NetUtil.ioService.newFileURI(this._file);
  1.1263 +        yield this._retrieveSearchXMLData(fileURI.spec);
  1.1264 +      } else {
  1.1265 +        ERROR("Unsuppored engine _dataType in _initFromFile: \"" +
  1.1266 +              this._dataType + "\"",
  1.1267 +              Cr.NS_ERROR_UNEXPECTED);
  1.1268 +      }
  1.1269 +
  1.1270 +      // Now that the data is loaded, initialize the engine object
  1.1271 +      this._initFromData();
  1.1272 +    }.bind(this));
  1.1273 +  },
  1.1274 +
  1.1275 +  /**
  1.1276 +   * Retrieves the engine data from a URI. Initializes the engine, flushes to
  1.1277 +   * disk, and notifies the search service once initialization is complete.
  1.1278 +   */
  1.1279 +  _initFromURIAndLoad: function SRCH_ENG_initFromURIAndLoad() {
  1.1280 +    ENSURE_WARN(this._uri instanceof Ci.nsIURI,
  1.1281 +                "Must have URI when calling _initFromURIAndLoad!",
  1.1282 +                Cr.NS_ERROR_UNEXPECTED);
  1.1283 +
  1.1284 +    LOG("_initFromURIAndLoad: Downloading engine from: \"" + this._uri.spec + "\".");
  1.1285 +
  1.1286 +    var chan = NetUtil.ioService.newChannelFromURI(this._uri);
  1.1287 +
  1.1288 +    if (this._engineToUpdate && (chan instanceof Ci.nsIHttpChannel)) {
  1.1289 +      var lastModified = engineMetadataService.getAttr(this._engineToUpdate,
  1.1290 +                                                       "updatelastmodified");
  1.1291 +      if (lastModified)
  1.1292 +        chan.setRequestHeader("If-Modified-Since", lastModified, false);
  1.1293 +    }
  1.1294 +    var listener = new loadListener(chan, this, this._onLoad);
  1.1295 +    chan.notificationCallbacks = listener;
  1.1296 +    chan.asyncOpen(listener, null);
  1.1297 +  },
  1.1298 +
  1.1299 +  /**
  1.1300 +   * Retrieves the engine data from a URI asynchronously and initializes it.
  1.1301 +   *
  1.1302 +   * @returns {Promise} A promise, resolved successfully if retrieveing data
  1.1303 +   * succeeds.
  1.1304 +   */
  1.1305 +  _asyncInitFromURI: function SRCH_ENG__asyncInitFromURI() {
  1.1306 +    return TaskUtils.spawn(function() {
  1.1307 +      LOG("_asyncInitFromURI: Loading engine from: \"" + this._uri.spec + "\".");
  1.1308 +      yield this._retrieveSearchXMLData(this._uri.spec);
  1.1309 +      // Now that the data is loaded, initialize the engine object
  1.1310 +      this._initFromData();
  1.1311 +    }.bind(this));
  1.1312 +  },
  1.1313 +
  1.1314 +  /**
  1.1315 +   * Retrieves the engine data for a given URI asynchronously.
  1.1316 +   *
  1.1317 +   * @returns {Promise} A promise, resolved successfully if retrieveing data
  1.1318 +   * succeeds.
  1.1319 +   */
  1.1320 +  _retrieveSearchXMLData: function SRCH_ENG__retrieveSearchXMLData(aURL) {
  1.1321 +    let deferred = Promise.defer();
  1.1322 +    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
  1.1323 +                    createInstance(Ci.nsIXMLHttpRequest);
  1.1324 +    request.overrideMimeType("text/xml");
  1.1325 +    request.onload = (aEvent) => {
  1.1326 +      let responseXML = aEvent.target.responseXML;
  1.1327 +      this._data = responseXML.documentElement;
  1.1328 +      deferred.resolve();
  1.1329 +    };
  1.1330 +    request.onerror = function(aEvent) {
  1.1331 +      deferred.resolve();
  1.1332 +    };
  1.1333 +    request.open("GET", aURL, true);
  1.1334 +    request.send();
  1.1335 +
  1.1336 +    return deferred.promise;
  1.1337 +  },
  1.1338 +
  1.1339 +  _initFromURISync: function SRCH_ENG_initFromURISync() {
  1.1340 +    ENSURE_WARN(this._uri instanceof Ci.nsIURI,
  1.1341 +                "Must have URI when calling _initFromURISync!",
  1.1342 +                Cr.NS_ERROR_UNEXPECTED);
  1.1343 +
  1.1344 +    ENSURE_WARN(this._uri.schemeIs("chrome"), "_initFromURISync called for non-chrome URI",
  1.1345 +                Cr.NS_ERROR_FAILURE);
  1.1346 +
  1.1347 +    LOG("_initFromURISync: Loading engine from: \"" + this._uri.spec + "\".");
  1.1348 +
  1.1349 +    var chan = NetUtil.ioService.newChannelFromURI(this._uri);
  1.1350 +
  1.1351 +    var stream = chan.open();
  1.1352 +    var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
  1.1353 +                 createInstance(Ci.nsIDOMParser);
  1.1354 +    var doc = parser.parseFromStream(stream, "UTF-8", stream.available(), "text/xml");
  1.1355 +
  1.1356 +    this._data = doc.documentElement;
  1.1357 +
  1.1358 +    // Now that the data is loaded, initialize the engine object
  1.1359 +    this._initFromData();
  1.1360 +  },
  1.1361 +
  1.1362 +  /**
  1.1363 +   * Attempts to find an EngineURL object in the set of EngineURLs for
  1.1364 +   * this Engine that has the given type string.  (This corresponds to the
  1.1365 +   * "type" attribute in the "Url" node in the OpenSearch spec.)
  1.1366 +   * This method will return the first matching URL object found, or null
  1.1367 +   * if no matching URL is found.
  1.1368 +   *
  1.1369 +   * @param aType string to match the EngineURL's type attribute
  1.1370 +   * @param aRel [optional] only return URLs that with this rel value
  1.1371 +   */
  1.1372 +  _getURLOfType: function SRCH_ENG__getURLOfType(aType, aRel) {
  1.1373 +    for (var i = 0; i < this._urls.length; ++i) {
  1.1374 +      if (this._urls[i].type == aType && (!aRel || this._urls[i]._hasRelation(aRel)))
  1.1375 +        return this._urls[i];
  1.1376 +    }
  1.1377 +
  1.1378 +    return null;
  1.1379 +  },
  1.1380 +
  1.1381 +  _confirmAddEngine: function SRCH_SVC_confirmAddEngine() {
  1.1382 +    var stringBundle = Services.strings.createBundle(SEARCH_BUNDLE);
  1.1383 +    var titleMessage = stringBundle.GetStringFromName("addEngineConfirmTitle");
  1.1384 +
  1.1385 +    // Display only the hostname portion of the URL.
  1.1386 +    var dialogMessage =
  1.1387 +        stringBundle.formatStringFromName("addEngineConfirmation",
  1.1388 +                                          [this._name, this._uri.host], 2);
  1.1389 +    var checkboxMessage = null;
  1.1390 +    if (!getBoolPref(BROWSER_SEARCH_PREF + "noCurrentEngine", false))
  1.1391 +      checkboxMessage = stringBundle.GetStringFromName("addEngineAsCurrentText");
  1.1392 +
  1.1393 +    var addButtonLabel =
  1.1394 +        stringBundle.GetStringFromName("addEngineAddButtonLabel");
  1.1395 +
  1.1396 +    var ps = Services.prompt;
  1.1397 +    var buttonFlags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
  1.1398 +                      (ps.BUTTON_TITLE_CANCEL    * ps.BUTTON_POS_1) +
  1.1399 +                       ps.BUTTON_POS_0_DEFAULT;
  1.1400 +
  1.1401 +    var checked = {value: false};
  1.1402 +    // confirmEx returns the index of the button that was pressed.  Since "Add"
  1.1403 +    // is button 0, we want to return the negation of that value.
  1.1404 +    var confirm = !ps.confirmEx(null,
  1.1405 +                                titleMessage,
  1.1406 +                                dialogMessage,
  1.1407 +                                buttonFlags,
  1.1408 +                                addButtonLabel,
  1.1409 +                                null, null, // button 1 & 2 names not used
  1.1410 +                                checkboxMessage,
  1.1411 +                                checked);
  1.1412 +
  1.1413 +    return {confirmed: confirm, useNow: checked.value};
  1.1414 +  },
  1.1415 +
  1.1416 +  /**
  1.1417 +   * Handle the successful download of an engine. Initializes the engine and
  1.1418 +   * triggers parsing of the data. The engine is then flushed to disk. Notifies
  1.1419 +   * the search service once initialization is complete.
  1.1420 +   */
  1.1421 +  _onLoad: function SRCH_ENG_onLoad(aBytes, aEngine) {
  1.1422 +    /**
  1.1423 +     * Handle an error during the load of an engine by notifying the engine's
  1.1424 +     * error callback, if any.
  1.1425 +     */
  1.1426 +    function onError(errorCode = Ci.nsISearchInstallCallback.ERROR_UNKNOWN_FAILURE) {
  1.1427 +      // Notify the callback of the failure
  1.1428 +      if (aEngine._installCallback) {
  1.1429 +        aEngine._installCallback(errorCode);
  1.1430 +      }
  1.1431 +    }
  1.1432 +
  1.1433 +    function promptError(strings = {}, error = undefined) {
  1.1434 +      onError(error);
  1.1435 +
  1.1436 +      if (aEngine._engineToUpdate) {
  1.1437 +        // We're in an update, so just fail quietly
  1.1438 +        LOG("updating " + aEngine._engineToUpdate.name + " failed");
  1.1439 +        return;
  1.1440 +      }
  1.1441 +      var brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
  1.1442 +      var brandName = brandBundle.GetStringFromName("brandShortName");
  1.1443 +
  1.1444 +      var searchBundle = Services.strings.createBundle(SEARCH_BUNDLE);
  1.1445 +      var msgStringName = strings.error || "error_loading_engine_msg2";
  1.1446 +      var titleStringName = strings.title || "error_loading_engine_title";
  1.1447 +      var title = searchBundle.GetStringFromName(titleStringName);
  1.1448 +      var text = searchBundle.formatStringFromName(msgStringName,
  1.1449 +                                                   [brandName, aEngine._location],
  1.1450 +                                                   2);
  1.1451 +
  1.1452 +      Services.ww.getNewPrompter(null).alert(title, text);
  1.1453 +    }
  1.1454 +
  1.1455 +    if (!aBytes) {
  1.1456 +      promptError();
  1.1457 +      return;
  1.1458 +    }
  1.1459 +
  1.1460 +    var engineToUpdate = null;
  1.1461 +    if (aEngine._engineToUpdate) {
  1.1462 +      engineToUpdate = aEngine._engineToUpdate.wrappedJSObject;
  1.1463 +
  1.1464 +      // Make this new engine use the old engine's file.
  1.1465 +      aEngine._file = engineToUpdate._file;
  1.1466 +    }
  1.1467 +
  1.1468 +    switch (aEngine._dataType) {
  1.1469 +      case SEARCH_DATA_XML:
  1.1470 +        var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
  1.1471 +                     createInstance(Ci.nsIDOMParser);
  1.1472 +        var doc = parser.parseFromBuffer(aBytes, aBytes.length, "text/xml");
  1.1473 +        aEngine._data = doc.documentElement;
  1.1474 +        break;
  1.1475 +      case SEARCH_DATA_TEXT:
  1.1476 +        aEngine._data = aBytes;
  1.1477 +        break;
  1.1478 +      default:
  1.1479 +        promptError();
  1.1480 +        LOG("_onLoad: Bogus engine _dataType: \"" + this._dataType + "\"");
  1.1481 +        return;
  1.1482 +    }
  1.1483 +
  1.1484 +    try {
  1.1485 +      // Initialize the engine from the obtained data
  1.1486 +      aEngine._initFromData();
  1.1487 +    } catch (ex) {
  1.1488 +      LOG("_onLoad: Failed to init engine!\n" + ex);
  1.1489 +      // Report an error to the user
  1.1490 +      promptError();
  1.1491 +      return;
  1.1492 +    }
  1.1493 +
  1.1494 +    // Check that when adding a new engine (e.g., not updating an
  1.1495 +    // existing one), a duplicate engine does not already exist.
  1.1496 +    if (!engineToUpdate) {
  1.1497 +      if (Services.search.getEngineByName(aEngine.name)) {
  1.1498 +        // If we're confirming the engine load, then display a "this is a
  1.1499 +        // duplicate engine" prompt; otherwise, fail silently.
  1.1500 +        if (aEngine._confirm) {
  1.1501 +          promptError({ error: "error_duplicate_engine_msg",
  1.1502 +                        title: "error_invalid_engine_title"
  1.1503 +                      }, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
  1.1504 +        } else {
  1.1505 +          onError(Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
  1.1506 +        }
  1.1507 +        LOG("_onLoad: duplicate engine found, bailing");
  1.1508 +        return;
  1.1509 +      }
  1.1510 +    }
  1.1511 +
  1.1512 +    // If requested, confirm the addition now that we have the title.
  1.1513 +    // This property is only ever true for engines added via
  1.1514 +    // nsIBrowserSearchService::addEngine.
  1.1515 +    if (aEngine._confirm) {
  1.1516 +      var confirmation = aEngine._confirmAddEngine();
  1.1517 +      LOG("_onLoad: confirm is " + confirmation.confirmed +
  1.1518 +          "; useNow is " + confirmation.useNow);
  1.1519 +      if (!confirmation.confirmed) {
  1.1520 +        onError();
  1.1521 +        return;
  1.1522 +      }
  1.1523 +      aEngine._useNow = confirmation.useNow;
  1.1524 +    }
  1.1525 +
  1.1526 +    // If we don't yet have a file, get one now. The only case where we would
  1.1527 +    // already have a file is if this is an update and _file was set above.
  1.1528 +    if (!aEngine._file)
  1.1529 +      aEngine._file = getSanitizedFile(aEngine.name);
  1.1530 +
  1.1531 +    if (engineToUpdate) {
  1.1532 +      // Keep track of the last modified date, so that we can make conditional
  1.1533 +      // requests for future updates.
  1.1534 +      engineMetadataService.setAttr(aEngine, "updatelastmodified",
  1.1535 +                                    (new Date()).toUTCString());
  1.1536 +
  1.1537 +      // If we're updating an app-shipped engine, ensure that the updateURLs
  1.1538 +      // are the same.
  1.1539 +      if (engineToUpdate._isInAppDir) {
  1.1540 +        let oldUpdateURL = engineToUpdate._updateURL;
  1.1541 +        let newUpdateURL = aEngine._updateURL;
  1.1542 +        let oldSelfURL = engineToUpdate._getURLOfType(URLTYPE_OPENSEARCH, "self");
  1.1543 +        if (oldSelfURL) {
  1.1544 +          oldUpdateURL = oldSelfURL.template;
  1.1545 +          let newSelfURL = aEngine._getURLOfType(URLTYPE_OPENSEARCH, "self");
  1.1546 +          if (!newSelfURL) {
  1.1547 +            LOG("_onLoad: updateURL missing in updated engine for " +
  1.1548 +                aEngine.name + " aborted");
  1.1549 +            onError();
  1.1550 +            return;
  1.1551 +          }
  1.1552 +          newUpdateURL = newSelfURL.template;
  1.1553 +        }
  1.1554 +
  1.1555 +        if (oldUpdateURL != newUpdateURL) {
  1.1556 +          LOG("_onLoad: updateURLs do not match! Update of " + aEngine.name + " aborted");
  1.1557 +          onError();
  1.1558 +          return;
  1.1559 +        }
  1.1560 +      }
  1.1561 +
  1.1562 +      // Set the new engine's icon, if it doesn't yet have one.
  1.1563 +      if (!aEngine._iconURI && engineToUpdate._iconURI)
  1.1564 +        aEngine._iconURI = engineToUpdate._iconURI;
  1.1565 +    }
  1.1566 +
  1.1567 +    // Write the engine to file. For readOnly engines, they'll be stored in the
  1.1568 +    // cache following the notification below.
  1.1569 +    if (!aEngine._readOnly)
  1.1570 +      aEngine._serializeToFile();
  1.1571 +
  1.1572 +    // Notify the search service of the successful load. It will deal with
  1.1573 +    // updates by checking aEngine._engineToUpdate.
  1.1574 +    notifyAction(aEngine, SEARCH_ENGINE_LOADED);
  1.1575 +
  1.1576 +    // Notify the callback if needed
  1.1577 +    if (aEngine._installCallback) {
  1.1578 +      aEngine._installCallback();
  1.1579 +    }
  1.1580 +  },
  1.1581 +
  1.1582 +  /**
  1.1583 +   * Creates a key by serializing an object that contains the icon's width
  1.1584 +   * and height.
  1.1585 +   *
  1.1586 +   * @param aWidth
  1.1587 +   *        Width of the icon.
  1.1588 +   * @param aHeight
  1.1589 +   *        Height of the icon.
  1.1590 +   * @returns key string
  1.1591 +   */
  1.1592 +  _getIconKey: function SRCH_ENG_getIconKey(aWidth, aHeight) {
  1.1593 +    let keyObj = {
  1.1594 +     width: aWidth,
  1.1595 +     height: aHeight
  1.1596 +    };
  1.1597 +
  1.1598 +    return JSON.stringify(keyObj);
  1.1599 +  },
  1.1600 +
  1.1601 +  /**
  1.1602 +   * Add an icon to the icon map used by getIconURIBySize() and getIcons().
  1.1603 +   *
  1.1604 +   * @param aWidth
  1.1605 +   *        Width of the icon.
  1.1606 +   * @param aHeight
  1.1607 +   *        Height of the icon.
  1.1608 +   * @param aURISpec
  1.1609 +   *        String with the icon's URI.
  1.1610 +   */
  1.1611 +  _addIconToMap: function SRCH_ENG_addIconToMap(aWidth, aHeight, aURISpec) {
  1.1612 +    // Use an object instead of a Map() because it needs to be serializable.
  1.1613 +    this._iconMapObj = this._iconMapObj || {};
  1.1614 +    let key = this._getIconKey(aWidth, aHeight);
  1.1615 +    this._iconMapObj[key] = aURISpec;
  1.1616 +  },
  1.1617 +
  1.1618 +  /**
  1.1619 +   * Sets the .iconURI property of the engine. If both aWidth and aHeight are
  1.1620 +   * provided an entry will be added to _iconMapObj that will enable accessing
  1.1621 +   * icon's data through getIcons() and getIconURIBySize() APIs.
  1.1622 +   *
  1.1623 +   *  @param aIconURL
  1.1624 +   *         A URI string pointing to the engine's icon. Must have a http[s],
  1.1625 +   *         ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be
  1.1626 +   *         downloaded and converted to data URIs for storage in the engine
  1.1627 +   *         XML files, if the engine is not readonly.
  1.1628 +   *  @param aIsPreferred
  1.1629 +   *         Whether or not this icon is to be preferred. Preferred icons can
  1.1630 +   *         override non-preferred icons.
  1.1631 +   *  @param aWidth (optional)
  1.1632 +   *         Width of the icon.
  1.1633 +   *  @param aHeight (optional)
  1.1634 +   *         Height of the icon.
  1.1635 +   */
  1.1636 +  _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred, aWidth, aHeight) {
  1.1637 +    var uri = makeURI(aIconURL);
  1.1638 +
  1.1639 +    // Ignore bad URIs
  1.1640 +    if (!uri)
  1.1641 +      return;
  1.1642 +
  1.1643 +    LOG("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \""
  1.1644 +        + this.name + "\".");
  1.1645 +    // Only accept remote icons from http[s] or ftp
  1.1646 +    switch (uri.scheme) {
  1.1647 +      case "data":
  1.1648 +        if (!this._hasPreferredIcon || aIsPreferred) {
  1.1649 +          this._iconURI = uri;
  1.1650 +          notifyAction(this, SEARCH_ENGINE_CHANGED);
  1.1651 +          this._hasPreferredIcon = aIsPreferred;
  1.1652 +        }
  1.1653 +
  1.1654 +        if (aWidth && aHeight) {
  1.1655 +          this._addIconToMap(aWidth, aHeight, aIconURL)
  1.1656 +        }
  1.1657 +        break;
  1.1658 +      case "http":
  1.1659 +      case "https":
  1.1660 +      case "ftp":
  1.1661 +        // No use downloading the icon if the engine file is read-only
  1.1662 +        if (!this._readOnly ||
  1.1663 +            getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) {
  1.1664 +          LOG("_setIcon: Downloading icon: \"" + uri.spec +
  1.1665 +              "\" for engine: \"" + this.name + "\"");
  1.1666 +          var chan = NetUtil.ioService.newChannelFromURI(uri);
  1.1667 +
  1.1668 +          function iconLoadCallback(aByteArray, aEngine) {
  1.1669 +            // This callback may run after we've already set a preferred icon,
  1.1670 +            // so check again.
  1.1671 +            if (aEngine._hasPreferredIcon && !aIsPreferred)
  1.1672 +              return;
  1.1673 +
  1.1674 +            if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) {
  1.1675 +              LOG("iconLoadCallback: load failed, or the icon was too large!");
  1.1676 +              return;
  1.1677 +            }
  1.1678 +
  1.1679 +            var str = btoa(String.fromCharCode.apply(null, aByteArray));
  1.1680 +            let dataURL = ICON_DATAURL_PREFIX + str;
  1.1681 +            aEngine._iconURI = makeURI(dataURL);
  1.1682 +
  1.1683 +            if (aWidth && aHeight) {
  1.1684 +              aEngine._addIconToMap(aWidth, aHeight, dataURL)
  1.1685 +            }
  1.1686 +
  1.1687 +            // The engine might not have a file yet, if it's being downloaded,
  1.1688 +            // because the request for the engine file itself (_onLoad) may not
  1.1689 +            // yet be complete. In that case, this change will be written to
  1.1690 +            // file when _onLoad is called. For readonly engines, we'll store
  1.1691 +            // the changes in the cache once notified below.
  1.1692 +            if (aEngine._file && !aEngine._readOnly)
  1.1693 +              aEngine._serializeToFile();
  1.1694 +
  1.1695 +            notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
  1.1696 +            aEngine._hasPreferredIcon = aIsPreferred;
  1.1697 +          }
  1.1698 +
  1.1699 +          // If we're currently acting as an "update engine", then the callback
  1.1700 +          // should set the icon on the engine we're updating and not us, since
  1.1701 +          // |this| might be gone by the time the callback runs.
  1.1702 +          var engineToSet = this._engineToUpdate || this;
  1.1703 +
  1.1704 +          var listener = new loadListener(chan, engineToSet, iconLoadCallback);
  1.1705 +          chan.notificationCallbacks = listener;
  1.1706 +          chan.asyncOpen(listener, null);
  1.1707 +        }
  1.1708 +        break;
  1.1709 +    }
  1.1710 +  },
  1.1711 +
  1.1712 +  /**
  1.1713 +   * Initialize this Engine object from the collected data.
  1.1714 +   */
  1.1715 +  _initFromData: function SRCH_ENG_initFromData() {
  1.1716 +    ENSURE_WARN(this._data, "Can't init an engine with no data!",
  1.1717 +                Cr.NS_ERROR_UNEXPECTED);
  1.1718 +
  1.1719 +    // Find out what type of engine we are
  1.1720 +    switch (this._dataType) {
  1.1721 +      case SEARCH_DATA_XML:
  1.1722 +        if (checkNameSpace(this._data, [MOZSEARCH_LOCALNAME],
  1.1723 +            [MOZSEARCH_NS_10])) {
  1.1724 +
  1.1725 +          LOG("_init: Initing MozSearch plugin from " + this._location);
  1.1726 +
  1.1727 +          this._type = SEARCH_TYPE_MOZSEARCH;
  1.1728 +          this._parseAsMozSearch();
  1.1729 +
  1.1730 +        } else if (checkNameSpace(this._data, [OPENSEARCH_LOCALNAME],
  1.1731 +                                  OPENSEARCH_NAMESPACES)) {
  1.1732 +
  1.1733 +          LOG("_init: Initing OpenSearch plugin from " + this._location);
  1.1734 +
  1.1735 +          this._type = SEARCH_TYPE_OPENSEARCH;
  1.1736 +          this._parseAsOpenSearch();
  1.1737 +
  1.1738 +        } else
  1.1739 +          FAIL(this._location + " is not a valid search plugin.", Cr.NS_ERROR_FAILURE);
  1.1740 +
  1.1741 +        break;
  1.1742 +      case SEARCH_DATA_TEXT:
  1.1743 +        LOG("_init: Initing Sherlock plugin from " + this._location);
  1.1744 +
  1.1745 +        // the only text-based format we support is Sherlock
  1.1746 +        this._type = SEARCH_TYPE_SHERLOCK;
  1.1747 +        this._parseAsSherlock();
  1.1748 +    }
  1.1749 +
  1.1750 +    // No need to keep a ref to our data (which in some cases can be a document
  1.1751 +    // element) past this point
  1.1752 +    this._data = null;
  1.1753 +  },
  1.1754 +
  1.1755 +  /**
  1.1756 +   * Initialize this Engine object from a collection of metadata.
  1.1757 +   */
  1.1758 +  _initFromMetadata: function SRCH_ENG_initMetaData(aName, aIconURL, aAlias,
  1.1759 +                                                    aDescription, aMethod,
  1.1760 +                                                    aTemplate) {
  1.1761 +    ENSURE_WARN(!this._readOnly,
  1.1762 +                "Can't call _initFromMetaData on a readonly engine!",
  1.1763 +                Cr.NS_ERROR_FAILURE);
  1.1764 +
  1.1765 +    this._urls.push(new EngineURL(URLTYPE_SEARCH_HTML, aMethod, aTemplate));
  1.1766 +
  1.1767 +    this._name = aName;
  1.1768 +    this.alias = aAlias;
  1.1769 +    this._description = aDescription;
  1.1770 +    this._setIcon(aIconURL, true);
  1.1771 +
  1.1772 +    this._serializeToFile();
  1.1773 +  },
  1.1774 +
  1.1775 +  /**
  1.1776 +   * Extracts data from an OpenSearch URL element and creates an EngineURL
  1.1777 +   * object which is then added to the engine's list of URLs.
  1.1778 +   *
  1.1779 +   * @throws NS_ERROR_FAILURE if a URL object could not be created.
  1.1780 +   *
  1.1781 +   * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag.
  1.1782 +   * @see EngineURL()
  1.1783 +   */
  1.1784 +  _parseURL: function SRCH_ENG_parseURL(aElement) {
  1.1785 +    var type     = aElement.getAttribute("type");
  1.1786 +    // According to the spec, method is optional, defaulting to "GET" if not
  1.1787 +    // specified
  1.1788 +    var method   = aElement.getAttribute("method") || "GET";
  1.1789 +    var template = aElement.getAttribute("template");
  1.1790 +    var resultDomain = aElement.getAttribute("resultdomain");
  1.1791 +
  1.1792 +    try {
  1.1793 +      var url = new EngineURL(type, method, template, resultDomain);
  1.1794 +    } catch (ex) {
  1.1795 +      FAIL("_parseURL: failed to add " + template + " as a URL",
  1.1796 +           Cr.NS_ERROR_FAILURE);
  1.1797 +    }
  1.1798 +
  1.1799 +    if (aElement.hasAttribute("rel"))
  1.1800 +      url.rels = aElement.getAttribute("rel").toLowerCase().split(/\s+/);
  1.1801 +
  1.1802 +    for (var i = 0; i < aElement.childNodes.length; ++i) {
  1.1803 +      var param = aElement.childNodes[i];
  1.1804 +      if (param.localName == "Param") {
  1.1805 +        try {
  1.1806 +          url.addParam(param.getAttribute("name"), param.getAttribute("value"));
  1.1807 +        } catch (ex) {
  1.1808 +          // Ignore failure
  1.1809 +          LOG("_parseURL: Url element has an invalid param");
  1.1810 +        }
  1.1811 +      } else if (param.localName == "MozParam" &&
  1.1812 +                 // We only support MozParams for default search engines
  1.1813 +                 this._isDefault) {
  1.1814 +        var value;
  1.1815 +        let condition = param.getAttribute("condition");
  1.1816 +
  1.1817 +        // MozParams must have a condition to be valid
  1.1818 +        if (!condition) {
  1.1819 +          let engineLoc = this._location;
  1.1820 +          let paramName = param.getAttribute("name");
  1.1821 +          LOG("_parseURL: MozParam (" + paramName + ") without a condition attribute found parsing engine: " + engineLoc);
  1.1822 +          continue;
  1.1823 +        }
  1.1824 +
  1.1825 +        switch (condition) {
  1.1826 +          case "purpose":
  1.1827 +            url.addParam(param.getAttribute("name"),
  1.1828 +                         param.getAttribute("value"),
  1.1829 +                         param.getAttribute("purpose"));
  1.1830 +            // _addMozParam is not needed here since it can be serialized fine without. _addMozParam
  1.1831 +            // also requires a unique "name" which is not normally the case when @purpose is used.
  1.1832 +            break;
  1.1833 +          case "defaultEngine":
  1.1834 +            // If this engine was the default search engine, use the true value
  1.1835 +            if (this._isDefaultEngine())
  1.1836 +              value = param.getAttribute("trueValue");
  1.1837 +            else
  1.1838 +              value = param.getAttribute("falseValue");
  1.1839 +            url.addParam(param.getAttribute("name"), value);
  1.1840 +            url._addMozParam({"name": param.getAttribute("name"),
  1.1841 +                              "falseValue": param.getAttribute("falseValue"),
  1.1842 +                              "trueValue": param.getAttribute("trueValue"),
  1.1843 +                              "condition": "defaultEngine"});
  1.1844 +            break;
  1.1845 +
  1.1846 +          case "pref":
  1.1847 +            try {
  1.1848 +              value = getMozParamPref(param.getAttribute("pref"), value);
  1.1849 +              url.addParam(param.getAttribute("name"), value);
  1.1850 +              url._addMozParam({"pref": param.getAttribute("pref"),
  1.1851 +                                "name": param.getAttribute("name"),
  1.1852 +                                "condition": "pref"});
  1.1853 +            } catch (e) { }
  1.1854 +            break;
  1.1855 +          default:
  1.1856 +            if (condition && condition.startsWith("top")) {
  1.1857 +              url.addParam(param.getAttribute("name"), param.getAttribute("falseValue"));
  1.1858 +              let mozparam = {"name": param.getAttribute("name"),
  1.1859 +                              "falseValue": param.getAttribute("falseValue"),
  1.1860 +                              "trueValue": param.getAttribute("trueValue"),
  1.1861 +                              "condition": condition,
  1.1862 +                              "positionDependent": true};
  1.1863 +              url._addMozParam(mozparam);
  1.1864 +            }
  1.1865 +          break;
  1.1866 +        }
  1.1867 +      }
  1.1868 +    }
  1.1869 +
  1.1870 +    this._urls.push(url);
  1.1871 +  },
  1.1872 +
  1.1873 +  _isDefaultEngine: function SRCH_ENG__isDefaultEngine() {
  1.1874 +    let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
  1.1875 +    let nsIPLS = Ci.nsIPrefLocalizedString;
  1.1876 +    let defaultEngine;
  1.1877 +    try {
  1.1878 +      defaultEngine = defaultPrefB.getComplexValue("defaultenginename", nsIPLS).data;
  1.1879 +    } catch (ex) {}
  1.1880 +    return this.name == defaultEngine;
  1.1881 +  },
  1.1882 +
  1.1883 +  /**
  1.1884 +   * Get the icon from an OpenSearch Image element.
  1.1885 +   * @see http://opensearch.a9.com/spec/1.1/description/#image
  1.1886 +   */
  1.1887 +  _parseImage: function SRCH_ENG_parseImage(aElement) {
  1.1888 +    LOG("_parseImage: Image textContent: \"" + limitURILength(aElement.textContent) + "\"");
  1.1889 +
  1.1890 +    let width = parseInt(aElement.getAttribute("width"), 10);
  1.1891 +    let height = parseInt(aElement.getAttribute("height"), 10);
  1.1892 +    let isPrefered = width == 16 && height == 16;
  1.1893 +
  1.1894 +    if (isNaN(width) || isNaN(height) || width <= 0 || height <=0) {
  1.1895 +      LOG("OpenSearch image element must have positive width and height.");
  1.1896 +      return;
  1.1897 +    }
  1.1898 +
  1.1899 +    this._setIcon(aElement.textContent, isPrefered, width, height);
  1.1900 +  },
  1.1901 +
  1.1902 +  _parseAsMozSearch: function SRCH_ENG_parseAsMoz() {
  1.1903 +    //forward to the OpenSearch parser
  1.1904 +    this._parseAsOpenSearch();
  1.1905 +  },
  1.1906 +
  1.1907 +  /**
  1.1908 +   * Extract search engine information from the collected data to initialize
  1.1909 +   * the engine object.
  1.1910 +   */
  1.1911 +  _parseAsOpenSearch: function SRCH_ENG_parseAsOS() {
  1.1912 +    var doc = this._data;
  1.1913 +
  1.1914 +    // The OpenSearch spec sets a default value for the input encoding.
  1.1915 +    this._queryCharset = OS_PARAM_INPUT_ENCODING_DEF;
  1.1916 +
  1.1917 +    for (var i = 0; i < doc.childNodes.length; ++i) {
  1.1918 +      var child = doc.childNodes[i];
  1.1919 +      switch (child.localName) {
  1.1920 +        case "ShortName":
  1.1921 +          this._name = child.textContent;
  1.1922 +          break;
  1.1923 +        case "Description":
  1.1924 +          this._description = child.textContent;
  1.1925 +          break;
  1.1926 +        case "Url":
  1.1927 +          try {
  1.1928 +            this._parseURL(child);
  1.1929 +          } catch (ex) {
  1.1930 +            // Parsing of the element failed, just skip it.
  1.1931 +            LOG("_parseAsOpenSearch: failed to parse URL child: " + ex);
  1.1932 +          }
  1.1933 +          break;
  1.1934 +        case "Image":
  1.1935 +          this._parseImage(child);
  1.1936 +          break;
  1.1937 +        case "InputEncoding":
  1.1938 +          this._queryCharset = child.textContent.toUpperCase();
  1.1939 +          break;
  1.1940 +
  1.1941 +        // Non-OpenSearch elements
  1.1942 +        case "SearchForm":
  1.1943 +          this._searchForm = child.textContent;
  1.1944 +          break;
  1.1945 +        case "UpdateUrl":
  1.1946 +          this._updateURL = child.textContent;
  1.1947 +          break;
  1.1948 +        case "UpdateInterval":
  1.1949 +          this._updateInterval = parseInt(child.textContent);
  1.1950 +          break;
  1.1951 +        case "IconUpdateUrl":
  1.1952 +          this._iconUpdateURL = child.textContent;
  1.1953 +          break;
  1.1954 +      }
  1.1955 +    }
  1.1956 +    if (!this.name || (this._urls.length == 0))
  1.1957 +      FAIL("_parseAsOpenSearch: No name, or missing URL!", Cr.NS_ERROR_FAILURE);
  1.1958 +    if (!this.supportsResponseType(URLTYPE_SEARCH_HTML))
  1.1959 +      FAIL("_parseAsOpenSearch: No text/html result type!", Cr.NS_ERROR_FAILURE);
  1.1960 +  },
  1.1961 +
  1.1962 +  /**
  1.1963 +   * Extract search engine information from the collected data to initialize
  1.1964 +   * the engine object.
  1.1965 +   */
  1.1966 +  _parseAsSherlock: function SRCH_ENG_parseAsSherlock() {
  1.1967 +    /**
  1.1968 +     * Extracts one Sherlock "section" from aSource. A section is essentially
  1.1969 +     * an HTML element with attributes, but each attribute must be on a new
  1.1970 +     * line, by definition.
  1.1971 +     *
  1.1972 +     * @param aLines
  1.1973 +     *        An array of lines from the sherlock file.
  1.1974 +     * @param aSection
  1.1975 +     *        The name of the section (e.g. "search" or "browser"). This value
  1.1976 +     *        is not case sensitive.
  1.1977 +     * @returns an object whose properties correspond to the section's
  1.1978 +     *          attributes.
  1.1979 +     */
  1.1980 +    function getSection(aLines, aSection) {
  1.1981 +      LOG("_parseAsSherlock::getSection: Sherlock lines:\n" +
  1.1982 +          aLines.join("\n"));
  1.1983 +      var lines = aLines;
  1.1984 +      var startMark = new RegExp("^\\s*<" + aSection.toLowerCase() + "\\s*",
  1.1985 +                                 "gi");
  1.1986 +      var endMark   = /\s*>\s*$/gi;
  1.1987 +
  1.1988 +      var foundStart = false;
  1.1989 +      var startLine, numberOfLines;
  1.1990 +      // Find the beginning and end of the section
  1.1991 +      for (var i = 0; i < lines.length; i++) {
  1.1992 +        if (foundStart) {
  1.1993 +          if (endMark.test(lines[i])) {
  1.1994 +            numberOfLines = i - startLine;
  1.1995 +            // Remove the end marker
  1.1996 +            lines[i] = lines[i].replace(endMark, "");
  1.1997 +            // If the endmarker was not the only thing on the line, include
  1.1998 +            // this line in the results
  1.1999 +            if (lines[i])
  1.2000 +              numberOfLines++;
  1.2001 +            break;
  1.2002 +          }
  1.2003 +        } else {
  1.2004 +          if (startMark.test(lines[i])) {
  1.2005 +            foundStart = true;
  1.2006 +            // Remove the start marker
  1.2007 +            lines[i] = lines[i].replace(startMark, "");
  1.2008 +            startLine = i;
  1.2009 +            // If the line is empty, don't include it in the result
  1.2010 +            if (!lines[i])
  1.2011 +              startLine++;
  1.2012 +          }
  1.2013 +        }
  1.2014 +      }
  1.2015 +      LOG("_parseAsSherlock::getSection: Start index: " + startLine +
  1.2016 +          "\nNumber of lines: " + numberOfLines);
  1.2017 +      lines = lines.splice(startLine, numberOfLines);
  1.2018 +      LOG("_parseAsSherlock::getSection: Section lines:\n" +
  1.2019 +          lines.join("\n"));
  1.2020 +
  1.2021 +      var section = {};
  1.2022 +      for (var i = 0; i < lines.length; i++) {
  1.2023 +        var line = lines[i].trim();
  1.2024 +
  1.2025 +        var els = line.split("=");
  1.2026 +        var name = els.shift().trim().toLowerCase();
  1.2027 +        var value = els.join("=").trim();
  1.2028 +
  1.2029 +        if (!name || !value)
  1.2030 +          continue;
  1.2031 +
  1.2032 +        // Strip leading and trailing whitespace, remove quotes from the
  1.2033 +        // value, and remove any trailing slashes or ">" characters
  1.2034 +        value = value.replace(/^["']/, "")
  1.2035 +                     .replace(/["']\s*[\\\/]?>?\s*$/, "") || "";
  1.2036 +        value = value.trim();
  1.2037 +
  1.2038 +        // Don't clobber existing attributes
  1.2039 +        if (!(name in section))
  1.2040 +          section[name] = value;
  1.2041 +      }
  1.2042 +      return section;
  1.2043 +    }
  1.2044 +
  1.2045 +    /**
  1.2046 +     * Returns an array of name-value pair arrays representing the Sherlock
  1.2047 +     * file's input elements. User defined inputs return USER_DEFINED
  1.2048 +     * as the value. Elements are returned in the order they appear in the
  1.2049 +     * source file.
  1.2050 +     *
  1.2051 +     *   Example:
  1.2052 +     *      <input name="foo" value="bar">
  1.2053 +     *      <input name="foopy" user>
  1.2054 +     *   Returns:
  1.2055 +     *      [["foo", "bar"], ["foopy", "{searchTerms}"]]
  1.2056 +     *
  1.2057 +     * @param aLines
  1.2058 +     *        An array of lines from the source file.
  1.2059 +     */
  1.2060 +    function getInputs(aLines) {
  1.2061 +
  1.2062 +      /**
  1.2063 +       * Extracts an attribute value from a given a line of text.
  1.2064 +       *    Example: <input value="foo" name="bar">
  1.2065 +       *      Extracts the string |foo| or |bar| given an input aAttr of
  1.2066 +       *      |value| or |name|.
  1.2067 +       * Attributes may be quoted or unquoted. If unquoted, any whitespace
  1.2068 +       * indicates the end of the attribute value.
  1.2069 +       *    Example: < value=22 33 name=44\334 >
  1.2070 +       *      Returns |22| for "value" and |44\334| for "name".
  1.2071 +       *
  1.2072 +       * @param aAttr
  1.2073 +       *        The name of the attribute for which to obtain the value. This
  1.2074 +       *        value is not case sensitive.
  1.2075 +       * @param aLine
  1.2076 +       *        The line containing the attribute.
  1.2077 +       *
  1.2078 +       * @returns the attribute value, or an empty string if the attribute
  1.2079 +       *          doesn't exist.
  1.2080 +       */
  1.2081 +      function getAttr(aAttr, aLine) {
  1.2082 +        // Used to determine whether an "input" line from a Sherlock file is a
  1.2083 +        // "user defined" input.
  1.2084 +        const userInput = /(\s|["'=])user(\s|[>="'\/\\+]|$)/i;
  1.2085 +
  1.2086 +        LOG("_parseAsSherlock::getAttr: Getting attr: \"" +
  1.2087 +            aAttr + "\" for line: \"" + aLine + "\"");
  1.2088 +        // We're not case sensitive, but we want to return the attribute value
  1.2089 +        // in its original case, so create a copy of the source
  1.2090 +        var lLine = aLine.toLowerCase();
  1.2091 +        var attr = aAttr.toLowerCase();
  1.2092 +
  1.2093 +        var attrStart = lLine.search(new RegExp("\\s" + attr, "i"));
  1.2094 +        if (attrStart == -1) {
  1.2095 +
  1.2096 +          // If this is the "user defined input" (i.e. contains the empty
  1.2097 +          // "user" attribute), return our special keyword
  1.2098 +          if (userInput.test(lLine) && attr == "value") {
  1.2099 +            LOG("_parseAsSherlock::getAttr: Found user input!\nLine:\"" + lLine
  1.2100 +                + "\"");
  1.2101 +            return USER_DEFINED;
  1.2102 +          }
  1.2103 +          // The attribute doesn't exist - ignore
  1.2104 +          LOG("_parseAsSherlock::getAttr: Failed to find attribute:\nLine:\""
  1.2105 +              + lLine + "\"\nAttr:\"" + attr + "\"");
  1.2106 +          return "";
  1.2107 +        }
  1.2108 +
  1.2109 +        var valueStart = lLine.indexOf("=", attrStart) + "=".length;
  1.2110 +        if (valueStart == -1)
  1.2111 +          return "";
  1.2112 +
  1.2113 +        var quoteStart = lLine.indexOf("\"", valueStart);
  1.2114 +        if (quoteStart == -1) {
  1.2115 +
  1.2116 +          // Unquoted attribute, get the rest of the line, trimmed at the first
  1.2117 +          // sign of whitespace. If the rest of the line is only whitespace,
  1.2118 +          // returns a blank string.
  1.2119 +          return lLine.substr(valueStart).replace(/\s.*$/, "");
  1.2120 +
  1.2121 +        } else {
  1.2122 +          // Make sure that there's only whitespace between the start of the
  1.2123 +          // value and the first quote. If there is, end the attribute value at
  1.2124 +          // the first sign of whitespace. This prevents us from falling into
  1.2125 +          // the next attribute if this is an unquoted attribute followed by a
  1.2126 +          // quoted attribute.
  1.2127 +          var betweenEqualAndQuote = lLine.substring(valueStart, quoteStart);
  1.2128 +          if (/\S/.test(betweenEqualAndQuote))
  1.2129 +            return lLine.substr(valueStart).replace(/\s.*$/, "");
  1.2130 +
  1.2131 +          // Adjust the start index to account for the opening quote
  1.2132 +          valueStart = quoteStart + "\"".length;
  1.2133 +          // Find the closing quote
  1.2134 +          var valueEnd = lLine.indexOf("\"", valueStart);
  1.2135 +          // If there is no closing quote, just go to the end of the line
  1.2136 +          if (valueEnd == -1)
  1.2137 +            valueEnd = aLine.length;
  1.2138 +        }
  1.2139 +        return aLine.substring(valueStart, valueEnd);
  1.2140 +      }
  1.2141 +
  1.2142 +      var inputs = [];
  1.2143 +
  1.2144 +      LOG("_parseAsSherlock::getInputs: Lines:\n" + aLines);
  1.2145 +      // Filter out everything but non-inputs
  1.2146 +      let lines = aLines.filter(function (line) {
  1.2147 +        return /^\s*<input/i.test(line);
  1.2148 +      });
  1.2149 +      LOG("_parseAsSherlock::getInputs: Filtered lines:\n" + lines);
  1.2150 +
  1.2151 +      lines.forEach(function (line) {
  1.2152 +        // Strip leading/trailing whitespace and remove the surrounding markup
  1.2153 +        // ("<input" and ">")
  1.2154 +        line = line.trim().replace(/^<input/i, "").replace(/>$/, "");
  1.2155 +
  1.2156 +        // If this is one of the "directional" inputs (<inputnext>/<inputprev>)
  1.2157 +        const directionalInput = /^(prev|next)/i;
  1.2158 +        if (directionalInput.test(line)) {
  1.2159 +
  1.2160 +          // Make it look like a normal input by removing "prev" or "next"
  1.2161 +          line = line.replace(directionalInput, "");
  1.2162 +
  1.2163 +          // If it has a name, give it a dummy value to match previous
  1.2164 +          // nsInternetSearchService behavior
  1.2165 +          if (/name\s*=/i.test(line)) {
  1.2166 +            line += " value=\"0\"";
  1.2167 +          } else
  1.2168 +            return; // Line has no name, skip it
  1.2169 +        }
  1.2170 +
  1.2171 +        var attrName = getAttr("name", line);
  1.2172 +        var attrValue = getAttr("value", line);
  1.2173 +        LOG("_parseAsSherlock::getInputs: Got input:\nName:\"" + attrName +
  1.2174 +            "\"\nValue:\"" + attrValue + "\"");
  1.2175 +        if (attrValue)
  1.2176 +          inputs.push([attrName, attrValue]);
  1.2177 +      });
  1.2178 +      return inputs;
  1.2179 +    }
  1.2180 +
  1.2181 +    function err(aErr) {
  1.2182 +      FAIL("_parseAsSherlock::err: Sherlock param error:\n" + aErr,
  1.2183 +           Cr.NS_ERROR_FAILURE);
  1.2184 +    }
  1.2185 +
  1.2186 +    // First try converting our byte array using the default Sherlock encoding.
  1.2187 +    // If this fails, or if we find a sourceTextEncoding attribute, we need to
  1.2188 +    // reconvert the byte array using the specified encoding.
  1.2189 +    var sherlockLines, searchSection, sourceTextEncoding, browserSection;
  1.2190 +    try {
  1.2191 +      sherlockLines = sherlockBytesToLines(this._data);
  1.2192 +      searchSection = getSection(sherlockLines, "search");
  1.2193 +      browserSection = getSection(sherlockLines, "browser");
  1.2194 +      sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]);
  1.2195 +      if (sourceTextEncoding) {
  1.2196 +        // Re-convert the bytes using the found sourceTextEncoding
  1.2197 +        sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding);
  1.2198 +        searchSection = getSection(sherlockLines, "search");
  1.2199 +        browserSection = getSection(sherlockLines, "browser");
  1.2200 +      }
  1.2201 +    } catch (ex) {
  1.2202 +      // The conversion using the default charset failed. Remove any non-ascii
  1.2203 +      // bytes and try to find a sourceTextEncoding.
  1.2204 +      var asciiBytes = this._data.filter(function (n) {return !(0x80 & n);});
  1.2205 +      var asciiString = String.fromCharCode.apply(null, asciiBytes);
  1.2206 +      sherlockLines = asciiString.split(NEW_LINES).filter(isUsefulLine);
  1.2207 +      searchSection = getSection(sherlockLines, "search");
  1.2208 +      sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]);
  1.2209 +      if (sourceTextEncoding) {
  1.2210 +        sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding);
  1.2211 +        searchSection = getSection(sherlockLines, "search");
  1.2212 +        browserSection = getSection(sherlockLines, "browser");
  1.2213 +      } else
  1.2214 +        ERROR("Couldn't find a working charset", Cr.NS_ERROR_FAILURE);
  1.2215 +    }
  1.2216 +
  1.2217 +    LOG("_parseAsSherlock: Search section:\n" + searchSection.toSource());
  1.2218 +
  1.2219 +    this._name = searchSection["name"] || err("Missing name!");
  1.2220 +    this._description = searchSection["description"] || "";
  1.2221 +    this._queryCharset = searchSection["querycharset"] ||
  1.2222 +                         queryCharsetFromCode(searchSection["queryencoding"]);
  1.2223 +    this._searchForm = searchSection["searchform"];
  1.2224 +
  1.2225 +    this._updateInterval = parseInt(browserSection["updatecheckdays"]);
  1.2226 +
  1.2227 +    this._updateURL = browserSection["update"];
  1.2228 +    this._iconUpdateURL = browserSection["updateicon"];
  1.2229 +
  1.2230 +    var method = (searchSection["method"] || "GET").toUpperCase();
  1.2231 +    var template = searchSection["action"] || err("Missing action!");
  1.2232 +
  1.2233 +    var inputs = getInputs(sherlockLines);
  1.2234 +    LOG("_parseAsSherlock: Inputs:\n" + inputs.toSource());
  1.2235 +
  1.2236 +    var url = null;
  1.2237 +
  1.2238 +    if (method == "GET") {
  1.2239 +      // Here's how we construct the input string:
  1.2240 +      // <input> is first:  Name Attr:  Prefix      Data           Example:
  1.2241 +      // YES                EMPTY       None        <value>        TEMPLATE<value>
  1.2242 +      // YES                NON-EMPTY   ?           <name>=<value> TEMPLATE?<name>=<value>
  1.2243 +      // NO                 EMPTY       ------------- <ignored> --------------
  1.2244 +      // NO                 NON-EMPTY   &           <name>=<value> TEMPLATE?<n1>=<v1>&<n2>=<v2>
  1.2245 +      for (var i = 0; i < inputs.length; i++) {
  1.2246 +        var name  = inputs[i][0];
  1.2247 +        var value = inputs[i][1];
  1.2248 +        if (i==0) {
  1.2249 +          if (name == "")
  1.2250 +            template += USER_DEFINED;
  1.2251 +          else
  1.2252 +            template += "?" + name + "=" + value;
  1.2253 +        } else if (name != "")
  1.2254 +          template += "&" + name + "=" + value;
  1.2255 +      }
  1.2256 +      url = new EngineURL(URLTYPE_SEARCH_HTML, method, template);
  1.2257 +
  1.2258 +    } else if (method == "POST") {
  1.2259 +      // Create the URL object and just add the parameters directly
  1.2260 +      url = new EngineURL(URLTYPE_SEARCH_HTML, method, template);
  1.2261 +      for (var i = 0; i < inputs.length; i++) {
  1.2262 +        var name  = inputs[i][0];
  1.2263 +        var value = inputs[i][1];
  1.2264 +        if (name)
  1.2265 +          url.addParam(name, value);
  1.2266 +      }
  1.2267 +    } else
  1.2268 +      err("Invalid method!");
  1.2269 +
  1.2270 +    this._urls.push(url);
  1.2271 +  },
  1.2272 +
  1.2273 +  /**
  1.2274 +   * Init from a JSON record.
  1.2275 +   **/
  1.2276 +  _initWithJSON: function SRCH_ENG__initWithJSON(aJson) {
  1.2277 +    this.__id = aJson._id;
  1.2278 +    this._name = aJson._name;
  1.2279 +    this._description = aJson.description;
  1.2280 +    if (aJson._hasPreferredIcon == undefined)
  1.2281 +      this._hasPreferredIcon = true;
  1.2282 +    else
  1.2283 +      this._hasPreferredIcon = false;
  1.2284 +    this._hidden = aJson._hidden;
  1.2285 +    this._type = aJson.type || SEARCH_TYPE_MOZSEARCH;
  1.2286 +    this._queryCharset = aJson.queryCharset || DEFAULT_QUERY_CHARSET;
  1.2287 +    this.__searchForm = aJson.__searchForm;
  1.2288 +    this.__installLocation = aJson._installLocation || SEARCH_APP_DIR;
  1.2289 +    this._updateInterval = aJson._updateInterval || null;
  1.2290 +    this._updateURL = aJson._updateURL || null;
  1.2291 +    this._iconUpdateURL = aJson._iconUpdateURL || null;
  1.2292 +    if (aJson._readOnly == undefined)
  1.2293 +      this._readOnly = true;
  1.2294 +    else
  1.2295 +      this._readOnly = false;
  1.2296 +    this._iconURI = makeURI(aJson._iconURL);
  1.2297 +    this._iconMapObj = aJson._iconMapObj;
  1.2298 +    for (let i = 0; i < aJson._urls.length; ++i) {
  1.2299 +      let url = aJson._urls[i];
  1.2300 +      let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML,
  1.2301 +                                    url.method || "GET", url.template,
  1.2302 +                                    url.resultDomain);
  1.2303 +      engineURL._initWithJSON(url, this);
  1.2304 +      this._urls.push(engineURL);
  1.2305 +    }
  1.2306 +  },
  1.2307 +
  1.2308 +  /**
  1.2309 +   * Creates a JavaScript object that represents this engine.
  1.2310 +   * @param aFilter
  1.2311 +   *        Whether or not to filter out common default values. Recommended for
  1.2312 +   *        use with _initWithJSON().
  1.2313 +   * @returns An object suitable for serialization as JSON.
  1.2314 +   **/
  1.2315 +  _serializeToJSON: function SRCH_ENG__serializeToJSON(aFilter) {
  1.2316 +    var json = {
  1.2317 +      _id: this._id,
  1.2318 +      _name: this._name,
  1.2319 +      _hidden: this.hidden,
  1.2320 +      description: this.description,
  1.2321 +      __searchForm: this.__searchForm,
  1.2322 +      _iconURL: this._iconURL,
  1.2323 +      _iconMapObj: this._iconMapObj,
  1.2324 +      _urls: [url._serializeToJSON() for each(url in this._urls)]
  1.2325 +    };
  1.2326 +
  1.2327 +    if (this._file instanceof Ci.nsILocalFile)
  1.2328 +      json.filePath = this._file.persistentDescriptor;
  1.2329 +    if (this._uri)
  1.2330 +      json._url = this._uri.spec;
  1.2331 +    if (this._installLocation != SEARCH_APP_DIR || !aFilter)
  1.2332 +      json._installLocation = this._installLocation;
  1.2333 +    if (this._updateInterval || !aFilter)
  1.2334 +      json._updateInterval = this._updateInterval;
  1.2335 +    if (this._updateURL || !aFilter)
  1.2336 +      json._updateURL = this._updateURL;
  1.2337 +    if (this._iconUpdateURL || !aFilter)
  1.2338 +      json._iconUpdateURL = this._iconUpdateURL;
  1.2339 +    if (!this._hasPreferredIcon || !aFilter)
  1.2340 +      json._hasPreferredIcon = this._hasPreferredIcon;
  1.2341 +    if (this.type != SEARCH_TYPE_MOZSEARCH || !aFilter)
  1.2342 +      json.type = this.type;
  1.2343 +    if (this.queryCharset != DEFAULT_QUERY_CHARSET || !aFilter)
  1.2344 +      json.queryCharset = this.queryCharset;
  1.2345 +    if (this._dataType != SEARCH_DATA_XML || !aFilter)
  1.2346 +      json._dataType = this._dataType;
  1.2347 +    if (!this._readOnly || !aFilter)
  1.2348 +      json._readOnly = this._readOnly;
  1.2349 +
  1.2350 +    return json;
  1.2351 +  },
  1.2352 +
  1.2353 +  /**
  1.2354 +   * Returns an XML document object containing the search plugin information,
  1.2355 +   * which can later be used to reload the engine.
  1.2356 +   */
  1.2357 +  _serializeToElement: function SRCH_ENG_serializeToEl() {
  1.2358 +    function appendTextNode(aNameSpace, aLocalName, aValue) {
  1.2359 +      if (!aValue)
  1.2360 +        return null;
  1.2361 +      var node = doc.createElementNS(aNameSpace, aLocalName);
  1.2362 +      node.appendChild(doc.createTextNode(aValue));
  1.2363 +      docElem.appendChild(node);
  1.2364 +      docElem.appendChild(doc.createTextNode("\n"));
  1.2365 +      return node;
  1.2366 +    }
  1.2367 +
  1.2368 +    var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
  1.2369 +                 createInstance(Ci.nsIDOMParser);
  1.2370 +
  1.2371 +    var doc = parser.parseFromString(EMPTY_DOC, "text/xml");
  1.2372 +    var docElem = doc.documentElement;
  1.2373 +
  1.2374 +    docElem.appendChild(doc.createTextNode("\n"));
  1.2375 +
  1.2376 +    appendTextNode(OPENSEARCH_NS_11, "ShortName", this.name);
  1.2377 +    appendTextNode(OPENSEARCH_NS_11, "Description", this._description);
  1.2378 +    appendTextNode(OPENSEARCH_NS_11, "InputEncoding", this._queryCharset);
  1.2379 +
  1.2380 +    if (this._iconURI) {
  1.2381 +      var imageNode = appendTextNode(OPENSEARCH_NS_11, "Image",
  1.2382 +                                     this._iconURI.spec);
  1.2383 +      if (imageNode) {
  1.2384 +        imageNode.setAttribute("width", "16");
  1.2385 +        imageNode.setAttribute("height", "16");
  1.2386 +      }
  1.2387 +    }
  1.2388 +
  1.2389 +    appendTextNode(MOZSEARCH_NS_10, "UpdateInterval", this._updateInterval);
  1.2390 +    appendTextNode(MOZSEARCH_NS_10, "UpdateUrl", this._updateURL);
  1.2391 +    appendTextNode(MOZSEARCH_NS_10, "IconUpdateUrl", this._iconUpdateURL);
  1.2392 +    appendTextNode(MOZSEARCH_NS_10, "SearchForm", this._searchForm);
  1.2393 +
  1.2394 +    for (var i = 0; i < this._urls.length; ++i)
  1.2395 +      this._urls[i]._serializeToElement(doc, docElem);
  1.2396 +    docElem.appendChild(doc.createTextNode("\n"));
  1.2397 +
  1.2398 +    return doc;
  1.2399 +  },
  1.2400 +
  1.2401 +  get lazySerializeTask() {
  1.2402 +    if (!this._lazySerializeTask) {
  1.2403 +      let task = function taskCallback() {
  1.2404 +        this._serializeToFile();
  1.2405 +      }.bind(this);
  1.2406 +      this._lazySerializeTask = new DeferredTask(task, LAZY_SERIALIZE_DELAY);
  1.2407 +    }
  1.2408 +
  1.2409 +    return this._lazySerializeTask;
  1.2410 +  },
  1.2411 +
  1.2412 +  /**
  1.2413 +   * Serializes the engine object to file.
  1.2414 +   */
  1.2415 +  _serializeToFile: function SRCH_ENG_serializeToFile() {
  1.2416 +    var file = this._file;
  1.2417 +    ENSURE_WARN(!this._readOnly, "Can't serialize a read only engine!",
  1.2418 +                Cr.NS_ERROR_FAILURE);
  1.2419 +    ENSURE_WARN(file && file.exists(), "Can't serialize: file doesn't exist!",
  1.2420 +                Cr.NS_ERROR_UNEXPECTED);
  1.2421 +
  1.2422 +    var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"].
  1.2423 +              createInstance(Ci.nsIFileOutputStream);
  1.2424 +
  1.2425 +    // Serialize the engine first - we don't want to overwrite a good file
  1.2426 +    // if this somehow fails.
  1.2427 +    var doc = this._serializeToElement();
  1.2428 +
  1.2429 +    fos.init(file, (MODE_WRONLY | MODE_TRUNCATE), PERMS_FILE, 0);
  1.2430 +
  1.2431 +    try {
  1.2432 +      var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
  1.2433 +                       createInstance(Ci.nsIDOMSerializer);
  1.2434 +      serializer.serializeToStream(doc.documentElement, fos, null);
  1.2435 +    } catch (e) {
  1.2436 +      LOG("_serializeToFile: Error serializing engine:\n" + e);
  1.2437 +    }
  1.2438 +
  1.2439 +    closeSafeOutputStream(fos);
  1.2440 +
  1.2441 +    Services.obs.notifyObservers(file.clone(), SEARCH_SERVICE_TOPIC,
  1.2442 +                                 "write-engine-to-disk-complete");
  1.2443 +  },
  1.2444 +
  1.2445 +  /**
  1.2446 +   * Remove the engine's file from disk. The search service calls this once it
  1.2447 +   * removes the engine from its internal store. This function will throw if
  1.2448 +   * the file cannot be removed.
  1.2449 +   */
  1.2450 +  _remove: function SRCH_ENG_remove() {
  1.2451 +    if (this._readOnly)
  1.2452 +      FAIL("Can't remove read only engine!", Cr.NS_ERROR_FAILURE);
  1.2453 +    if (!this._file || !this._file.exists())
  1.2454 +      FAIL("Can't remove engine: file doesn't exist!", Cr.NS_ERROR_FILE_NOT_FOUND);
  1.2455 +
  1.2456 +    this._file.remove(false);
  1.2457 +  },
  1.2458 +
  1.2459 +  // nsISearchEngine
  1.2460 +  get alias() {
  1.2461 +    if (this._alias === undefined)
  1.2462 +      this._alias = engineMetadataService.getAttr(this, "alias");
  1.2463 +
  1.2464 +    return this._alias;
  1.2465 +  },
  1.2466 +  set alias(val) {
  1.2467 +    this._alias = val;
  1.2468 +    engineMetadataService.setAttr(this, "alias", val);
  1.2469 +    notifyAction(this, SEARCH_ENGINE_CHANGED);
  1.2470 +  },
  1.2471 +
  1.2472 +  /**
  1.2473 +   * Return the built-in identifier of app-provided engines.
  1.2474 +   *
  1.2475 +   * Note that this identifier is substantially similar to _id, with the
  1.2476 +   * following exceptions:
  1.2477 +   *
  1.2478 +   * * There is no trailing file extension.
  1.2479 +   * * There is no [app] prefix.
  1.2480 +   *
  1.2481 +   * @return a string identifier, or null.
  1.2482 +   */
  1.2483 +  get identifier() {
  1.2484 +    if (this._identifier !== undefined) {
  1.2485 +      return this._identifier;
  1.2486 +    }
  1.2487 +
  1.2488 +    // No identifier if If the engine isn't app-provided
  1.2489 +    if (!this._isInAppDir && !this._isInJAR) {
  1.2490 +      return this._identifier = null;
  1.2491 +    }
  1.2492 +
  1.2493 +    let leaf = this._getLeafName();
  1.2494 +    ENSURE_WARN(leaf, "identifier: app-provided engine has no leafName");
  1.2495 +
  1.2496 +    // Strip file extension.
  1.2497 +    let ext = leaf.lastIndexOf(".");
  1.2498 +    if (ext == -1) {
  1.2499 +      return this._identifier = leaf;
  1.2500 +    }
  1.2501 +    return this._identifier = leaf.substring(0, ext);
  1.2502 +  },
  1.2503 +
  1.2504 +  get description() {
  1.2505 +    return this._description;
  1.2506 +  },
  1.2507 +
  1.2508 +  get hidden() {
  1.2509 +    if (this._hidden === null)
  1.2510 +      this._hidden = engineMetadataService.getAttr(this, "hidden") || false;
  1.2511 +    return this._hidden;
  1.2512 +  },
  1.2513 +  set hidden(val) {
  1.2514 +    var value = !!val;
  1.2515 +    if (value != this._hidden) {
  1.2516 +      this._hidden = value;
  1.2517 +      engineMetadataService.setAttr(this, "hidden", value);
  1.2518 +      notifyAction(this, SEARCH_ENGINE_CHANGED);
  1.2519 +    }
  1.2520 +  },
  1.2521 +
  1.2522 +  get iconURI() {
  1.2523 +    if (this._iconURI)
  1.2524 +      return this._iconURI;
  1.2525 +    return null;
  1.2526 +  },
  1.2527 +
  1.2528 +  get _iconURL() {
  1.2529 +    if (!this._iconURI)
  1.2530 +      return "";
  1.2531 +    return this._iconURI.spec;
  1.2532 +  },
  1.2533 +
  1.2534 +  // Where the engine is being loaded from: will return the URI's spec if the
  1.2535 +  // engine is being downloaded and does not yet have a file. This is only used
  1.2536 +  // for logging and error messages.
  1.2537 +  get _location() {
  1.2538 +    if (this._file)
  1.2539 +      return this._file.path;
  1.2540 +
  1.2541 +    if (this._uri)
  1.2542 +      return this._uri.spec;
  1.2543 +
  1.2544 +    return "";
  1.2545 +  },
  1.2546 +
  1.2547 +  /**
  1.2548 +   * @return the leaf name of the filename or URI of this plugin,
  1.2549 +   *         or null if no file or URI is known.
  1.2550 +   */
  1.2551 +  _getLeafName: function () {
  1.2552 +    if (this._file) {
  1.2553 +      return this._file.leafName;
  1.2554 +    }
  1.2555 +    if (this._uri && this._uri instanceof Ci.nsIURL) {
  1.2556 +      return this._uri.fileName;
  1.2557 +    }
  1.2558 +    return null;
  1.2559 +  },
  1.2560 +    
  1.2561 +  // The file that the plugin is loaded from is a unique identifier for it.  We
  1.2562 +  // use this as the identifier to store data in the sqlite database
  1.2563 +  __id: null,
  1.2564 +  get _id() {
  1.2565 +    if (this.__id) {
  1.2566 +      return this.__id;
  1.2567 +    }
  1.2568 +
  1.2569 +    let leafName = this._getLeafName();
  1.2570 +
  1.2571 +    // Treat engines loaded from JARs the same way we treat app shipped
  1.2572 +    // engines.
  1.2573 +    // Theoretically, these could also come from extensions, but there's no
  1.2574 +    // real way for extensions to register their chrome locations at the
  1.2575 +    // moment, so let's not deal with that case.
  1.2576 +    // This means we're vulnerable to conflicts if a file loaded from a JAR
  1.2577 +    // has the same filename as a file loaded from the app dir, but with a
  1.2578 +    // different engine name. People using the JAR functionality should be
  1.2579 +    // careful not to do that!
  1.2580 +    if (this._isInAppDir || this._isInJAR) {
  1.2581 +      // App dir and JAR engines should always have leafNames
  1.2582 +      ENSURE_WARN(leafName, "_id: no leafName for appDir or JAR engine",
  1.2583 +                  Cr.NS_ERROR_UNEXPECTED);
  1.2584 +      return this.__id = "[app]/" + leafName;
  1.2585 +    }
  1.2586 +
  1.2587 +    if (this._isInProfile) {
  1.2588 +      ENSURE_WARN(leafName, "_id: no leafName for profile engine",
  1.2589 +                  Cr.NS_ERROR_UNEXPECTED);
  1.2590 +      return this.__id = "[profile]/" + leafName;
  1.2591 +    }
  1.2592 +
  1.2593 +    // If the engine isn't a JAR engine, it should have a file.
  1.2594 +    ENSURE_WARN(this._file, "_id: no _file for non-JAR engine",
  1.2595 +                Cr.NS_ERROR_UNEXPECTED);
  1.2596 +
  1.2597 +    // We're not in the profile or appdir, so this must be an extension-shipped
  1.2598 +    // plugin. Use the full filename.
  1.2599 +    return this.__id = this._file.path;
  1.2600 +  },
  1.2601 +
  1.2602 +  get _installLocation() {
  1.2603 +    if (this.__installLocation === null) {
  1.2604 +      if (!this._file) {
  1.2605 +        ENSURE_WARN(this._uri, "Engines without files must have URIs",
  1.2606 +                    Cr.NS_ERROR_UNEXPECTED);
  1.2607 +        this.__installLocation = SEARCH_JAR;
  1.2608 +      }
  1.2609 +      else if (this._file.parent.equals(getDir(NS_APP_SEARCH_DIR)))
  1.2610 +        this.__installLocation = SEARCH_APP_DIR;
  1.2611 +      else if (this._file.parent.equals(getDir(NS_APP_USER_SEARCH_DIR)))
  1.2612 +        this.__installLocation = SEARCH_PROFILE_DIR;
  1.2613 +      else
  1.2614 +        this.__installLocation = SEARCH_IN_EXTENSION;
  1.2615 +    }
  1.2616 +
  1.2617 +    return this.__installLocation;
  1.2618 +  },
  1.2619 +
  1.2620 +  get _isInJAR() {
  1.2621 +    return this._installLocation == SEARCH_JAR;
  1.2622 +  },
  1.2623 +  get _isInAppDir() {
  1.2624 +    return this._installLocation == SEARCH_APP_DIR;
  1.2625 +  },
  1.2626 +  get _isInProfile() {
  1.2627 +    return this._installLocation == SEARCH_PROFILE_DIR;
  1.2628 +  },
  1.2629 +
  1.2630 +  get _isDefault() {
  1.2631 +    // For now, our concept of a "default engine" is "one that is not in the
  1.2632 +    // user's profile directory", which is currently equivalent to "is app- or
  1.2633 +    // extension-shipped".
  1.2634 +    return !this._isInProfile;
  1.2635 +  },
  1.2636 +
  1.2637 +  get _hasUpdates() {
  1.2638 +    // Whether or not the engine has an update URL
  1.2639 +    let selfURL = this._getURLOfType(URLTYPE_OPENSEARCH, "self");
  1.2640 +    return !!(this._updateURL || this._iconUpdateURL || selfURL);
  1.2641 +  },
  1.2642 +
  1.2643 +  get name() {
  1.2644 +    return this._name;
  1.2645 +  },
  1.2646 +
  1.2647 +  get type() {
  1.2648 +    return this._type;
  1.2649 +  },
  1.2650 +
  1.2651 +  get searchForm() {
  1.2652 +    // First look for a <Url rel="searchform">
  1.2653 +    var searchFormURL = this._getURLOfType(URLTYPE_SEARCH_HTML, "searchform");
  1.2654 +    if (searchFormURL) {
  1.2655 +      let submission = searchFormURL.getSubmission("", this);
  1.2656 +
  1.2657 +      // If the rel=searchform URL is not type="get" (i.e. has postData),
  1.2658 +      // ignore it, since we can only return a URL.
  1.2659 +      if (!submission.postData)
  1.2660 +        return submission.uri.spec;
  1.2661 +    }
  1.2662 +
  1.2663 +    if (!this._searchForm) {
  1.2664 +      // No SearchForm specified in the engine definition file, use the prePath
  1.2665 +      // (e.g. https://foo.com for https://foo.com/search.php?q=bar).
  1.2666 +      var htmlUrl = this._getURLOfType(URLTYPE_SEARCH_HTML);
  1.2667 +      ENSURE_WARN(htmlUrl, "Engine has no HTML URL!", Cr.NS_ERROR_UNEXPECTED);
  1.2668 +      this._searchForm = makeURI(htmlUrl.template).prePath;
  1.2669 +    }
  1.2670 +
  1.2671 +    return ParamSubstitution(this._searchForm, "", this);
  1.2672 +  },
  1.2673 +
  1.2674 +  get queryCharset() {
  1.2675 +    if (this._queryCharset)
  1.2676 +      return this._queryCharset;
  1.2677 +    return this._queryCharset = queryCharsetFromCode(/* get the default */);
  1.2678 +  },
  1.2679 +
  1.2680 +  // from nsISearchEngine
  1.2681 +  addParam: function SRCH_ENG_addParam(aName, aValue, aResponseType) {
  1.2682 +    if (!aName || (aValue == null))
  1.2683 +      FAIL("missing name or value for nsISearchEngine::addParam!");
  1.2684 +    ENSURE_WARN(!this._readOnly,
  1.2685 +                "called nsISearchEngine::addParam on a read-only engine!",
  1.2686 +                Cr.NS_ERROR_FAILURE);
  1.2687 +    if (!aResponseType)
  1.2688 +      aResponseType = URLTYPE_SEARCH_HTML;
  1.2689 +
  1.2690 +    var url = this._getURLOfType(aResponseType);
  1.2691 +    if (!url)
  1.2692 +      FAIL("Engine object has no URL for response type " + aResponseType,
  1.2693 +           Cr.NS_ERROR_FAILURE);
  1.2694 +
  1.2695 +    url.addParam(aName, aValue);
  1.2696 +
  1.2697 +    // Serialize the changes to file lazily
  1.2698 +    this.lazySerializeTask.arm();
  1.2699 +  },
  1.2700 +
  1.2701 +#ifdef ANDROID
  1.2702 +  get _defaultMobileResponseType() {
  1.2703 +    let type = URLTYPE_SEARCH_HTML;
  1.2704 +
  1.2705 +    let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
  1.2706 +    let isTablet = sysInfo.get("tablet");
  1.2707 +    if (isTablet && this.supportsResponseType("application/x-moz-tabletsearch")) {
  1.2708 +      // Check for a tablet-specific search URL override
  1.2709 +      type = "application/x-moz-tabletsearch";
  1.2710 +    } else if (!isTablet && this.supportsResponseType("application/x-moz-phonesearch")) {
  1.2711 +      // Check for a phone-specific search URL override
  1.2712 +      type = "application/x-moz-phonesearch";
  1.2713 +    }
  1.2714 +
  1.2715 +    delete this._defaultMobileResponseType;
  1.2716 +    return this._defaultMobileResponseType = type;
  1.2717 +  },
  1.2718 +#endif
  1.2719 +
  1.2720 +  // from nsISearchEngine
  1.2721 +  getSubmission: function SRCH_ENG_getSubmission(aData, aResponseType, aPurpose) {
  1.2722 +#ifdef ANDROID
  1.2723 +    if (!aResponseType) {
  1.2724 +      aResponseType = this._defaultMobileResponseType;
  1.2725 +    }
  1.2726 +#endif
  1.2727 +    if (!aResponseType) {
  1.2728 +      aResponseType = URLTYPE_SEARCH_HTML;
  1.2729 +    }
  1.2730 +
  1.2731 +    var url = this._getURLOfType(aResponseType);
  1.2732 +
  1.2733 +    if (!url)
  1.2734 +      return null;
  1.2735 +
  1.2736 +    if (!aData) {
  1.2737 +      // Return a dummy submission object with our searchForm attribute
  1.2738 +      return new Submission(makeURI(this.searchForm), null);
  1.2739 +    }
  1.2740 +
  1.2741 +    LOG("getSubmission: In data: \"" + aData + "\"; Purpose: \"" + aPurpose + "\"");
  1.2742 +    var textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
  1.2743 +                       getService(Ci.nsITextToSubURI);
  1.2744 +    var data = "";
  1.2745 +    try {
  1.2746 +      data = textToSubURI.ConvertAndEscape(this.queryCharset, aData);
  1.2747 +    } catch (ex) {
  1.2748 +      LOG("getSubmission: Falling back to default queryCharset!");
  1.2749 +      data = textToSubURI.ConvertAndEscape(DEFAULT_QUERY_CHARSET, aData);
  1.2750 +    }
  1.2751 +    LOG("getSubmission: Out data: \"" + data + "\"");
  1.2752 +    return url.getSubmission(data, this, aPurpose);
  1.2753 +  },
  1.2754 +
  1.2755 +  // from nsISearchEngine
  1.2756 +  supportsResponseType: function SRCH_ENG_supportsResponseType(type) {
  1.2757 +    return (this._getURLOfType(type) != null);
  1.2758 +  },
  1.2759 +
  1.2760 +  // from nsISearchEngine
  1.2761 +  getResultDomain: function SRCH_ENG_getResultDomain(aResponseType) {
  1.2762 +#ifdef ANDROID
  1.2763 +    if (!aResponseType) {
  1.2764 +      aResponseType = this._defaultMobileResponseType;
  1.2765 +    }
  1.2766 +#endif
  1.2767 +    if (!aResponseType) {
  1.2768 +      aResponseType = URLTYPE_SEARCH_HTML;
  1.2769 +    }
  1.2770 +
  1.2771 +    LOG("getResultDomain: responseType: \"" + aResponseType + "\"");
  1.2772 +
  1.2773 +    let url = this._getURLOfType(aResponseType);
  1.2774 +    if (url)
  1.2775 +      return url.resultDomain;
  1.2776 +    return "";
  1.2777 +  },
  1.2778 +
  1.2779 +  // nsISupports
  1.2780 +  QueryInterface: function SRCH_ENG_QI(aIID) {
  1.2781 +    if (aIID.equals(Ci.nsISearchEngine) ||
  1.2782 +        aIID.equals(Ci.nsISupports))
  1.2783 +      return this;
  1.2784 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.2785 +  },
  1.2786 +
  1.2787 +  get wrappedJSObject() {
  1.2788 +    return this;
  1.2789 +  },
  1.2790 +
  1.2791 +  /**
  1.2792 +   * Returns a string with the URL to an engine's icon matching both width and
  1.2793 +   * height. Returns null if icon with specified dimensions is not found.
  1.2794 +   *
  1.2795 +   * @param width
  1.2796 +   *        Width of the requested icon.
  1.2797 +   * @param height
  1.2798 +   *        Height of the requested icon.
  1.2799 +   */
  1.2800 +  getIconURLBySize: function SRCH_ENG_getIconURLBySize(aWidth, aHeight) {
  1.2801 +    if (!this._iconMapObj)
  1.2802 +      return null;
  1.2803 +
  1.2804 +    let key = this._getIconKey(aWidth, aHeight);
  1.2805 +    if (key in this._iconMapObj) {
  1.2806 +      return this._iconMapObj[key];
  1.2807 +    }
  1.2808 +    return null;
  1.2809 +  },
  1.2810 +
  1.2811 +  /**
  1.2812 +   * Gets an array of all available icons. Each entry is an object with
  1.2813 +   * width, height and url properties. width and height are numeric and
  1.2814 +   * represent the icon's dimensions. url is a string with the URL for
  1.2815 +   * the icon.
  1.2816 +   */
  1.2817 +  getIcons: function SRCH_ENG_getIcons() {
  1.2818 +    let result = [];
  1.2819 +
  1.2820 +    if (!this._iconMapObj)
  1.2821 +      return result;
  1.2822 +
  1.2823 +    for (let key of Object.keys(this._iconMapObj)) {
  1.2824 +      let iconSize = JSON.parse(key);
  1.2825 +      result.push({
  1.2826 +        width: iconSize.width,
  1.2827 +        height: iconSize.height,
  1.2828 +        url: this._iconMapObj[key]
  1.2829 +      });
  1.2830 +    }
  1.2831 +
  1.2832 +    return result;
  1.2833 +  }
  1.2834 +};
  1.2835 +
  1.2836 +// nsISearchSubmission
  1.2837 +function Submission(aURI, aPostData = null) {
  1.2838 +  this._uri = aURI;
  1.2839 +  this._postData = aPostData;
  1.2840 +}
  1.2841 +Submission.prototype = {
  1.2842 +  get uri() {
  1.2843 +    return this._uri;
  1.2844 +  },
  1.2845 +  get postData() {
  1.2846 +    return this._postData;
  1.2847 +  },
  1.2848 +  QueryInterface: function SRCH_SUBM_QI(aIID) {
  1.2849 +    if (aIID.equals(Ci.nsISearchSubmission) ||
  1.2850 +        aIID.equals(Ci.nsISupports))
  1.2851 +      return this;
  1.2852 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.2853 +  }
  1.2854 +}
  1.2855 +
  1.2856 +function executeSoon(func) {
  1.2857 +  Services.tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
  1.2858 +}
  1.2859 +
  1.2860 +/**
  1.2861 + * Check for sync initialization has completed or not.
  1.2862 + *
  1.2863 + * @param {aPromise} A promise.
  1.2864 + *
  1.2865 + * @returns the value returned by the invoked method.
  1.2866 + * @throws NS_ERROR_ALREADY_INITIALIZED if sync initialization has completed.
  1.2867 + */
  1.2868 +function checkForSyncCompletion(aPromise) {
  1.2869 +  return aPromise.then(function(aValue) {
  1.2870 +    if (gInitialized) {
  1.2871 +      throw Components.Exception("Synchronous fallback was called and has " +
  1.2872 +                                 "finished so no need to pursue asynchronous " +
  1.2873 +                                 "initialization",
  1.2874 +                                 Cr.NS_ERROR_ALREADY_INITIALIZED);
  1.2875 +    }
  1.2876 +    return aValue;
  1.2877 +  });
  1.2878 +}
  1.2879 +
  1.2880 +// nsIBrowserSearchService
  1.2881 +function SearchService() {
  1.2882 +  // Replace empty LOG function with the useful one if the log pref is set.
  1.2883 +  if (getBoolPref(BROWSER_SEARCH_PREF + "log", false))
  1.2884 +    LOG = DO_LOG;
  1.2885 +
  1.2886 +  this._initObservers = Promise.defer();
  1.2887 +}
  1.2888 +
  1.2889 +SearchService.prototype = {
  1.2890 +  classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"),
  1.2891 +
  1.2892 +  // The current status of initialization. Note that it does not determine if
  1.2893 +  // initialization is complete, only if an error has been encountered so far.
  1.2894 +  _initRV: Cr.NS_OK,
  1.2895 +
  1.2896 +  // The boolean indicates that the initialization has started or not.
  1.2897 +  _initStarted: null,
  1.2898 +
  1.2899 +  // If initialization has not been completed yet, perform synchronous
  1.2900 +  // initialization.
  1.2901 +  // Throws in case of initialization error.
  1.2902 +  _ensureInitialized: function  SRCH_SVC__ensureInitialized() {
  1.2903 +    if (gInitialized) {
  1.2904 +      if (!Components.isSuccessCode(this._initRV)) {
  1.2905 +        LOG("_ensureInitialized: failure");
  1.2906 +        throw this._initRV;
  1.2907 +      }
  1.2908 +      return;
  1.2909 +    }
  1.2910 +
  1.2911 +    let warning =
  1.2912 +      "Search service falling back to synchronous initialization. " +
  1.2913 +      "This is generally the consequence of an add-on using a deprecated " +
  1.2914 +      "search service API.";
  1.2915 +    // Bug 785487 - Disable warning until our own callers are fixed.
  1.2916 +    //Deprecated.warning(warning, "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIBrowserSearchService#async_warning");
  1.2917 +    LOG(warning);
  1.2918 +
  1.2919 +    engineMetadataService.syncInit();
  1.2920 +    this._syncInit();
  1.2921 +    if (!Components.isSuccessCode(this._initRV)) {
  1.2922 +      throw this._initRV;
  1.2923 +    }
  1.2924 +  },
  1.2925 +
  1.2926 +  // Synchronous implementation of the initializer.
  1.2927 +  // Used by |_ensureInitialized| as a fallback if initialization is not
  1.2928 +  // complete.
  1.2929 +  _syncInit: function SRCH_SVC__syncInit() {
  1.2930 +    LOG("_syncInit start");
  1.2931 +    this._initStarted = true;
  1.2932 +    try {
  1.2933 +      this._syncLoadEngines();
  1.2934 +    } catch (ex) {
  1.2935 +      this._initRV = Cr.NS_ERROR_FAILURE;
  1.2936 +      LOG("_syncInit: failure loading engines: " + ex);
  1.2937 +    }
  1.2938 +    this._addObservers();
  1.2939 +
  1.2940 +    gInitialized = true;
  1.2941 +
  1.2942 +    this._initObservers.resolve(this._initRV);
  1.2943 +
  1.2944 +    Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
  1.2945 +
  1.2946 +    LOG("_syncInit end");
  1.2947 +  },
  1.2948 +
  1.2949 +  /**
  1.2950 +   * Asynchronous implementation of the initializer.
  1.2951 +   *
  1.2952 +   * @returns {Promise} A promise, resolved successfully if the initialization
  1.2953 +   * succeeds.
  1.2954 +   */
  1.2955 +  _asyncInit: function SRCH_SVC__asyncInit() {
  1.2956 +    return TaskUtils.spawn(function() {
  1.2957 +      LOG("_asyncInit start");
  1.2958 +      try {
  1.2959 +        yield checkForSyncCompletion(this._asyncLoadEngines());
  1.2960 +      } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  1.2961 +        this._initRV = Cr.NS_ERROR_FAILURE;
  1.2962 +        LOG("_asyncInit: failure loading engines: " + ex);
  1.2963 +      }
  1.2964 +      this._addObservers();
  1.2965 +      gInitialized = true;
  1.2966 +      this._initObservers.resolve(this._initRV);
  1.2967 +      Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
  1.2968 +      LOG("_asyncInit: Completed _asyncInit");
  1.2969 +    }.bind(this));
  1.2970 +  },
  1.2971 +
  1.2972 +
  1.2973 +  _engines: { },
  1.2974 +  __sortedEngines: null,
  1.2975 +  get _sortedEngines() {
  1.2976 +    if (!this.__sortedEngines)
  1.2977 +      return this._buildSortedEngineList();
  1.2978 +    return this.__sortedEngines;
  1.2979 +  },
  1.2980 +
  1.2981 +  // Get the original Engine object that belongs to the defaultenginename pref
  1.2982 +  // of the default branch.
  1.2983 +  get _originalDefaultEngine() {
  1.2984 +    let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
  1.2985 +    let nsIPLS = Ci.nsIPrefLocalizedString;
  1.2986 +    let defaultEngine;
  1.2987 +    try {
  1.2988 +      defaultEngine = defaultPrefB.getComplexValue("defaultenginename", nsIPLS).data;
  1.2989 +    } catch (ex) {
  1.2990 +      // If the default pref is invalid (e.g. an add-on set it to a bogus value)
  1.2991 +      // getEngineByName will just return null, which is the best we can do.
  1.2992 +    }
  1.2993 +    return this.getEngineByName(defaultEngine);
  1.2994 +  },
  1.2995 +
  1.2996 +  _buildCache: function SRCH_SVC__buildCache() {
  1.2997 +    if (!getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true))
  1.2998 +      return;
  1.2999 +
  1.3000 +    TelemetryStopwatch.start("SEARCH_SERVICE_BUILD_CACHE_MS");
  1.3001 +    let cache = {};
  1.3002 +    let locale = getLocale();
  1.3003 +    let buildID = Services.appinfo.platformBuildID;
  1.3004 +
  1.3005 +    // Allows us to force a cache refresh should the cache format change.
  1.3006 +    cache.version = CACHE_VERSION;
  1.3007 +    // We don't want to incur the costs of stat()ing each plugin on every
  1.3008 +    // startup when the only (supported) time they will change is during
  1.3009 +    // runtime (where we refresh for changes through the API) and app updates
  1.3010 +    // (where the buildID is obviously going to change).
  1.3011 +    // Extension-shipped plugins are the only exception to this, but their
  1.3012 +    // directories are blown away during updates, so we'll detect their changes.
  1.3013 +    cache.buildID = buildID;
  1.3014 +    cache.locale = locale;
  1.3015 +
  1.3016 +    cache.directories = {};
  1.3017 +
  1.3018 +    function getParent(engine) {
  1.3019 +      if (engine._file)
  1.3020 +        return engine._file.parent;
  1.3021 +
  1.3022 +      let uri = engine._uri;
  1.3023 +      if (!uri.schemeIs("chrome")) {
  1.3024 +        LOG("getParent: engine URI must be a chrome URI if it has no file");
  1.3025 +        return null;
  1.3026 +      }
  1.3027 +
  1.3028 +      // use the underlying JAR file, for chrome URIs
  1.3029 +      try {
  1.3030 +        uri = gChromeReg.convertChromeURL(uri);
  1.3031 +        if (uri instanceof Ci.nsINestedURI)
  1.3032 +          uri = uri.innermostURI;
  1.3033 +        uri.QueryInterface(Ci.nsIFileURL)
  1.3034 +
  1.3035 +        return uri.file;
  1.3036 +      } catch (ex) {
  1.3037 +        LOG("getParent: couldn't map chrome:// URI to a file: " + ex)
  1.3038 +      }
  1.3039 +
  1.3040 +      return null;
  1.3041 +    }
  1.3042 +
  1.3043 +    for each (let engine in this._engines) {
  1.3044 +      let parent = getParent(engine);
  1.3045 +      if (!parent) {
  1.3046 +        LOG("Error: no parent for engine " + engine._location + ", failing to cache it");
  1.3047 +
  1.3048 +        continue;
  1.3049 +      }
  1.3050 +
  1.3051 +      let cacheKey = parent.path;
  1.3052 +      if (!cache.directories[cacheKey]) {
  1.3053 +        let cacheEntry = {};
  1.3054 +        cacheEntry.lastModifiedTime = parent.lastModifiedTime;
  1.3055 +        cacheEntry.engines = [];
  1.3056 +        cache.directories[cacheKey] = cacheEntry;
  1.3057 +      }
  1.3058 +      cache.directories[cacheKey].engines.push(engine._serializeToJSON(true));
  1.3059 +    }
  1.3060 +
  1.3061 +    try {
  1.3062 +      LOG("_buildCache: Writing to cache file.");
  1.3063 +      let path = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
  1.3064 +      let data = gEncoder.encode(JSON.stringify(cache));
  1.3065 +      let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp"});
  1.3066 +
  1.3067 +      promise.then(
  1.3068 +        function onSuccess() {
  1.3069 +          Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, SEARCH_SERVICE_CACHE_WRITTEN);
  1.3070 +        },
  1.3071 +        function onError(e) {
  1.3072 +          LOG("_buildCache: failure during writeAtomic: " + e);
  1.3073 +        }
  1.3074 +      );
  1.3075 +    } catch (ex) {
  1.3076 +      LOG("_buildCache: Could not write to cache file: " + ex);
  1.3077 +    }
  1.3078 +    TelemetryStopwatch.finish("SEARCH_SERVICE_BUILD_CACHE_MS");
  1.3079 +  },
  1.3080 +
  1.3081 +  _syncLoadEngines: function SRCH_SVC__syncLoadEngines() {
  1.3082 +    LOG("_syncLoadEngines: start");
  1.3083 +    // See if we have a cache file so we don't have to parse a bunch of XML.
  1.3084 +    let cache = {};
  1.3085 +    let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
  1.3086 +    if (cacheEnabled) {
  1.3087 +      let cacheFile = getDir(NS_APP_USER_PROFILE_50_DIR);
  1.3088 +      cacheFile.append("search.json");
  1.3089 +      if (cacheFile.exists())
  1.3090 +        cache = this._readCacheFile(cacheFile);
  1.3091 +    }
  1.3092 +
  1.3093 +    let loadDirs = [];
  1.3094 +    let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
  1.3095 +    while (locations.hasMoreElements()) {
  1.3096 +      let dir = locations.getNext().QueryInterface(Ci.nsIFile);
  1.3097 +      if (dir.directoryEntries.hasMoreElements())
  1.3098 +        loadDirs.push(dir);
  1.3099 +    }
  1.3100 +
  1.3101 +    let loadFromJARs = getBoolPref(BROWSER_SEARCH_PREF + "loadFromJars", false);
  1.3102 +    let chromeURIs = [];
  1.3103 +    let chromeFiles = [];
  1.3104 +    if (loadFromJARs)
  1.3105 +      [chromeFiles, chromeURIs] = this._findJAREngines();
  1.3106 +
  1.3107 +    let toLoad = chromeFiles.concat(loadDirs);
  1.3108 +
  1.3109 +    function modifiedDir(aDir) {
  1.3110 +      return (!cache.directories || !cache.directories[aDir.path] ||
  1.3111 +              cache.directories[aDir.path].lastModifiedTime != aDir.lastModifiedTime);
  1.3112 +    }
  1.3113 +
  1.3114 +    function notInCachePath(aPathToLoad)
  1.3115 +      cachePaths.indexOf(aPathToLoad.path) == -1;
  1.3116 +
  1.3117 +    let buildID = Services.appinfo.platformBuildID;
  1.3118 +    let cachePaths = [path for (path in cache.directories)];
  1.3119 +
  1.3120 +    let rebuildCache = !cache.directories ||
  1.3121 +                       cache.version != CACHE_VERSION ||
  1.3122 +                       cache.locale != getLocale() ||
  1.3123 +                       cache.buildID != buildID ||
  1.3124 +                       cachePaths.length != toLoad.length ||
  1.3125 +                       toLoad.some(notInCachePath) ||
  1.3126 +                       toLoad.some(modifiedDir);
  1.3127 +
  1.3128 +    if (!cacheEnabled || rebuildCache) {
  1.3129 +      LOG("_loadEngines: Absent or outdated cache. Loading engines from disk.");
  1.3130 +      loadDirs.forEach(this._loadEnginesFromDir, this);
  1.3131 +
  1.3132 +      this._loadFromChromeURLs(chromeURIs);
  1.3133 +
  1.3134 +      if (cacheEnabled)
  1.3135 +        this._buildCache();
  1.3136 +      return;
  1.3137 +    }
  1.3138 +
  1.3139 +    LOG("_loadEngines: loading from cache directories");
  1.3140 +    for each (let dir in cache.directories)
  1.3141 +      this._loadEnginesFromCache(dir);
  1.3142 +
  1.3143 +    LOG("_loadEngines: done");
  1.3144 +  },
  1.3145 +
  1.3146 +  /**
  1.3147 +   * Loads engines asynchronously.
  1.3148 +   *
  1.3149 +   * @returns {Promise} A promise, resolved successfully if loading data
  1.3150 +   * succeeds.
  1.3151 +   */
  1.3152 +  _asyncLoadEngines: function SRCH_SVC__asyncLoadEngines() {
  1.3153 +    return TaskUtils.spawn(function() {
  1.3154 +      LOG("_asyncLoadEngines: start");
  1.3155 +      // See if we have a cache file so we don't have to parse a bunch of XML.
  1.3156 +      let cache = {};
  1.3157 +      let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
  1.3158 +      if (cacheEnabled) {
  1.3159 +        let cacheFilePath = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
  1.3160 +        cache = yield checkForSyncCompletion(this._asyncReadCacheFile(cacheFilePath));
  1.3161 +      }
  1.3162 +
  1.3163 +      // Add all the non-empty directories of NS_APP_SEARCH_DIR_LIST to
  1.3164 +      // loadDirs.
  1.3165 +      let loadDirs = [];
  1.3166 +      let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
  1.3167 +      while (locations.hasMoreElements()) {
  1.3168 +        let dir = locations.getNext().QueryInterface(Ci.nsIFile);
  1.3169 +        let iterator = new OS.File.DirectoryIterator(dir.path,
  1.3170 +                                                     { winPattern: "*.xml" });
  1.3171 +        try {
  1.3172 +          // Add dir to loadDirs if it contains any files.
  1.3173 +          yield checkForSyncCompletion(iterator.next());
  1.3174 +          loadDirs.push(dir);
  1.3175 +        } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  1.3176 +          // Catch for StopIteration exception.
  1.3177 +        } finally {
  1.3178 +          iterator.close();
  1.3179 +        }
  1.3180 +      }
  1.3181 +
  1.3182 +      let loadFromJARs = getBoolPref(BROWSER_SEARCH_PREF + "loadFromJars", false);
  1.3183 +      let chromeURIs = [];
  1.3184 +      let chromeFiles = [];
  1.3185 +      if (loadFromJARs) {
  1.3186 +        Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "find-jar-engines");
  1.3187 +        [chromeFiles, chromeURIs] =
  1.3188 +          yield checkForSyncCompletion(this._asyncFindJAREngines());
  1.3189 +      }
  1.3190 +
  1.3191 +      let toLoad = chromeFiles.concat(loadDirs);
  1.3192 +      function hasModifiedDir(aList) {
  1.3193 +        return TaskUtils.spawn(function() {
  1.3194 +          let modifiedDir = false;
  1.3195 +
  1.3196 +          for (let dir of aList) {
  1.3197 +            if (!cache.directories || !cache.directories[dir.path]) {
  1.3198 +              modifiedDir = true;
  1.3199 +              break;
  1.3200 +            }
  1.3201 +
  1.3202 +            let info = yield OS.File.stat(dir.path);
  1.3203 +            if (cache.directories[dir.path].lastModifiedTime !=
  1.3204 +                info.lastModificationDate.getTime()) {
  1.3205 +              modifiedDir = true;
  1.3206 +              break;
  1.3207 +            }
  1.3208 +          }
  1.3209 +          throw new Task.Result(modifiedDir);
  1.3210 +        });
  1.3211 +      }
  1.3212 +
  1.3213 +      function notInCachePath(aPathToLoad)
  1.3214 +        cachePaths.indexOf(aPathToLoad.path) == -1;
  1.3215 +
  1.3216 +      let buildID = Services.appinfo.platformBuildID;
  1.3217 +      let cachePaths = [path for (path in cache.directories)];
  1.3218 +
  1.3219 +      let rebuildCache = !cache.directories ||
  1.3220 +                         cache.version != CACHE_VERSION ||
  1.3221 +                         cache.locale != getLocale() ||
  1.3222 +                         cache.buildID != buildID ||
  1.3223 +                         cachePaths.length != toLoad.length ||
  1.3224 +                         toLoad.some(notInCachePath) ||
  1.3225 +                         (yield checkForSyncCompletion(hasModifiedDir(toLoad)));
  1.3226 +
  1.3227 +      if (!cacheEnabled || rebuildCache) {
  1.3228 +        LOG("_asyncLoadEngines: Absent or outdated cache. Loading engines from disk.");
  1.3229 +        let engines = [];
  1.3230 +        for (let loadDir of loadDirs) {
  1.3231 +          let enginesFromDir =
  1.3232 +            yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
  1.3233 +          engines = engines.concat(enginesFromDir);
  1.3234 +        }
  1.3235 +        let enginesFromURLs =
  1.3236 +           yield checkForSyncCompletion(this._asyncLoadFromChromeURLs(chromeURIs));
  1.3237 +        engines = engines.concat(enginesFromURLs);
  1.3238 +
  1.3239 +        for (let engine of engines) {
  1.3240 +          this._addEngineToStore(engine);
  1.3241 +        }
  1.3242 +        if (cacheEnabled)
  1.3243 +          this._buildCache();
  1.3244 +        return;
  1.3245 +      }
  1.3246 +
  1.3247 +      LOG("_asyncLoadEngines: loading from cache directories");
  1.3248 +      for each (let dir in cache.directories)
  1.3249 +        this._loadEnginesFromCache(dir);
  1.3250 +
  1.3251 +      LOG("_asyncLoadEngines: done");
  1.3252 +    }.bind(this));
  1.3253 +  },
  1.3254 +
  1.3255 +  _readCacheFile: function SRCH_SVC__readCacheFile(aFile) {
  1.3256 +    let stream = Cc["@mozilla.org/network/file-input-stream;1"].
  1.3257 +                 createInstance(Ci.nsIFileInputStream);
  1.3258 +    let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  1.3259 +
  1.3260 +    try {
  1.3261 +      stream.init(aFile, MODE_RDONLY, PERMS_FILE, 0);
  1.3262 +      return json.decodeFromStream(stream, stream.available());
  1.3263 +    } catch (ex) {
  1.3264 +      LOG("_readCacheFile: Error reading cache file: " + ex);
  1.3265 +    } finally {
  1.3266 +      stream.close();
  1.3267 +    }
  1.3268 +    return false;
  1.3269 +  },
  1.3270 +
  1.3271 +  /**
  1.3272 +   * Read from a given cache file asynchronously.
  1.3273 +   *
  1.3274 +   * @param aPath the file path.
  1.3275 +   *
  1.3276 +   * @returns {Promise} A promise, resolved successfully if retrieveing data
  1.3277 +   * succeeds.
  1.3278 +   */
  1.3279 +  _asyncReadCacheFile: function SRCH_SVC__asyncReadCacheFile(aPath) {
  1.3280 +    return TaskUtils.spawn(function() {
  1.3281 +      let json;
  1.3282 +      try {
  1.3283 +        let bytes = yield OS.File.read(aPath);
  1.3284 +        json = JSON.parse(new TextDecoder().decode(bytes));
  1.3285 +      } catch (ex) {
  1.3286 +        LOG("_asyncReadCacheFile: Error reading cache file: " + ex);
  1.3287 +        json = {};
  1.3288 +      }
  1.3289 +      throw new Task.Result(json);
  1.3290 +    });
  1.3291 +  },
  1.3292 +
  1.3293 +  _batchTask: null,
  1.3294 +  get batchTask() {
  1.3295 +    if (!this._batchTask) {
  1.3296 +      let task = function taskCallback() {
  1.3297 +        LOG("batchTask: Invalidating engine cache");
  1.3298 +        this._buildCache();
  1.3299 +      }.bind(this);
  1.3300 +      this._batchTask = new DeferredTask(task, CACHE_INVALIDATION_DELAY);
  1.3301 +    }
  1.3302 +    return this._batchTask;
  1.3303 +  },
  1.3304 +
  1.3305 +  _addEngineToStore: function SRCH_SVC_addEngineToStore(aEngine) {
  1.3306 +    LOG("_addEngineToStore: Adding engine: \"" + aEngine.name + "\"");
  1.3307 +
  1.3308 +    // See if there is an existing engine with the same name. However, if this
  1.3309 +    // engine is updating another engine, it's allowed to have the same name.
  1.3310 +    var hasSameNameAsUpdate = (aEngine._engineToUpdate &&
  1.3311 +                               aEngine.name == aEngine._engineToUpdate.name);
  1.3312 +    if (aEngine.name in this._engines && !hasSameNameAsUpdate) {
  1.3313 +      LOG("_addEngineToStore: Duplicate engine found, aborting!");
  1.3314 +      return;
  1.3315 +    }
  1.3316 +
  1.3317 +    if (aEngine._engineToUpdate) {
  1.3318 +      // We need to replace engineToUpdate with the engine that just loaded.
  1.3319 +      var oldEngine = aEngine._engineToUpdate;
  1.3320 +
  1.3321 +      // Remove the old engine from the hash, since it's keyed by name, and our
  1.3322 +      // name might change (the update might have a new name).
  1.3323 +      delete this._engines[oldEngine.name];
  1.3324 +
  1.3325 +      // Hack: we want to replace the old engine with the new one, but since
  1.3326 +      // people may be holding refs to the nsISearchEngine objects themselves,
  1.3327 +      // we'll just copy over all "private" properties (those without a getter
  1.3328 +      // or setter) from one object to the other.
  1.3329 +      for (var p in aEngine) {
  1.3330 +        if (!(aEngine.__lookupGetter__(p) || aEngine.__lookupSetter__(p)))
  1.3331 +          oldEngine[p] = aEngine[p];
  1.3332 +      }
  1.3333 +      aEngine = oldEngine;
  1.3334 +      aEngine._engineToUpdate = null;
  1.3335 +
  1.3336 +      // Add the engine back
  1.3337 +      this._engines[aEngine.name] = aEngine;
  1.3338 +      notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
  1.3339 +    } else {
  1.3340 +      // Not an update, just add the new engine.
  1.3341 +      this._engines[aEngine.name] = aEngine;
  1.3342 +      // Only add the engine to the list of sorted engines if the initial list
  1.3343 +      // has already been built (i.e. if this.__sortedEngines is non-null). If
  1.3344 +      // it hasn't, we're loading engines from disk and the sorted engine list
  1.3345 +      // will be built once we need it.
  1.3346 +      if (this.__sortedEngines) {
  1.3347 +        this.__sortedEngines.push(aEngine);
  1.3348 +        this._saveSortedEngineList();
  1.3349 +      }
  1.3350 +      notifyAction(aEngine, SEARCH_ENGINE_ADDED);
  1.3351 +    }
  1.3352 +
  1.3353 +    if (aEngine._hasUpdates) {
  1.3354 +      // Schedule the engine's next update, if it isn't already.
  1.3355 +      if (!engineMetadataService.getAttr(aEngine, "updateexpir"))
  1.3356 +        engineUpdateService.scheduleNextUpdate(aEngine);
  1.3357 +  
  1.3358 +      // We need to save the engine's _dataType, if this is the first time the
  1.3359 +      // engine is added to the dataStore, since ._dataType isn't persisted
  1.3360 +      // and will change on the next startup (since the engine will then be
  1.3361 +      // XML). We need this so that we know how to load any future updates from
  1.3362 +      // this engine.
  1.3363 +      if (!engineMetadataService.getAttr(aEngine, "updatedatatype"))
  1.3364 +        engineMetadataService.setAttr(aEngine, "updatedatatype",
  1.3365 +                                      aEngine._dataType);
  1.3366 +    }
  1.3367 +  },
  1.3368 +
  1.3369 +  _loadEnginesFromCache: function SRCH_SVC__loadEnginesFromCache(aDir) {
  1.3370 +    let engines = aDir.engines;
  1.3371 +    LOG("_loadEnginesFromCache: Loading from cache. " + engines.length + " engines to load.");
  1.3372 +    for (let i = 0; i < engines.length; i++) {
  1.3373 +      let json = engines[i];
  1.3374 +
  1.3375 +      try {
  1.3376 +        let engine;
  1.3377 +        if (json.filePath)
  1.3378 +          engine = new Engine({type: "filePath", value: json.filePath}, json._dataType,
  1.3379 +                               json._readOnly);
  1.3380 +        else if (json._url)
  1.3381 +          engine = new Engine({type: "uri", value: json._url}, json._dataType, json._readOnly);
  1.3382 +
  1.3383 +        engine._initWithJSON(json);
  1.3384 +        this._addEngineToStore(engine);
  1.3385 +      } catch (ex) {
  1.3386 +        LOG("Failed to load " + engines[i]._name + " from cache: " + ex);
  1.3387 +        LOG("Engine JSON: " + engines[i].toSource());
  1.3388 +      }
  1.3389 +    }
  1.3390 +  },
  1.3391 +
  1.3392 +  _loadEnginesFromDir: function SRCH_SVC__loadEnginesFromDir(aDir) {
  1.3393 +    LOG("_loadEnginesFromDir: Searching in " + aDir.path + " for search engines.");
  1.3394 +
  1.3395 +    // Check whether aDir is the user profile dir
  1.3396 +    var isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR));
  1.3397 +
  1.3398 +    var files = aDir.directoryEntries
  1.3399 +                    .QueryInterface(Ci.nsIDirectoryEnumerator);
  1.3400 +
  1.3401 +    while (files.hasMoreElements()) {
  1.3402 +      var file = files.nextFile;
  1.3403 +
  1.3404 +      // Ignore hidden and empty files, and directories
  1.3405 +      if (!file.isFile() || file.fileSize == 0 || file.isHidden())
  1.3406 +        continue;
  1.3407 +
  1.3408 +      var fileURL = NetUtil.ioService.newFileURI(file).QueryInterface(Ci.nsIURL);
  1.3409 +      var fileExtension = fileURL.fileExtension.toLowerCase();
  1.3410 +      var isWritable = isInProfile && file.isWritable();
  1.3411 +
  1.3412 +      if (fileExtension != "xml") {
  1.3413 +        // Not an engine
  1.3414 +        continue;
  1.3415 +      }
  1.3416 +
  1.3417 +      var addedEngine = null;
  1.3418 +      try {
  1.3419 +        addedEngine = new Engine(file, SEARCH_DATA_XML, !isWritable);
  1.3420 +        addedEngine._initFromFile();
  1.3421 +      } catch (ex) {
  1.3422 +        LOG("_loadEnginesFromDir: Failed to load " + file.path + "!\n" + ex);
  1.3423 +        continue;
  1.3424 +      }
  1.3425 +
  1.3426 +      this._addEngineToStore(addedEngine);
  1.3427 +    }
  1.3428 +  },
  1.3429 +
  1.3430 +  /**
  1.3431 +   * Loads engines from a given directory asynchronously.
  1.3432 +   *
  1.3433 +   * @param aDir the directory.
  1.3434 +   *
  1.3435 +   * @returns {Promise} A promise, resolved successfully if retrieveing data
  1.3436 +   * succeeds.
  1.3437 +   */
  1.3438 +  _asyncLoadEnginesFromDir: function SRCH_SVC__asyncLoadEnginesFromDir(aDir) {
  1.3439 +    LOG("_asyncLoadEnginesFromDir: Searching in " + aDir.path + " for search engines.");
  1.3440 +
  1.3441 +    // Check whether aDir is the user profile dir
  1.3442 +    let isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR));
  1.3443 +    let iterator = new OS.File.DirectoryIterator(aDir.path);
  1.3444 +    return TaskUtils.spawn(function() {
  1.3445 +      let osfiles = yield iterator.nextBatch();
  1.3446 +      iterator.close();
  1.3447 +
  1.3448 +      let engines = [];
  1.3449 +      for (let osfile of osfiles) {
  1.3450 +        if (osfile.isDir || osfile.isSymLink)
  1.3451 +          continue;
  1.3452 +
  1.3453 +        let fileInfo = yield OS.File.stat(osfile.path);
  1.3454 +        if (fileInfo.size == 0)
  1.3455 +          continue;
  1.3456 +
  1.3457 +        let parts = osfile.path.split(".");
  1.3458 +        if (parts.length <= 1 || (parts.pop()).toLowerCase() != "xml") {
  1.3459 +          // Not an engine
  1.3460 +          continue;
  1.3461 +        }
  1.3462 +
  1.3463 +        let addedEngine = null;
  1.3464 +        try {
  1.3465 +          let file = new FileUtils.File(osfile.path);
  1.3466 +          let isWritable = isInProfile;
  1.3467 +          addedEngine = new Engine(file, SEARCH_DATA_XML, !isWritable);
  1.3468 +          yield checkForSyncCompletion(addedEngine._asyncInitFromFile());
  1.3469 +        } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  1.3470 +          LOG("_asyncLoadEnginesFromDir: Failed to load " + osfile.path + "!\n" + ex);
  1.3471 +          continue;
  1.3472 +        }
  1.3473 +        engines.push(addedEngine);
  1.3474 +      }
  1.3475 +      throw new Task.Result(engines);
  1.3476 +    }.bind(this));
  1.3477 +  },
  1.3478 +
  1.3479 +  _loadFromChromeURLs: function SRCH_SVC_loadFromChromeURLs(aURLs) {
  1.3480 +    aURLs.forEach(function (url) {
  1.3481 +      try {
  1.3482 +        LOG("_loadFromChromeURLs: loading engine from chrome url: " + url);
  1.3483 +
  1.3484 +        let engine = new Engine(makeURI(url), SEARCH_DATA_XML, true);
  1.3485 +
  1.3486 +        engine._initFromURISync();
  1.3487 +
  1.3488 +        this._addEngineToStore(engine);
  1.3489 +      } catch (ex) {
  1.3490 +        LOG("_loadFromChromeURLs: failed to load engine: " + ex);
  1.3491 +      }
  1.3492 +    }, this);
  1.3493 +  },
  1.3494 +
  1.3495 +  /**
  1.3496 +   * Loads engines from Chrome URLs asynchronously.
  1.3497 +   *
  1.3498 +   * @param aURLs a list of URLs.
  1.3499 +   *
  1.3500 +   * @returns {Promise} A promise, resolved successfully if loading data
  1.3501 +   * succeeds.
  1.3502 +   */
  1.3503 +  _asyncLoadFromChromeURLs: function SRCH_SVC__asyncLoadFromChromeURLs(aURLs) {
  1.3504 +    return TaskUtils.spawn(function() {
  1.3505 +      let engines = [];
  1.3506 +      for (let url of aURLs) {
  1.3507 +        try {
  1.3508 +          LOG("_asyncLoadFromChromeURLs: loading engine from chrome url: " + url);
  1.3509 +          let engine = new Engine(NetUtil.newURI(url), SEARCH_DATA_XML, true);
  1.3510 +          yield checkForSyncCompletion(engine._asyncInitFromURI());
  1.3511 +          engines.push(engine);
  1.3512 +        } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  1.3513 +          LOG("_asyncLoadFromChromeURLs: failed to load engine: " + ex);
  1.3514 +        }
  1.3515 +      }
  1.3516 +      throw new Task.Result(engines);
  1.3517 +    }.bind(this));
  1.3518 +  },
  1.3519 +
  1.3520 +  _findJAREngines: function SRCH_SVC_findJAREngines() {
  1.3521 +    LOG("_findJAREngines: looking for engines in JARs")
  1.3522 +
  1.3523 +    let rootURIPref = ""
  1.3524 +    try {
  1.3525 +      rootURIPref = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "jarURIs");
  1.3526 +    } catch (ex) {}
  1.3527 +
  1.3528 +    if (!rootURIPref) {
  1.3529 +      LOG("_findJAREngines: no JAR URIs were specified");
  1.3530 +
  1.3531 +      return [[], []];
  1.3532 +    }
  1.3533 +
  1.3534 +    let rootURIs = rootURIPref.split(",");
  1.3535 +    let uris = [];
  1.3536 +    let chromeFiles = [];
  1.3537 +
  1.3538 +    rootURIs.forEach(function (root) {
  1.3539 +      // Find the underlying JAR file for this chrome package (_loadEngines uses
  1.3540 +      // it to determine whether it needs to invalidate the cache)
  1.3541 +      let chromeFile;
  1.3542 +      try {
  1.3543 +        let chromeURI = gChromeReg.convertChromeURL(makeURI(root));
  1.3544 +        let fileURI = chromeURI; // flat packaging
  1.3545 +        while (fileURI instanceof Ci.nsIJARURI)
  1.3546 +          fileURI = fileURI.JARFile; // JAR packaging
  1.3547 +        fileURI.QueryInterface(Ci.nsIFileURL);
  1.3548 +        chromeFile = fileURI.file;
  1.3549 +      } catch (ex) {
  1.3550 +        LOG("_findJAREngines: failed to get chromeFile for " + root + ": " + ex);
  1.3551 +      }
  1.3552 +
  1.3553 +      if (!chromeFile)
  1.3554 +        return;
  1.3555 +
  1.3556 +      chromeFiles.push(chromeFile);
  1.3557 +
  1.3558 +      // Read list.txt from the chrome package to find the engines we need to
  1.3559 +      // load
  1.3560 +      let listURL = root + "list.txt";
  1.3561 +      let names = [];
  1.3562 +      try {
  1.3563 +        let chan = NetUtil.ioService.newChannelFromURI(makeURI(listURL));
  1.3564 +        let sis = Cc["@mozilla.org/scriptableinputstream;1"].
  1.3565 +                  createInstance(Ci.nsIScriptableInputStream);
  1.3566 +        sis.init(chan.open());
  1.3567 +        let list = sis.read(sis.available());
  1.3568 +        names = list.split("\n").filter(function (n) !!n);
  1.3569 +      } catch (ex) {
  1.3570 +        LOG("_findJAREngines: failed to retrieve list.txt from " + listURL + ": " + ex);
  1.3571 +
  1.3572 +        return;
  1.3573 +      }
  1.3574 +
  1.3575 +      names.forEach(function (n) uris.push(root + n + ".xml"));
  1.3576 +    });
  1.3577 +    
  1.3578 +    return [chromeFiles, uris];
  1.3579 +  },
  1.3580 +
  1.3581 +  /**
  1.3582 +   * Loads jar engines asynchronously.
  1.3583 +   *
  1.3584 +   * @returns {Promise} A promise, resolved successfully if finding jar engines
  1.3585 +   * succeeds.
  1.3586 +   */
  1.3587 +  _asyncFindJAREngines: function SRCH_SVC__asyncFindJAREngines() {
  1.3588 +    return TaskUtils.spawn(function() {
  1.3589 +      LOG("_asyncFindJAREngines: looking for engines in JARs")
  1.3590 +
  1.3591 +      let rootURIPref = "";
  1.3592 +      try {
  1.3593 +        rootURIPref = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "jarURIs");
  1.3594 +      } catch (ex) {}
  1.3595 +
  1.3596 +      if (!rootURIPref) {
  1.3597 +        LOG("_asyncFindJAREngines: no JAR URIs were specified");
  1.3598 +        throw new Task.Result([[], []]);
  1.3599 +      }
  1.3600 +
  1.3601 +      let rootURIs = rootURIPref.split(",");
  1.3602 +      let uris = [];
  1.3603 +      let chromeFiles = [];
  1.3604 +
  1.3605 +      for (let root of rootURIs) {
  1.3606 +        // Find the underlying JAR file for this chrome package (_loadEngines uses
  1.3607 +        // it to determine whether it needs to invalidate the cache)
  1.3608 +        let chromeFile;
  1.3609 +        try {
  1.3610 +          let chromeURI = gChromeReg.convertChromeURL(makeURI(root));
  1.3611 +          let fileURI = chromeURI; // flat packaging
  1.3612 +          while (fileURI instanceof Ci.nsIJARURI)
  1.3613 +            fileURI = fileURI.JARFile; // JAR packaging
  1.3614 +          fileURI.QueryInterface(Ci.nsIFileURL);
  1.3615 +          chromeFile = fileURI.file;
  1.3616 +        } catch (ex) {
  1.3617 +          LOG("_asyncFindJAREngines: failed to get chromeFile for " + root + ": " + ex);
  1.3618 +        }
  1.3619 +
  1.3620 +        if (!chromeFile) {
  1.3621 +          return;
  1.3622 +        }
  1.3623 +
  1.3624 +        chromeFiles.push(chromeFile);
  1.3625 +
  1.3626 +        // Read list.txt from the chrome package to find the engines we need to
  1.3627 +        // load
  1.3628 +        let listURL = root + "list.txt";
  1.3629 +        let deferred = Promise.defer();
  1.3630 +        let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
  1.3631 +                        createInstance(Ci.nsIXMLHttpRequest);
  1.3632 +        request.onload = function(aEvent) {
  1.3633 +          deferred.resolve(aEvent.target.responseText);
  1.3634 +        };
  1.3635 +        request.onerror = function(aEvent) {
  1.3636 +          LOG("_asyncFindJAREngines: failed to retrieve list.txt from " + listURL);
  1.3637 +          deferred.resolve("");
  1.3638 +        };
  1.3639 +        request.open("GET", NetUtil.newURI(listURL).spec, true);
  1.3640 +        request.send();
  1.3641 +        let list = yield deferred.promise;
  1.3642 +
  1.3643 +        let names = [];
  1.3644 +        names = list.split("\n").filter(function (n) !!n);
  1.3645 +        names.forEach(function (n) uris.push(root + n + ".xml"));
  1.3646 +      }
  1.3647 +      throw new Task.Result([chromeFiles, uris]);
  1.3648 +    });
  1.3649 +  },
  1.3650 +
  1.3651 +
  1.3652 +  _saveSortedEngineList: function SRCH_SVC_saveSortedEngineList() {
  1.3653 +    LOG("SRCH_SVC_saveSortedEngineList: starting");
  1.3654 +
  1.3655 +    // Set the useDB pref to indicate that from now on we should use the order
  1.3656 +    // information stored in the database.
  1.3657 +    Services.prefs.setBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", true);
  1.3658 +
  1.3659 +    var engines = this._getSortedEngines(true);
  1.3660 +
  1.3661 +    let instructions = [];
  1.3662 +    for (var i = 0; i < engines.length; ++i) {
  1.3663 +      instructions.push(
  1.3664 +        {key: "order",
  1.3665 +         value: i+1,
  1.3666 +         engine: engines[i]
  1.3667 +        });
  1.3668 +    }
  1.3669 +
  1.3670 +    engineMetadataService.setAttrs(instructions);
  1.3671 +    LOG("SRCH_SVC_saveSortedEngineList: done");
  1.3672 +  },
  1.3673 +
  1.3674 +  _buildSortedEngineList: function SRCH_SVC_buildSortedEngineList() {
  1.3675 +    LOG("_buildSortedEngineList: building list");
  1.3676 +    var addedEngines = { };
  1.3677 +    this.__sortedEngines = [];
  1.3678 +    var engine;
  1.3679 +
  1.3680 +    // If the user has specified a custom engine order, read the order
  1.3681 +    // information from the engineMetadataService instead of the default
  1.3682 +    // prefs.
  1.3683 +    if (getBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", false)) {
  1.3684 +      LOG("_buildSortedEngineList: using db for order");
  1.3685 +
  1.3686 +      // Flag to keep track of whether or not we need to call _saveSortedEngineList. 
  1.3687 +      let needToSaveEngineList = false;
  1.3688 +
  1.3689 +      for each (engine in this._engines) {
  1.3690 +        var orderNumber = engineMetadataService.getAttr(engine, "order");
  1.3691 +
  1.3692 +        // Since the DB isn't regularly cleared, and engine files may disappear
  1.3693 +        // without us knowing, we may already have an engine in this slot. If
  1.3694 +        // that happens, we just skip it - it will be added later on as an
  1.3695 +        // unsorted engine.
  1.3696 +        if (orderNumber && !this.__sortedEngines[orderNumber-1]) {
  1.3697 +          this.__sortedEngines[orderNumber-1] = engine;
  1.3698 +          addedEngines[engine.name] = engine;
  1.3699 +        } else {
  1.3700 +          // We need to call _saveSortedEngineList so this gets sorted out.
  1.3701 +          needToSaveEngineList = true;
  1.3702 +        }
  1.3703 +      }
  1.3704 +
  1.3705 +      // Filter out any nulls for engines that may have been removed
  1.3706 +      var filteredEngines = this.__sortedEngines.filter(function(a) { return !!a; });
  1.3707 +      if (this.__sortedEngines.length != filteredEngines.length)
  1.3708 +        needToSaveEngineList = true;
  1.3709 +      this.__sortedEngines = filteredEngines;
  1.3710 +
  1.3711 +      if (needToSaveEngineList)
  1.3712 +        this._saveSortedEngineList();
  1.3713 +    } else {
  1.3714 +      // The DB isn't being used, so just read the engine order from the prefs
  1.3715 +      var i = 0;
  1.3716 +      var engineName;
  1.3717 +      var prefName;
  1.3718 +
  1.3719 +      try {
  1.3720 +        var extras =
  1.3721 +          Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
  1.3722 +
  1.3723 +        for each (prefName in extras) {
  1.3724 +          engineName = Services.prefs.getCharPref(prefName);
  1.3725 +
  1.3726 +          engine = this._engines[engineName];
  1.3727 +          if (!engine || engine.name in addedEngines)
  1.3728 +            continue;
  1.3729 +
  1.3730 +          this.__sortedEngines.push(engine);
  1.3731 +          addedEngines[engine.name] = engine;
  1.3732 +        }
  1.3733 +      }
  1.3734 +      catch (e) { }
  1.3735 +
  1.3736 +      while (true) {
  1.3737 +        engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + (++i));
  1.3738 +        if (!engineName)
  1.3739 +          break;
  1.3740 +
  1.3741 +        engine = this._engines[engineName];
  1.3742 +        if (!engine || engine.name in addedEngines)
  1.3743 +          continue;
  1.3744 +        
  1.3745 +        this.__sortedEngines.push(engine);
  1.3746 +        addedEngines[engine.name] = engine;
  1.3747 +      }
  1.3748 +    }
  1.3749 +
  1.3750 +    // Array for the remaining engines, alphabetically sorted
  1.3751 +    var alphaEngines = [];
  1.3752 +
  1.3753 +    for each (engine in this._engines) {
  1.3754 +      if (!(engine.name in addedEngines))
  1.3755 +        alphaEngines.push(this._engines[engine.name]);
  1.3756 +    }
  1.3757 +    alphaEngines = alphaEngines.sort(function (a, b) {
  1.3758 +                                       return a.name.localeCompare(b.name);
  1.3759 +                                     });
  1.3760 +    return this.__sortedEngines = this.__sortedEngines.concat(alphaEngines);
  1.3761 +  },
  1.3762 +
  1.3763 +  /**
  1.3764 +   * Get a sorted array of engines.
  1.3765 +   * @param aWithHidden
  1.3766 +   *        True if hidden plugins should be included in the result.
  1.3767 +   */
  1.3768 +  _getSortedEngines: function SRCH_SVC_getSorted(aWithHidden) {
  1.3769 +    if (aWithHidden)
  1.3770 +      return this._sortedEngines;
  1.3771 +
  1.3772 +    return this._sortedEngines.filter(function (engine) {
  1.3773 +                                        return !engine.hidden;
  1.3774 +                                      });
  1.3775 +  },
  1.3776 +
  1.3777 +  _setEngineByPref: function SRCH_SVC_setEngineByPref(aEngineType, aPref) {
  1.3778 +    this._ensureInitialized();
  1.3779 +    let newEngine = this.getEngineByName(getLocalizedPref(aPref, ""));
  1.3780 +    if (!newEngine)
  1.3781 +      FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
  1.3782 +
  1.3783 +    this[aEngineType] = newEngine;
  1.3784 +  },
  1.3785 +
  1.3786 +  // nsIBrowserSearchService
  1.3787 +  init: function SRCH_SVC_init(observer) {
  1.3788 +    LOG("SearchService.init");
  1.3789 +    let self = this;
  1.3790 +    if (!this._initStarted) {
  1.3791 +      TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
  1.3792 +      this._initStarted = true;
  1.3793 +      TaskUtils.spawn(function task() {
  1.3794 +        try {
  1.3795 +          yield checkForSyncCompletion(engineMetadataService.init());
  1.3796 +          // Complete initialization by calling asynchronous initializer.
  1.3797 +          yield self._asyncInit();
  1.3798 +          TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
  1.3799 +        } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
  1.3800 +          // No need to pursue asynchronous because synchronous fallback was
  1.3801 +          // called and has finished.
  1.3802 +          TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
  1.3803 +        } catch (ex) {
  1.3804 +          self._initObservers.reject(ex);
  1.3805 +          TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
  1.3806 +        }
  1.3807 +      });
  1.3808 +    }
  1.3809 +    if (observer) {
  1.3810 +      TaskUtils.captureErrors(this._initObservers.promise.then(
  1.3811 +        function onSuccess() {
  1.3812 +          observer.onInitComplete(self._initRV);
  1.3813 +        },
  1.3814 +        function onError(aReason) {
  1.3815 +          Components.utils.reportError("Internal error while initializing SearchService: " + aReason);
  1.3816 +          observer.onInitComplete(Components.results.NS_ERROR_UNEXPECTED);
  1.3817 +        }
  1.3818 +      ));
  1.3819 +    }
  1.3820 +  },
  1.3821 +
  1.3822 +  get isInitialized() {
  1.3823 +    return gInitialized;
  1.3824 +  },
  1.3825 +
  1.3826 +  getEngines: function SRCH_SVC_getEngines(aCount) {
  1.3827 +    this._ensureInitialized();
  1.3828 +    LOG("getEngines: getting all engines");
  1.3829 +    var engines = this._getSortedEngines(true);
  1.3830 +    aCount.value = engines.length;
  1.3831 +    return engines;
  1.3832 +  },
  1.3833 +
  1.3834 +  getVisibleEngines: function SRCH_SVC_getVisible(aCount) {
  1.3835 +    this._ensureInitialized();
  1.3836 +    LOG("getVisibleEngines: getting all visible engines");
  1.3837 +    var engines = this._getSortedEngines(false);
  1.3838 +    aCount.value = engines.length;
  1.3839 +    return engines;
  1.3840 +  },
  1.3841 +
  1.3842 +  getDefaultEngines: function SRCH_SVC_getDefault(aCount) {
  1.3843 +    this._ensureInitialized();
  1.3844 +    function isDefault(engine) {
  1.3845 +      return engine._isDefault;
  1.3846 +    };
  1.3847 +    var engines = this._sortedEngines.filter(isDefault);
  1.3848 +    var engineOrder = {};
  1.3849 +    var engineName;
  1.3850 +    var i = 1;
  1.3851 +
  1.3852 +    // Build a list of engines which we have ordering information for.
  1.3853 +    // We're rebuilding the list here because _sortedEngines contain the
  1.3854 +    // current order, but we want the original order.
  1.3855 +
  1.3856 +    // First, look at the "browser.search.order.extra" branch.
  1.3857 +    try {
  1.3858 +      var extras = Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
  1.3859 +
  1.3860 +      for each (var prefName in extras) {
  1.3861 +        engineName = Services.prefs.getCharPref(prefName);
  1.3862 +
  1.3863 +        if (!(engineName in engineOrder))
  1.3864 +          engineOrder[engineName] = i++;
  1.3865 +      }
  1.3866 +    } catch (e) {
  1.3867 +      LOG("Getting extra order prefs failed: " + e);
  1.3868 +    }
  1.3869 +
  1.3870 +    // Now look through the "browser.search.order" branch.
  1.3871 +    for (var j = 1; ; j++) {
  1.3872 +      engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + j);
  1.3873 +      if (!engineName)
  1.3874 +        break;
  1.3875 +
  1.3876 +      if (!(engineName in engineOrder))
  1.3877 +        engineOrder[engineName] = i++;
  1.3878 +    }
  1.3879 +
  1.3880 +    LOG("getDefaultEngines: engineOrder: " + engineOrder.toSource());
  1.3881 +
  1.3882 +    function compareEngines (a, b) {
  1.3883 +      var aIdx = engineOrder[a.name];
  1.3884 +      var bIdx = engineOrder[b.name];
  1.3885 +
  1.3886 +      if (aIdx && bIdx)
  1.3887 +        return aIdx - bIdx;
  1.3888 +      if (aIdx)
  1.3889 +        return -1;
  1.3890 +      if (bIdx)
  1.3891 +        return 1;
  1.3892 +
  1.3893 +      return a.name.localeCompare(b.name);
  1.3894 +    }
  1.3895 +    engines.sort(compareEngines);
  1.3896 +
  1.3897 +    aCount.value = engines.length;
  1.3898 +    return engines;
  1.3899 +  },
  1.3900 +
  1.3901 +  getEngineByName: function SRCH_SVC_getEngineByName(aEngineName) {
  1.3902 +    this._ensureInitialized();
  1.3903 +    return this._engines[aEngineName] || null;
  1.3904 +  },
  1.3905 +
  1.3906 +  getEngineByAlias: function SRCH_SVC_getEngineByAlias(aAlias) {
  1.3907 +    this._ensureInitialized();
  1.3908 +    for (var engineName in this._engines) {
  1.3909 +      var engine = this._engines[engineName];
  1.3910 +      if (engine && engine.alias == aAlias)
  1.3911 +        return engine;
  1.3912 +    }
  1.3913 +    return null;
  1.3914 +  },
  1.3915 +
  1.3916 +  addEngineWithDetails: function SRCH_SVC_addEWD(aName, aIconURL, aAlias,
  1.3917 +                                                 aDescription, aMethod,
  1.3918 +                                                 aTemplate) {
  1.3919 +    this._ensureInitialized();
  1.3920 +    if (!aName)
  1.3921 +      FAIL("Invalid name passed to addEngineWithDetails!");
  1.3922 +    if (!aMethod)
  1.3923 +      FAIL("Invalid method passed to addEngineWithDetails!");
  1.3924 +    if (!aTemplate)
  1.3925 +      FAIL("Invalid template passed to addEngineWithDetails!");
  1.3926 +    if (this._engines[aName])
  1.3927 +      FAIL("An engine with that name already exists!", Cr.NS_ERROR_FILE_ALREADY_EXISTS);
  1.3928 +
  1.3929 +    var engine = new Engine(getSanitizedFile(aName), SEARCH_DATA_XML, false);
  1.3930 +    engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
  1.3931 +                             aMethod, aTemplate);
  1.3932 +    this._addEngineToStore(engine);
  1.3933 +    this.batchTask.disarm();
  1.3934 +    this.batchTask.arm();
  1.3935 +  },
  1.3936 +
  1.3937 +  addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL,
  1.3938 +                                         aConfirm, aCallback) {
  1.3939 +    LOG("addEngine: Adding \"" + aEngineURL + "\".");
  1.3940 +    this._ensureInitialized();
  1.3941 +    try {
  1.3942 +      var uri = makeURI(aEngineURL);
  1.3943 +      var engine = new Engine(uri, aDataType, false);
  1.3944 +      if (aCallback) {
  1.3945 +        engine._installCallback = function (errorCode) {
  1.3946 +          try {
  1.3947 +            if (errorCode == null)
  1.3948 +              aCallback.onSuccess(engine);
  1.3949 +            else
  1.3950 +              aCallback.onError(errorCode);
  1.3951 +          } catch (ex) {
  1.3952 +            Cu.reportError("Error invoking addEngine install callback: " + ex);
  1.3953 +          }
  1.3954 +          // Clear the reference to the callback now that it's been invoked.
  1.3955 +          engine._installCallback = null;
  1.3956 +        };
  1.3957 +      }
  1.3958 +      engine._initFromURIAndLoad();
  1.3959 +    } catch (ex) {
  1.3960 +      // Drop the reference to the callback, if set
  1.3961 +      if (engine)
  1.3962 +        engine._installCallback = null;
  1.3963 +      FAIL("addEngine: Error adding engine:\n" + ex, Cr.NS_ERROR_FAILURE);
  1.3964 +    }
  1.3965 +    engine._setIcon(aIconURL, false);
  1.3966 +    engine._confirm = aConfirm;
  1.3967 +  },
  1.3968 +
  1.3969 +  removeEngine: function SRCH_SVC_removeEngine(aEngine) {
  1.3970 +    this._ensureInitialized();
  1.3971 +    if (!aEngine)
  1.3972 +      FAIL("no engine passed to removeEngine!");
  1.3973 +
  1.3974 +    var engineToRemove = null;
  1.3975 +    for (var e in this._engines) {
  1.3976 +      if (aEngine.wrappedJSObject == this._engines[e])
  1.3977 +        engineToRemove = this._engines[e];
  1.3978 +    }
  1.3979 +
  1.3980 +    if (!engineToRemove)
  1.3981 +      FAIL("removeEngine: Can't find engine to remove!", Cr.NS_ERROR_FILE_NOT_FOUND);
  1.3982 +
  1.3983 +    if (engineToRemove == this.currentEngine) {
  1.3984 +      this._currentEngine = null;
  1.3985 +    }
  1.3986 +
  1.3987 +    if (engineToRemove == this.defaultEngine) {
  1.3988 +      this._defaultEngine = null;
  1.3989 +    }
  1.3990 +
  1.3991 +    if (engineToRemove._readOnly) {
  1.3992 +      // Just hide it (the "hidden" setter will notify) and remove its alias to
  1.3993 +      // avoid future conflicts with other engines.
  1.3994 +      engineToRemove.hidden = true;
  1.3995 +      engineToRemove.alias = null;
  1.3996 +    } else {
  1.3997 +      // Cancel the serialized task if it's pending.  Since the task is a
  1.3998 +      // synchronous function, we don't need to wait on the "finalize" method.
  1.3999 +      if (engineToRemove._lazySerializeTask) {
  1.4000 +        engineToRemove._lazySerializeTask.disarm();
  1.4001 +        engineToRemove._lazySerializeTask = null;
  1.4002 +      }
  1.4003 +
  1.4004 +      // Remove the engine file from disk (this might throw)
  1.4005 +      engineToRemove._remove();
  1.4006 +      engineToRemove._file = null;
  1.4007 +
  1.4008 +      // Remove the engine from _sortedEngines
  1.4009 +      var index = this._sortedEngines.indexOf(engineToRemove);
  1.4010 +      if (index == -1)
  1.4011 +        FAIL("Can't find engine to remove in _sortedEngines!", Cr.NS_ERROR_FAILURE);
  1.4012 +      this.__sortedEngines.splice(index, 1);
  1.4013 +
  1.4014 +      // Remove the engine from the internal store
  1.4015 +      delete this._engines[engineToRemove.name];
  1.4016 +
  1.4017 +      notifyAction(engineToRemove, SEARCH_ENGINE_REMOVED);
  1.4018 +
  1.4019 +      // Since we removed an engine, we need to update the preferences.
  1.4020 +      this._saveSortedEngineList();
  1.4021 +    }
  1.4022 +  },
  1.4023 +
  1.4024 +  moveEngine: function SRCH_SVC_moveEngine(aEngine, aNewIndex) {
  1.4025 +    this._ensureInitialized();
  1.4026 +    if ((aNewIndex > this._sortedEngines.length) || (aNewIndex < 0))
  1.4027 +      FAIL("SRCH_SVC_moveEngine: Index out of bounds!");
  1.4028 +    if (!(aEngine instanceof Ci.nsISearchEngine))
  1.4029 +      FAIL("SRCH_SVC_moveEngine: Invalid engine passed to moveEngine!");
  1.4030 +    if (aEngine.hidden)
  1.4031 +      FAIL("moveEngine: Can't move a hidden engine!", Cr.NS_ERROR_FAILURE);
  1.4032 +
  1.4033 +    var engine = aEngine.wrappedJSObject;
  1.4034 +
  1.4035 +    var currentIndex = this._sortedEngines.indexOf(engine);
  1.4036 +    if (currentIndex == -1)
  1.4037 +      FAIL("moveEngine: Can't find engine to move!", Cr.NS_ERROR_UNEXPECTED);
  1.4038 +
  1.4039 +    // Our callers only take into account non-hidden engines when calculating
  1.4040 +    // aNewIndex, but we need to move it in the array of all engines, so we
  1.4041 +    // need to adjust aNewIndex accordingly. To do this, we count the number
  1.4042 +    // of hidden engines in the list before the engine that we're taking the
  1.4043 +    // place of. We do this by first finding newIndexEngine (the engine that
  1.4044 +    // we were supposed to replace) and then iterating through the complete 
  1.4045 +    // engine list until we reach it, increasing aNewIndex for each hidden
  1.4046 +    // engine we find on our way there.
  1.4047 +    //
  1.4048 +    // This could be further simplified by having our caller pass in
  1.4049 +    // newIndexEngine directly instead of aNewIndex.
  1.4050 +    var newIndexEngine = this._getSortedEngines(false)[aNewIndex];
  1.4051 +    if (!newIndexEngine)
  1.4052 +      FAIL("moveEngine: Can't find engine to replace!", Cr.NS_ERROR_UNEXPECTED);
  1.4053 +
  1.4054 +    for (var i = 0; i < this._sortedEngines.length; ++i) {
  1.4055 +      if (newIndexEngine == this._sortedEngines[i])
  1.4056 +        break;
  1.4057 +      if (this._sortedEngines[i].hidden)
  1.4058 +        aNewIndex++;
  1.4059 +    }
  1.4060 +
  1.4061 +    if (currentIndex == aNewIndex)
  1.4062 +      return; // nothing to do!
  1.4063 +
  1.4064 +    // Move the engine
  1.4065 +    var movedEngine = this.__sortedEngines.splice(currentIndex, 1)[0];
  1.4066 +    this.__sortedEngines.splice(aNewIndex, 0, movedEngine);
  1.4067 +
  1.4068 +    notifyAction(engine, SEARCH_ENGINE_CHANGED);
  1.4069 +
  1.4070 +    // Since we moved an engine, we need to update the preferences.
  1.4071 +    this._saveSortedEngineList();
  1.4072 +  },
  1.4073 +
  1.4074 +  restoreDefaultEngines: function SRCH_SVC_resetDefaultEngines() {
  1.4075 +    this._ensureInitialized();
  1.4076 +    for each (var e in this._engines) {
  1.4077 +      // Unhide all default engines
  1.4078 +      if (e.hidden && e._isDefault)
  1.4079 +        e.hidden = false;
  1.4080 +    }
  1.4081 +  },
  1.4082 +
  1.4083 +  get defaultEngine() {
  1.4084 +    this._ensureInitialized();
  1.4085 +    if (!this._defaultEngine) {
  1.4086 +      let defPref = BROWSER_SEARCH_PREF + "defaultenginename";
  1.4087 +      let defaultEngine = this.getEngineByName(getLocalizedPref(defPref, ""))
  1.4088 +      if (!defaultEngine)
  1.4089 +        defaultEngine = this._getSortedEngines(false)[0] || null;
  1.4090 +      this._defaultEngine = defaultEngine;
  1.4091 +    }
  1.4092 +    if (this._defaultEngine.hidden)
  1.4093 +      return this._getSortedEngines(false)[0];
  1.4094 +    return this._defaultEngine;
  1.4095 +  },
  1.4096 +
  1.4097 +  set defaultEngine(val) {
  1.4098 +    this._ensureInitialized();
  1.4099 +    // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
  1.4100 +    // and sometimes we get raw Engine JS objects (callers in this file), so
  1.4101 +    // handle both.
  1.4102 +    if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine))
  1.4103 +      FAIL("Invalid argument passed to defaultEngine setter");
  1.4104 +
  1.4105 +    let newDefaultEngine = this.getEngineByName(val.name);
  1.4106 +    if (!newDefaultEngine)
  1.4107 +      FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
  1.4108 +
  1.4109 +    if (newDefaultEngine == this._defaultEngine)
  1.4110 +      return;
  1.4111 +
  1.4112 +    this._defaultEngine = newDefaultEngine;
  1.4113 +
  1.4114 +    // Set a flag to keep track that this setter was called properly, not by
  1.4115 +    // setting the pref alone.
  1.4116 +    this._changingDefaultEngine = true;
  1.4117 +    let defPref = BROWSER_SEARCH_PREF + "defaultenginename";
  1.4118 +    // If we change the default engine in the future, that change should impact
  1.4119 +    // users who have switched away from and then back to the build's "default"
  1.4120 +    // engine. So clear the user pref when the defaultEngine is set to the
  1.4121 +    // build's default engine, so that the defaultEngine getter falls back to
  1.4122 +    // whatever the default is.
  1.4123 +    if (this._defaultEngine == this._originalDefaultEngine) {
  1.4124 +      Services.prefs.clearUserPref(defPref);
  1.4125 +    }
  1.4126 +    else {
  1.4127 +      setLocalizedPref(defPref, this._defaultEngine.name);
  1.4128 +    }
  1.4129 +    this._changingDefaultEngine = false;
  1.4130 +
  1.4131 +    notifyAction(this._defaultEngine, SEARCH_ENGINE_DEFAULT);
  1.4132 +  },
  1.4133 +
  1.4134 +  get currentEngine() {
  1.4135 +    this._ensureInitialized();
  1.4136 +    if (!this._currentEngine) {
  1.4137 +      let selectedEngine = getLocalizedPref(BROWSER_SEARCH_PREF +
  1.4138 +                                            "selectedEngine");
  1.4139 +      this._currentEngine = this.getEngineByName(selectedEngine);
  1.4140 +    }
  1.4141 +
  1.4142 +    if (!this._currentEngine || this._currentEngine.hidden)
  1.4143 +      this._currentEngine = this.defaultEngine;
  1.4144 +    return this._currentEngine;
  1.4145 +  },
  1.4146 +
  1.4147 +  set currentEngine(val) {
  1.4148 +    this._ensureInitialized();
  1.4149 +    // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
  1.4150 +    // and sometimes we get raw Engine JS objects (callers in this file), so
  1.4151 +    // handle both.
  1.4152 +    if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine))
  1.4153 +      FAIL("Invalid argument passed to currentEngine setter");
  1.4154 +
  1.4155 +    var newCurrentEngine = this.getEngineByName(val.name);
  1.4156 +    if (!newCurrentEngine)
  1.4157 +      FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
  1.4158 +
  1.4159 +    if (newCurrentEngine == this._currentEngine)
  1.4160 +      return;
  1.4161 +
  1.4162 +    this._currentEngine = newCurrentEngine;
  1.4163 +
  1.4164 +    var currentEnginePref = BROWSER_SEARCH_PREF + "selectedEngine";
  1.4165 +
  1.4166 +    // Set a flag to keep track that this setter was called properly, not by
  1.4167 +    // setting the pref alone.
  1.4168 +    this._changingCurrentEngine = true;
  1.4169 +    // If we change the default engine in the future, that change should impact
  1.4170 +    // users who have switched away from and then back to the build's "default"
  1.4171 +    // engine. So clear the user pref when the currentEngine is set to the
  1.4172 +    // build's default engine, so that the currentEngine getter falls back to
  1.4173 +    // whatever the default is.
  1.4174 +    if (this._currentEngine == this._originalDefaultEngine) {
  1.4175 +      Services.prefs.clearUserPref(currentEnginePref);
  1.4176 +    }
  1.4177 +    else {
  1.4178 +      setLocalizedPref(currentEnginePref, this._currentEngine.name);
  1.4179 +    }
  1.4180 +    this._changingCurrentEngine = false;
  1.4181 +
  1.4182 +    notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT);
  1.4183 +  },
  1.4184 +
  1.4185 +  // nsIObserver
  1.4186 +  observe: function SRCH_SVC_observe(aEngine, aTopic, aVerb) {
  1.4187 +    switch (aTopic) {
  1.4188 +      case SEARCH_ENGINE_TOPIC:
  1.4189 +        switch (aVerb) {
  1.4190 +          case SEARCH_ENGINE_LOADED:
  1.4191 +            var engine = aEngine.QueryInterface(Ci.nsISearchEngine);
  1.4192 +            LOG("nsSearchService::observe: Done installation of " + engine.name
  1.4193 +                + ".");
  1.4194 +            this._addEngineToStore(engine.wrappedJSObject);
  1.4195 +            if (engine.wrappedJSObject._useNow) {
  1.4196 +              LOG("nsSearchService::observe: setting current");
  1.4197 +              this.currentEngine = aEngine;
  1.4198 +            }
  1.4199 +            this.batchTask.disarm();
  1.4200 +            this.batchTask.arm();
  1.4201 +            break;
  1.4202 +          case SEARCH_ENGINE_CHANGED:
  1.4203 +          case SEARCH_ENGINE_REMOVED:
  1.4204 +            this.batchTask.disarm();
  1.4205 +            this.batchTask.arm();
  1.4206 +            break;
  1.4207 +        }
  1.4208 +        break;
  1.4209 +
  1.4210 +      case QUIT_APPLICATION_TOPIC:
  1.4211 +        this._removeObservers();
  1.4212 +        break;
  1.4213 +
  1.4214 +      case "nsPref:changed":
  1.4215 +        let currPref = BROWSER_SEARCH_PREF + "selectedEngine";
  1.4216 +        let defPref = BROWSER_SEARCH_PREF + "defaultenginename";
  1.4217 +        if (aVerb == currPref && !this._changingCurrentEngine) {
  1.4218 +          this._setEngineByPref("currentEngine", currPref);
  1.4219 +        } else if (aVerb == defPref && !this._changingDefaultEngine) {
  1.4220 +          this._setEngineByPref("defaultEngine", defPref);
  1.4221 +        }
  1.4222 +        break;
  1.4223 +    }
  1.4224 +  },
  1.4225 +
  1.4226 +  // nsITimerCallback
  1.4227 +  notify: function SRCH_SVC_notify(aTimer) {
  1.4228 +    LOG("_notify: checking for updates");
  1.4229 +
  1.4230 +    if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true))
  1.4231 +      return;
  1.4232 +
  1.4233 +    // Our timer has expired, but unfortunately, we can't get any data from it.
  1.4234 +    // Therefore, we need to walk our engine-list, looking for expired engines
  1.4235 +    var currentTime = Date.now();
  1.4236 +    LOG("currentTime: " + currentTime);
  1.4237 +    for each (engine in this._engines) {
  1.4238 +      engine = engine.wrappedJSObject;
  1.4239 +      if (!engine._hasUpdates)
  1.4240 +        continue;
  1.4241 +
  1.4242 +      LOG("checking " + engine.name);
  1.4243 +
  1.4244 +      var expirTime = engineMetadataService.getAttr(engine, "updateexpir");
  1.4245 +      LOG("expirTime: " + expirTime + "\nupdateURL: " + engine._updateURL +
  1.4246 +          "\niconUpdateURL: " + engine._iconUpdateURL);
  1.4247 +
  1.4248 +      var engineExpired = expirTime <= currentTime;
  1.4249 +
  1.4250 +      if (!expirTime || !engineExpired) {
  1.4251 +        LOG("skipping engine");
  1.4252 +        continue;
  1.4253 +      }
  1.4254 +
  1.4255 +      LOG(engine.name + " has expired");
  1.4256 +
  1.4257 +      engineUpdateService.update(engine);
  1.4258 +
  1.4259 +      // Schedule the next update
  1.4260 +      engineUpdateService.scheduleNextUpdate(engine);
  1.4261 +
  1.4262 +    } // end engine iteration
  1.4263 +  },
  1.4264 +
  1.4265 +  _addObservers: function SRCH_SVC_addObservers() {
  1.4266 +    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, false);
  1.4267 +    Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC, false);
  1.4268 +    Services.prefs.addObserver(BROWSER_SEARCH_PREF + "defaultenginename", this, false);
  1.4269 +    Services.prefs.addObserver(BROWSER_SEARCH_PREF + "selectedEngine", this, false);
  1.4270 +
  1.4271 +    AsyncShutdown.profileBeforeChange.addBlocker(
  1.4272 +      "Search service: shutting down",
  1.4273 +      () => Task.spawn(function () {
  1.4274 +        if (this._batchTask) {
  1.4275 +          yield this._batchTask.finalize().then(null, Cu.reportError);
  1.4276 +        }
  1.4277 +        yield engineMetadataService.finalize();
  1.4278 +      }.bind(this))
  1.4279 +    );
  1.4280 +  },
  1.4281 +
  1.4282 +  _removeObservers: function SRCH_SVC_removeObservers() {
  1.4283 +    Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
  1.4284 +    Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC);
  1.4285 +    Services.prefs.removeObserver(BROWSER_SEARCH_PREF + "defaultenginename", this);
  1.4286 +    Services.prefs.removeObserver(BROWSER_SEARCH_PREF + "selectedEngine", this);
  1.4287 +  },
  1.4288 +
  1.4289 +  QueryInterface: function SRCH_SVC_QI(aIID) {
  1.4290 +    if (aIID.equals(Ci.nsIBrowserSearchService) ||
  1.4291 +        aIID.equals(Ci.nsIObserver)             ||
  1.4292 +        aIID.equals(Ci.nsITimerCallback)        ||
  1.4293 +        aIID.equals(Ci.nsISupports))
  1.4294 +      return this;
  1.4295 +    throw Cr.NS_ERROR_NO_INTERFACE;
  1.4296 +  }
  1.4297 +};
  1.4298 +
  1.4299 +var engineMetadataService = {
  1.4300 +  _jsonFile: OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json"),
  1.4301 +
  1.4302 +  /**
  1.4303 +   * Possible values for |_initState|.
  1.4304 +   *
  1.4305 +   * We have two paths to perform initialization: a default asynchronous
  1.4306 +   * path and a fallback synchronous path that can interrupt the async
  1.4307 +   * path. For this reason, initialization is actually something of a
  1.4308 +   * finite state machine, represented with the following states:
  1.4309 +   *
  1.4310 +   * @enum
  1.4311 +   */
  1.4312 +  _InitStates: {
  1.4313 +    NOT_STARTED: "NOT_STARTED"
  1.4314 +      /**Initialization has not started*/,
  1.4315 +    FINISHED_SUCCESS: "FINISHED_SUCCESS"
  1.4316 +      /**Setup complete, with a success*/
  1.4317 +  },
  1.4318 +
  1.4319 +  /**
  1.4320 +   * The latest step completed by initialization. One of |InitStates|
  1.4321 +   *
  1.4322 +   * @type {engineMetadataService._InitStates}
  1.4323 +   */
  1.4324 +  _initState: null,
  1.4325 +
  1.4326 +  // A promise fulfilled once initialization is complete
  1.4327 +  _initializer: null,
  1.4328 +
  1.4329 +  /**
  1.4330 +   * Asynchronous initializer
  1.4331 +   *
  1.4332 +   * Note: In the current implementation, initialization never fails.
  1.4333 +   */
  1.4334 +  init: function epsInit() {
  1.4335 +    if (!this._initializer) {
  1.4336 +      // Launch asynchronous initialization
  1.4337 +      let initializer = this._initializer = Promise.defer();
  1.4338 +      TaskUtils.spawn((function task_init() {
  1.4339 +        LOG("metadata init: starting");
  1.4340 +        switch (this._initState) {
  1.4341 +          case engineMetadataService._InitStates.NOT_STARTED:
  1.4342 +            // 1. Load json file if it exists
  1.4343 +            try {
  1.4344 +              let contents = yield OS.File.read(this._jsonFile);
  1.4345 +              if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) {
  1.4346 +                // No need to pursue asynchronous initialization,
  1.4347 +                // synchronous fallback was called and has finished.
  1.4348 +                return;
  1.4349 +              }
  1.4350 +              this._store = JSON.parse(new TextDecoder().decode(contents));
  1.4351 +            } catch (ex) {
  1.4352 +              if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) {
  1.4353 +                // No need to pursue asynchronous initialization,
  1.4354 +                // synchronous fallback was called and has finished.
  1.4355 +                return;
  1.4356 +              }
  1.4357 +              // Couldn't load json, use an empty store
  1.4358 +              LOG("metadata init: could not load JSON file " + ex);
  1.4359 +              this._store = {};
  1.4360 +            }
  1.4361 +            break;
  1.4362 +
  1.4363 +          default:
  1.4364 +            throw new Error("metadata init: invalid state " + this._initState);
  1.4365 +        }
  1.4366 +
  1.4367 +        this._initState = this._InitStates.FINISHED_SUCCESS;
  1.4368 +        LOG("metadata init: complete");
  1.4369 +      }).bind(this)).then(
  1.4370 +        // 3. Inform any observers
  1.4371 +        function onSuccess() {
  1.4372 +          initializer.resolve();
  1.4373 +        },
  1.4374 +        function onError() {
  1.4375 +          initializer.reject();
  1.4376 +        }
  1.4377 +      );
  1.4378 +    }
  1.4379 +    return TaskUtils.captureErrors(this._initializer.promise);
  1.4380 +  },
  1.4381 +
  1.4382 +  /**
  1.4383 +   * Synchronous implementation of initializer
  1.4384 +   *
  1.4385 +   * This initializer is able to pick wherever the async initializer
  1.4386 +   * is waiting. The asynchronous initializer is expected to stop
  1.4387 +   * if it detects that the synchronous initializer has completed
  1.4388 +   * initialization.
  1.4389 +   */
  1.4390 +  syncInit: function epsSyncInit() {
  1.4391 +    LOG("metadata syncInit start");
  1.4392 +    if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) {
  1.4393 +      return;
  1.4394 +    }
  1.4395 +    switch (this._initState) {
  1.4396 +      case engineMetadataService._InitStates.NOT_STARTED:
  1.4397 +        let jsonFile = new FileUtils.File(this._jsonFile);
  1.4398 +        // 1. Load json file if it exists
  1.4399 +        if (jsonFile.exists()) {
  1.4400 +          try {
  1.4401 +            let uri = Services.io.newFileURI(jsonFile);
  1.4402 +            let stream = Services.io.newChannelFromURI(uri).open();
  1.4403 +            this._store = parseJsonFromStream(stream);
  1.4404 +          } catch (x) {
  1.4405 +            LOG("metadata syncInit: could not load JSON file " + x);
  1.4406 +            this._store = {};
  1.4407 +          }
  1.4408 +        } else {
  1.4409 +          LOG("metadata syncInit: using an empty store");
  1.4410 +          this._store = {};
  1.4411 +        }
  1.4412 +
  1.4413 +        this._initState = this._InitStates.FINISHED_SUCCESS;
  1.4414 +        break;
  1.4415 +
  1.4416 +      default:
  1.4417 +        throw new Error("metadata syncInit: invalid state " + this._initState);
  1.4418 +    }
  1.4419 +
  1.4420 +    // 3. Inform any observers
  1.4421 +    if (this._initializer) {
  1.4422 +      this._initializer.resolve();
  1.4423 +    } else {
  1.4424 +      this._initializer = Promise.resolve();
  1.4425 +    }
  1.4426 +    LOG("metadata syncInit end");
  1.4427 +  },
  1.4428 +
  1.4429 +  getAttr: function epsGetAttr(engine, name) {
  1.4430 +    let record = this._store[engine._id];
  1.4431 +    if (!record) {
  1.4432 +      return null;
  1.4433 +    }
  1.4434 +
  1.4435 +    // attr names must be lower case
  1.4436 +    let aName = name.toLowerCase();
  1.4437 +    if (!record[aName])
  1.4438 +      return null;
  1.4439 +    return record[aName];
  1.4440 +  },
  1.4441 +
  1.4442 +  _setAttr: function epsSetAttr(engine, name, value) {
  1.4443 +    // attr names must be lower case
  1.4444 +    name = name.toLowerCase();
  1.4445 +    let db = this._store;
  1.4446 +    let record = db[engine._id];
  1.4447 +    if (!record) {
  1.4448 +      record = db[engine._id] = {};
  1.4449 +    }
  1.4450 +    if (!record[name] || (record[name] != value)) {
  1.4451 +      record[name] = value;
  1.4452 +      return true;
  1.4453 +    }
  1.4454 +    return false;
  1.4455 +  },
  1.4456 +
  1.4457 +  /**
  1.4458 +   * Set one metadata attribute for an engine.
  1.4459 +   *
  1.4460 +   * If an actual change has taken place, the attribute is committed
  1.4461 +   * automatically (and lazily), using this._commit.
  1.4462 +   *
  1.4463 +   * @param {nsISearchEngine} engine The engine to update.
  1.4464 +   * @param {string} key The name of the attribute. Case-insensitive. In
  1.4465 +   * the current implementation, this _must not_ conflict with properties
  1.4466 +   * of |Object|.
  1.4467 +   * @param {*} value A value to store.
  1.4468 +   */
  1.4469 +  setAttr: function epsSetAttr(engine, key, value) {
  1.4470 +    if (this._setAttr(engine, key, value)) {
  1.4471 +      this._commit();
  1.4472 +    }
  1.4473 +  },
  1.4474 +
  1.4475 +  /**
  1.4476 +   * Bulk set metadata attributes for a number of engines.
  1.4477 +   *
  1.4478 +   * If actual changes have taken place, the store is committed
  1.4479 +   * automatically (and lazily), using this._commit.
  1.4480 +   *
  1.4481 +   * @param {Array.<{engine: nsISearchEngine, key: string, value: *}>} changes
  1.4482 +   * The list of changes to effect. See |setAttr| for the documentation of
  1.4483 +   * |engine|, |key|, |value|.
  1.4484 +   */
  1.4485 +  setAttrs: function epsSetAttrs(changes) {
  1.4486 +    let self = this;
  1.4487 +    let changed = false;
  1.4488 +    changes.forEach(function(change) {
  1.4489 +      changed |= self._setAttr(change.engine, change.key, change.value);
  1.4490 +    });
  1.4491 +    if (changed) {
  1.4492 +      this._commit();
  1.4493 +    }
  1.4494 +  },
  1.4495 +
  1.4496 +  /**
  1.4497 +   * Flush any waiting write.
  1.4498 +   */
  1.4499 +  finalize: function () this._lazyWriter ? this._lazyWriter.finalize()
  1.4500 +                                         : Promise.resolve(),
  1.4501 +
  1.4502 +  /**
  1.4503 +   * Commit changes to disk, asynchronously.
  1.4504 +   *
  1.4505 +   * Calls to this function are actually delayed by LAZY_SERIALIZE_DELAY
  1.4506 +   * (= 100ms). If the function is called again before the expiration of
  1.4507 +   * the delay, commits are merged and the function is again delayed by
  1.4508 +   * the same amount of time.
  1.4509 +   *
  1.4510 +   * @param aStore is an optional parameter specifying the object to serialize.
  1.4511 +   *               If not specified, this._store is used.
  1.4512 +   */
  1.4513 +  _commit: function epsCommit(aStore) {
  1.4514 +    LOG("metadata _commit: start");
  1.4515 +    let store = aStore || this._store;
  1.4516 +    if (!store) {
  1.4517 +      LOG("metadata _commit: nothing to do");
  1.4518 +      return;
  1.4519 +    }
  1.4520 +
  1.4521 +    if (!this._lazyWriter) {
  1.4522 +      LOG("metadata _commit: initializing lazy writer");
  1.4523 +      function writeCommit() {
  1.4524 +        LOG("metadata writeCommit: start");
  1.4525 +        let data = gEncoder.encode(JSON.stringify(store));
  1.4526 +        let path = engineMetadataService._jsonFile;
  1.4527 +        LOG("metadata writeCommit: path " + path);
  1.4528 +        let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
  1.4529 +        promise = promise.then(
  1.4530 +          function onSuccess() {
  1.4531 +            Services.obs.notifyObservers(null,
  1.4532 +              SEARCH_SERVICE_TOPIC,
  1.4533 +              SEARCH_SERVICE_METADATA_WRITTEN);
  1.4534 +            LOG("metadata writeCommit: done");
  1.4535 +          }
  1.4536 +        );
  1.4537 +        // Use our error logging instead of the default one.
  1.4538 +        return TaskUtils.captureErrors(promise).then(null, () => {});
  1.4539 +      }
  1.4540 +      this._lazyWriter = new DeferredTask(writeCommit, LAZY_SERIALIZE_DELAY);
  1.4541 +    }
  1.4542 +    LOG("metadata _commit: (re)setting timer");
  1.4543 +    this._lazyWriter.disarm();
  1.4544 +    this._lazyWriter.arm();
  1.4545 +  },
  1.4546 +  _lazyWriter: null
  1.4547 +};
  1.4548 +
  1.4549 +engineMetadataService._initState = engineMetadataService._InitStates.NOT_STARTED;
  1.4550 +
  1.4551 +const SEARCH_UPDATE_LOG_PREFIX = "*** Search update: ";
  1.4552 +
  1.4553 +/**
  1.4554 + * Outputs aText to the JavaScript console as well as to stdout, if the search
  1.4555 + * logging pref (browser.search.update.log) is set to true.
  1.4556 + */
  1.4557 +function ULOG(aText) {
  1.4558 +  if (getBoolPref(BROWSER_SEARCH_PREF + "update.log", false)) {
  1.4559 +    dump(SEARCH_UPDATE_LOG_PREFIX + aText + "\n");
  1.4560 +    Services.console.logStringMessage(aText);
  1.4561 +  }
  1.4562 +}
  1.4563 +
  1.4564 +var engineUpdateService = {
  1.4565 +  scheduleNextUpdate: function eus_scheduleNextUpdate(aEngine) {
  1.4566 +    var interval = aEngine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL;
  1.4567 +    var milliseconds = interval * 86400000; // |interval| is in days
  1.4568 +    engineMetadataService.setAttr(aEngine, "updateexpir",
  1.4569 +                                  Date.now() + milliseconds);
  1.4570 +  },
  1.4571 +
  1.4572 +  update: function eus_Update(aEngine) {
  1.4573 +    let engine = aEngine.wrappedJSObject;
  1.4574 +    ULOG("update called for " + aEngine._name);
  1.4575 +    if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true) || !engine._hasUpdates)
  1.4576 +      return;
  1.4577 +
  1.4578 +    // We use the cache to store updated app engines, so refuse to update if the
  1.4579 +    // cache is disabled.
  1.4580 +    if (engine._readOnly &&
  1.4581 +        !getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true))
  1.4582 +      return;
  1.4583 +
  1.4584 +    let testEngine = null;
  1.4585 +    let updateURL = engine._getURLOfType(URLTYPE_OPENSEARCH);
  1.4586 +    let updateURI = (updateURL && updateURL._hasRelation("self")) ? 
  1.4587 +                     updateURL.getSubmission("", engine).uri :
  1.4588 +                     makeURI(engine._updateURL);
  1.4589 +    if (updateURI) {
  1.4590 +      if (engine._isDefault && !updateURI.schemeIs("https")) {
  1.4591 +        ULOG("Invalid scheme for default engine update");
  1.4592 +        return;
  1.4593 +      }
  1.4594 +
  1.4595 +      let dataType = engineMetadataService.getAttr(engine, "updatedatatype");
  1.4596 +      if (!dataType) {
  1.4597 +        ULOG("No loadtype to update engine!");
  1.4598 +        return;
  1.4599 +      }
  1.4600 +
  1.4601 +      ULOG("updating " + engine.name + " from " + updateURI.spec);
  1.4602 +      testEngine = new Engine(updateURI, dataType, false);
  1.4603 +      testEngine._engineToUpdate = engine;
  1.4604 +      testEngine._initFromURIAndLoad();
  1.4605 +    } else
  1.4606 +      ULOG("invalid updateURI");
  1.4607 +
  1.4608 +    if (engine._iconUpdateURL) {
  1.4609 +      // If we're updating the engine too, use the new engine object,
  1.4610 +      // otherwise use the existing engine object.
  1.4611 +      (testEngine || engine)._setIcon(engine._iconUpdateURL, true);
  1.4612 +    }
  1.4613 +  }
  1.4614 +};
  1.4615 +
  1.4616 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SearchService]);
  1.4617 +
  1.4618 +#include ../../../toolkit/modules/debug.js

mercurial