Sat, 03 Jan 2015 20:18:00 +0100
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 |