toolkit/components/search/nsSearchService.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

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

mercurial