toolkit/components/search/nsSearchService.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:00efed7a1cf9
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

mercurial