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