toolkit/components/search/nsSearchService.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 # This Source Code Form is subject to the terms of the Mozilla Public
     2 # License, v. 2.0. If a copy of the MPL was not distributed with this
     3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5 const Ci = Components.interfaces;
     6 const Cc = Components.classes;
     7 const Cr = Components.results;
     8 const Cu = Components.utils;
    10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    11 Components.utils.import("resource://gre/modules/Services.jsm");
    12 Components.utils.import("resource://gre/modules/Promise.jsm");
    14 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
    15   "resource://gre/modules/AsyncShutdown.jsm");
    16 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
    17   "resource://gre/modules/DeferredTask.jsm");
    18 XPCOMUtils.defineLazyModuleGetter(this, "OS",
    19   "resource://gre/modules/osfile.jsm");
    20 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    21   "resource://gre/modules/Task.jsm");
    22 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
    23   "resource://gre/modules/TelemetryStopwatch.jsm");
    25 // A text encoder to UTF8, used whenever we commit the
    26 // engine metadata to disk.
    27 XPCOMUtils.defineLazyGetter(this, "gEncoder",
    28                             function() {
    29                               return new TextEncoder();
    30                             });
    32 const PERMS_FILE      = 0644;
    33 const PERMS_DIRECTORY = 0755;
    35 const MODE_RDONLY   = 0x01;
    36 const MODE_WRONLY   = 0x02;
    37 const MODE_CREATE   = 0x08;
    38 const MODE_APPEND   = 0x10;
    39 const MODE_TRUNCATE = 0x20;
    41 // Directory service keys
    42 const NS_APP_SEARCH_DIR_LIST  = "SrchPluginsDL";
    43 const NS_APP_USER_SEARCH_DIR  = "UsrSrchPlugns";
    44 const NS_APP_SEARCH_DIR       = "SrchPlugns";
    45 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
    47 // Search engine "locations". If this list is changed, be sure to update
    48 // the engine's _isDefault function accordingly.
    49 const SEARCH_APP_DIR = 1;
    50 const SEARCH_PROFILE_DIR = 2;
    51 const SEARCH_IN_EXTENSION = 3;
    52 const SEARCH_JAR = 4;
    54 // See documentation in nsIBrowserSearchService.idl.
    55 const SEARCH_ENGINE_TOPIC        = "browser-search-engine-modified";
    56 const QUIT_APPLICATION_TOPIC     = "quit-application";
    58 const SEARCH_ENGINE_REMOVED      = "engine-removed";
    59 const SEARCH_ENGINE_ADDED        = "engine-added";
    60 const SEARCH_ENGINE_CHANGED      = "engine-changed";
    61 const SEARCH_ENGINE_LOADED       = "engine-loaded";
    62 const SEARCH_ENGINE_CURRENT      = "engine-current";
    63 const SEARCH_ENGINE_DEFAULT      = "engine-default";
    65 // The following constants are left undocumented in nsIBrowserSearchService.idl
    66 // For the moment, they are meant for testing/debugging purposes only.
    68 /**
    69  * Topic used for events involving the service itself.
    70  */
    71 const SEARCH_SERVICE_TOPIC       = "browser-search-service";
    73 /**
    74  * Sent whenever metadata is fully written to disk.
    75  */
    76 const SEARCH_SERVICE_METADATA_WRITTEN  = "write-metadata-to-disk-complete";
    78 /**
    79  * Sent whenever the cache is fully written to disk.
    80  */
    81 const SEARCH_SERVICE_CACHE_WRITTEN  = "write-cache-to-disk-complete";
    83 const SEARCH_TYPE_MOZSEARCH      = Ci.nsISearchEngine.TYPE_MOZSEARCH;
    84 const SEARCH_TYPE_OPENSEARCH     = Ci.nsISearchEngine.TYPE_OPENSEARCH;
    85 const SEARCH_TYPE_SHERLOCK       = Ci.nsISearchEngine.TYPE_SHERLOCK;
    87 const SEARCH_DATA_XML            = Ci.nsISearchEngine.DATA_XML;
    88 const SEARCH_DATA_TEXT           = Ci.nsISearchEngine.DATA_TEXT;
    90 // Delay for lazy serialization (ms)
    91 const LAZY_SERIALIZE_DELAY = 100;
    93 // Delay for batching invalidation of the JSON cache (ms)
    94 const CACHE_INVALIDATION_DELAY = 1000;
    96 // Current cache version. This should be incremented if the format of the cache
    97 // file is modified.
    98 const CACHE_VERSION = 7;
   100 const ICON_DATAURL_PREFIX = "data:image/x-icon;base64,";
   102 const NEW_LINES = /(\r\n|\r|\n)/;
   104 // Set an arbitrary cap on the maximum icon size. Without this, large icons can
   105 // cause big delays when loading them at startup.
   106 const MAX_ICON_SIZE   = 10000;
   108 // Default charset to use for sending search parameters. ISO-8859-1 is used to
   109 // match previous nsInternetSearchService behavior.
   110 const DEFAULT_QUERY_CHARSET = "ISO-8859-1";
   112 const SEARCH_BUNDLE = "chrome://global/locale/search/search.properties";
   113 const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
   115 const OPENSEARCH_NS_10  = "http://a9.com/-/spec/opensearch/1.0/";
   116 const OPENSEARCH_NS_11  = "http://a9.com/-/spec/opensearch/1.1/";
   118 // Although the specification at http://opensearch.a9.com/spec/1.1/description/
   119 // gives the namespace names defined above, many existing OpenSearch engines
   120 // are using the following versions.  We therefore allow either.
   121 const OPENSEARCH_NAMESPACES = [
   122   OPENSEARCH_NS_11, OPENSEARCH_NS_10,
   123   "http://a9.com/-/spec/opensearchdescription/1.1/",
   124   "http://a9.com/-/spec/opensearchdescription/1.0/"
   125 ];
   127 const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
   129 const MOZSEARCH_NS_10     = "http://www.mozilla.org/2006/browser/search/";
   130 const MOZSEARCH_LOCALNAME = "SearchPlugin";
   132 const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
   133 const URLTYPE_SEARCH_HTML  = "text/html";
   134 const URLTYPE_OPENSEARCH   = "application/opensearchdescription+xml";
   136 // Empty base document used to serialize engines to file.
   137 const EMPTY_DOC = "<?xml version=\"1.0\"?>\n" +
   138                   "<" + MOZSEARCH_LOCALNAME +
   139                   " xmlns=\"" + MOZSEARCH_NS_10 + "\"" +
   140                   " xmlns:os=\"" + OPENSEARCH_NS_11 + "\"" +
   141                   "/>";
   143 const BROWSER_SEARCH_PREF = "browser.search.";
   145 const USER_DEFINED = "{searchTerms}";
   147 // Custom search parameters
   148 #ifdef MOZ_OFFICIAL_BRANDING
   149 const MOZ_OFFICIAL = "official";
   150 #else
   151 const MOZ_OFFICIAL = "unofficial";
   152 #endif
   153 #expand const MOZ_DISTRIBUTION_ID = __MOZ_DISTRIBUTION_ID__;
   155 const MOZ_PARAM_LOCALE         = /\{moz:locale\}/g;
   156 const MOZ_PARAM_DIST_ID        = /\{moz:distributionID\}/g;
   157 const MOZ_PARAM_OFFICIAL       = /\{moz:official\}/g;
   159 // Supported OpenSearch parameters
   160 // See http://opensearch.a9.com/spec/1.1/querysyntax/#core
   161 const OS_PARAM_USER_DEFINED    = /\{searchTerms\??\}/g;
   162 const OS_PARAM_INPUT_ENCODING  = /\{inputEncoding\??\}/g;
   163 const OS_PARAM_LANGUAGE        = /\{language\??\}/g;
   164 const OS_PARAM_OUTPUT_ENCODING = /\{outputEncoding\??\}/g;
   166 // Default values
   167 const OS_PARAM_LANGUAGE_DEF         = "*";
   168 const OS_PARAM_OUTPUT_ENCODING_DEF  = "UTF-8";
   169 const OS_PARAM_INPUT_ENCODING_DEF   = "UTF-8";
   171 // "Unsupported" OpenSearch parameters. For example, we don't support
   172 // page-based results, so if the engine requires that we send the "page index"
   173 // parameter, we'll always send "1".
   174 const OS_PARAM_COUNT        = /\{count\??\}/g;
   175 const OS_PARAM_START_INDEX  = /\{startIndex\??\}/g;
   176 const OS_PARAM_START_PAGE   = /\{startPage\??\}/g;
   178 // Default values
   179 const OS_PARAM_COUNT_DEF        = "20"; // 20 results
   180 const OS_PARAM_START_INDEX_DEF  = "1";  // start at 1st result
   181 const OS_PARAM_START_PAGE_DEF   = "1";  // 1st page
   183 // Optional parameter
   184 const OS_PARAM_OPTIONAL     = /\{(?:\w+:)?\w+\?\}/g;
   186 // A array of arrays containing parameters that we don't fully support, and
   187 // their default values. We will only send values for these parameters if
   188 // required, since our values are just really arbitrary "guesses" that should
   189 // give us the output we want.
   190 var OS_UNSUPPORTED_PARAMS = [
   191   [OS_PARAM_COUNT, OS_PARAM_COUNT_DEF],
   192   [OS_PARAM_START_INDEX, OS_PARAM_START_INDEX_DEF],
   193   [OS_PARAM_START_PAGE, OS_PARAM_START_PAGE_DEF],
   194 ];
   196 // The default engine update interval, in days. This is only used if an engine
   197 // specifies an updateURL, but not an updateInterval.
   198 const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
   200 // Returns false for whitespace-only or commented out lines in a
   201 // Sherlock file, true otherwise.
   202 function isUsefulLine(aLine) {
   203   return !(/^\s*($|#)/i.test(aLine));
   204 }
   206 this.__defineGetter__("FileUtils", function() {
   207   delete this.FileUtils;
   208   Components.utils.import("resource://gre/modules/FileUtils.jsm");
   209   return FileUtils;
   210 });
   212 this.__defineGetter__("NetUtil", function() {
   213   delete this.NetUtil;
   214   Components.utils.import("resource://gre/modules/NetUtil.jsm");
   215   return NetUtil;
   216 });
   218 this.__defineGetter__("gChromeReg", function() {
   219   delete this.gChromeReg;
   220   return this.gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
   221                            getService(Ci.nsIChromeRegistry);
   222 });
   224 /**
   225  * Prefixed to all search debug output.
   226  */
   227 const SEARCH_LOG_PREFIX = "*** Search: ";
   229 /**
   230  * Outputs aText to the JavaScript console as well as to stdout.
   231  */
   232 function DO_LOG(aText) {
   233   dump(SEARCH_LOG_PREFIX + aText + "\n");
   234   Services.console.logStringMessage(aText);
   235 }
   237 #ifdef DEBUG
   238 /**
   239  * In debug builds, use a live, pref-based (browser.search.log) LOG function
   240  * to allow enabling/disabling without a restart.
   241  */
   242 function PREF_LOG(aText) {
   243   if (getBoolPref(BROWSER_SEARCH_PREF + "log", false))
   244     DO_LOG(aText);
   245 }
   246 var LOG = PREF_LOG;
   248 #else
   250 /**
   251  * Otherwise, don't log at all by default. This can be overridden at startup
   252  * by the pref, see SearchService's _init method.
   253  */
   254 var LOG = function(){};
   256 #endif
   258 /**
   259  * Presents an assertion dialog in non-release builds and throws.
   260  * @param  message
   261  *         A message to display
   262  * @param  resultCode
   263  *         The NS_ERROR_* value to throw.
   264  * @throws resultCode
   265  */
   266 function ERROR(message, resultCode) {
   267   NS_ASSERT(false, SEARCH_LOG_PREFIX + message);
   268   throw Components.Exception(message, resultCode);
   269 }
   271 /**
   272  * Logs the failure message (if browser.search.log is enabled) and throws.
   273  * @param  message
   274  *         A message to display
   275  * @param  resultCode
   276  *         The NS_ERROR_* value to throw.
   277  * @throws resultCode or NS_ERROR_INVALID_ARG if resultCode isn't specified.
   278  */
   279 function FAIL(message, resultCode) {
   280   LOG(message);
   281   throw Components.Exception(message, resultCode || Cr.NS_ERROR_INVALID_ARG);
   282 }
   284 /**
   285  * Truncates big blobs of (data-)URIs to console-friendly sizes
   286  * @param str
   287  *        String to tone down
   288  * @param len
   289  *        Maximum length of the string to return. Defaults to the length of a tweet.
   290  */
   291 function limitURILength(str, len) {
   292   len = len || 140;
   293   if (str.length > len)
   294     return str.slice(0, len) + "...";
   295   return str;
   296 }
   298 /**
   299  * Utilities for dealing with promises and Task.jsm
   300  */
   301 const TaskUtils = {
   302   /**
   303    * Add logging to a promise.
   304    *
   305    * @param {Promise} promise
   306    * @return {Promise} A promise behaving as |promise|, but with additional
   307    * logging in case of uncaught error.
   308    */
   309   captureErrors: function captureErrors(promise) {
   310     return promise.then(
   311       null,
   312       function onError(reason) {
   313         LOG("Uncaught asynchronous error: " + reason + " at\n" + reason.stack);
   314         throw reason;
   315       }
   316     );
   317   },
   318   /**
   319    * Spawn a new Task from a generator.
   320    *
   321    * This function behaves as |Task.spawn|, with the exception that it
   322    * adds logging in case of uncaught error. For more information, see
   323    * the documentation of |Task.jsm|.
   324    *
   325    * @param {generator} gen Some generator.
   326    * @return {Promise} A promise built from |gen|, with the same semantics
   327    * as |Task.spawn(gen)|.
   328    */
   329   spawn: function spawn(gen) {
   330     return this.captureErrors(Task.spawn(gen));
   331   },
   332   /**
   333    * Execute a mozIStorage statement asynchronously, wrapping the
   334    * result in a promise.
   335    *
   336    * @param {mozIStorageStaement} statement A statement to be executed
   337    * asynchronously. The semantics are the same as these of |statement.execute|.
   338    * @param {function*} onResult A callback, called for each successive result.
   339    *
   340    * @return {Promise} A promise, resolved successfully if |statement.execute|
   341    * succeeds, rejected if it fails.
   342    */
   343   executeStatement: function executeStatement(statement, onResult) {
   344     let deferred = Promise.defer();
   345     onResult = onResult || function() {};
   346     statement.executeAsync({
   347       handleResult: onResult,
   348       handleError: function handleError(aError) {
   349         deferred.reject(aError);
   350       },
   351       handleCompletion: function handleCompletion(aReason) {
   352         statement.finalize();
   353         // Note that, in case of error, deferred.reject(aError)
   354         // has already been called by this point, so the call to
   355         // |deferred.resolve| is simply ignored.
   356         deferred.resolve(aReason);
   357       }
   358     });
   359     return deferred.promise;
   360   }
   361 };
   363 /**
   364  * Ensures an assertion is met before continuing. Should be used to indicate
   365  * fatal errors.
   366  * @param  assertion
   367  *         An assertion that must be met
   368  * @param  message
   369  *         A message to display if the assertion is not met
   370  * @param  resultCode
   371  *         The NS_ERROR_* value to throw if the assertion is not met
   372  * @throws resultCode
   373  */
   374 function ENSURE_WARN(assertion, message, resultCode) {
   375   NS_ASSERT(assertion, SEARCH_LOG_PREFIX + message);
   376   if (!assertion)
   377     throw Components.Exception(message, resultCode);
   378 }
   380 function loadListener(aChannel, aEngine, aCallback) {
   381   this._channel = aChannel;
   382   this._bytes = [];
   383   this._engine = aEngine;
   384   this._callback = aCallback;
   385 }
   386 loadListener.prototype = {
   387   _callback: null,
   388   _channel: null,
   389   _countRead: 0,
   390   _engine: null,
   391   _stream: null,
   393   QueryInterface: function SRCH_loadQI(aIID) {
   394     if (aIID.equals(Ci.nsISupports)           ||
   395         aIID.equals(Ci.nsIRequestObserver)    ||
   396         aIID.equals(Ci.nsIStreamListener)     ||
   397         aIID.equals(Ci.nsIChannelEventSink)   ||
   398         aIID.equals(Ci.nsIInterfaceRequestor) ||
   399         // See FIXME comment below
   400         aIID.equals(Ci.nsIHttpEventSink)      ||
   401         aIID.equals(Ci.nsIProgressEventSink)  ||
   402         false)
   403       return this;
   405     throw Cr.NS_ERROR_NO_INTERFACE;
   406   },
   408   // nsIRequestObserver
   409   onStartRequest: function SRCH_loadStartR(aRequest, aContext) {
   410     LOG("loadListener: Starting request: " + aRequest.name);
   411     this._stream = Cc["@mozilla.org/binaryinputstream;1"].
   412                    createInstance(Ci.nsIBinaryInputStream);
   413   },
   415   onStopRequest: function SRCH_loadStopR(aRequest, aContext, aStatusCode) {
   416     LOG("loadListener: Stopping request: " + aRequest.name);
   418     var requestFailed = !Components.isSuccessCode(aStatusCode);
   419     if (!requestFailed && (aRequest instanceof Ci.nsIHttpChannel))
   420       requestFailed = !aRequest.requestSucceeded;
   422     if (requestFailed || this._countRead == 0) {
   423       LOG("loadListener: request failed!");
   424       // send null so the callback can deal with the failure
   425       this._callback(null, this._engine);
   426     } else
   427       this._callback(this._bytes, this._engine);
   428     this._channel = null;
   429     this._engine  = null;
   430   },
   432   // nsIStreamListener
   433   onDataAvailable: function SRCH_loadDAvailable(aRequest, aContext,
   434                                                 aInputStream, aOffset,
   435                                                 aCount) {
   436     this._stream.setInputStream(aInputStream);
   438     // Get a byte array of the data
   439     this._bytes = this._bytes.concat(this._stream.readByteArray(aCount));
   440     this._countRead += aCount;
   441   },
   443   // nsIChannelEventSink
   444   asyncOnChannelRedirect: function SRCH_loadCRedirect(aOldChannel, aNewChannel,
   445                                                       aFlags, callback) {
   446     this._channel = aNewChannel;
   447     callback.onRedirectVerifyCallback(Components.results.NS_OK);
   448   },
   450   // nsIInterfaceRequestor
   451   getInterface: function SRCH_load_GI(aIID) {
   452     return this.QueryInterface(aIID);
   453   },
   455   // FIXME: bug 253127
   456   // nsIHttpEventSink
   457   onRedirect: function (aChannel, aNewChannel) {},
   458   // nsIProgressEventSink
   459   onProgress: function (aRequest, aContext, aProgress, aProgressMax) {},
   460   onStatus: function (aRequest, aContext, aStatus, aStatusArg) {}
   461 }
   464 /**
   465  * Used to verify a given DOM node's localName and namespaceURI.
   466  * @param aElement
   467  *        The element to verify.
   468  * @param aLocalNameArray
   469  *        An array of strings to compare against aElement's localName.
   470  * @param aNameSpaceArray
   471  *        An array of strings to compare against aElement's namespaceURI.
   472  *
   473  * @returns false if aElement is null, or if its localName or namespaceURI
   474  *          does not match one of the elements in the aLocalNameArray or
   475  *          aNameSpaceArray arrays, respectively.
   476  * @throws NS_ERROR_INVALID_ARG if aLocalNameArray or aNameSpaceArray are null.
   477  */
   478 function checkNameSpace(aElement, aLocalNameArray, aNameSpaceArray) {
   479   if (!aLocalNameArray || !aNameSpaceArray)
   480     FAIL("missing aLocalNameArray or aNameSpaceArray for checkNameSpace");
   481   return (aElement                                                &&
   482           (aLocalNameArray.indexOf(aElement.localName)    != -1)  &&
   483           (aNameSpaceArray.indexOf(aElement.namespaceURI) != -1));
   484 }
   486 /**
   487  * Safely close a nsISafeOutputStream.
   488  * @param aFOS
   489  *        The file output stream to close.
   490  */
   491 function closeSafeOutputStream(aFOS) {
   492   if (aFOS instanceof Ci.nsISafeOutputStream) {
   493     try {
   494       aFOS.finish();
   495       return;
   496     } catch (e) { }
   497   }
   498   aFOS.close();
   499 }
   501 /**
   502  * Wrapper function for nsIIOService::newURI.
   503  * @param aURLSpec
   504  *        The URL string from which to create an nsIURI.
   505  * @returns an nsIURI object, or null if the creation of the URI failed.
   506  */
   507 function makeURI(aURLSpec, aCharset) {
   508   try {
   509     return NetUtil.newURI(aURLSpec, aCharset);
   510   } catch (ex) { }
   512   return null;
   513 }
   515 /**
   516  * Gets a directory from the directory service.
   517  * @param aKey
   518  *        The directory service key indicating the directory to get.
   519  */
   520 function getDir(aKey, aIFace) {
   521   if (!aKey)
   522     FAIL("getDir requires a directory key!");
   524   return Services.dirsvc.get(aKey, aIFace || Ci.nsIFile);
   525 }
   527 /**
   528  * The following two functions are essentially copied from
   529  * nsInternetSearchService. They are required for backwards compatibility.
   530  */
   531 function queryCharsetFromCode(aCode) {
   532   const codes = [];
   533   codes[0] = "macintosh";
   534   codes[6] = "x-mac-greek";
   535   codes[35] = "x-mac-turkish";
   536   codes[513] = "ISO-8859-1";
   537   codes[514] = "ISO-8859-2";
   538   codes[517] = "ISO-8859-5";
   539   codes[518] = "ISO-8859-6";
   540   codes[519] = "ISO-8859-7";
   541   codes[520] = "ISO-8859-8";
   542   codes[521] = "ISO-8859-9";
   543   codes[1280] = "windows-1252";
   544   codes[1281] = "windows-1250";
   545   codes[1282] = "windows-1251";
   546   codes[1283] = "windows-1253";
   547   codes[1284] = "windows-1254";
   548   codes[1285] = "windows-1255";
   549   codes[1286] = "windows-1256";
   550   codes[1536] = "us-ascii";
   551   codes[1584] = "GB2312";
   552   codes[1585] = "gbk";
   553   codes[1600] = "EUC-KR";
   554   codes[2080] = "ISO-2022-JP";
   555   codes[2096] = "ISO-2022-CN";
   556   codes[2112] = "ISO-2022-KR";
   557   codes[2336] = "EUC-JP";
   558   codes[2352] = "GB2312";
   559   codes[2353] = "x-euc-tw";
   560   codes[2368] = "EUC-KR";
   561   codes[2561] = "Shift_JIS";
   562   codes[2562] = "KOI8-R";
   563   codes[2563] = "Big5";
   564   codes[2565] = "HZ-GB-2312";
   566   if (codes[aCode])
   567     return codes[aCode];
   569   // Don't bother being fancy about what to return in the failure case.
   570   return "windows-1252";
   571 }
   572 function fileCharsetFromCode(aCode) {
   573   const codes = [
   574     "macintosh",             // 0
   575     "Shift_JIS",             // 1
   576     "Big5",                  // 2
   577     "EUC-KR",                // 3
   578     "X-MAC-ARABIC",          // 4
   579     "X-MAC-HEBREW",          // 5
   580     "X-MAC-GREEK",           // 6
   581     "X-MAC-CYRILLIC",        // 7
   582     "X-MAC-DEVANAGARI" ,     // 9
   583     "X-MAC-GURMUKHI",        // 10
   584     "X-MAC-GUJARATI",        // 11
   585     "X-MAC-ORIYA",           // 12
   586     "X-MAC-BENGALI",         // 13
   587     "X-MAC-TAMIL",           // 14
   588     "X-MAC-TELUGU",          // 15
   589     "X-MAC-KANNADA",         // 16
   590     "X-MAC-MALAYALAM",       // 17
   591     "X-MAC-SINHALESE",       // 18
   592     "X-MAC-BURMESE",         // 19
   593     "X-MAC-KHMER",           // 20
   594     "X-MAC-THAI",            // 21
   595     "X-MAC-LAOTIAN",         // 22
   596     "X-MAC-GEORGIAN",        // 23
   597     "X-MAC-ARMENIAN",        // 24
   598     "GB2312",                // 25
   599     "X-MAC-TIBETAN",         // 26
   600     "X-MAC-MONGOLIAN",       // 27
   601     "X-MAC-ETHIOPIC",        // 28
   602     "X-MAC-CENTRALEURROMAN", // 29
   603     "X-MAC-VIETNAMESE",      // 30
   604     "X-MAC-EXTARABIC"        // 31
   605   ];
   606   // Sherlock files have always defaulted to macintosh, so do that here too
   607   return codes[aCode] || codes[0];
   608 }
   610 /**
   611  * Returns a string interpretation of aBytes using aCharset, or null on
   612  * failure.
   613  */
   614 function bytesToString(aBytes, aCharset) {
   615   var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
   616                   createInstance(Ci.nsIScriptableUnicodeConverter);
   617   LOG("bytesToString: converting using charset: " + aCharset);
   619   try {
   620     converter.charset = aCharset;
   621     return converter.convertFromByteArray(aBytes, aBytes.length);
   622   } catch (ex) {}
   624   return null;
   625 }
   627 /**
   628  * Converts an array of bytes representing a Sherlock file into an array of
   629  * lines representing the useful data from the file.
   630  *
   631  * @param aBytes
   632  *        The array of bytes representing the Sherlock file.
   633  * @param aCharsetCode
   634  *        An integer value representing a character set code to be passed to
   635  *        fileCharsetFromCode, or null for the default Sherlock encoding.
   636  */
   637 function sherlockBytesToLines(aBytes, aCharsetCode) {
   638   // fileCharsetFromCode returns the default encoding if aCharsetCode is null
   639   var charset = fileCharsetFromCode(aCharsetCode);
   641   var dataString = bytesToString(aBytes, charset);
   642   if (!dataString)
   643     FAIL("sherlockBytesToLines: Couldn't convert byte array!", Cr.NS_ERROR_FAILURE);
   645   // Split the string into lines, and filter out comments and
   646   // whitespace-only lines
   647   return dataString.split(NEW_LINES).filter(isUsefulLine);
   648 }
   650 /**
   651  * Gets the current value of the locale.  It's possible for this preference to
   652  * be localized, so we have to do a little extra work here.  Similar code
   653  * exists in nsHttpHandler.cpp when building the UA string.
   654  */
   655 function getLocale() {
   656   const localePref = "general.useragent.locale";
   657   var locale = getLocalizedPref(localePref);
   658   if (locale)
   659     return locale;
   661   // Not localized
   662   return Services.prefs.getCharPref(localePref);
   663 }
   665 /**
   666  * Wrapper for nsIPrefBranch::getComplexValue.
   667  * @param aPrefName
   668  *        The name of the pref to get.
   669  * @returns aDefault if the requested pref doesn't exist.
   670  */
   671 function getLocalizedPref(aPrefName, aDefault) {
   672   const nsIPLS = Ci.nsIPrefLocalizedString;
   673   try {
   674     return Services.prefs.getComplexValue(aPrefName, nsIPLS).data;
   675   } catch (ex) {}
   677   return aDefault;
   678 }
   680 /**
   681  * Wrapper for nsIPrefBranch::setComplexValue.
   682  * @param aPrefName
   683  *        The name of the pref to set.
   684  */
   685 function setLocalizedPref(aPrefName, aValue) {
   686   const nsIPLS = Ci.nsIPrefLocalizedString;
   687   try {
   688     var pls = Components.classes["@mozilla.org/pref-localizedstring;1"]
   689                         .createInstance(Ci.nsIPrefLocalizedString);
   690     pls.data = aValue;
   691     Services.prefs.setComplexValue(aPrefName, nsIPLS, pls);
   692   } catch (ex) {}
   693 }
   695 /**
   696  * Wrapper for nsIPrefBranch::getBoolPref.
   697  * @param aPrefName
   698  *        The name of the pref to get.
   699  * @returns aDefault if the requested pref doesn't exist.
   700  */
   701 function getBoolPref(aName, aDefault) {
   702   try {
   703     return Services.prefs.getBoolPref(aName);
   704   } catch (ex) {
   705     return aDefault;
   706   }
   707 }
   709 /**
   710  * Get a unique nsIFile object with a sanitized name, based on the engine name.
   711  * @param aName
   712  *        A name to "sanitize". Can be an empty string, in which case a random
   713  *        8 character filename will be produced.
   714  * @returns A nsIFile object in the user's search engines directory with a
   715  *          unique sanitized name.
   716  */
   717 function getSanitizedFile(aName) {
   718   var fileName = sanitizeName(aName) + ".xml";
   719   var file = getDir(NS_APP_USER_SEARCH_DIR);
   720   file.append(fileName);
   721   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
   722   return file;
   723 }
   725 /**
   726  * @return a sanitized name to be used as a filename, or a random name
   727  *         if a sanitized name cannot be obtained (if aName contains
   728  *         no valid characters).
   729  */
   730 function sanitizeName(aName) {
   731   const maxLength = 60;
   732   const minLength = 1;
   733   var name = aName.toLowerCase();
   734   name = name.replace(/\s+/g, "-");
   735   name = name.replace(/[^-a-z0-9]/g, "");
   737   // Use a random name if our input had no valid characters.
   738   if (name.length < minLength)
   739     name = Math.random().toString(36).replace(/^.*\./, '');
   741   // Force max length.
   742   return name.substring(0, maxLength);
   743 }
   745 /**
   746  * Retrieve a pref from the search param branch.
   747  *
   748  * @param prefName
   749  *        The name of the pref.
   750  **/
   751 function getMozParamPref(prefName)
   752   Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "param." + prefName);
   754 /**
   755  * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to
   756  * the state of the search service.
   757  *
   758  * @param aEngine
   759  *        The nsISearchEngine object to which the change applies.
   760  * @param aVerb
   761  *        A verb describing the change.
   762  *
   763  * @see nsIBrowserSearchService.idl
   764  */
   765 let gInitialized = false;
   766 function notifyAction(aEngine, aVerb) {
   767   if (gInitialized) {
   768     LOG("NOTIFY: Engine: \"" + aEngine.name + "\"; Verb: \"" + aVerb + "\"");
   769     Services.obs.notifyObservers(aEngine, SEARCH_ENGINE_TOPIC, aVerb);
   770   }
   771 }
   773 function  parseJsonFromStream(aInputStream) {
   774   const json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
   775   const data = json.decodeFromStream(aInputStream, aInputStream.available());
   776   return data;
   777 }
   779 /**
   780  * Simple object representing a name/value pair.
   781  */
   782 function QueryParameter(aName, aValue, aPurpose) {
   783   if (!aName || (aValue == null))
   784     FAIL("missing name or value for QueryParameter!");
   786   this.name = aName;
   787   this.value = aValue;
   788   this.purpose = aPurpose;
   789 }
   791 /**
   792  * Perform OpenSearch parameter substitution on aParamValue.
   793  *
   794  * @param aParamValue
   795  *        A string containing OpenSearch search parameters.
   796  * @param aSearchTerms
   797  *        The user-provided search terms. This string will inserted into
   798  *        aParamValue as the value of the OS_PARAM_USER_DEFINED parameter.
   799  *        This value must already be escaped appropriately - it is inserted
   800  *        as-is.
   801  * @param aEngine
   802  *        The engine which owns the string being acted on.
   803  *
   804  * @see http://opensearch.a9.com/spec/1.1/querysyntax/#core
   805  */
   806 function ParamSubstitution(aParamValue, aSearchTerms, aEngine) {
   807   var value = aParamValue;
   809   var distributionID = MOZ_DISTRIBUTION_ID;
   810   try {
   811     distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID");
   812   }
   813   catch (ex) { }
   814   var official = MOZ_OFFICIAL;
   815   try {
   816     if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official"))
   817       official = "official";
   818     else
   819       official = "unofficial";
   820   }
   821   catch (ex) { }
   823   // Custom search parameters. These are only available to default search
   824   // engines.
   825   if (aEngine._isDefault) {
   826     value = value.replace(MOZ_PARAM_LOCALE, getLocale());
   827     value = value.replace(MOZ_PARAM_DIST_ID, distributionID);
   828     value = value.replace(MOZ_PARAM_OFFICIAL, official);
   829   }
   831   // Insert the OpenSearch parameters we're confident about
   832   value = value.replace(OS_PARAM_USER_DEFINED, aSearchTerms);
   833   value = value.replace(OS_PARAM_INPUT_ENCODING, aEngine.queryCharset);
   834   value = value.replace(OS_PARAM_LANGUAGE,
   835                         getLocale() || OS_PARAM_LANGUAGE_DEF);
   836   value = value.replace(OS_PARAM_OUTPUT_ENCODING,
   837                         OS_PARAM_OUTPUT_ENCODING_DEF);
   839   // Replace any optional parameters
   840   value = value.replace(OS_PARAM_OPTIONAL, "");
   842   // Insert any remaining required params with our default values
   843   for (var i = 0; i < OS_UNSUPPORTED_PARAMS.length; ++i) {
   844     value = value.replace(OS_UNSUPPORTED_PARAMS[i][0],
   845                           OS_UNSUPPORTED_PARAMS[i][1]);
   846   }
   848   return value;
   849 }
   851 /**
   852  * Creates an engineURL object, which holds the query URL and all parameters.
   853  *
   854  * @param aType
   855  *        A string containing the name of the MIME type of the search results
   856  *        returned by this URL.
   857  * @param aMethod
   858  *        The HTTP request method. Must be a case insensitive value of either
   859  *        "GET" or "POST".
   860  * @param aTemplate
   861  *        The URL to which search queries should be sent. For GET requests,
   862  *        must contain the string "{searchTerms}", to indicate where the user
   863  *        entered search terms should be inserted.
   864  * @param aResultDomain
   865  *        The root domain for this URL.  Defaults to the template's host.
   866  *
   867  * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
   868  *
   869  * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported.
   870  */
   871 function EngineURL(aType, aMethod, aTemplate, aResultDomain) {
   872   if (!aType || !aMethod || !aTemplate)
   873     FAIL("missing type, method or template for EngineURL!");
   875   var method = aMethod.toUpperCase();
   876   var type   = aType.toLowerCase();
   878   if (method != "GET" && method != "POST")
   879     FAIL("method passed to EngineURL must be \"GET\" or \"POST\"");
   881   this.type     = type;
   882   this.method   = method;
   883   this.params   = [];
   884   this.rels     = [];
   885   // Don't serialize expanded mozparams
   886   this.mozparams = {};
   888   var templateURI = makeURI(aTemplate);
   889   if (!templateURI)
   890     FAIL("new EngineURL: template is not a valid URI!", Cr.NS_ERROR_FAILURE);
   892   switch (templateURI.scheme) {
   893     case "http":
   894     case "https":
   895     // Disable these for now, see bug 295018
   896     // case "file":
   897     // case "resource":
   898       this.template = aTemplate;
   899       break;
   900     default:
   901       FAIL("new EngineURL: template uses invalid scheme!", Cr.NS_ERROR_FAILURE);
   902   }
   904   // If no resultDomain was specified in the engine definition file, use the
   905   // host from the template.
   906   this.resultDomain = aResultDomain || templateURI.host;
   907   // We never want to return a "www." prefix, so eventually strip it.
   908   if (this.resultDomain.startsWith("www.")) {
   909     this.resultDomain = this.resultDomain.substr(4);
   910   }
   911 }
   912 EngineURL.prototype = {
   914   addParam: function SRCH_EURL_addParam(aName, aValue, aPurpose) {
   915     this.params.push(new QueryParameter(aName, aValue, aPurpose));
   916   },
   918   // Note: This method requires that aObj has a unique name or the previous MozParams entry with
   919   // that name will be overwritten.
   920   _addMozParam: function SRCH_EURL__addMozParam(aObj) {
   921     aObj.mozparam = true;
   922     this.mozparams[aObj.name] = aObj;
   923   },
   925   reevalMozParams: function(engine) {
   926     for (let param of this.params) {
   927       let mozparam = this.mozparams[param.name];
   928       if (mozparam && mozparam.positionDependent) {
   929         // the condition is a string in the form of "topN", extract N as int
   930         let positionStr = mozparam.condition.slice("top".length);
   931         let position = parseInt(positionStr, 10);
   932         let engines;
   933         try {
   934           // This will throw if we're not initialized yet (which shouldn't happen), just 
   935           // ignore and move on with the false Value (checking isInitialized also throws)
   936           // XXX
   937           engines = Services.search.getVisibleEngines({});
   938         } catch (ex) {
   939           LOG("reevalMozParams called before search service initialization!?");
   940           break;
   941         }
   942         let index = engines.map((e) => e.wrappedJSObject).indexOf(engine.wrappedJSObject);
   943         let isTopN = index > -1 && (index + 1) <= position;
   944         param.value = isTopN ? mozparam.trueValue : mozparam.falseValue;
   945       }
   946     }
   947   },
   949   getSubmission: function SRCH_EURL_getSubmission(aSearchTerms, aEngine, aPurpose) {
   950     this.reevalMozParams(aEngine);
   952     var url = ParamSubstitution(this.template, aSearchTerms, aEngine);
   953     // Default to an empty string if the purpose is not provided so that default purpose params
   954     // (purpose="") work consistently rather than having to define "null" and "" purposes.
   955     var purpose = aPurpose || "";
   957     // Create an application/x-www-form-urlencoded representation of our params
   958     // (name=value&name=value&name=value)
   959     var dataString = "";
   960     for (var i = 0; i < this.params.length; ++i) {
   961       var param = this.params[i];
   963       // If this parameter has a purpose, only add it if the purpose matches
   964       if (param.purpose !== undefined && param.purpose != purpose)
   965         continue;
   967       var value = ParamSubstitution(param.value, aSearchTerms, aEngine);
   969       dataString += (i > 0 ? "&" : "") + param.name + "=" + value;
   970     }
   972     var postData = null;
   973     if (this.method == "GET") {
   974       // GET method requests have no post data, and append the encoded
   975       // query string to the url...
   976       if (url.indexOf("?") == -1 && dataString)
   977         url += "?";
   978       url += dataString;
   979     } else if (this.method == "POST") {
   980       // POST method requests must wrap the encoded text in a MIME
   981       // stream and supply that as POSTDATA.
   982       var stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
   983                          createInstance(Ci.nsIStringInputStream);
   984       stringStream.data = dataString;
   986       postData = Cc["@mozilla.org/network/mime-input-stream;1"].
   987                  createInstance(Ci.nsIMIMEInputStream);
   988       postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
   989       postData.addContentLength = true;
   990       postData.setData(stringStream);
   991     }
   993     return new Submission(makeURI(url), postData);
   994   },
   996   _hasRelation: function SRC_EURL__hasRelation(aRel)
   997     this.rels.some(function(e) e == aRel.toLowerCase()),
   999   _initWithJSON: function SRC_EURL__initWithJSON(aJson, aEngine) {
  1000     if (!aJson.params)
  1001       return;
  1003     this.rels = aJson.rels;
  1005     for (let i = 0; i < aJson.params.length; ++i) {
  1006       let param = aJson.params[i];
  1007       if (param.mozparam) {
  1008         if (param.condition == "defaultEngine") {
  1009           if (aEngine._isDefaultEngine())
  1010             this.addParam(param.name, param.trueValue);
  1011           else
  1012             this.addParam(param.name, param.falseValue);
  1013         } else if (param.condition == "pref") {
  1014           let value = getMozParamPref(param.pref);
  1015           this.addParam(param.name, value);
  1017         this._addMozParam(param);
  1019       else
  1020         this.addParam(param.name, param.value, param.purpose);
  1022   },
  1024   /**
  1025    * Creates a JavaScript object that represents this URL.
  1026    * @returns An object suitable for serialization as JSON.
  1027    **/
  1028   _serializeToJSON: function SRCH_EURL__serializeToJSON() {
  1029     var json = {
  1030       template: this.template,
  1031       rels: this.rels,
  1032       resultDomain: this.resultDomain
  1033     };
  1035     if (this.type != URLTYPE_SEARCH_HTML)
  1036       json.type = this.type;
  1037     if (this.method != "GET")
  1038       json.method = this.method;
  1040     function collapseMozParams(aParam)
  1041       this.mozparams[aParam.name] || aParam;
  1042     json.params = this.params.map(collapseMozParams, this);
  1044     return json;
  1045   },
  1047   /**
  1048    * Serializes the engine object to a OpenSearch Url element.
  1049    * @param aDoc
  1050    *        The document to use to create the Url element.
  1051    * @param aElement
  1052    *        The element to which the created Url element is appended.
  1054    * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
  1055    */
  1056   _serializeToElement: function SRCH_EURL_serializeToEl(aDoc, aElement) {
  1057     var url = aDoc.createElementNS(OPENSEARCH_NS_11, "Url");
  1058     url.setAttribute("type", this.type);
  1059     url.setAttribute("method", this.method);
  1060     url.setAttribute("template", this.template);
  1061     if (this.rels.length)
  1062       url.setAttribute("rel", this.rels.join(" "));
  1063     if (this.resultDomain)
  1064       url.setAttribute("resultDomain", this.resultDomain);
  1066     for (var i = 0; i < this.params.length; ++i) {
  1067       var param = aDoc.createElementNS(OPENSEARCH_NS_11, "Param");
  1068       param.setAttribute("name", this.params[i].name);
  1069       param.setAttribute("value", this.params[i].value);
  1070       url.appendChild(aDoc.createTextNode("\n  "));
  1071       url.appendChild(param);
  1073     url.appendChild(aDoc.createTextNode("\n"));
  1074     aElement.appendChild(url);
  1076 };
  1078 /**
  1079  * nsISearchEngine constructor.
  1080  * @param aLocation
  1081  *        A nsILocalFile or nsIURI object representing the location of the
  1082  *        search engine data file.
  1083  * @param aSourceDataType
  1084  *        The data type of the file used to describe the engine. Must be either
  1085  *        DATA_XML or DATA_TEXT.
  1086  * @param aIsReadOnly
  1087  *        Boolean indicating whether the engine should be treated as read-only.
  1088  *        Read only engines cannot be serialized to file.
  1089  */
  1090 function Engine(aLocation, aSourceDataType, aIsReadOnly) {
  1091   this._dataType = aSourceDataType;
  1092   this._readOnly = aIsReadOnly;
  1093   this._urls = [];
  1095   if (aLocation.type) {
  1096     if (aLocation.type == "filePath")
  1097       this._file = aLocation.value;
  1098     else if (aLocation.type == "uri")
  1099       this._uri = aLocation.value;
  1100   } else if (aLocation instanceof Ci.nsILocalFile) {
  1101     // we already have a file (e.g. loading engines from disk)
  1102     this._file = aLocation;
  1103   } else if (aLocation instanceof Ci.nsIURI) {
  1104     switch (aLocation.scheme) {
  1105       case "https":
  1106       case "http":
  1107       case "ftp":
  1108       case "data":
  1109       case "file":
  1110       case "resource":
  1111       case "chrome":
  1112         this._uri = aLocation;
  1113         break;
  1114       default:
  1115         ERROR("Invalid URI passed to the nsISearchEngine constructor",
  1116               Cr.NS_ERROR_INVALID_ARG);
  1118   } else
  1119     ERROR("Engine location is neither a File nor a URI object",
  1120           Cr.NS_ERROR_INVALID_ARG);
  1123 Engine.prototype = {
  1124   // The engine's alias (can be null). Initialized to |undefined| to indicate
  1125   // not-initialized-from-engineMetadataService.
  1126   _alias: undefined,
  1127   // A distribution-unique identifier for the engine. Either null or set
  1128   // when loaded. See getter.
  1129   _identifier: undefined,
  1130   // The data describing the engine. Is either an array of bytes, for Sherlock
  1131   // files, or an XML document element, for XML plugins.
  1132   _data: null,
  1133   // The engine's data type. See data types (DATA_) defined above.
  1134   _dataType: null,
  1135   // Whether or not the engine is readonly.
  1136   _readOnly: true,
  1137   // The engine's description
  1138   _description: "",
  1139   // Used to store the engine to replace, if we're an update to an existing
  1140   // engine.
  1141   _engineToUpdate: null,
  1142   // The file from which the plugin was loaded.
  1143   __file: null,
  1144   get _file() {
  1145     if (this.__file && !(this.__file instanceof Ci.nsILocalFile)) {
  1146       let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
  1147       file.persistentDescriptor = this.__file;
  1148       return this.__file = file;
  1150     return this.__file;
  1151   },
  1152   set _file(aValue) {
  1153     this.__file = aValue;
  1154   },
  1155   // Set to true if the engine has a preferred icon (an icon that should not be
  1156   // overridden by a non-preferred icon).
  1157   _hasPreferredIcon: null,
  1158   // Whether the engine is hidden from the user.
  1159   _hidden: null,
  1160   // The engine's name.
  1161   _name: null,
  1162   // The engine type. See engine types (TYPE_) defined above.
  1163   _type: null,
  1164   // The name of the charset used to submit the search terms.
  1165   _queryCharset: null,
  1166   // The engine's raw SearchForm value (URL string pointing to a search form).
  1167   __searchForm: null,
  1168   get _searchForm() {
  1169     return this.__searchForm;
  1170   },
  1171   set _searchForm(aValue) {
  1172     if (/^https?:/i.test(aValue))
  1173       this.__searchForm = aValue;
  1174     else
  1175       LOG("_searchForm: Invalid URL dropped for " + this._name ||
  1176           "the current engine");
  1177   },
  1178   // The URI object from which the engine was retrieved.
  1179   // This is null for engines loaded from disk, but present for engines loaded
  1180   // from chrome:// URIs.
  1181   __uri: null,
  1182   get _uri() {
  1183     if (this.__uri && !(this.__uri instanceof Ci.nsIURI))
  1184       this.__uri = makeURI(this.__uri);
  1186     return this.__uri;
  1187   },
  1188   set _uri(aValue) {
  1189     this.__uri = aValue;
  1190   },
  1191   // Whether to obtain user confirmation before adding the engine. This is only
  1192   // used when the engine is first added to the list.
  1193   _confirm: false,
  1194   // Whether to set this as the current engine as soon as it is loaded.  This
  1195   // is only used when the engine is first added to the list.
  1196   _useNow: false,
  1197   // A function to be invoked when this engine object's addition completes (or
  1198   // fails). Only used for installation via addEngine.
  1199   _installCallback: null,
  1200   // Where the engine was loaded from. Can be one of: SEARCH_APP_DIR,
  1201   // SEARCH_PROFILE_DIR, SEARCH_IN_EXTENSION.
  1202   __installLocation: null,
  1203   // The number of days between update checks for new versions
  1204   _updateInterval: null,
  1205   // The url to check at for a new update
  1206   _updateURL: null,
  1207   // The url to check for a new icon
  1208   _iconUpdateURL: null,
  1209   /* Deferred serialization task. */
  1210   _lazySerializeTask: null,
  1212   /**
  1213    * Retrieves the data from the engine's file. If the engine's dataType is
  1214    * XML, the document element is placed in the engine's data field. For text
  1215    * engines, the data is just read directly from file and placed as an array
  1216    * of lines in the engine's data field.
  1217    */
  1218   _initFromFile: function SRCH_ENG_initFromFile() {
  1219     if (!this._file || !this._file.exists())
  1220       FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED);
  1222     var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"].
  1223                        createInstance(Ci.nsIFileInputStream);
  1225     fileInStream.init(this._file, MODE_RDONLY, PERMS_FILE, false);
  1227     if (this._dataType == SEARCH_DATA_XML) {
  1228       var domParser = Cc["@mozilla.org/xmlextras/domparser;1"].
  1229                       createInstance(Ci.nsIDOMParser);
  1230       var doc = domParser.parseFromStream(fileInStream, "UTF-8",
  1231                                           this._file.fileSize,
  1232                                           "text/xml");
  1234       this._data = doc.documentElement;
  1235     } else {
  1236       ERROR("Unsuppored engine _dataType in _initFromFile: \"" +
  1237             this._dataType + "\"",
  1238             Cr.NS_ERROR_UNEXPECTED);
  1240     fileInStream.close();
  1242     // Now that the data is loaded, initialize the engine object
  1243     this._initFromData();
  1244   },
  1246   /**
  1247    * Retrieves the data from the engine's file asynchronously. If the engine's
  1248    * dataType is XML, the document element is placed in the engine's data field.
  1250    * @returns {Promise} A promise, resolved successfully if initializing from
  1251    * data succeeds, rejected if it fails.
  1252    */
  1253   _asyncInitFromFile: function SRCH_ENG__asyncInitFromFile() {
  1254     return TaskUtils.spawn(function() {
  1255       if (!this._file || !(yield OS.File.exists(this._file.path)))
  1256         FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED);
  1258       if (this._dataType == SEARCH_DATA_XML) {
  1259         let fileURI = NetUtil.ioService.newFileURI(this._file);
  1260         yield this._retrieveSearchXMLData(fileURI.spec);
  1261       } else {
  1262         ERROR("Unsuppored engine _dataType in _initFromFile: \"" +
  1263               this._dataType + "\"",
  1264               Cr.NS_ERROR_UNEXPECTED);
  1267       // Now that the data is loaded, initialize the engine object
  1268       this._initFromData();
  1269     }.bind(this));
  1270   },
  1272   /**
  1273    * Retrieves the engine data from a URI. Initializes the engine, flushes to
  1274    * disk, and notifies the search service once initialization is complete.
  1275    */
  1276   _initFromURIAndLoad: function SRCH_ENG_initFromURIAndLoad() {
  1277     ENSURE_WARN(this._uri instanceof Ci.nsIURI,
  1278                 "Must have URI when calling _initFromURIAndLoad!",
  1279                 Cr.NS_ERROR_UNEXPECTED);
  1281     LOG("_initFromURIAndLoad: Downloading engine from: \"" + this._uri.spec + "\".");
  1283     var chan = NetUtil.ioService.newChannelFromURI(this._uri);
  1285     if (this._engineToUpdate && (chan instanceof Ci.nsIHttpChannel)) {
  1286       var lastModified = engineMetadataService.getAttr(this._engineToUpdate,
  1287                                                        "updatelastmodified");
  1288       if (lastModified)
  1289         chan.setRequestHeader("If-Modified-Since", lastModified, false);
  1291     var listener = new loadListener(chan, this, this._onLoad);
  1292     chan.notificationCallbacks = listener;
  1293     chan.asyncOpen(listener, null);
  1294   },
  1296   /**
  1297    * Retrieves the engine data from a URI asynchronously and initializes it.
  1299    * @returns {Promise} A promise, resolved successfully if retrieveing data
  1300    * succeeds.
  1301    */
  1302   _asyncInitFromURI: function SRCH_ENG__asyncInitFromURI() {
  1303     return TaskUtils.spawn(function() {
  1304       LOG("_asyncInitFromURI: Loading engine from: \"" + this._uri.spec + "\".");
  1305       yield this._retrieveSearchXMLData(this._uri.spec);
  1306       // Now that the data is loaded, initialize the engine object
  1307       this._initFromData();
  1308     }.bind(this));
  1309   },
  1311   /**
  1312    * Retrieves the engine data for a given URI asynchronously.
  1314    * @returns {Promise} A promise, resolved successfully if retrieveing data
  1315    * succeeds.
  1316    */
  1317   _retrieveSearchXMLData: function SRCH_ENG__retrieveSearchXMLData(aURL) {
  1318     let deferred = Promise.defer();
  1319     let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
  1320                     createInstance(Ci.nsIXMLHttpRequest);
  1321     request.overrideMimeType("text/xml");
  1322     request.onload = (aEvent) => {
  1323       let responseXML = aEvent.target.responseXML;
  1324       this._data = responseXML.documentElement;
  1325       deferred.resolve();
  1326     };
  1327     request.onerror = function(aEvent) {
  1328       deferred.resolve();
  1329     };
  1330     request.open("GET", aURL, true);
  1331     request.send();
  1333     return deferred.promise;
  1334   },
  1336   _initFromURISync: function SRCH_ENG_initFromURISync() {
  1337     ENSURE_WARN(this._uri instanceof Ci.nsIURI,
  1338                 "Must have URI when calling _initFromURISync!",
  1339                 Cr.NS_ERROR_UNEXPECTED);
  1341     ENSURE_WARN(this._uri.schemeIs("chrome"), "_initFromURISync called for non-chrome URI",
  1342                 Cr.NS_ERROR_FAILURE);
  1344     LOG("_initFromURISync: Loading engine from: \"" + this._uri.spec + "\".");
  1346     var chan = NetUtil.ioService.newChannelFromURI(this._uri);
  1348     var stream = chan.open();
  1349     var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
  1350                  createInstance(Ci.nsIDOMParser);
  1351     var doc = parser.parseFromStream(stream, "UTF-8", stream.available(), "text/xml");
  1353     this._data = doc.documentElement;
  1355     // Now that the data is loaded, initialize the engine object
  1356     this._initFromData();
  1357   },
  1359   /**
  1360    * Attempts to find an EngineURL object in the set of EngineURLs for
  1361    * this Engine that has the given type string.  (This corresponds to the
  1362    * "type" attribute in the "Url" node in the OpenSearch spec.)
  1363    * This method will return the first matching URL object found, or null
  1364    * if no matching URL is found.
  1366    * @param aType string to match the EngineURL's type attribute
  1367    * @param aRel [optional] only return URLs that with this rel value
  1368    */
  1369   _getURLOfType: function SRCH_ENG__getURLOfType(aType, aRel) {
  1370     for (var i = 0; i < this._urls.length; ++i) {
  1371       if (this._urls[i].type == aType && (!aRel || this._urls[i]._hasRelation(aRel)))
  1372         return this._urls[i];
  1375     return null;
  1376   },
  1378   _confirmAddEngine: function SRCH_SVC_confirmAddEngine() {
  1379     var stringBundle = Services.strings.createBundle(SEARCH_BUNDLE);
  1380     var titleMessage = stringBundle.GetStringFromName("addEngineConfirmTitle");
  1382     // Display only the hostname portion of the URL.
  1383     var dialogMessage =
  1384         stringBundle.formatStringFromName("addEngineConfirmation",
  1385                                           [this._name, this._uri.host], 2);
  1386     var checkboxMessage = null;
  1387     if (!getBoolPref(BROWSER_SEARCH_PREF + "noCurrentEngine", false))
  1388       checkboxMessage = stringBundle.GetStringFromName("addEngineAsCurrentText");
  1390     var addButtonLabel =
  1391         stringBundle.GetStringFromName("addEngineAddButtonLabel");
  1393     var ps = Services.prompt;
  1394     var buttonFlags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
  1395                       (ps.BUTTON_TITLE_CANCEL    * ps.BUTTON_POS_1) +
  1396                        ps.BUTTON_POS_0_DEFAULT;
  1398     var checked = {value: false};
  1399     // confirmEx returns the index of the button that was pressed.  Since "Add"
  1400     // is button 0, we want to return the negation of that value.
  1401     var confirm = !ps.confirmEx(null,
  1402                                 titleMessage,
  1403                                 dialogMessage,
  1404                                 buttonFlags,
  1405                                 addButtonLabel,
  1406                                 null, null, // button 1 & 2 names not used
  1407                                 checkboxMessage,
  1408                                 checked);
  1410     return {confirmed: confirm, useNow: checked.value};
  1411   },
  1413   /**
  1414    * Handle the successful download of an engine. Initializes the engine and
  1415    * triggers parsing of the data. The engine is then flushed to disk. Notifies
  1416    * the search service once initialization is complete.
  1417    */
  1418   _onLoad: function SRCH_ENG_onLoad(aBytes, aEngine) {
  1419     /**
  1420      * Handle an error during the load of an engine by notifying the engine's
  1421      * error callback, if any.
  1422      */
  1423     function onError(errorCode = Ci.nsISearchInstallCallback.ERROR_UNKNOWN_FAILURE) {
  1424       // Notify the callback of the failure
  1425       if (aEngine._installCallback) {
  1426         aEngine._installCallback(errorCode);
  1430     function promptError(strings = {}, error = undefined) {
  1431       onError(error);
  1433       if (aEngine._engineToUpdate) {
  1434         // We're in an update, so just fail quietly
  1435         LOG("updating " + aEngine._engineToUpdate.name + " failed");
  1436         return;
  1438       var brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
  1439       var brandName = brandBundle.GetStringFromName("brandShortName");
  1441       var searchBundle = Services.strings.createBundle(SEARCH_BUNDLE);
  1442       var msgStringName = strings.error || "error_loading_engine_msg2";
  1443       var titleStringName = strings.title || "error_loading_engine_title";
  1444       var title = searchBundle.GetStringFromName(titleStringName);
  1445       var text = searchBundle.formatStringFromName(msgStringName,
  1446                                                    [brandName, aEngine._location],
  1447                                                    2);
  1449       Services.ww.getNewPrompter(null).alert(title, text);
  1452     if (!aBytes) {
  1453       promptError();
  1454       return;
  1457     var engineToUpdate = null;
  1458     if (aEngine._engineToUpdate) {
  1459       engineToUpdate = aEngine._engineToUpdate.wrappedJSObject;
  1461       // Make this new engine use the old engine's file.
  1462       aEngine._file = engineToUpdate._file;
  1465     switch (aEngine._dataType) {
  1466       case SEARCH_DATA_XML:
  1467         var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
  1468                      createInstance(Ci.nsIDOMParser);
  1469         var doc = parser.parseFromBuffer(aBytes, aBytes.length, "text/xml");
  1470         aEngine._data = doc.documentElement;
  1471         break;
  1472       case SEARCH_DATA_TEXT:
  1473         aEngine._data = aBytes;
  1474         break;
  1475       default:
  1476         promptError();
  1477         LOG("_onLoad: Bogus engine _dataType: \"" + this._dataType + "\"");
  1478         return;
  1481     try {
  1482       // Initialize the engine from the obtained data
  1483       aEngine._initFromData();
  1484     } catch (ex) {
  1485       LOG("_onLoad: Failed to init engine!\n" + ex);
  1486       // Report an error to the user
  1487       promptError();
  1488       return;
  1491     // Check that when adding a new engine (e.g., not updating an
  1492     // existing one), a duplicate engine does not already exist.
  1493     if (!engineToUpdate) {
  1494       if (Services.search.getEngineByName(aEngine.name)) {
  1495         // If we're confirming the engine load, then display a "this is a
  1496         // duplicate engine" prompt; otherwise, fail silently.
  1497         if (aEngine._confirm) {
  1498           promptError({ error: "error_duplicate_engine_msg",
  1499                         title: "error_invalid_engine_title"
  1500                       }, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
  1501         } else {
  1502           onError(Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
  1504         LOG("_onLoad: duplicate engine found, bailing");
  1505         return;
  1509     // If requested, confirm the addition now that we have the title.
  1510     // This property is only ever true for engines added via
  1511     // nsIBrowserSearchService::addEngine.
  1512     if (aEngine._confirm) {
  1513       var confirmation = aEngine._confirmAddEngine();
  1514       LOG("_onLoad: confirm is " + confirmation.confirmed +
  1515           "; useNow is " + confirmation.useNow);
  1516       if (!confirmation.confirmed) {
  1517         onError();
  1518         return;
  1520       aEngine._useNow = confirmation.useNow;
  1523     // If we don't yet have a file, get one now. The only case where we would
  1524     // already have a file is if this is an update and _file was set above.
  1525     if (!aEngine._file)
  1526       aEngine._file = getSanitizedFile(aEngine.name);
  1528     if (engineToUpdate) {
  1529       // Keep track of the last modified date, so that we can make conditional
  1530       // requests for future updates.
  1531       engineMetadataService.setAttr(aEngine, "updatelastmodified",
  1532                                     (new Date()).toUTCString());
  1534       // If we're updating an app-shipped engine, ensure that the updateURLs
  1535       // are the same.
  1536       if (engineToUpdate._isInAppDir) {
  1537         let oldUpdateURL = engineToUpdate._updateURL;
  1538         let newUpdateURL = aEngine._updateURL;
  1539         let oldSelfURL = engineToUpdate._getURLOfType(URLTYPE_OPENSEARCH, "self");
  1540         if (oldSelfURL) {
  1541           oldUpdateURL = oldSelfURL.template;
  1542           let newSelfURL = aEngine._getURLOfType(URLTYPE_OPENSEARCH, "self");
  1543           if (!newSelfURL) {
  1544             LOG("_onLoad: updateURL missing in updated engine for " +
  1545                 aEngine.name + " aborted");
  1546             onError();
  1547             return;
  1549           newUpdateURL = newSelfURL.template;
  1552         if (oldUpdateURL != newUpdateURL) {
  1553           LOG("_onLoad: updateURLs do not match! Update of " + aEngine.name + " aborted");
  1554           onError();
  1555           return;
  1559       // Set the new engine's icon, if it doesn't yet have one.
  1560       if (!aEngine._iconURI && engineToUpdate._iconURI)
  1561         aEngine._iconURI = engineToUpdate._iconURI;
  1564     // Write the engine to file. For readOnly engines, they'll be stored in the
  1565     // cache following the notification below.
  1566     if (!aEngine._readOnly)
  1567       aEngine._serializeToFile();
  1569     // Notify the search service of the successful load. It will deal with
  1570     // updates by checking aEngine._engineToUpdate.
  1571     notifyAction(aEngine, SEARCH_ENGINE_LOADED);
  1573     // Notify the callback if needed
  1574     if (aEngine._installCallback) {
  1575       aEngine._installCallback();
  1577   },
  1579   /**
  1580    * Creates a key by serializing an object that contains the icon's width
  1581    * and height.
  1583    * @param aWidth
  1584    *        Width of the icon.
  1585    * @param aHeight
  1586    *        Height of the icon.
  1587    * @returns key string
  1588    */
  1589   _getIconKey: function SRCH_ENG_getIconKey(aWidth, aHeight) {
  1590     let keyObj = {
  1591      width: aWidth,
  1592      height: aHeight
  1593     };
  1595     return JSON.stringify(keyObj);
  1596   },
  1598   /**
  1599    * Add an icon to the icon map used by getIconURIBySize() and getIcons().
  1601    * @param aWidth
  1602    *        Width of the icon.
  1603    * @param aHeight
  1604    *        Height of the icon.
  1605    * @param aURISpec
  1606    *        String with the icon's URI.
  1607    */
  1608   _addIconToMap: function SRCH_ENG_addIconToMap(aWidth, aHeight, aURISpec) {
  1609     // Use an object instead of a Map() because it needs to be serializable.
  1610     this._iconMapObj = this._iconMapObj || {};
  1611     let key = this._getIconKey(aWidth, aHeight);
  1612     this._iconMapObj[key] = aURISpec;
  1613   },
  1615   /**
  1616    * Sets the .iconURI property of the engine. If both aWidth and aHeight are
  1617    * provided an entry will be added to _iconMapObj that will enable accessing
  1618    * icon's data through getIcons() and getIconURIBySize() APIs.
  1620    *  @param aIconURL
  1621    *         A URI string pointing to the engine's icon. Must have a http[s],
  1622    *         ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be
  1623    *         downloaded and converted to data URIs for storage in the engine
  1624    *         XML files, if the engine is not readonly.
  1625    *  @param aIsPreferred
  1626    *         Whether or not this icon is to be preferred. Preferred icons can
  1627    *         override non-preferred icons.
  1628    *  @param aWidth (optional)
  1629    *         Width of the icon.
  1630    *  @param aHeight (optional)
  1631    *         Height of the icon.
  1632    */
  1633   _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred, aWidth, aHeight) {
  1634     var uri = makeURI(aIconURL);
  1636     // Ignore bad URIs
  1637     if (!uri)
  1638       return;
  1640     LOG("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \""
  1641         + this.name + "\".");
  1642     // Only accept remote icons from http[s] or ftp
  1643     switch (uri.scheme) {
  1644       case "data":
  1645         if (!this._hasPreferredIcon || aIsPreferred) {
  1646           this._iconURI = uri;
  1647           notifyAction(this, SEARCH_ENGINE_CHANGED);
  1648           this._hasPreferredIcon = aIsPreferred;
  1651         if (aWidth && aHeight) {
  1652           this._addIconToMap(aWidth, aHeight, aIconURL)
  1654         break;
  1655       case "http":
  1656       case "https":
  1657       case "ftp":
  1658         // No use downloading the icon if the engine file is read-only
  1659         if (!this._readOnly ||
  1660             getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) {
  1661           LOG("_setIcon: Downloading icon: \"" + uri.spec +
  1662               "\" for engine: \"" + this.name + "\"");
  1663           var chan = NetUtil.ioService.newChannelFromURI(uri);
  1665           function iconLoadCallback(aByteArray, aEngine) {
  1666             // This callback may run after we've already set a preferred icon,
  1667             // so check again.
  1668             if (aEngine._hasPreferredIcon && !aIsPreferred)
  1669               return;
  1671             if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) {
  1672               LOG("iconLoadCallback: load failed, or the icon was too large!");
  1673               return;
  1676             var str = btoa(String.fromCharCode.apply(null, aByteArray));
  1677             let dataURL = ICON_DATAURL_PREFIX + str;
  1678             aEngine._iconURI = makeURI(dataURL);
  1680             if (aWidth && aHeight) {
  1681               aEngine._addIconToMap(aWidth, aHeight, dataURL)
  1684             // The engine might not have a file yet, if it's being downloaded,
  1685             // because the request for the engine file itself (_onLoad) may not
  1686             // yet be complete. In that case, this change will be written to
  1687             // file when _onLoad is called. For readonly engines, we'll store
  1688             // the changes in the cache once notified below.
  1689             if (aEngine._file && !aEngine._readOnly)
  1690               aEngine._serializeToFile();
  1692             notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
  1693             aEngine._hasPreferredIcon = aIsPreferred;
  1696           // If we're currently acting as an "update engine", then the callback
  1697           // should set the icon on the engine we're updating and not us, since
  1698           // |this| might be gone by the time the callback runs.
  1699           var engineToSet = this._engineToUpdate || this;
  1701           var listener = new loadListener(chan, engineToSet, iconLoadCallback);
  1702           chan.notificationCallbacks = listener;
  1703           chan.asyncOpen(listener, null);
  1705         break;
  1707   },
  1709   /**
  1710    * Initialize this Engine object from the collected data.
  1711    */
  1712   _initFromData: function SRCH_ENG_initFromData() {
  1713     ENSURE_WARN(this._data, "Can't init an engine with no data!",
  1714                 Cr.NS_ERROR_UNEXPECTED);
  1716     // Find out what type of engine we are
  1717     switch (this._dataType) {
  1718       case SEARCH_DATA_XML:
  1719         if (checkNameSpace(this._data, [MOZSEARCH_LOCALNAME],
  1720             [MOZSEARCH_NS_10])) {
  1722           LOG("_init: Initing MozSearch plugin from " + this._location);
  1724           this._type = SEARCH_TYPE_MOZSEARCH;
  1725           this._parseAsMozSearch();
  1727         } else if (checkNameSpace(this._data, [OPENSEARCH_LOCALNAME],
  1728                                   OPENSEARCH_NAMESPACES)) {
  1730           LOG("_init: Initing OpenSearch plugin from " + this._location);
  1732           this._type = SEARCH_TYPE_OPENSEARCH;
  1733           this._parseAsOpenSearch();
  1735         } else
  1736           FAIL(this._location + " is not a valid search plugin.", Cr.NS_ERROR_FAILURE);
  1738         break;
  1739       case SEARCH_DATA_TEXT:
  1740         LOG("_init: Initing Sherlock plugin from " + this._location);
  1742         // the only text-based format we support is Sherlock
  1743         this._type = SEARCH_TYPE_SHERLOCK;
  1744         this._parseAsSherlock();
  1747     // No need to keep a ref to our data (which in some cases can be a document
  1748     // element) past this point
  1749     this._data = null;
  1750   },
  1752   /**
  1753    * Initialize this Engine object from a collection of metadata.
  1754    */
  1755   _initFromMetadata: function SRCH_ENG_initMetaData(aName, aIconURL, aAlias,
  1756                                                     aDescription, aMethod,
  1757                                                     aTemplate) {
  1758     ENSURE_WARN(!this._readOnly,
  1759                 "Can't call _initFromMetaData on a readonly engine!",
  1760                 Cr.NS_ERROR_FAILURE);
  1762     this._urls.push(new EngineURL(URLTYPE_SEARCH_HTML, aMethod, aTemplate));
  1764     this._name = aName;
  1765     this.alias = aAlias;
  1766     this._description = aDescription;
  1767     this._setIcon(aIconURL, true);
  1769     this._serializeToFile();
  1770   },
  1772   /**
  1773    * Extracts data from an OpenSearch URL element and creates an EngineURL
  1774    * object which is then added to the engine's list of URLs.
  1776    * @throws NS_ERROR_FAILURE if a URL object could not be created.
  1778    * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag.
  1779    * @see EngineURL()
  1780    */
  1781   _parseURL: function SRCH_ENG_parseURL(aElement) {
  1782     var type     = aElement.getAttribute("type");
  1783     // According to the spec, method is optional, defaulting to "GET" if not
  1784     // specified
  1785     var method   = aElement.getAttribute("method") || "GET";
  1786     var template = aElement.getAttribute("template");
  1787     var resultDomain = aElement.getAttribute("resultdomain");
  1789     try {
  1790       var url = new EngineURL(type, method, template, resultDomain);
  1791     } catch (ex) {
  1792       FAIL("_parseURL: failed to add " + template + " as a URL",
  1793            Cr.NS_ERROR_FAILURE);
  1796     if (aElement.hasAttribute("rel"))
  1797       url.rels = aElement.getAttribute("rel").toLowerCase().split(/\s+/);
  1799     for (var i = 0; i < aElement.childNodes.length; ++i) {
  1800       var param = aElement.childNodes[i];
  1801       if (param.localName == "Param") {
  1802         try {
  1803           url.addParam(param.getAttribute("name"), param.getAttribute("value"));
  1804         } catch (ex) {
  1805           // Ignore failure
  1806           LOG("_parseURL: Url element has an invalid param");
  1808       } else if (param.localName == "MozParam" &&
  1809                  // We only support MozParams for default search engines
  1810                  this._isDefault) {
  1811         var value;
  1812         let condition = param.getAttribute("condition");
  1814         // MozParams must have a condition to be valid
  1815         if (!condition) {
  1816           let engineLoc = this._location;
  1817           let paramName = param.getAttribute("name");
  1818           LOG("_parseURL: MozParam (" + paramName + ") without a condition attribute found parsing engine: " + engineLoc);
  1819           continue;
  1822         switch (condition) {
  1823           case "purpose":
  1824             url.addParam(param.getAttribute("name"),
  1825                          param.getAttribute("value"),
  1826                          param.getAttribute("purpose"));
  1827             // _addMozParam is not needed here since it can be serialized fine without. _addMozParam
  1828             // also requires a unique "name" which is not normally the case when @purpose is used.
  1829             break;
  1830           case "defaultEngine":
  1831             // If this engine was the default search engine, use the true value
  1832             if (this._isDefaultEngine())
  1833               value = param.getAttribute("trueValue");
  1834             else
  1835               value = param.getAttribute("falseValue");
  1836             url.addParam(param.getAttribute("name"), value);
  1837             url._addMozParam({"name": param.getAttribute("name"),
  1838                               "falseValue": param.getAttribute("falseValue"),
  1839                               "trueValue": param.getAttribute("trueValue"),
  1840                               "condition": "defaultEngine"});
  1841             break;
  1843           case "pref":
  1844             try {
  1845               value = getMozParamPref(param.getAttribute("pref"), value);
  1846               url.addParam(param.getAttribute("name"), value);
  1847               url._addMozParam({"pref": param.getAttribute("pref"),
  1848                                 "name": param.getAttribute("name"),
  1849                                 "condition": "pref"});
  1850             } catch (e) { }
  1851             break;
  1852           default:
  1853             if (condition && condition.startsWith("top")) {
  1854               url.addParam(param.getAttribute("name"), param.getAttribute("falseValue"));
  1855               let mozparam = {"name": param.getAttribute("name"),
  1856                               "falseValue": param.getAttribute("falseValue"),
  1857                               "trueValue": param.getAttribute("trueValue"),
  1858                               "condition": condition,
  1859                               "positionDependent": true};
  1860               url._addMozParam(mozparam);
  1862           break;
  1867     this._urls.push(url);
  1868   },
  1870   _isDefaultEngine: function SRCH_ENG__isDefaultEngine() {
  1871     let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
  1872     let nsIPLS = Ci.nsIPrefLocalizedString;
  1873     let defaultEngine;
  1874     try {
  1875       defaultEngine = defaultPrefB.getComplexValue("defaultenginename", nsIPLS).data;
  1876     } catch (ex) {}
  1877     return this.name == defaultEngine;
  1878   },
  1880   /**
  1881    * Get the icon from an OpenSearch Image element.
  1882    * @see http://opensearch.a9.com/spec/1.1/description/#image
  1883    */
  1884   _parseImage: function SRCH_ENG_parseImage(aElement) {
  1885     LOG("_parseImage: Image textContent: \"" + limitURILength(aElement.textContent) + "\"");
  1887     let width = parseInt(aElement.getAttribute("width"), 10);
  1888     let height = parseInt(aElement.getAttribute("height"), 10);
  1889     let isPrefered = width == 16 && height == 16;
  1891     if (isNaN(width) || isNaN(height) || width <= 0 || height <=0) {
  1892       LOG("OpenSearch image element must have positive width and height.");
  1893       return;
  1896     this._setIcon(aElement.textContent, isPrefered, width, height);
  1897   },
  1899   _parseAsMozSearch: function SRCH_ENG_parseAsMoz() {
  1900     //forward to the OpenSearch parser
  1901     this._parseAsOpenSearch();
  1902   },
  1904   /**
  1905    * Extract search engine information from the collected data to initialize
  1906    * the engine object.
  1907    */
  1908   _parseAsOpenSearch: function SRCH_ENG_parseAsOS() {
  1909     var doc = this._data;
  1911     // The OpenSearch spec sets a default value for the input encoding.
  1912     this._queryCharset = OS_PARAM_INPUT_ENCODING_DEF;
  1914     for (var i = 0; i < doc.childNodes.length; ++i) {
  1915       var child = doc.childNodes[i];
  1916       switch (child.localName) {
  1917         case "ShortName":
  1918           this._name = child.textContent;
  1919           break;
  1920         case "Description":
  1921           this._description = child.textContent;
  1922           break;
  1923         case "Url":
  1924           try {
  1925             this._parseURL(child);
  1926           } catch (ex) {
  1927             // Parsing of the element failed, just skip it.
  1928             LOG("_parseAsOpenSearch: failed to parse URL child: " + ex);
  1930           break;
  1931         case "Image":
  1932           this._parseImage(child);
  1933           break;
  1934         case "InputEncoding":
  1935           this._queryCharset = child.textContent.toUpperCase();
  1936           break;
  1938         // Non-OpenSearch elements
  1939         case "SearchForm":
  1940           this._searchForm = child.textContent;
  1941           break;
  1942         case "UpdateUrl":
  1943           this._updateURL = child.textContent;
  1944           break;
  1945         case "UpdateInterval":
  1946           this._updateInterval = parseInt(child.textContent);
  1947           break;
  1948         case "IconUpdateUrl":
  1949           this._iconUpdateURL = child.textContent;
  1950           break;
  1953     if (!this.name || (this._urls.length == 0))
  1954       FAIL("_parseAsOpenSearch: No name, or missing URL!", Cr.NS_ERROR_FAILURE);
  1955     if (!this.supportsResponseType(URLTYPE_SEARCH_HTML))
  1956       FAIL("_parseAsOpenSearch: No text/html result type!", Cr.NS_ERROR_FAILURE);
  1957   },
  1959   /**
  1960    * Extract search engine information from the collected data to initialize
  1961    * the engine object.
  1962    */
  1963   _parseAsSherlock: function SRCH_ENG_parseAsSherlock() {
  1964     /**
  1965      * Extracts one Sherlock "section" from aSource. A section is essentially
  1966      * an HTML element with attributes, but each attribute must be on a new
  1967      * line, by definition.
  1969      * @param aLines
  1970      *        An array of lines from the sherlock file.
  1971      * @param aSection
  1972      *        The name of the section (e.g. "search" or "browser"). This value
  1973      *        is not case sensitive.
  1974      * @returns an object whose properties correspond to the section's
  1975      *          attributes.
  1976      */
  1977     function getSection(aLines, aSection) {
  1978       LOG("_parseAsSherlock::getSection: Sherlock lines:\n" +
  1979           aLines.join("\n"));
  1980       var lines = aLines;
  1981       var startMark = new RegExp("^\\s*<" + aSection.toLowerCase() + "\\s*",
  1982                                  "gi");
  1983       var endMark   = /\s*>\s*$/gi;
  1985       var foundStart = false;
  1986       var startLine, numberOfLines;
  1987       // Find the beginning and end of the section
  1988       for (var i = 0; i < lines.length; i++) {
  1989         if (foundStart) {
  1990           if (endMark.test(lines[i])) {
  1991             numberOfLines = i - startLine;
  1992             // Remove the end marker
  1993             lines[i] = lines[i].replace(endMark, "");
  1994             // If the endmarker was not the only thing on the line, include
  1995             // this line in the results
  1996             if (lines[i])
  1997               numberOfLines++;
  1998             break;
  2000         } else {
  2001           if (startMark.test(lines[i])) {
  2002             foundStart = true;
  2003             // Remove the start marker
  2004             lines[i] = lines[i].replace(startMark, "");
  2005             startLine = i;
  2006             // If the line is empty, don't include it in the result
  2007             if (!lines[i])
  2008               startLine++;
  2012       LOG("_parseAsSherlock::getSection: Start index: " + startLine +
  2013           "\nNumber of lines: " + numberOfLines);
  2014       lines = lines.splice(startLine, numberOfLines);
  2015       LOG("_parseAsSherlock::getSection: Section lines:\n" +
  2016           lines.join("\n"));
  2018       var section = {};
  2019       for (var i = 0; i < lines.length; i++) {
  2020         var line = lines[i].trim();
  2022         var els = line.split("=");
  2023         var name = els.shift().trim().toLowerCase();
  2024         var value = els.join("=").trim();
  2026         if (!name || !value)
  2027           continue;
  2029         // Strip leading and trailing whitespace, remove quotes from the
  2030         // value, and remove any trailing slashes or ">" characters
  2031         value = value.replace(/^["']/, "")
  2032                      .replace(/["']\s*[\\\/]?>?\s*$/, "") || "";
  2033         value = value.trim();
  2035         // Don't clobber existing attributes
  2036         if (!(name in section))
  2037           section[name] = value;
  2039       return section;
  2042     /**
  2043      * Returns an array of name-value pair arrays representing the Sherlock
  2044      * file's input elements. User defined inputs return USER_DEFINED
  2045      * as the value. Elements are returned in the order they appear in the
  2046      * source file.
  2048      *   Example:
  2049      *      <input name="foo" value="bar">
  2050      *      <input name="foopy" user>
  2051      *   Returns:
  2052      *      [["foo", "bar"], ["foopy", "{searchTerms}"]]
  2054      * @param aLines
  2055      *        An array of lines from the source file.
  2056      */
  2057     function getInputs(aLines) {
  2059       /**
  2060        * Extracts an attribute value from a given a line of text.
  2061        *    Example: <input value="foo" name="bar">
  2062        *      Extracts the string |foo| or |bar| given an input aAttr of
  2063        *      |value| or |name|.
  2064        * Attributes may be quoted or unquoted. If unquoted, any whitespace
  2065        * indicates the end of the attribute value.
  2066        *    Example: < value=22 33 name=44\334 >
  2067        *      Returns |22| for "value" and |44\334| for "name".
  2069        * @param aAttr
  2070        *        The name of the attribute for which to obtain the value. This
  2071        *        value is not case sensitive.
  2072        * @param aLine
  2073        *        The line containing the attribute.
  2075        * @returns the attribute value, or an empty string if the attribute
  2076        *          doesn't exist.
  2077        */
  2078       function getAttr(aAttr, aLine) {
  2079         // Used to determine whether an "input" line from a Sherlock file is a
  2080         // "user defined" input.
  2081         const userInput = /(\s|["'=])user(\s|[>="'\/\\+]|$)/i;
  2083         LOG("_parseAsSherlock::getAttr: Getting attr: \"" +
  2084             aAttr + "\" for line: \"" + aLine + "\"");
  2085         // We're not case sensitive, but we want to return the attribute value
  2086         // in its original case, so create a copy of the source
  2087         var lLine = aLine.toLowerCase();
  2088         var attr = aAttr.toLowerCase();
  2090         var attrStart = lLine.search(new RegExp("\\s" + attr, "i"));
  2091         if (attrStart == -1) {
  2093           // If this is the "user defined input" (i.e. contains the empty
  2094           // "user" attribute), return our special keyword
  2095           if (userInput.test(lLine) && attr == "value") {
  2096             LOG("_parseAsSherlock::getAttr: Found user input!\nLine:\"" + lLine
  2097                 + "\"");
  2098             return USER_DEFINED;
  2100           // The attribute doesn't exist - ignore
  2101           LOG("_parseAsSherlock::getAttr: Failed to find attribute:\nLine:\""
  2102               + lLine + "\"\nAttr:\"" + attr + "\"");
  2103           return "";
  2106         var valueStart = lLine.indexOf("=", attrStart) + "=".length;
  2107         if (valueStart == -1)
  2108           return "";
  2110         var quoteStart = lLine.indexOf("\"", valueStart);
  2111         if (quoteStart == -1) {
  2113           // Unquoted attribute, get the rest of the line, trimmed at the first
  2114           // sign of whitespace. If the rest of the line is only whitespace,
  2115           // returns a blank string.
  2116           return lLine.substr(valueStart).replace(/\s.*$/, "");
  2118         } else {
  2119           // Make sure that there's only whitespace between the start of the
  2120           // value and the first quote. If there is, end the attribute value at
  2121           // the first sign of whitespace. This prevents us from falling into
  2122           // the next attribute if this is an unquoted attribute followed by a
  2123           // quoted attribute.
  2124           var betweenEqualAndQuote = lLine.substring(valueStart, quoteStart);
  2125           if (/\S/.test(betweenEqualAndQuote))
  2126             return lLine.substr(valueStart).replace(/\s.*$/, "");
  2128           // Adjust the start index to account for the opening quote
  2129           valueStart = quoteStart + "\"".length;
  2130           // Find the closing quote
  2131           var valueEnd = lLine.indexOf("\"", valueStart);
  2132           // If there is no closing quote, just go to the end of the line
  2133           if (valueEnd == -1)
  2134             valueEnd = aLine.length;
  2136         return aLine.substring(valueStart, valueEnd);
  2139       var inputs = [];
  2141       LOG("_parseAsSherlock::getInputs: Lines:\n" + aLines);
  2142       // Filter out everything but non-inputs
  2143       let lines = aLines.filter(function (line) {
  2144         return /^\s*<input/i.test(line);
  2145       });
  2146       LOG("_parseAsSherlock::getInputs: Filtered lines:\n" + lines);
  2148       lines.forEach(function (line) {
  2149         // Strip leading/trailing whitespace and remove the surrounding markup
  2150         // ("<input" and ">")
  2151         line = line.trim().replace(/^<input/i, "").replace(/>$/, "");
  2153         // If this is one of the "directional" inputs (<inputnext>/<inputprev>)
  2154         const directionalInput = /^(prev|next)/i;
  2155         if (directionalInput.test(line)) {
  2157           // Make it look like a normal input by removing "prev" or "next"
  2158           line = line.replace(directionalInput, "");
  2160           // If it has a name, give it a dummy value to match previous
  2161           // nsInternetSearchService behavior
  2162           if (/name\s*=/i.test(line)) {
  2163             line += " value=\"0\"";
  2164           } else
  2165             return; // Line has no name, skip it
  2168         var attrName = getAttr("name", line);
  2169         var attrValue = getAttr("value", line);
  2170         LOG("_parseAsSherlock::getInputs: Got input:\nName:\"" + attrName +
  2171             "\"\nValue:\"" + attrValue + "\"");
  2172         if (attrValue)
  2173           inputs.push([attrName, attrValue]);
  2174       });
  2175       return inputs;
  2178     function err(aErr) {
  2179       FAIL("_parseAsSherlock::err: Sherlock param error:\n" + aErr,
  2180            Cr.NS_ERROR_FAILURE);
  2183     // First try converting our byte array using the default Sherlock encoding.
  2184     // If this fails, or if we find a sourceTextEncoding attribute, we need to
  2185     // reconvert the byte array using the specified encoding.
  2186     var sherlockLines, searchSection, sourceTextEncoding, browserSection;
  2187     try {
  2188       sherlockLines = sherlockBytesToLines(this._data);
  2189       searchSection = getSection(sherlockLines, "search");
  2190       browserSection = getSection(sherlockLines, "browser");
  2191       sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]);
  2192       if (sourceTextEncoding) {
  2193         // Re-convert the bytes using the found sourceTextEncoding
  2194         sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding);
  2195         searchSection = getSection(sherlockLines, "search");
  2196         browserSection = getSection(sherlockLines, "browser");
  2198     } catch (ex) {
  2199       // The conversion using the default charset failed. Remove any non-ascii
  2200       // bytes and try to find a sourceTextEncoding.
  2201       var asciiBytes = this._data.filter(function (n) {return !(0x80 & n);});
  2202       var asciiString = String.fromCharCode.apply(null, asciiBytes);
  2203       sherlockLines = asciiString.split(NEW_LINES).filter(isUsefulLine);
  2204       searchSection = getSection(sherlockLines, "search");
  2205       sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]);
  2206       if (sourceTextEncoding) {
  2207         sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding);
  2208         searchSection = getSection(sherlockLines, "search");
  2209         browserSection = getSection(sherlockLines, "browser");
  2210       } else
  2211         ERROR("Couldn't find a working charset", Cr.NS_ERROR_FAILURE);
  2214     LOG("_parseAsSherlock: Search section:\n" + searchSection.toSource());
  2216     this._name = searchSection["name"] || err("Missing name!");
  2217     this._description = searchSection["description"] || "";
  2218     this._queryCharset = searchSection["querycharset"] ||
  2219                          queryCharsetFromCode(searchSection["queryencoding"]);
  2220     this._searchForm = searchSection["searchform"];
  2222     this._updateInterval = parseInt(browserSection["updatecheckdays"]);
  2224     this._updateURL = browserSection["update"];
  2225     this._iconUpdateURL = browserSection["updateicon"];
  2227     var method = (searchSection["method"] || "GET").toUpperCase();
  2228     var template = searchSection["action"] || err("Missing action!");
  2230     var inputs = getInputs(sherlockLines);
  2231     LOG("_parseAsSherlock: Inputs:\n" + inputs.toSource());
  2233     var url = null;
  2235     if (method == "GET") {
  2236       // Here's how we construct the input string:
  2237       // <input> is first:  Name Attr:  Prefix      Data           Example:
  2238       // YES                EMPTY       None        <value>        TEMPLATE<value>
  2239       // YES                NON-EMPTY   ?           <name>=<value> TEMPLATE?<name>=<value>
  2240       // NO                 EMPTY       ------------- <ignored> --------------
  2241       // NO                 NON-EMPTY   &           <name>=<value> TEMPLATE?<n1>=<v1>&<n2>=<v2>
  2242       for (var i = 0; i < inputs.length; i++) {
  2243         var name  = inputs[i][0];
  2244         var value = inputs[i][1];
  2245         if (i==0) {
  2246           if (name == "")
  2247             template += USER_DEFINED;
  2248           else
  2249             template += "?" + name + "=" + value;
  2250         } else if (name != "")
  2251           template += "&" + name + "=" + value;
  2253       url = new EngineURL(URLTYPE_SEARCH_HTML, method, template);
  2255     } else if (method == "POST") {
  2256       // Create the URL object and just add the parameters directly
  2257       url = new EngineURL(URLTYPE_SEARCH_HTML, method, template);
  2258       for (var i = 0; i < inputs.length; i++) {
  2259         var name  = inputs[i][0];
  2260         var value = inputs[i][1];
  2261         if (name)
  2262           url.addParam(name, value);
  2264     } else
  2265       err("Invalid method!");
  2267     this._urls.push(url);
  2268   },
  2270   /**
  2271    * Init from a JSON record.
  2272    **/
  2273   _initWithJSON: function SRCH_ENG__initWithJSON(aJson) {
  2274     this.__id = aJson._id;
  2275     this._name = aJson._name;
  2276     this._description = aJson.description;
  2277     if (aJson._hasPreferredIcon == undefined)
  2278       this._hasPreferredIcon = true;
  2279     else
  2280       this._hasPreferredIcon = false;
  2281     this._hidden = aJson._hidden;
  2282     this._type = aJson.type || SEARCH_TYPE_MOZSEARCH;
  2283     this._queryCharset = aJson.queryCharset || DEFAULT_QUERY_CHARSET;
  2284     this.__searchForm = aJson.__searchForm;
  2285     this.__installLocation = aJson._installLocation || SEARCH_APP_DIR;
  2286     this._updateInterval = aJson._updateInterval || null;
  2287     this._updateURL = aJson._updateURL || null;
  2288     this._iconUpdateURL = aJson._iconUpdateURL || null;
  2289     if (aJson._readOnly == undefined)
  2290       this._readOnly = true;
  2291     else
  2292       this._readOnly = false;
  2293     this._iconURI = makeURI(aJson._iconURL);
  2294     this._iconMapObj = aJson._iconMapObj;
  2295     for (let i = 0; i < aJson._urls.length; ++i) {
  2296       let url = aJson._urls[i];
  2297       let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML,
  2298                                     url.method || "GET", url.template,
  2299                                     url.resultDomain);
  2300       engineURL._initWithJSON(url, this);
  2301       this._urls.push(engineURL);
  2303   },
  2305   /**
  2306    * Creates a JavaScript object that represents this engine.
  2307    * @param aFilter
  2308    *        Whether or not to filter out common default values. Recommended for
  2309    *        use with _initWithJSON().
  2310    * @returns An object suitable for serialization as JSON.
  2311    **/
  2312   _serializeToJSON: function SRCH_ENG__serializeToJSON(aFilter) {
  2313     var json = {
  2314       _id: this._id,
  2315       _name: this._name,
  2316       _hidden: this.hidden,
  2317       description: this.description,
  2318       __searchForm: this.__searchForm,
  2319       _iconURL: this._iconURL,
  2320       _iconMapObj: this._iconMapObj,
  2321       _urls: [url._serializeToJSON() for each(url in this._urls)]
  2322     };
  2324     if (this._file instanceof Ci.nsILocalFile)
  2325       json.filePath = this._file.persistentDescriptor;
  2326     if (this._uri)
  2327       json._url = this._uri.spec;
  2328     if (this._installLocation != SEARCH_APP_DIR || !aFilter)
  2329       json._installLocation = this._installLocation;
  2330     if (this._updateInterval || !aFilter)
  2331       json._updateInterval = this._updateInterval;
  2332     if (this._updateURL || !aFilter)
  2333       json._updateURL = this._updateURL;
  2334     if (this._iconUpdateURL || !aFilter)
  2335       json._iconUpdateURL = this._iconUpdateURL;
  2336     if (!this._hasPreferredIcon || !aFilter)
  2337       json._hasPreferredIcon = this._hasPreferredIcon;
  2338     if (this.type != SEARCH_TYPE_MOZSEARCH || !aFilter)
  2339       json.type = this.type;
  2340     if (this.queryCharset != DEFAULT_QUERY_CHARSET || !aFilter)
  2341       json.queryCharset = this.queryCharset;
  2342     if (this._dataType != SEARCH_DATA_XML || !aFilter)
  2343       json._dataType = this._dataType;
  2344     if (!this._readOnly || !aFilter)
  2345       json._readOnly = this._readOnly;
  2347     return json;
  2348   },
  2350   /**
  2351    * Returns an XML document object containing the search plugin information,
  2352    * which can later be used to reload the engine.
  2353    */
  2354   _serializeToElement: function SRCH_ENG_serializeToEl() {
  2355     function appendTextNode(aNameSpace, aLocalName, aValue) {
  2356       if (!aValue)
  2357         return null;
  2358       var node = doc.createElementNS(aNameSpace, aLocalName);
  2359       node.appendChild(doc.createTextNode(aValue));
  2360       docElem.appendChild(node);
  2361       docElem.appendChild(doc.createTextNode("\n"));
  2362       return node;
  2365     var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
  2366                  createInstance(Ci.nsIDOMParser);
  2368     var doc = parser.parseFromString(EMPTY_DOC, "text/xml");
  2369     var docElem = doc.documentElement;
  2371     docElem.appendChild(doc.createTextNode("\n"));
  2373     appendTextNode(OPENSEARCH_NS_11, "ShortName", this.name);
  2374     appendTextNode(OPENSEARCH_NS_11, "Description", this._description);
  2375     appendTextNode(OPENSEARCH_NS_11, "InputEncoding", this._queryCharset);
  2377     if (this._iconURI) {
  2378       var imageNode = appendTextNode(OPENSEARCH_NS_11, "Image",
  2379                                      this._iconURI.spec);
  2380       if (imageNode) {
  2381         imageNode.setAttribute("width", "16");
  2382         imageNode.setAttribute("height", "16");
  2386     appendTextNode(MOZSEARCH_NS_10, "UpdateInterval", this._updateInterval);
  2387     appendTextNode(MOZSEARCH_NS_10, "UpdateUrl", this._updateURL);
  2388     appendTextNode(MOZSEARCH_NS_10, "IconUpdateUrl", this._iconUpdateURL);
  2389     appendTextNode(MOZSEARCH_NS_10, "SearchForm", this._searchForm);
  2391     for (var i = 0; i < this._urls.length; ++i)
  2392       this._urls[i]._serializeToElement(doc, docElem);
  2393     docElem.appendChild(doc.createTextNode("\n"));
  2395     return doc;
  2396   },
  2398   get lazySerializeTask() {
  2399     if (!this._lazySerializeTask) {
  2400       let task = function taskCallback() {
  2401         this._serializeToFile();
  2402       }.bind(this);
  2403       this._lazySerializeTask = new DeferredTask(task, LAZY_SERIALIZE_DELAY);
  2406     return this._lazySerializeTask;
  2407   },
  2409   /**
  2410    * Serializes the engine object to file.
  2411    */
  2412   _serializeToFile: function SRCH_ENG_serializeToFile() {
  2413     var file = this._file;
  2414     ENSURE_WARN(!this._readOnly, "Can't serialize a read only engine!",
  2415                 Cr.NS_ERROR_FAILURE);
  2416     ENSURE_WARN(file && file.exists(), "Can't serialize: file doesn't exist!",
  2417                 Cr.NS_ERROR_UNEXPECTED);
  2419     var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"].
  2420               createInstance(Ci.nsIFileOutputStream);
  2422     // Serialize the engine first - we don't want to overwrite a good file
  2423     // if this somehow fails.
  2424     var doc = this._serializeToElement();
  2426     fos.init(file, (MODE_WRONLY | MODE_TRUNCATE), PERMS_FILE, 0);
  2428     try {
  2429       var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
  2430                        createInstance(Ci.nsIDOMSerializer);
  2431       serializer.serializeToStream(doc.documentElement, fos, null);
  2432     } catch (e) {
  2433       LOG("_serializeToFile: Error serializing engine:\n" + e);
  2436     closeSafeOutputStream(fos);
  2438     Services.obs.notifyObservers(file.clone(), SEARCH_SERVICE_TOPIC,
  2439                                  "write-engine-to-disk-complete");
  2440   },
  2442   /**
  2443    * Remove the engine's file from disk. The search service calls this once it
  2444    * removes the engine from its internal store. This function will throw if
  2445    * the file cannot be removed.
  2446    */
  2447   _remove: function SRCH_ENG_remove() {
  2448     if (this._readOnly)
  2449       FAIL("Can't remove read only engine!", Cr.NS_ERROR_FAILURE);
  2450     if (!this._file || !this._file.exists())
  2451       FAIL("Can't remove engine: file doesn't exist!", Cr.NS_ERROR_FILE_NOT_FOUND);
  2453     this._file.remove(false);
  2454   },
  2456   // nsISearchEngine
  2457   get alias() {
  2458     if (this._alias === undefined)
  2459       this._alias = engineMetadataService.getAttr(this, "alias");
  2461     return this._alias;
  2462   },
  2463   set alias(val) {
  2464     this._alias = val;
  2465     engineMetadataService.setAttr(this, "alias", val);
  2466     notifyAction(this, SEARCH_ENGINE_CHANGED);
  2467   },
  2469   /**
  2470    * Return the built-in identifier of app-provided engines.
  2472    * Note that this identifier is substantially similar to _id, with the
  2473    * following exceptions:
  2475    * * There is no trailing file extension.
  2476    * * There is no [app] prefix.
  2478    * @return a string identifier, or null.
  2479    */
  2480   get identifier() {
  2481     if (this._identifier !== undefined) {
  2482       return this._identifier;
  2485     // No identifier if If the engine isn't app-provided
  2486     if (!this._isInAppDir && !this._isInJAR) {
  2487       return this._identifier = null;
  2490     let leaf = this._getLeafName();
  2491     ENSURE_WARN(leaf, "identifier: app-provided engine has no leafName");
  2493     // Strip file extension.
  2494     let ext = leaf.lastIndexOf(".");
  2495     if (ext == -1) {
  2496       return this._identifier = leaf;
  2498     return this._identifier = leaf.substring(0, ext);
  2499   },
  2501   get description() {
  2502     return this._description;
  2503   },
  2505   get hidden() {
  2506     if (this._hidden === null)
  2507       this._hidden = engineMetadataService.getAttr(this, "hidden") || false;
  2508     return this._hidden;
  2509   },
  2510   set hidden(val) {
  2511     var value = !!val;
  2512     if (value != this._hidden) {
  2513       this._hidden = value;
  2514       engineMetadataService.setAttr(this, "hidden", value);
  2515       notifyAction(this, SEARCH_ENGINE_CHANGED);
  2517   },
  2519   get iconURI() {
  2520     if (this._iconURI)
  2521       return this._iconURI;
  2522     return null;
  2523   },
  2525   get _iconURL() {
  2526     if (!this._iconURI)
  2527       return "";
  2528     return this._iconURI.spec;
  2529   },
  2531   // Where the engine is being loaded from: will return the URI's spec if the
  2532   // engine is being downloaded and does not yet have a file. This is only used
  2533   // for logging and error messages.
  2534   get _location() {
  2535     if (this._file)
  2536       return this._file.path;
  2538     if (this._uri)
  2539       return this._uri.spec;
  2541     return "";
  2542   },
  2544   /**
  2545    * @return the leaf name of the filename or URI of this plugin,
  2546    *         or null if no file or URI is known.
  2547    */
  2548   _getLeafName: function () {
  2549     if (this._file) {
  2550       return this._file.leafName;
  2552     if (this._uri && this._uri instanceof Ci.nsIURL) {
  2553       return this._uri.fileName;
  2555     return null;
  2556   },
  2558   // The file that the plugin is loaded from is a unique identifier for it.  We
  2559   // use this as the identifier to store data in the sqlite database
  2560   __id: null,
  2561   get _id() {
  2562     if (this.__id) {
  2563       return this.__id;
  2566     let leafName = this._getLeafName();
  2568     // Treat engines loaded from JARs the same way we treat app shipped
  2569     // engines.
  2570     // Theoretically, these could also come from extensions, but there's no
  2571     // real way for extensions to register their chrome locations at the
  2572     // moment, so let's not deal with that case.
  2573     // This means we're vulnerable to conflicts if a file loaded from a JAR
  2574     // has the same filename as a file loaded from the app dir, but with a
  2575     // different engine name. People using the JAR functionality should be
  2576     // careful not to do that!
  2577     if (this._isInAppDir || this._isInJAR) {
  2578       // App dir and JAR engines should always have leafNames
  2579       ENSURE_WARN(leafName, "_id: no leafName for appDir or JAR engine",
  2580                   Cr.NS_ERROR_UNEXPECTED);
  2581       return this.__id = "[app]/" + leafName;
  2584     if (this._isInProfile) {
  2585       ENSURE_WARN(leafName, "_id: no leafName for profile engine",
  2586                   Cr.NS_ERROR_UNEXPECTED);
  2587       return this.__id = "[profile]/" + leafName;
  2590     // If the engine isn't a JAR engine, it should have a file.
  2591     ENSURE_WARN(this._file, "_id: no _file for non-JAR engine",
  2592                 Cr.NS_ERROR_UNEXPECTED);
  2594     // We're not in the profile or appdir, so this must be an extension-shipped
  2595     // plugin. Use the full filename.
  2596     return this.__id = this._file.path;
  2597   },
  2599   get _installLocation() {
  2600     if (this.__installLocation === null) {
  2601       if (!this._file) {
  2602         ENSURE_WARN(this._uri, "Engines without files must have URIs",
  2603                     Cr.NS_ERROR_UNEXPECTED);
  2604         this.__installLocation = SEARCH_JAR;
  2606       else if (this._file.parent.equals(getDir(NS_APP_SEARCH_DIR)))
  2607         this.__installLocation = SEARCH_APP_DIR;
  2608       else if (this._file.parent.equals(getDir(NS_APP_USER_SEARCH_DIR)))
  2609         this.__installLocation = SEARCH_PROFILE_DIR;
  2610       else
  2611         this.__installLocation = SEARCH_IN_EXTENSION;
  2614     return this.__installLocation;
  2615   },
  2617   get _isInJAR() {
  2618     return this._installLocation == SEARCH_JAR;
  2619   },
  2620   get _isInAppDir() {
  2621     return this._installLocation == SEARCH_APP_DIR;
  2622   },
  2623   get _isInProfile() {
  2624     return this._installLocation == SEARCH_PROFILE_DIR;
  2625   },
  2627   get _isDefault() {
  2628     // For now, our concept of a "default engine" is "one that is not in the
  2629     // user's profile directory", which is currently equivalent to "is app- or
  2630     // extension-shipped".
  2631     return !this._isInProfile;
  2632   },
  2634   get _hasUpdates() {
  2635     // Whether or not the engine has an update URL
  2636     let selfURL = this._getURLOfType(URLTYPE_OPENSEARCH, "self");
  2637     return !!(this._updateURL || this._iconUpdateURL || selfURL);
  2638   },
  2640   get name() {
  2641     return this._name;
  2642   },
  2644   get type() {
  2645     return this._type;
  2646   },
  2648   get searchForm() {
  2649     // First look for a <Url rel="searchform">
  2650     var searchFormURL = this._getURLOfType(URLTYPE_SEARCH_HTML, "searchform");
  2651     if (searchFormURL) {
  2652       let submission = searchFormURL.getSubmission("", this);
  2654       // If the rel=searchform URL is not type="get" (i.e. has postData),
  2655       // ignore it, since we can only return a URL.
  2656       if (!submission.postData)
  2657         return submission.uri.spec;
  2660     if (!this._searchForm) {
  2661       // No SearchForm specified in the engine definition file, use the prePath
  2662       // (e.g. https://foo.com for https://foo.com/search.php?q=bar).
  2663       var htmlUrl = this._getURLOfType(URLTYPE_SEARCH_HTML);
  2664       ENSURE_WARN(htmlUrl, "Engine has no HTML URL!", Cr.NS_ERROR_UNEXPECTED);
  2665       this._searchForm = makeURI(htmlUrl.template).prePath;
  2668     return ParamSubstitution(this._searchForm, "", this);
  2669   },
  2671   get queryCharset() {
  2672     if (this._queryCharset)
  2673       return this._queryCharset;
  2674     return this._queryCharset = queryCharsetFromCode(/* get the default */);
  2675   },
  2677   // from nsISearchEngine
  2678   addParam: function SRCH_ENG_addParam(aName, aValue, aResponseType) {
  2679     if (!aName || (aValue == null))
  2680       FAIL("missing name or value for nsISearchEngine::addParam!");
  2681     ENSURE_WARN(!this._readOnly,
  2682                 "called nsISearchEngine::addParam on a read-only engine!",
  2683                 Cr.NS_ERROR_FAILURE);
  2684     if (!aResponseType)
  2685       aResponseType = URLTYPE_SEARCH_HTML;
  2687     var url = this._getURLOfType(aResponseType);
  2688     if (!url)
  2689       FAIL("Engine object has no URL for response type " + aResponseType,
  2690            Cr.NS_ERROR_FAILURE);
  2692     url.addParam(aName, aValue);
  2694     // Serialize the changes to file lazily
  2695     this.lazySerializeTask.arm();
  2696   },
  2698 #ifdef ANDROID
  2699   get _defaultMobileResponseType() {
  2700     let type = URLTYPE_SEARCH_HTML;
  2702     let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
  2703     let isTablet = sysInfo.get("tablet");
  2704     if (isTablet && this.supportsResponseType("application/x-moz-tabletsearch")) {
  2705       // Check for a tablet-specific search URL override
  2706       type = "application/x-moz-tabletsearch";
  2707     } else if (!isTablet && this.supportsResponseType("application/x-moz-phonesearch")) {
  2708       // Check for a phone-specific search URL override
  2709       type = "application/x-moz-phonesearch";
  2712     delete this._defaultMobileResponseType;
  2713     return this._defaultMobileResponseType = type;
  2714   },
  2715 #endif
  2717   // from nsISearchEngine
  2718   getSubmission: function SRCH_ENG_getSubmission(aData, aResponseType, aPurpose) {
  2719 #ifdef ANDROID
  2720     if (!aResponseType) {
  2721       aResponseType = this._defaultMobileResponseType;
  2723 #endif
  2724     if (!aResponseType) {
  2725       aResponseType = URLTYPE_SEARCH_HTML;
  2728     var url = this._getURLOfType(aResponseType);
  2730     if (!url)
  2731       return null;
  2733     if (!aData) {
  2734       // Return a dummy submission object with our searchForm attribute
  2735       return new Submission(makeURI(this.searchForm), null);
  2738     LOG("getSubmission: In data: \"" + aData + "\"; Purpose: \"" + aPurpose + "\"");
  2739     var textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
  2740                        getService(Ci.nsITextToSubURI);
  2741     var data = "";
  2742     try {
  2743       data = textToSubURI.ConvertAndEscape(this.queryCharset, aData);
  2744     } catch (ex) {
  2745       LOG("getSubmission: Falling back to default queryCharset!");
  2746       data = textToSubURI.ConvertAndEscape(DEFAULT_QUERY_CHARSET, aData);
  2748     LOG("getSubmission: Out data: \"" + data + "\"");
  2749     return url.getSubmission(data, this, aPurpose);
  2750   },
  2752   // from nsISearchEngine
  2753   supportsResponseType: function SRCH_ENG_supportsResponseType(type) {
  2754     return (this._getURLOfType(type) != null);
  2755   },
  2757   // from nsISearchEngine
  2758   getResultDomain: function SRCH_ENG_getResultDomain(aResponseType) {
  2759 #ifdef ANDROID
  2760     if (!aResponseType) {
  2761       aResponseType = this._defaultMobileResponseType;
  2763 #endif
  2764     if (!aResponseType) {
  2765       aResponseType = URLTYPE_SEARCH_HTML;
  2768     LOG("getResultDomain: responseType: \"" + aResponseType + "\"");
  2770     let url = this._getURLOfType(aResponseType);
  2771     if (url)
  2772       return url.resultDomain;
  2773     return "";
  2774   },
  2776   // nsISupports
  2777   QueryInterface: function SRCH_ENG_QI(aIID) {
  2778     if (aIID.equals(Ci.nsISearchEngine) ||
  2779         aIID.equals(Ci.nsISupports))
  2780       return this;
  2781     throw Cr.NS_ERROR_NO_INTERFACE;
  2782   },
  2784   get wrappedJSObject() {
  2785     return this;
  2786   },
  2788   /**
  2789    * Returns a string with the URL to an engine's icon matching both width and
  2790    * height. Returns null if icon with specified dimensions is not found.
  2792    * @param width
  2793    *        Width of the requested icon.
  2794    * @param height
  2795    *        Height of the requested icon.
  2796    */
  2797   getIconURLBySize: function SRCH_ENG_getIconURLBySize(aWidth, aHeight) {
  2798     if (!this._iconMapObj)
  2799       return null;
  2801     let key = this._getIconKey(aWidth, aHeight);
  2802     if (key in this._iconMapObj) {
  2803       return this._iconMapObj[key];
  2805     return null;
  2806   },
  2808   /**
  2809    * Gets an array of all available icons. Each entry is an object with
  2810    * width, height and url properties. width and height are numeric and
  2811    * represent the icon's dimensions. url is a string with the URL for
  2812    * the icon.
  2813    */
  2814   getIcons: function SRCH_ENG_getIcons() {
  2815     let result = [];
  2817     if (!this._iconMapObj)
  2818       return result;
  2820     for (let key of Object.keys(this._iconMapObj)) {
  2821       let iconSize = JSON.parse(key);
  2822       result.push({
  2823         width: iconSize.width,
  2824         height: iconSize.height,
  2825         url: this._iconMapObj[key]
  2826       });
  2829     return result;
  2831 };
  2833 // nsISearchSubmission
  2834 function Submission(aURI, aPostData = null) {
  2835   this._uri = aURI;
  2836   this._postData = aPostData;
  2838 Submission.prototype = {
  2839   get uri() {
  2840     return this._uri;
  2841   },
  2842   get postData() {
  2843     return this._postData;
  2844   },
  2845   QueryInterface: function SRCH_SUBM_QI(aIID) {
  2846     if (aIID.equals(Ci.nsISearchSubmission) ||
  2847         aIID.equals(Ci.nsISupports))
  2848       return this;
  2849     throw Cr.NS_ERROR_NO_INTERFACE;
  2853 function executeSoon(func) {
  2854   Services.tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
  2857 /**
  2858  * Check for sync initialization has completed or not.
  2860  * @param {aPromise} A promise.
  2862  * @returns the value returned by the invoked method.
  2863  * @throws NS_ERROR_ALREADY_INITIALIZED if sync initialization has completed.
  2864  */
  2865 function checkForSyncCompletion(aPromise) {
  2866   return aPromise.then(function(aValue) {
  2867     if (gInitialized) {
  2868       throw Components.Exception("Synchronous fallback was called and has " +
  2869                                  "finished so no need to pursue asynchronous " +
  2870                                  "initialization",
  2871                                  Cr.NS_ERROR_ALREADY_INITIALIZED);
  2873     return aValue;
  2874   });
  2877 // nsIBrowserSearchService
  2878 function SearchService() {
  2879   // Replace empty LOG function with the useful one if the log pref is set.
  2880   if (getBoolPref(BROWSER_SEARCH_PREF + "log", false))
  2881     LOG = DO_LOG;
  2883   this._initObservers = Promise.defer();
  2886 SearchService.prototype = {
  2887   classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"),
  2889   // The current status of initialization. Note that it does not determine if
  2890   // initialization is complete, only if an error has been encountered so far.
  2891   _initRV: Cr.NS_OK,
  2893   // The boolean indicates that the initialization has started or not.
  2894   _initStarted: null,
  2896   // If initialization has not been completed yet, perform synchronous
  2897   // initialization.
  2898   // Throws in case of initialization error.
  2899   _ensureInitialized: function  SRCH_SVC__ensureInitialized() {
  2900     if (gInitialized) {
  2901       if (!Components.isSuccessCode(this._initRV)) {
  2902         LOG("_ensureInitialized: failure");
  2903         throw this._initRV;
  2905       return;
  2908     let warning =
  2909       "Search service falling back to synchronous initialization. " +
  2910       "This is generally the consequence of an add-on using a deprecated " +
  2911       "search service API.";
  2912     // Bug 785487 - Disable warning until our own callers are fixed.
  2913     //Deprecated.warning(warning, "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIBrowserSearchService#async_warning");
  2914     LOG(warning);
  2916     engineMetadataService.syncInit();
  2917     this._syncInit();
  2918     if (!Components.isSuccessCode(this._initRV)) {
  2919       throw this._initRV;
  2921   },
  2923   // Synchronous implementation of the initializer.
  2924   // Used by |_ensureInitialized| as a fallback if initialization is not
  2925   // complete.
  2926   _syncInit: function SRCH_SVC__syncInit() {
  2927     LOG("_syncInit start");
  2928     this._initStarted = true;
  2929     try {
  2930       this._syncLoadEngines();
  2931     } catch (ex) {
  2932       this._initRV = Cr.NS_ERROR_FAILURE;
  2933       LOG("_syncInit: failure loading engines: " + ex);
  2935     this._addObservers();
  2937     gInitialized = true;
  2939     this._initObservers.resolve(this._initRV);
  2941     Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
  2943     LOG("_syncInit end");
  2944   },
  2946   /**
  2947    * Asynchronous implementation of the initializer.
  2949    * @returns {Promise} A promise, resolved successfully if the initialization
  2950    * succeeds.
  2951    */
  2952   _asyncInit: function SRCH_SVC__asyncInit() {
  2953     return TaskUtils.spawn(function() {
  2954       LOG("_asyncInit start");
  2955       try {
  2956         yield checkForSyncCompletion(this._asyncLoadEngines());
  2957       } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  2958         this._initRV = Cr.NS_ERROR_FAILURE;
  2959         LOG("_asyncInit: failure loading engines: " + ex);
  2961       this._addObservers();
  2962       gInitialized = true;
  2963       this._initObservers.resolve(this._initRV);
  2964       Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
  2965       LOG("_asyncInit: Completed _asyncInit");
  2966     }.bind(this));
  2967   },
  2970   _engines: { },
  2971   __sortedEngines: null,
  2972   get _sortedEngines() {
  2973     if (!this.__sortedEngines)
  2974       return this._buildSortedEngineList();
  2975     return this.__sortedEngines;
  2976   },
  2978   // Get the original Engine object that belongs to the defaultenginename pref
  2979   // of the default branch.
  2980   get _originalDefaultEngine() {
  2981     let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
  2982     let nsIPLS = Ci.nsIPrefLocalizedString;
  2983     let defaultEngine;
  2984     try {
  2985       defaultEngine = defaultPrefB.getComplexValue("defaultenginename", nsIPLS).data;
  2986     } catch (ex) {
  2987       // If the default pref is invalid (e.g. an add-on set it to a bogus value)
  2988       // getEngineByName will just return null, which is the best we can do.
  2990     return this.getEngineByName(defaultEngine);
  2991   },
  2993   _buildCache: function SRCH_SVC__buildCache() {
  2994     if (!getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true))
  2995       return;
  2997     TelemetryStopwatch.start("SEARCH_SERVICE_BUILD_CACHE_MS");
  2998     let cache = {};
  2999     let locale = getLocale();
  3000     let buildID = Services.appinfo.platformBuildID;
  3002     // Allows us to force a cache refresh should the cache format change.
  3003     cache.version = CACHE_VERSION;
  3004     // We don't want to incur the costs of stat()ing each plugin on every
  3005     // startup when the only (supported) time they will change is during
  3006     // runtime (where we refresh for changes through the API) and app updates
  3007     // (where the buildID is obviously going to change).
  3008     // Extension-shipped plugins are the only exception to this, but their
  3009     // directories are blown away during updates, so we'll detect their changes.
  3010     cache.buildID = buildID;
  3011     cache.locale = locale;
  3013     cache.directories = {};
  3015     function getParent(engine) {
  3016       if (engine._file)
  3017         return engine._file.parent;
  3019       let uri = engine._uri;
  3020       if (!uri.schemeIs("chrome")) {
  3021         LOG("getParent: engine URI must be a chrome URI if it has no file");
  3022         return null;
  3025       // use the underlying JAR file, for chrome URIs
  3026       try {
  3027         uri = gChromeReg.convertChromeURL(uri);
  3028         if (uri instanceof Ci.nsINestedURI)
  3029           uri = uri.innermostURI;
  3030         uri.QueryInterface(Ci.nsIFileURL)
  3032         return uri.file;
  3033       } catch (ex) {
  3034         LOG("getParent: couldn't map chrome:// URI to a file: " + ex)
  3037       return null;
  3040     for each (let engine in this._engines) {
  3041       let parent = getParent(engine);
  3042       if (!parent) {
  3043         LOG("Error: no parent for engine " + engine._location + ", failing to cache it");
  3045         continue;
  3048       let cacheKey = parent.path;
  3049       if (!cache.directories[cacheKey]) {
  3050         let cacheEntry = {};
  3051         cacheEntry.lastModifiedTime = parent.lastModifiedTime;
  3052         cacheEntry.engines = [];
  3053         cache.directories[cacheKey] = cacheEntry;
  3055       cache.directories[cacheKey].engines.push(engine._serializeToJSON(true));
  3058     try {
  3059       LOG("_buildCache: Writing to cache file.");
  3060       let path = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
  3061       let data = gEncoder.encode(JSON.stringify(cache));
  3062       let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp"});
  3064       promise.then(
  3065         function onSuccess() {
  3066           Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, SEARCH_SERVICE_CACHE_WRITTEN);
  3067         },
  3068         function onError(e) {
  3069           LOG("_buildCache: failure during writeAtomic: " + e);
  3071       );
  3072     } catch (ex) {
  3073       LOG("_buildCache: Could not write to cache file: " + ex);
  3075     TelemetryStopwatch.finish("SEARCH_SERVICE_BUILD_CACHE_MS");
  3076   },
  3078   _syncLoadEngines: function SRCH_SVC__syncLoadEngines() {
  3079     LOG("_syncLoadEngines: start");
  3080     // See if we have a cache file so we don't have to parse a bunch of XML.
  3081     let cache = {};
  3082     let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
  3083     if (cacheEnabled) {
  3084       let cacheFile = getDir(NS_APP_USER_PROFILE_50_DIR);
  3085       cacheFile.append("search.json");
  3086       if (cacheFile.exists())
  3087         cache = this._readCacheFile(cacheFile);
  3090     let loadDirs = [];
  3091     let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
  3092     while (locations.hasMoreElements()) {
  3093       let dir = locations.getNext().QueryInterface(Ci.nsIFile);
  3094       if (dir.directoryEntries.hasMoreElements())
  3095         loadDirs.push(dir);
  3098     let loadFromJARs = getBoolPref(BROWSER_SEARCH_PREF + "loadFromJars", false);
  3099     let chromeURIs = [];
  3100     let chromeFiles = [];
  3101     if (loadFromJARs)
  3102       [chromeFiles, chromeURIs] = this._findJAREngines();
  3104     let toLoad = chromeFiles.concat(loadDirs);
  3106     function modifiedDir(aDir) {
  3107       return (!cache.directories || !cache.directories[aDir.path] ||
  3108               cache.directories[aDir.path].lastModifiedTime != aDir.lastModifiedTime);
  3111     function notInCachePath(aPathToLoad)
  3112       cachePaths.indexOf(aPathToLoad.path) == -1;
  3114     let buildID = Services.appinfo.platformBuildID;
  3115     let cachePaths = [path for (path in cache.directories)];
  3117     let rebuildCache = !cache.directories ||
  3118                        cache.version != CACHE_VERSION ||
  3119                        cache.locale != getLocale() ||
  3120                        cache.buildID != buildID ||
  3121                        cachePaths.length != toLoad.length ||
  3122                        toLoad.some(notInCachePath) ||
  3123                        toLoad.some(modifiedDir);
  3125     if (!cacheEnabled || rebuildCache) {
  3126       LOG("_loadEngines: Absent or outdated cache. Loading engines from disk.");
  3127       loadDirs.forEach(this._loadEnginesFromDir, this);
  3129       this._loadFromChromeURLs(chromeURIs);
  3131       if (cacheEnabled)
  3132         this._buildCache();
  3133       return;
  3136     LOG("_loadEngines: loading from cache directories");
  3137     for each (let dir in cache.directories)
  3138       this._loadEnginesFromCache(dir);
  3140     LOG("_loadEngines: done");
  3141   },
  3143   /**
  3144    * Loads engines asynchronously.
  3146    * @returns {Promise} A promise, resolved successfully if loading data
  3147    * succeeds.
  3148    */
  3149   _asyncLoadEngines: function SRCH_SVC__asyncLoadEngines() {
  3150     return TaskUtils.spawn(function() {
  3151       LOG("_asyncLoadEngines: start");
  3152       // See if we have a cache file so we don't have to parse a bunch of XML.
  3153       let cache = {};
  3154       let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
  3155       if (cacheEnabled) {
  3156         let cacheFilePath = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
  3157         cache = yield checkForSyncCompletion(this._asyncReadCacheFile(cacheFilePath));
  3160       // Add all the non-empty directories of NS_APP_SEARCH_DIR_LIST to
  3161       // loadDirs.
  3162       let loadDirs = [];
  3163       let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
  3164       while (locations.hasMoreElements()) {
  3165         let dir = locations.getNext().QueryInterface(Ci.nsIFile);
  3166         let iterator = new OS.File.DirectoryIterator(dir.path,
  3167                                                      { winPattern: "*.xml" });
  3168         try {
  3169           // Add dir to loadDirs if it contains any files.
  3170           yield checkForSyncCompletion(iterator.next());
  3171           loadDirs.push(dir);
  3172         } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  3173           // Catch for StopIteration exception.
  3174         } finally {
  3175           iterator.close();
  3179       let loadFromJARs = getBoolPref(BROWSER_SEARCH_PREF + "loadFromJars", false);
  3180       let chromeURIs = [];
  3181       let chromeFiles = [];
  3182       if (loadFromJARs) {
  3183         Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "find-jar-engines");
  3184         [chromeFiles, chromeURIs] =
  3185           yield checkForSyncCompletion(this._asyncFindJAREngines());
  3188       let toLoad = chromeFiles.concat(loadDirs);
  3189       function hasModifiedDir(aList) {
  3190         return TaskUtils.spawn(function() {
  3191           let modifiedDir = false;
  3193           for (let dir of aList) {
  3194             if (!cache.directories || !cache.directories[dir.path]) {
  3195               modifiedDir = true;
  3196               break;
  3199             let info = yield OS.File.stat(dir.path);
  3200             if (cache.directories[dir.path].lastModifiedTime !=
  3201                 info.lastModificationDate.getTime()) {
  3202               modifiedDir = true;
  3203               break;
  3206           throw new Task.Result(modifiedDir);
  3207         });
  3210       function notInCachePath(aPathToLoad)
  3211         cachePaths.indexOf(aPathToLoad.path) == -1;
  3213       let buildID = Services.appinfo.platformBuildID;
  3214       let cachePaths = [path for (path in cache.directories)];
  3216       let rebuildCache = !cache.directories ||
  3217                          cache.version != CACHE_VERSION ||
  3218                          cache.locale != getLocale() ||
  3219                          cache.buildID != buildID ||
  3220                          cachePaths.length != toLoad.length ||
  3221                          toLoad.some(notInCachePath) ||
  3222                          (yield checkForSyncCompletion(hasModifiedDir(toLoad)));
  3224       if (!cacheEnabled || rebuildCache) {
  3225         LOG("_asyncLoadEngines: Absent or outdated cache. Loading engines from disk.");
  3226         let engines = [];
  3227         for (let loadDir of loadDirs) {
  3228           let enginesFromDir =
  3229             yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
  3230           engines = engines.concat(enginesFromDir);
  3232         let enginesFromURLs =
  3233            yield checkForSyncCompletion(this._asyncLoadFromChromeURLs(chromeURIs));
  3234         engines = engines.concat(enginesFromURLs);
  3236         for (let engine of engines) {
  3237           this._addEngineToStore(engine);
  3239         if (cacheEnabled)
  3240           this._buildCache();
  3241         return;
  3244       LOG("_asyncLoadEngines: loading from cache directories");
  3245       for each (let dir in cache.directories)
  3246         this._loadEnginesFromCache(dir);
  3248       LOG("_asyncLoadEngines: done");
  3249     }.bind(this));
  3250   },
  3252   _readCacheFile: function SRCH_SVC__readCacheFile(aFile) {
  3253     let stream = Cc["@mozilla.org/network/file-input-stream;1"].
  3254                  createInstance(Ci.nsIFileInputStream);
  3255     let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  3257     try {
  3258       stream.init(aFile, MODE_RDONLY, PERMS_FILE, 0);
  3259       return json.decodeFromStream(stream, stream.available());
  3260     } catch (ex) {
  3261       LOG("_readCacheFile: Error reading cache file: " + ex);
  3262     } finally {
  3263       stream.close();
  3265     return false;
  3266   },
  3268   /**
  3269    * Read from a given cache file asynchronously.
  3271    * @param aPath the file path.
  3273    * @returns {Promise} A promise, resolved successfully if retrieveing data
  3274    * succeeds.
  3275    */
  3276   _asyncReadCacheFile: function SRCH_SVC__asyncReadCacheFile(aPath) {
  3277     return TaskUtils.spawn(function() {
  3278       let json;
  3279       try {
  3280         let bytes = yield OS.File.read(aPath);
  3281         json = JSON.parse(new TextDecoder().decode(bytes));
  3282       } catch (ex) {
  3283         LOG("_asyncReadCacheFile: Error reading cache file: " + ex);
  3284         json = {};
  3286       throw new Task.Result(json);
  3287     });
  3288   },
  3290   _batchTask: null,
  3291   get batchTask() {
  3292     if (!this._batchTask) {
  3293       let task = function taskCallback() {
  3294         LOG("batchTask: Invalidating engine cache");
  3295         this._buildCache();
  3296       }.bind(this);
  3297       this._batchTask = new DeferredTask(task, CACHE_INVALIDATION_DELAY);
  3299     return this._batchTask;
  3300   },
  3302   _addEngineToStore: function SRCH_SVC_addEngineToStore(aEngine) {
  3303     LOG("_addEngineToStore: Adding engine: \"" + aEngine.name + "\"");
  3305     // See if there is an existing engine with the same name. However, if this
  3306     // engine is updating another engine, it's allowed to have the same name.
  3307     var hasSameNameAsUpdate = (aEngine._engineToUpdate &&
  3308                                aEngine.name == aEngine._engineToUpdate.name);
  3309     if (aEngine.name in this._engines && !hasSameNameAsUpdate) {
  3310       LOG("_addEngineToStore: Duplicate engine found, aborting!");
  3311       return;
  3314     if (aEngine._engineToUpdate) {
  3315       // We need to replace engineToUpdate with the engine that just loaded.
  3316       var oldEngine = aEngine._engineToUpdate;
  3318       // Remove the old engine from the hash, since it's keyed by name, and our
  3319       // name might change (the update might have a new name).
  3320       delete this._engines[oldEngine.name];
  3322       // Hack: we want to replace the old engine with the new one, but since
  3323       // people may be holding refs to the nsISearchEngine objects themselves,
  3324       // we'll just copy over all "private" properties (those without a getter
  3325       // or setter) from one object to the other.
  3326       for (var p in aEngine) {
  3327         if (!(aEngine.__lookupGetter__(p) || aEngine.__lookupSetter__(p)))
  3328           oldEngine[p] = aEngine[p];
  3330       aEngine = oldEngine;
  3331       aEngine._engineToUpdate = null;
  3333       // Add the engine back
  3334       this._engines[aEngine.name] = aEngine;
  3335       notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
  3336     } else {
  3337       // Not an update, just add the new engine.
  3338       this._engines[aEngine.name] = aEngine;
  3339       // Only add the engine to the list of sorted engines if the initial list
  3340       // has already been built (i.e. if this.__sortedEngines is non-null). If
  3341       // it hasn't, we're loading engines from disk and the sorted engine list
  3342       // will be built once we need it.
  3343       if (this.__sortedEngines) {
  3344         this.__sortedEngines.push(aEngine);
  3345         this._saveSortedEngineList();
  3347       notifyAction(aEngine, SEARCH_ENGINE_ADDED);
  3350     if (aEngine._hasUpdates) {
  3351       // Schedule the engine's next update, if it isn't already.
  3352       if (!engineMetadataService.getAttr(aEngine, "updateexpir"))
  3353         engineUpdateService.scheduleNextUpdate(aEngine);
  3355       // We need to save the engine's _dataType, if this is the first time the
  3356       // engine is added to the dataStore, since ._dataType isn't persisted
  3357       // and will change on the next startup (since the engine will then be
  3358       // XML). We need this so that we know how to load any future updates from
  3359       // this engine.
  3360       if (!engineMetadataService.getAttr(aEngine, "updatedatatype"))
  3361         engineMetadataService.setAttr(aEngine, "updatedatatype",
  3362                                       aEngine._dataType);
  3364   },
  3366   _loadEnginesFromCache: function SRCH_SVC__loadEnginesFromCache(aDir) {
  3367     let engines = aDir.engines;
  3368     LOG("_loadEnginesFromCache: Loading from cache. " + engines.length + " engines to load.");
  3369     for (let i = 0; i < engines.length; i++) {
  3370       let json = engines[i];
  3372       try {
  3373         let engine;
  3374         if (json.filePath)
  3375           engine = new Engine({type: "filePath", value: json.filePath}, json._dataType,
  3376                                json._readOnly);
  3377         else if (json._url)
  3378           engine = new Engine({type: "uri", value: json._url}, json._dataType, json._readOnly);
  3380         engine._initWithJSON(json);
  3381         this._addEngineToStore(engine);
  3382       } catch (ex) {
  3383         LOG("Failed to load " + engines[i]._name + " from cache: " + ex);
  3384         LOG("Engine JSON: " + engines[i].toSource());
  3387   },
  3389   _loadEnginesFromDir: function SRCH_SVC__loadEnginesFromDir(aDir) {
  3390     LOG("_loadEnginesFromDir: Searching in " + aDir.path + " for search engines.");
  3392     // Check whether aDir is the user profile dir
  3393     var isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR));
  3395     var files = aDir.directoryEntries
  3396                     .QueryInterface(Ci.nsIDirectoryEnumerator);
  3398     while (files.hasMoreElements()) {
  3399       var file = files.nextFile;
  3401       // Ignore hidden and empty files, and directories
  3402       if (!file.isFile() || file.fileSize == 0 || file.isHidden())
  3403         continue;
  3405       var fileURL = NetUtil.ioService.newFileURI(file).QueryInterface(Ci.nsIURL);
  3406       var fileExtension = fileURL.fileExtension.toLowerCase();
  3407       var isWritable = isInProfile && file.isWritable();
  3409       if (fileExtension != "xml") {
  3410         // Not an engine
  3411         continue;
  3414       var addedEngine = null;
  3415       try {
  3416         addedEngine = new Engine(file, SEARCH_DATA_XML, !isWritable);
  3417         addedEngine._initFromFile();
  3418       } catch (ex) {
  3419         LOG("_loadEnginesFromDir: Failed to load " + file.path + "!\n" + ex);
  3420         continue;
  3423       this._addEngineToStore(addedEngine);
  3425   },
  3427   /**
  3428    * Loads engines from a given directory asynchronously.
  3430    * @param aDir the directory.
  3432    * @returns {Promise} A promise, resolved successfully if retrieveing data
  3433    * succeeds.
  3434    */
  3435   _asyncLoadEnginesFromDir: function SRCH_SVC__asyncLoadEnginesFromDir(aDir) {
  3436     LOG("_asyncLoadEnginesFromDir: Searching in " + aDir.path + " for search engines.");
  3438     // Check whether aDir is the user profile dir
  3439     let isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR));
  3440     let iterator = new OS.File.DirectoryIterator(aDir.path);
  3441     return TaskUtils.spawn(function() {
  3442       let osfiles = yield iterator.nextBatch();
  3443       iterator.close();
  3445       let engines = [];
  3446       for (let osfile of osfiles) {
  3447         if (osfile.isDir || osfile.isSymLink)
  3448           continue;
  3450         let fileInfo = yield OS.File.stat(osfile.path);
  3451         if (fileInfo.size == 0)
  3452           continue;
  3454         let parts = osfile.path.split(".");
  3455         if (parts.length <= 1 || (parts.pop()).toLowerCase() != "xml") {
  3456           // Not an engine
  3457           continue;
  3460         let addedEngine = null;
  3461         try {
  3462           let file = new FileUtils.File(osfile.path);
  3463           let isWritable = isInProfile;
  3464           addedEngine = new Engine(file, SEARCH_DATA_XML, !isWritable);
  3465           yield checkForSyncCompletion(addedEngine._asyncInitFromFile());
  3466         } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  3467           LOG("_asyncLoadEnginesFromDir: Failed to load " + osfile.path + "!\n" + ex);
  3468           continue;
  3470         engines.push(addedEngine);
  3472       throw new Task.Result(engines);
  3473     }.bind(this));
  3474   },
  3476   _loadFromChromeURLs: function SRCH_SVC_loadFromChromeURLs(aURLs) {
  3477     aURLs.forEach(function (url) {
  3478       try {
  3479         LOG("_loadFromChromeURLs: loading engine from chrome url: " + url);
  3481         let engine = new Engine(makeURI(url), SEARCH_DATA_XML, true);
  3483         engine._initFromURISync();
  3485         this._addEngineToStore(engine);
  3486       } catch (ex) {
  3487         LOG("_loadFromChromeURLs: failed to load engine: " + ex);
  3489     }, this);
  3490   },
  3492   /**
  3493    * Loads engines from Chrome URLs asynchronously.
  3495    * @param aURLs a list of URLs.
  3497    * @returns {Promise} A promise, resolved successfully if loading data
  3498    * succeeds.
  3499    */
  3500   _asyncLoadFromChromeURLs: function SRCH_SVC__asyncLoadFromChromeURLs(aURLs) {
  3501     return TaskUtils.spawn(function() {
  3502       let engines = [];
  3503       for (let url of aURLs) {
  3504         try {
  3505           LOG("_asyncLoadFromChromeURLs: loading engine from chrome url: " + url);
  3506           let engine = new Engine(NetUtil.newURI(url), SEARCH_DATA_XML, true);
  3507           yield checkForSyncCompletion(engine._asyncInitFromURI());
  3508           engines.push(engine);
  3509         } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) {
  3510           LOG("_asyncLoadFromChromeURLs: failed to load engine: " + ex);
  3513       throw new Task.Result(engines);
  3514     }.bind(this));
  3515   },
  3517   _findJAREngines: function SRCH_SVC_findJAREngines() {
  3518     LOG("_findJAREngines: looking for engines in JARs")
  3520     let rootURIPref = ""
  3521     try {
  3522       rootURIPref = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "jarURIs");
  3523     } catch (ex) {}
  3525     if (!rootURIPref) {
  3526       LOG("_findJAREngines: no JAR URIs were specified");
  3528       return [[], []];
  3531     let rootURIs = rootURIPref.split(",");
  3532     let uris = [];
  3533     let chromeFiles = [];
  3535     rootURIs.forEach(function (root) {
  3536       // Find the underlying JAR file for this chrome package (_loadEngines uses
  3537       // it to determine whether it needs to invalidate the cache)
  3538       let chromeFile;
  3539       try {
  3540         let chromeURI = gChromeReg.convertChromeURL(makeURI(root));
  3541         let fileURI = chromeURI; // flat packaging
  3542         while (fileURI instanceof Ci.nsIJARURI)
  3543           fileURI = fileURI.JARFile; // JAR packaging
  3544         fileURI.QueryInterface(Ci.nsIFileURL);
  3545         chromeFile = fileURI.file;
  3546       } catch (ex) {
  3547         LOG("_findJAREngines: failed to get chromeFile for " + root + ": " + ex);
  3550       if (!chromeFile)
  3551         return;
  3553       chromeFiles.push(chromeFile);
  3555       // Read list.txt from the chrome package to find the engines we need to
  3556       // load
  3557       let listURL = root + "list.txt";
  3558       let names = [];
  3559       try {
  3560         let chan = NetUtil.ioService.newChannelFromURI(makeURI(listURL));
  3561         let sis = Cc["@mozilla.org/scriptableinputstream;1"].
  3562                   createInstance(Ci.nsIScriptableInputStream);
  3563         sis.init(chan.open());
  3564         let list = sis.read(sis.available());
  3565         names = list.split("\n").filter(function (n) !!n);
  3566       } catch (ex) {
  3567         LOG("_findJAREngines: failed to retrieve list.txt from " + listURL + ": " + ex);
  3569         return;
  3572       names.forEach(function (n) uris.push(root + n + ".xml"));
  3573     });
  3575     return [chromeFiles, uris];
  3576   },
  3578   /**
  3579    * Loads jar engines asynchronously.
  3581    * @returns {Promise} A promise, resolved successfully if finding jar engines
  3582    * succeeds.
  3583    */
  3584   _asyncFindJAREngines: function SRCH_SVC__asyncFindJAREngines() {
  3585     return TaskUtils.spawn(function() {
  3586       LOG("_asyncFindJAREngines: looking for engines in JARs")
  3588       let rootURIPref = "";
  3589       try {
  3590         rootURIPref = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "jarURIs");
  3591       } catch (ex) {}
  3593       if (!rootURIPref) {
  3594         LOG("_asyncFindJAREngines: no JAR URIs were specified");
  3595         throw new Task.Result([[], []]);
  3598       let rootURIs = rootURIPref.split(",");
  3599       let uris = [];
  3600       let chromeFiles = [];
  3602       for (let root of rootURIs) {
  3603         // Find the underlying JAR file for this chrome package (_loadEngines uses
  3604         // it to determine whether it needs to invalidate the cache)
  3605         let chromeFile;
  3606         try {
  3607           let chromeURI = gChromeReg.convertChromeURL(makeURI(root));
  3608           let fileURI = chromeURI; // flat packaging
  3609           while (fileURI instanceof Ci.nsIJARURI)
  3610             fileURI = fileURI.JARFile; // JAR packaging
  3611           fileURI.QueryInterface(Ci.nsIFileURL);
  3612           chromeFile = fileURI.file;
  3613         } catch (ex) {
  3614           LOG("_asyncFindJAREngines: failed to get chromeFile for " + root + ": " + ex);
  3617         if (!chromeFile) {
  3618           return;
  3621         chromeFiles.push(chromeFile);
  3623         // Read list.txt from the chrome package to find the engines we need to
  3624         // load
  3625         let listURL = root + "list.txt";
  3626         let deferred = Promise.defer();
  3627         let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
  3628                         createInstance(Ci.nsIXMLHttpRequest);
  3629         request.onload = function(aEvent) {
  3630           deferred.resolve(aEvent.target.responseText);
  3631         };
  3632         request.onerror = function(aEvent) {
  3633           LOG("_asyncFindJAREngines: failed to retrieve list.txt from " + listURL);
  3634           deferred.resolve("");
  3635         };
  3636         request.open("GET", NetUtil.newURI(listURL).spec, true);
  3637         request.send();
  3638         let list = yield deferred.promise;
  3640         let names = [];
  3641         names = list.split("\n").filter(function (n) !!n);
  3642         names.forEach(function (n) uris.push(root + n + ".xml"));
  3644       throw new Task.Result([chromeFiles, uris]);
  3645     });
  3646   },
  3649   _saveSortedEngineList: function SRCH_SVC_saveSortedEngineList() {
  3650     LOG("SRCH_SVC_saveSortedEngineList: starting");
  3652     // Set the useDB pref to indicate that from now on we should use the order
  3653     // information stored in the database.
  3654     Services.prefs.setBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", true);
  3656     var engines = this._getSortedEngines(true);
  3658     let instructions = [];
  3659     for (var i = 0; i < engines.length; ++i) {
  3660       instructions.push(
  3661         {key: "order",
  3662          value: i+1,
  3663          engine: engines[i]
  3664         });
  3667     engineMetadataService.setAttrs(instructions);
  3668     LOG("SRCH_SVC_saveSortedEngineList: done");
  3669   },
  3671   _buildSortedEngineList: function SRCH_SVC_buildSortedEngineList() {
  3672     LOG("_buildSortedEngineList: building list");
  3673     var addedEngines = { };
  3674     this.__sortedEngines = [];
  3675     var engine;
  3677     // If the user has specified a custom engine order, read the order
  3678     // information from the engineMetadataService instead of the default
  3679     // prefs.
  3680     if (getBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", false)) {
  3681       LOG("_buildSortedEngineList: using db for order");
  3683       // Flag to keep track of whether or not we need to call _saveSortedEngineList. 
  3684       let needToSaveEngineList = false;
  3686       for each (engine in this._engines) {
  3687         var orderNumber = engineMetadataService.getAttr(engine, "order");
  3689         // Since the DB isn't regularly cleared, and engine files may disappear
  3690         // without us knowing, we may already have an engine in this slot. If
  3691         // that happens, we just skip it - it will be added later on as an
  3692         // unsorted engine.
  3693         if (orderNumber && !this.__sortedEngines[orderNumber-1]) {
  3694           this.__sortedEngines[orderNumber-1] = engine;
  3695           addedEngines[engine.name] = engine;
  3696         } else {
  3697           // We need to call _saveSortedEngineList so this gets sorted out.
  3698           needToSaveEngineList = true;
  3702       // Filter out any nulls for engines that may have been removed
  3703       var filteredEngines = this.__sortedEngines.filter(function(a) { return !!a; });
  3704       if (this.__sortedEngines.length != filteredEngines.length)
  3705         needToSaveEngineList = true;
  3706       this.__sortedEngines = filteredEngines;
  3708       if (needToSaveEngineList)
  3709         this._saveSortedEngineList();
  3710     } else {
  3711       // The DB isn't being used, so just read the engine order from the prefs
  3712       var i = 0;
  3713       var engineName;
  3714       var prefName;
  3716       try {
  3717         var extras =
  3718           Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
  3720         for each (prefName in extras) {
  3721           engineName = Services.prefs.getCharPref(prefName);
  3723           engine = this._engines[engineName];
  3724           if (!engine || engine.name in addedEngines)
  3725             continue;
  3727           this.__sortedEngines.push(engine);
  3728           addedEngines[engine.name] = engine;
  3731       catch (e) { }
  3733       while (true) {
  3734         engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + (++i));
  3735         if (!engineName)
  3736           break;
  3738         engine = this._engines[engineName];
  3739         if (!engine || engine.name in addedEngines)
  3740           continue;
  3742         this.__sortedEngines.push(engine);
  3743         addedEngines[engine.name] = engine;
  3747     // Array for the remaining engines, alphabetically sorted
  3748     var alphaEngines = [];
  3750     for each (engine in this._engines) {
  3751       if (!(engine.name in addedEngines))
  3752         alphaEngines.push(this._engines[engine.name]);
  3754     alphaEngines = alphaEngines.sort(function (a, b) {
  3755                                        return a.name.localeCompare(b.name);
  3756                                      });
  3757     return this.__sortedEngines = this.__sortedEngines.concat(alphaEngines);
  3758   },
  3760   /**
  3761    * Get a sorted array of engines.
  3762    * @param aWithHidden
  3763    *        True if hidden plugins should be included in the result.
  3764    */
  3765   _getSortedEngines: function SRCH_SVC_getSorted(aWithHidden) {
  3766     if (aWithHidden)
  3767       return this._sortedEngines;
  3769     return this._sortedEngines.filter(function (engine) {
  3770                                         return !engine.hidden;
  3771                                       });
  3772   },
  3774   _setEngineByPref: function SRCH_SVC_setEngineByPref(aEngineType, aPref) {
  3775     this._ensureInitialized();
  3776     let newEngine = this.getEngineByName(getLocalizedPref(aPref, ""));
  3777     if (!newEngine)
  3778       FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
  3780     this[aEngineType] = newEngine;
  3781   },
  3783   // nsIBrowserSearchService
  3784   init: function SRCH_SVC_init(observer) {
  3785     LOG("SearchService.init");
  3786     let self = this;
  3787     if (!this._initStarted) {
  3788       TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
  3789       this._initStarted = true;
  3790       TaskUtils.spawn(function task() {
  3791         try {
  3792           yield checkForSyncCompletion(engineMetadataService.init());
  3793           // Complete initialization by calling asynchronous initializer.
  3794           yield self._asyncInit();
  3795           TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
  3796         } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
  3797           // No need to pursue asynchronous because synchronous fallback was
  3798           // called and has finished.
  3799           TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
  3800         } catch (ex) {
  3801           self._initObservers.reject(ex);
  3802           TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
  3804       });
  3806     if (observer) {
  3807       TaskUtils.captureErrors(this._initObservers.promise.then(
  3808         function onSuccess() {
  3809           observer.onInitComplete(self._initRV);
  3810         },
  3811         function onError(aReason) {
  3812           Components.utils.reportError("Internal error while initializing SearchService: " + aReason);
  3813           observer.onInitComplete(Components.results.NS_ERROR_UNEXPECTED);
  3815       ));
  3817   },
  3819   get isInitialized() {
  3820     return gInitialized;
  3821   },
  3823   getEngines: function SRCH_SVC_getEngines(aCount) {
  3824     this._ensureInitialized();
  3825     LOG("getEngines: getting all engines");
  3826     var engines = this._getSortedEngines(true);
  3827     aCount.value = engines.length;
  3828     return engines;
  3829   },
  3831   getVisibleEngines: function SRCH_SVC_getVisible(aCount) {
  3832     this._ensureInitialized();
  3833     LOG("getVisibleEngines: getting all visible engines");
  3834     var engines = this._getSortedEngines(false);
  3835     aCount.value = engines.length;
  3836     return engines;
  3837   },
  3839   getDefaultEngines: function SRCH_SVC_getDefault(aCount) {
  3840     this._ensureInitialized();
  3841     function isDefault(engine) {
  3842       return engine._isDefault;
  3843     };
  3844     var engines = this._sortedEngines.filter(isDefault);
  3845     var engineOrder = {};
  3846     var engineName;
  3847     var i = 1;
  3849     // Build a list of engines which we have ordering information for.
  3850     // We're rebuilding the list here because _sortedEngines contain the
  3851     // current order, but we want the original order.
  3853     // First, look at the "browser.search.order.extra" branch.
  3854     try {
  3855       var extras = Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
  3857       for each (var prefName in extras) {
  3858         engineName = Services.prefs.getCharPref(prefName);
  3860         if (!(engineName in engineOrder))
  3861           engineOrder[engineName] = i++;
  3863     } catch (e) {
  3864       LOG("Getting extra order prefs failed: " + e);
  3867     // Now look through the "browser.search.order" branch.
  3868     for (var j = 1; ; j++) {
  3869       engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + j);
  3870       if (!engineName)
  3871         break;
  3873       if (!(engineName in engineOrder))
  3874         engineOrder[engineName] = i++;
  3877     LOG("getDefaultEngines: engineOrder: " + engineOrder.toSource());
  3879     function compareEngines (a, b) {
  3880       var aIdx = engineOrder[a.name];
  3881       var bIdx = engineOrder[b.name];
  3883       if (aIdx && bIdx)
  3884         return aIdx - bIdx;
  3885       if (aIdx)
  3886         return -1;
  3887       if (bIdx)
  3888         return 1;
  3890       return a.name.localeCompare(b.name);
  3892     engines.sort(compareEngines);
  3894     aCount.value = engines.length;
  3895     return engines;
  3896   },
  3898   getEngineByName: function SRCH_SVC_getEngineByName(aEngineName) {
  3899     this._ensureInitialized();
  3900     return this._engines[aEngineName] || null;
  3901   },
  3903   getEngineByAlias: function SRCH_SVC_getEngineByAlias(aAlias) {
  3904     this._ensureInitialized();
  3905     for (var engineName in this._engines) {
  3906       var engine = this._engines[engineName];
  3907       if (engine && engine.alias == aAlias)
  3908         return engine;
  3910     return null;
  3911   },
  3913   addEngineWithDetails: function SRCH_SVC_addEWD(aName, aIconURL, aAlias,
  3914                                                  aDescription, aMethod,
  3915                                                  aTemplate) {
  3916     this._ensureInitialized();
  3917     if (!aName)
  3918       FAIL("Invalid name passed to addEngineWithDetails!");
  3919     if (!aMethod)
  3920       FAIL("Invalid method passed to addEngineWithDetails!");
  3921     if (!aTemplate)
  3922       FAIL("Invalid template passed to addEngineWithDetails!");
  3923     if (this._engines[aName])
  3924       FAIL("An engine with that name already exists!", Cr.NS_ERROR_FILE_ALREADY_EXISTS);
  3926     var engine = new Engine(getSanitizedFile(aName), SEARCH_DATA_XML, false);
  3927     engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
  3928                              aMethod, aTemplate);
  3929     this._addEngineToStore(engine);
  3930     this.batchTask.disarm();
  3931     this.batchTask.arm();
  3932   },
  3934   addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL,
  3935                                          aConfirm, aCallback) {
  3936     LOG("addEngine: Adding \"" + aEngineURL + "\".");
  3937     this._ensureInitialized();
  3938     try {
  3939       var uri = makeURI(aEngineURL);
  3940       var engine = new Engine(uri, aDataType, false);
  3941       if (aCallback) {
  3942         engine._installCallback = function (errorCode) {
  3943           try {
  3944             if (errorCode == null)
  3945               aCallback.onSuccess(engine);
  3946             else
  3947               aCallback.onError(errorCode);
  3948           } catch (ex) {
  3949             Cu.reportError("Error invoking addEngine install callback: " + ex);
  3951           // Clear the reference to the callback now that it's been invoked.
  3952           engine._installCallback = null;
  3953         };
  3955       engine._initFromURIAndLoad();
  3956     } catch (ex) {
  3957       // Drop the reference to the callback, if set
  3958       if (engine)
  3959         engine._installCallback = null;
  3960       FAIL("addEngine: Error adding engine:\n" + ex, Cr.NS_ERROR_FAILURE);
  3962     engine._setIcon(aIconURL, false);
  3963     engine._confirm = aConfirm;
  3964   },
  3966   removeEngine: function SRCH_SVC_removeEngine(aEngine) {
  3967     this._ensureInitialized();
  3968     if (!aEngine)
  3969       FAIL("no engine passed to removeEngine!");
  3971     var engineToRemove = null;
  3972     for (var e in this._engines) {
  3973       if (aEngine.wrappedJSObject == this._engines[e])
  3974         engineToRemove = this._engines[e];
  3977     if (!engineToRemove)
  3978       FAIL("removeEngine: Can't find engine to remove!", Cr.NS_ERROR_FILE_NOT_FOUND);
  3980     if (engineToRemove == this.currentEngine) {
  3981       this._currentEngine = null;
  3984     if (engineToRemove == this.defaultEngine) {
  3985       this._defaultEngine = null;
  3988     if (engineToRemove._readOnly) {
  3989       // Just hide it (the "hidden" setter will notify) and remove its alias to
  3990       // avoid future conflicts with other engines.
  3991       engineToRemove.hidden = true;
  3992       engineToRemove.alias = null;
  3993     } else {
  3994       // Cancel the serialized task if it's pending.  Since the task is a
  3995       // synchronous function, we don't need to wait on the "finalize" method.
  3996       if (engineToRemove._lazySerializeTask) {
  3997         engineToRemove._lazySerializeTask.disarm();
  3998         engineToRemove._lazySerializeTask = null;
  4001       // Remove the engine file from disk (this might throw)
  4002       engineToRemove._remove();
  4003       engineToRemove._file = null;
  4005       // Remove the engine from _sortedEngines
  4006       var index = this._sortedEngines.indexOf(engineToRemove);
  4007       if (index == -1)
  4008         FAIL("Can't find engine to remove in _sortedEngines!", Cr.NS_ERROR_FAILURE);
  4009       this.__sortedEngines.splice(index, 1);
  4011       // Remove the engine from the internal store
  4012       delete this._engines[engineToRemove.name];
  4014       notifyAction(engineToRemove, SEARCH_ENGINE_REMOVED);
  4016       // Since we removed an engine, we need to update the preferences.
  4017       this._saveSortedEngineList();
  4019   },
  4021   moveEngine: function SRCH_SVC_moveEngine(aEngine, aNewIndex) {
  4022     this._ensureInitialized();
  4023     if ((aNewIndex > this._sortedEngines.length) || (aNewIndex < 0))
  4024       FAIL("SRCH_SVC_moveEngine: Index out of bounds!");
  4025     if (!(aEngine instanceof Ci.nsISearchEngine))
  4026       FAIL("SRCH_SVC_moveEngine: Invalid engine passed to moveEngine!");
  4027     if (aEngine.hidden)
  4028       FAIL("moveEngine: Can't move a hidden engine!", Cr.NS_ERROR_FAILURE);
  4030     var engine = aEngine.wrappedJSObject;
  4032     var currentIndex = this._sortedEngines.indexOf(engine);
  4033     if (currentIndex == -1)
  4034       FAIL("moveEngine: Can't find engine to move!", Cr.NS_ERROR_UNEXPECTED);
  4036     // Our callers only take into account non-hidden engines when calculating
  4037     // aNewIndex, but we need to move it in the array of all engines, so we
  4038     // need to adjust aNewIndex accordingly. To do this, we count the number
  4039     // of hidden engines in the list before the engine that we're taking the
  4040     // place of. We do this by first finding newIndexEngine (the engine that
  4041     // we were supposed to replace) and then iterating through the complete 
  4042     // engine list until we reach it, increasing aNewIndex for each hidden
  4043     // engine we find on our way there.
  4044     //
  4045     // This could be further simplified by having our caller pass in
  4046     // newIndexEngine directly instead of aNewIndex.
  4047     var newIndexEngine = this._getSortedEngines(false)[aNewIndex];
  4048     if (!newIndexEngine)
  4049       FAIL("moveEngine: Can't find engine to replace!", Cr.NS_ERROR_UNEXPECTED);
  4051     for (var i = 0; i < this._sortedEngines.length; ++i) {
  4052       if (newIndexEngine == this._sortedEngines[i])
  4053         break;
  4054       if (this._sortedEngines[i].hidden)
  4055         aNewIndex++;
  4058     if (currentIndex == aNewIndex)
  4059       return; // nothing to do!
  4061     // Move the engine
  4062     var movedEngine = this.__sortedEngines.splice(currentIndex, 1)[0];
  4063     this.__sortedEngines.splice(aNewIndex, 0, movedEngine);
  4065     notifyAction(engine, SEARCH_ENGINE_CHANGED);
  4067     // Since we moved an engine, we need to update the preferences.
  4068     this._saveSortedEngineList();
  4069   },
  4071   restoreDefaultEngines: function SRCH_SVC_resetDefaultEngines() {
  4072     this._ensureInitialized();
  4073     for each (var e in this._engines) {
  4074       // Unhide all default engines
  4075       if (e.hidden && e._isDefault)
  4076         e.hidden = false;
  4078   },
  4080   get defaultEngine() {
  4081     this._ensureInitialized();
  4082     if (!this._defaultEngine) {
  4083       let defPref = BROWSER_SEARCH_PREF + "defaultenginename";
  4084       let defaultEngine = this.getEngineByName(getLocalizedPref(defPref, ""))
  4085       if (!defaultEngine)
  4086         defaultEngine = this._getSortedEngines(false)[0] || null;
  4087       this._defaultEngine = defaultEngine;
  4089     if (this._defaultEngine.hidden)
  4090       return this._getSortedEngines(false)[0];
  4091     return this._defaultEngine;
  4092   },
  4094   set defaultEngine(val) {
  4095     this._ensureInitialized();
  4096     // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
  4097     // and sometimes we get raw Engine JS objects (callers in this file), so
  4098     // handle both.
  4099     if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine))
  4100       FAIL("Invalid argument passed to defaultEngine setter");
  4102     let newDefaultEngine = this.getEngineByName(val.name);
  4103     if (!newDefaultEngine)
  4104       FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
  4106     if (newDefaultEngine == this._defaultEngine)
  4107       return;
  4109     this._defaultEngine = newDefaultEngine;
  4111     // Set a flag to keep track that this setter was called properly, not by
  4112     // setting the pref alone.
  4113     this._changingDefaultEngine = true;
  4114     let defPref = BROWSER_SEARCH_PREF + "defaultenginename";
  4115     // If we change the default engine in the future, that change should impact
  4116     // users who have switched away from and then back to the build's "default"
  4117     // engine. So clear the user pref when the defaultEngine is set to the
  4118     // build's default engine, so that the defaultEngine getter falls back to
  4119     // whatever the default is.
  4120     if (this._defaultEngine == this._originalDefaultEngine) {
  4121       Services.prefs.clearUserPref(defPref);
  4123     else {
  4124       setLocalizedPref(defPref, this._defaultEngine.name);
  4126     this._changingDefaultEngine = false;
  4128     notifyAction(this._defaultEngine, SEARCH_ENGINE_DEFAULT);
  4129   },
  4131   get currentEngine() {
  4132     this._ensureInitialized();
  4133     if (!this._currentEngine) {
  4134       let selectedEngine = getLocalizedPref(BROWSER_SEARCH_PREF +
  4135                                             "selectedEngine");
  4136       this._currentEngine = this.getEngineByName(selectedEngine);
  4139     if (!this._currentEngine || this._currentEngine.hidden)
  4140       this._currentEngine = this.defaultEngine;
  4141     return this._currentEngine;
  4142   },
  4144   set currentEngine(val) {
  4145     this._ensureInitialized();
  4146     // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
  4147     // and sometimes we get raw Engine JS objects (callers in this file), so
  4148     // handle both.
  4149     if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine))
  4150       FAIL("Invalid argument passed to currentEngine setter");
  4152     var newCurrentEngine = this.getEngineByName(val.name);
  4153     if (!newCurrentEngine)
  4154       FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
  4156     if (newCurrentEngine == this._currentEngine)
  4157       return;
  4159     this._currentEngine = newCurrentEngine;
  4161     var currentEnginePref = BROWSER_SEARCH_PREF + "selectedEngine";
  4163     // Set a flag to keep track that this setter was called properly, not by
  4164     // setting the pref alone.
  4165     this._changingCurrentEngine = true;
  4166     // If we change the default engine in the future, that change should impact
  4167     // users who have switched away from and then back to the build's "default"
  4168     // engine. So clear the user pref when the currentEngine is set to the
  4169     // build's default engine, so that the currentEngine getter falls back to
  4170     // whatever the default is.
  4171     if (this._currentEngine == this._originalDefaultEngine) {
  4172       Services.prefs.clearUserPref(currentEnginePref);
  4174     else {
  4175       setLocalizedPref(currentEnginePref, this._currentEngine.name);
  4177     this._changingCurrentEngine = false;
  4179     notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT);
  4180   },
  4182   // nsIObserver
  4183   observe: function SRCH_SVC_observe(aEngine, aTopic, aVerb) {
  4184     switch (aTopic) {
  4185       case SEARCH_ENGINE_TOPIC:
  4186         switch (aVerb) {
  4187           case SEARCH_ENGINE_LOADED:
  4188             var engine = aEngine.QueryInterface(Ci.nsISearchEngine);
  4189             LOG("nsSearchService::observe: Done installation of " + engine.name
  4190                 + ".");
  4191             this._addEngineToStore(engine.wrappedJSObject);
  4192             if (engine.wrappedJSObject._useNow) {
  4193               LOG("nsSearchService::observe: setting current");
  4194               this.currentEngine = aEngine;
  4196             this.batchTask.disarm();
  4197             this.batchTask.arm();
  4198             break;
  4199           case SEARCH_ENGINE_CHANGED:
  4200           case SEARCH_ENGINE_REMOVED:
  4201             this.batchTask.disarm();
  4202             this.batchTask.arm();
  4203             break;
  4205         break;
  4207       case QUIT_APPLICATION_TOPIC:
  4208         this._removeObservers();
  4209         break;
  4211       case "nsPref:changed":
  4212         let currPref = BROWSER_SEARCH_PREF + "selectedEngine";
  4213         let defPref = BROWSER_SEARCH_PREF + "defaultenginename";
  4214         if (aVerb == currPref && !this._changingCurrentEngine) {
  4215           this._setEngineByPref("currentEngine", currPref);
  4216         } else if (aVerb == defPref && !this._changingDefaultEngine) {
  4217           this._setEngineByPref("defaultEngine", defPref);
  4219         break;
  4221   },
  4223   // nsITimerCallback
  4224   notify: function SRCH_SVC_notify(aTimer) {
  4225     LOG("_notify: checking for updates");
  4227     if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true))
  4228       return;
  4230     // Our timer has expired, but unfortunately, we can't get any data from it.
  4231     // Therefore, we need to walk our engine-list, looking for expired engines
  4232     var currentTime = Date.now();
  4233     LOG("currentTime: " + currentTime);
  4234     for each (engine in this._engines) {
  4235       engine = engine.wrappedJSObject;
  4236       if (!engine._hasUpdates)
  4237         continue;
  4239       LOG("checking " + engine.name);
  4241       var expirTime = engineMetadataService.getAttr(engine, "updateexpir");
  4242       LOG("expirTime: " + expirTime + "\nupdateURL: " + engine._updateURL +
  4243           "\niconUpdateURL: " + engine._iconUpdateURL);
  4245       var engineExpired = expirTime <= currentTime;
  4247       if (!expirTime || !engineExpired) {
  4248         LOG("skipping engine");
  4249         continue;
  4252       LOG(engine.name + " has expired");
  4254       engineUpdateService.update(engine);
  4256       // Schedule the next update
  4257       engineUpdateService.scheduleNextUpdate(engine);
  4259     } // end engine iteration
  4260   },
  4262   _addObservers: function SRCH_SVC_addObservers() {
  4263     Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, false);
  4264     Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC, false);
  4265     Services.prefs.addObserver(BROWSER_SEARCH_PREF + "defaultenginename", this, false);
  4266     Services.prefs.addObserver(BROWSER_SEARCH_PREF + "selectedEngine", this, false);
  4268     AsyncShutdown.profileBeforeChange.addBlocker(
  4269       "Search service: shutting down",
  4270       () => Task.spawn(function () {
  4271         if (this._batchTask) {
  4272           yield this._batchTask.finalize().then(null, Cu.reportError);
  4274         yield engineMetadataService.finalize();
  4275       }.bind(this))
  4276     );
  4277   },
  4279   _removeObservers: function SRCH_SVC_removeObservers() {
  4280     Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
  4281     Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC);
  4282     Services.prefs.removeObserver(BROWSER_SEARCH_PREF + "defaultenginename", this);
  4283     Services.prefs.removeObserver(BROWSER_SEARCH_PREF + "selectedEngine", this);
  4284   },
  4286   QueryInterface: function SRCH_SVC_QI(aIID) {
  4287     if (aIID.equals(Ci.nsIBrowserSearchService) ||
  4288         aIID.equals(Ci.nsIObserver)             ||
  4289         aIID.equals(Ci.nsITimerCallback)        ||
  4290         aIID.equals(Ci.nsISupports))
  4291       return this;
  4292     throw Cr.NS_ERROR_NO_INTERFACE;
  4294 };
  4296 var engineMetadataService = {
  4297   _jsonFile: OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json"),
  4299   /**
  4300    * Possible values for |_initState|.
  4302    * We have two paths to perform initialization: a default asynchronous
  4303    * path and a fallback synchronous path that can interrupt the async
  4304    * path. For this reason, initialization is actually something of a
  4305    * finite state machine, represented with the following states:
  4307    * @enum
  4308    */
  4309   _InitStates: {
  4310     NOT_STARTED: "NOT_STARTED"
  4311       /**Initialization has not started*/,
  4312     FINISHED_SUCCESS: "FINISHED_SUCCESS"
  4313       /**Setup complete, with a success*/
  4314   },
  4316   /**
  4317    * The latest step completed by initialization. One of |InitStates|
  4319    * @type {engineMetadataService._InitStates}
  4320    */
  4321   _initState: null,
  4323   // A promise fulfilled once initialization is complete
  4324   _initializer: null,
  4326   /**
  4327    * Asynchronous initializer
  4329    * Note: In the current implementation, initialization never fails.
  4330    */
  4331   init: function epsInit() {
  4332     if (!this._initializer) {
  4333       // Launch asynchronous initialization
  4334       let initializer = this._initializer = Promise.defer();
  4335       TaskUtils.spawn((function task_init() {
  4336         LOG("metadata init: starting");
  4337         switch (this._initState) {
  4338           case engineMetadataService._InitStates.NOT_STARTED:
  4339             // 1. Load json file if it exists
  4340             try {
  4341               let contents = yield OS.File.read(this._jsonFile);
  4342               if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) {
  4343                 // No need to pursue asynchronous initialization,
  4344                 // synchronous fallback was called and has finished.
  4345                 return;
  4347               this._store = JSON.parse(new TextDecoder().decode(contents));
  4348             } catch (ex) {
  4349               if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) {
  4350                 // No need to pursue asynchronous initialization,
  4351                 // synchronous fallback was called and has finished.
  4352                 return;
  4354               // Couldn't load json, use an empty store
  4355               LOG("metadata init: could not load JSON file " + ex);
  4356               this._store = {};
  4358             break;
  4360           default:
  4361             throw new Error("metadata init: invalid state " + this._initState);
  4364         this._initState = this._InitStates.FINISHED_SUCCESS;
  4365         LOG("metadata init: complete");
  4366       }).bind(this)).then(
  4367         // 3. Inform any observers
  4368         function onSuccess() {
  4369           initializer.resolve();
  4370         },
  4371         function onError() {
  4372           initializer.reject();
  4374       );
  4376     return TaskUtils.captureErrors(this._initializer.promise);
  4377   },
  4379   /**
  4380    * Synchronous implementation of initializer
  4382    * This initializer is able to pick wherever the async initializer
  4383    * is waiting. The asynchronous initializer is expected to stop
  4384    * if it detects that the synchronous initializer has completed
  4385    * initialization.
  4386    */
  4387   syncInit: function epsSyncInit() {
  4388     LOG("metadata syncInit start");
  4389     if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) {
  4390       return;
  4392     switch (this._initState) {
  4393       case engineMetadataService._InitStates.NOT_STARTED:
  4394         let jsonFile = new FileUtils.File(this._jsonFile);
  4395         // 1. Load json file if it exists
  4396         if (jsonFile.exists()) {
  4397           try {
  4398             let uri = Services.io.newFileURI(jsonFile);
  4399             let stream = Services.io.newChannelFromURI(uri).open();
  4400             this._store = parseJsonFromStream(stream);
  4401           } catch (x) {
  4402             LOG("metadata syncInit: could not load JSON file " + x);
  4403             this._store = {};
  4405         } else {
  4406           LOG("metadata syncInit: using an empty store");
  4407           this._store = {};
  4410         this._initState = this._InitStates.FINISHED_SUCCESS;
  4411         break;
  4413       default:
  4414         throw new Error("metadata syncInit: invalid state " + this._initState);
  4417     // 3. Inform any observers
  4418     if (this._initializer) {
  4419       this._initializer.resolve();
  4420     } else {
  4421       this._initializer = Promise.resolve();
  4423     LOG("metadata syncInit end");
  4424   },
  4426   getAttr: function epsGetAttr(engine, name) {
  4427     let record = this._store[engine._id];
  4428     if (!record) {
  4429       return null;
  4432     // attr names must be lower case
  4433     let aName = name.toLowerCase();
  4434     if (!record[aName])
  4435       return null;
  4436     return record[aName];
  4437   },
  4439   _setAttr: function epsSetAttr(engine, name, value) {
  4440     // attr names must be lower case
  4441     name = name.toLowerCase();
  4442     let db = this._store;
  4443     let record = db[engine._id];
  4444     if (!record) {
  4445       record = db[engine._id] = {};
  4447     if (!record[name] || (record[name] != value)) {
  4448       record[name] = value;
  4449       return true;
  4451     return false;
  4452   },
  4454   /**
  4455    * Set one metadata attribute for an engine.
  4457    * If an actual change has taken place, the attribute is committed
  4458    * automatically (and lazily), using this._commit.
  4460    * @param {nsISearchEngine} engine The engine to update.
  4461    * @param {string} key The name of the attribute. Case-insensitive. In
  4462    * the current implementation, this _must not_ conflict with properties
  4463    * of |Object|.
  4464    * @param {*} value A value to store.
  4465    */
  4466   setAttr: function epsSetAttr(engine, key, value) {
  4467     if (this._setAttr(engine, key, value)) {
  4468       this._commit();
  4470   },
  4472   /**
  4473    * Bulk set metadata attributes for a number of engines.
  4475    * If actual changes have taken place, the store is committed
  4476    * automatically (and lazily), using this._commit.
  4478    * @param {Array.<{engine: nsISearchEngine, key: string, value: *}>} changes
  4479    * The list of changes to effect. See |setAttr| for the documentation of
  4480    * |engine|, |key|, |value|.
  4481    */
  4482   setAttrs: function epsSetAttrs(changes) {
  4483     let self = this;
  4484     let changed = false;
  4485     changes.forEach(function(change) {
  4486       changed |= self._setAttr(change.engine, change.key, change.value);
  4487     });
  4488     if (changed) {
  4489       this._commit();
  4491   },
  4493   /**
  4494    * Flush any waiting write.
  4495    */
  4496   finalize: function () this._lazyWriter ? this._lazyWriter.finalize()
  4497                                          : Promise.resolve(),
  4499   /**
  4500    * Commit changes to disk, asynchronously.
  4502    * Calls to this function are actually delayed by LAZY_SERIALIZE_DELAY
  4503    * (= 100ms). If the function is called again before the expiration of
  4504    * the delay, commits are merged and the function is again delayed by
  4505    * the same amount of time.
  4507    * @param aStore is an optional parameter specifying the object to serialize.
  4508    *               If not specified, this._store is used.
  4509    */
  4510   _commit: function epsCommit(aStore) {
  4511     LOG("metadata _commit: start");
  4512     let store = aStore || this._store;
  4513     if (!store) {
  4514       LOG("metadata _commit: nothing to do");
  4515       return;
  4518     if (!this._lazyWriter) {
  4519       LOG("metadata _commit: initializing lazy writer");
  4520       function writeCommit() {
  4521         LOG("metadata writeCommit: start");
  4522         let data = gEncoder.encode(JSON.stringify(store));
  4523         let path = engineMetadataService._jsonFile;
  4524         LOG("metadata writeCommit: path " + path);
  4525         let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
  4526         promise = promise.then(
  4527           function onSuccess() {
  4528             Services.obs.notifyObservers(null,
  4529               SEARCH_SERVICE_TOPIC,
  4530               SEARCH_SERVICE_METADATA_WRITTEN);
  4531             LOG("metadata writeCommit: done");
  4533         );
  4534         // Use our error logging instead of the default one.
  4535         return TaskUtils.captureErrors(promise).then(null, () => {});
  4537       this._lazyWriter = new DeferredTask(writeCommit, LAZY_SERIALIZE_DELAY);
  4539     LOG("metadata _commit: (re)setting timer");
  4540     this._lazyWriter.disarm();
  4541     this._lazyWriter.arm();
  4542   },
  4543   _lazyWriter: null
  4544 };
  4546 engineMetadataService._initState = engineMetadataService._InitStates.NOT_STARTED;
  4548 const SEARCH_UPDATE_LOG_PREFIX = "*** Search update: ";
  4550 /**
  4551  * Outputs aText to the JavaScript console as well as to stdout, if the search
  4552  * logging pref (browser.search.update.log) is set to true.
  4553  */
  4554 function ULOG(aText) {
  4555   if (getBoolPref(BROWSER_SEARCH_PREF + "update.log", false)) {
  4556     dump(SEARCH_UPDATE_LOG_PREFIX + aText + "\n");
  4557     Services.console.logStringMessage(aText);
  4561 var engineUpdateService = {
  4562   scheduleNextUpdate: function eus_scheduleNextUpdate(aEngine) {
  4563     var interval = aEngine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL;
  4564     var milliseconds = interval * 86400000; // |interval| is in days
  4565     engineMetadataService.setAttr(aEngine, "updateexpir",
  4566                                   Date.now() + milliseconds);
  4567   },
  4569   update: function eus_Update(aEngine) {
  4570     let engine = aEngine.wrappedJSObject;
  4571     ULOG("update called for " + aEngine._name);
  4572     if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true) || !engine._hasUpdates)
  4573       return;
  4575     // We use the cache to store updated app engines, so refuse to update if the
  4576     // cache is disabled.
  4577     if (engine._readOnly &&
  4578         !getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true))
  4579       return;
  4581     let testEngine = null;
  4582     let updateURL = engine._getURLOfType(URLTYPE_OPENSEARCH);
  4583     let updateURI = (updateURL && updateURL._hasRelation("self")) ? 
  4584                      updateURL.getSubmission("", engine).uri :
  4585                      makeURI(engine._updateURL);
  4586     if (updateURI) {
  4587       if (engine._isDefault && !updateURI.schemeIs("https")) {
  4588         ULOG("Invalid scheme for default engine update");
  4589         return;
  4592       let dataType = engineMetadataService.getAttr(engine, "updatedatatype");
  4593       if (!dataType) {
  4594         ULOG("No loadtype to update engine!");
  4595         return;
  4598       ULOG("updating " + engine.name + " from " + updateURI.spec);
  4599       testEngine = new Engine(updateURI, dataType, false);
  4600       testEngine._engineToUpdate = engine;
  4601       testEngine._initFromURIAndLoad();
  4602     } else
  4603       ULOG("invalid updateURI");
  4605     if (engine._iconUpdateURL) {
  4606       // If we're updating the engine too, use the new engine object,
  4607       // otherwise use the existing engine object.
  4608       (testEngine || engine)._setIcon(engine._iconUpdateURL, true);
  4611 };
  4613 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SearchService]);
  4615 #include ../../../toolkit/modules/debug.js

mercurial