Bug 449371 Firefox/Thunderbird crashes at exit [@ gdk_display_x11_finalize], p=Brian...
[wine-gecko.git] / browser / components / search / nsSearchService.js
blobb6bf6c5349dc0af3f4b15b0965c69050ff73020b
1 # ***** BEGIN LICENSE BLOCK *****
2 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 # The contents of this file are subject to the Mozilla Public License Version
5 # 1.1 (the "License"); you may not use this file except in compliance with
6 # the License. You may obtain a copy of the License at
7 # http://www.mozilla.org/MPL/
9 # Software distributed under the License is distributed on an "AS IS" basis,
10 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 # for the specific language governing rights and limitations under the
12 # License.
14 # The Original Code is the Browser Search Service.
16 # The Initial Developer of the Original Code is
17 # Google Inc.
18 # Portions created by the Initial Developer are Copyright (C) 2005-2006
19 # the Initial Developer. All Rights Reserved.
21 # Contributor(s):
22 # Ben Goodger <beng@google.com> (Original author)
23 # Gavin Sharp <gavin@gavinsharp.com>
24 # Joe Hughes <joe@retrovirus.com>
25 # Pamela Greene <pamg.bugs@gmail.com>
27 # Alternatively, the contents of this file may be used under the terms of
28 # either the GNU General Public License Version 2 or later (the "GPL"), or
29 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
30 # in which case the provisions of the GPL or the LGPL are applicable instead
31 # of those above. If you wish to allow use of your version of this file only
32 # under the terms of either the GPL or the LGPL, and not to allow others to
33 # use your version of this file under the terms of the MPL, indicate your
34 # decision by deleting the provisions above and replace them with the notice
35 # and other provisions required by the GPL or the LGPL. If you do not delete
36 # the provisions above, a recipient may use your version of this file under
37 # the terms of any one of the MPL, the GPL or the LGPL.
39 # ***** END LICENSE BLOCK *****
41 const Ci = Components.interfaces;
42 const Cc = Components.classes;
43 const Cr = Components.results;
45 const PERMS_FILE = 0644;
46 const PERMS_DIRECTORY = 0755;
48 const MODE_RDONLY = 0x01;
49 const MODE_WRONLY = 0x02;
50 const MODE_CREATE = 0x08;
51 const MODE_APPEND = 0x10;
52 const MODE_TRUNCATE = 0x20;
54 // Directory service keys
55 const NS_APP_SEARCH_DIR_LIST = "SrchPluginsDL";
56 const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
57 const NS_APP_SEARCH_DIR = "SrchPlugns";
58 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
60 // Search engine "locations". If this list is changed, be sure to update
61 // the engine's _isDefault function accordingly.
62 const SEARCH_APP_DIR = 1;
63 const SEARCH_PROFILE_DIR = 2;
64 const SEARCH_IN_EXTENSION = 3;
66 // See documentation in nsIBrowserSearchService.idl.
67 const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
68 const QUIT_APPLICATION_TOPIC = "quit-application";
70 const SEARCH_ENGINE_REMOVED = "engine-removed";
71 const SEARCH_ENGINE_ADDED = "engine-added";
72 const SEARCH_ENGINE_CHANGED = "engine-changed";
73 const SEARCH_ENGINE_LOADED = "engine-loaded";
74 const SEARCH_ENGINE_CURRENT = "engine-current";
76 const SEARCH_TYPE_MOZSEARCH = Ci.nsISearchEngine.TYPE_MOZSEARCH;
77 const SEARCH_TYPE_OPENSEARCH = Ci.nsISearchEngine.TYPE_OPENSEARCH;
78 const SEARCH_TYPE_SHERLOCK = Ci.nsISearchEngine.TYPE_SHERLOCK;
80 const SEARCH_DATA_XML = Ci.nsISearchEngine.DATA_XML;
81 const SEARCH_DATA_TEXT = Ci.nsISearchEngine.DATA_TEXT;
83 // File extensions for search plugin description files
84 const XML_FILE_EXT = "xml";
85 const SHERLOCK_FILE_EXT = "src";
87 // Delay for lazy serialization (ms)
88 const LAZY_SERIALIZE_DELAY = 100;
90 const ICON_DATAURL_PREFIX = "data:image/x-icon;base64,";
92 // Supported extensions for Sherlock plugin icons
93 const SHERLOCK_ICON_EXTENSIONS = [".gif", ".png", ".jpg", ".jpeg"];
95 const NEW_LINES = /(\r\n|\r|\n)/;
97 // Set an arbitrary cap on the maximum icon size. Without this, large icons can
98 // cause big delays when loading them at startup.
99 const MAX_ICON_SIZE = 10000;
101 // Default charset to use for sending search parameters. ISO-8859-1 is used to
102 // match previous nsInternetSearchService behavior.
103 const DEFAULT_QUERY_CHARSET = "ISO-8859-1";
105 const SEARCH_BUNDLE = "chrome://browser/locale/search.properties";
106 const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
108 const OPENSEARCH_NS_10 = "http://a9.com/-/spec/opensearch/1.0/";
109 const OPENSEARCH_NS_11 = "http://a9.com/-/spec/opensearch/1.1/";
111 // Although the specification at http://opensearch.a9.com/spec/1.1/description/
112 // gives the namespace names defined above, many existing OpenSearch engines
113 // are using the following versions. We therefore allow either.
114 const OPENSEARCH_NAMESPACES = [
115 OPENSEARCH_NS_11, OPENSEARCH_NS_10,
116 "http://a9.com/-/spec/opensearchdescription/1.1/",
117 "http://a9.com/-/spec/opensearchdescription/1.0/"
120 const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
122 const MOZSEARCH_NS_10 = "http://www.mozilla.org/2006/browser/search/";
123 const MOZSEARCH_LOCALNAME = "SearchPlugin";
125 const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
126 const URLTYPE_SEARCH_HTML = "text/html";
128 // Empty base document used to serialize engines to file.
129 const EMPTY_DOC = "<?xml version=\"1.0\"?>\n" +
130 "<" + MOZSEARCH_LOCALNAME +
131 " xmlns=\"" + MOZSEARCH_NS_10 + "\"" +
132 " xmlns:os=\"" + OPENSEARCH_NS_11 + "\"" +
133 "/>";
135 const BROWSER_SEARCH_PREF = "browser.search.";
137 const USER_DEFINED = "{searchTerms}";
139 // Custom search parameters
140 #ifdef OFFICIAL_BUILD
141 const MOZ_OFFICIAL = "official";
142 #else
143 const MOZ_OFFICIAL = "unofficial";
144 #endif
145 #expand const MOZ_DISTRIBUTION_ID = __MOZ_DISTRIBUTION_ID__;
147 const MOZ_PARAM_LOCALE = /\{moz:locale\}/g;
148 const MOZ_PARAM_DIST_ID = /\{moz:distributionID\}/g;
149 const MOZ_PARAM_OFFICIAL = /\{moz:official\}/g;
151 // Supported OpenSearch parameters
152 // See http://opensearch.a9.com/spec/1.1/querysyntax/#core
153 const OS_PARAM_USER_DEFINED = /\{searchTerms\??\}/g;
154 const OS_PARAM_INPUT_ENCODING = /\{inputEncoding\??\}/g;
155 const OS_PARAM_LANGUAGE = /\{language\??\}/g;
156 const OS_PARAM_OUTPUT_ENCODING = /\{outputEncoding\??\}/g;
158 // Default values
159 const OS_PARAM_LANGUAGE_DEF = "*";
160 const OS_PARAM_OUTPUT_ENCODING_DEF = "UTF-8";
161 const OS_PARAM_INPUT_ENCODING_DEF = "UTF-8";
163 // "Unsupported" OpenSearch parameters. For example, we don't support
164 // page-based results, so if the engine requires that we send the "page index"
165 // parameter, we'll always send "1".
166 const OS_PARAM_COUNT = /\{count\??\}/g;
167 const OS_PARAM_START_INDEX = /\{startIndex\??\}/g;
168 const OS_PARAM_START_PAGE = /\{startPage\??\}/g;
170 // Default values
171 const OS_PARAM_COUNT_DEF = "20"; // 20 results
172 const OS_PARAM_START_INDEX_DEF = "1"; // start at 1st result
173 const OS_PARAM_START_PAGE_DEF = "1"; // 1st page
175 // Optional parameter
176 const OS_PARAM_OPTIONAL = /\{(?:\w+:)?\w+\?\}/g;
178 // A array of arrays containing parameters that we don't fully support, and
179 // their default values. We will only send values for these parameters if
180 // required, since our values are just really arbitrary "guesses" that should
181 // give us the output we want.
182 var OS_UNSUPPORTED_PARAMS = [
183 [OS_PARAM_COUNT, OS_PARAM_COUNT_DEF],
184 [OS_PARAM_START_INDEX, OS_PARAM_START_INDEX_DEF],
185 [OS_PARAM_START_PAGE, OS_PARAM_START_PAGE_DEF],
188 // The default engine update interval, in days. This is only used if an engine
189 // specifies an updateURL, but not an updateInterval.
190 const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
192 // Returns false for whitespace-only or commented out lines in a
193 // Sherlock file, true otherwise.
194 function isUsefulLine(aLine) {
195 return !(/^\s*($|#)/i.test(aLine));
199 * Prefixed to all search debug output.
201 const SEARCH_LOG_PREFIX = "*** Search: ";
204 * Outputs aText to the JavaScript console as well as to stdout.
206 function DO_LOG(aText) {
207 dump(SEARCH_LOG_PREFIX + aText + "\n");
208 var consoleService = Cc["@mozilla.org/consoleservice;1"].
209 getService(Ci.nsIConsoleService);
210 consoleService.logStringMessage(aText);
213 #ifdef DEBUG
215 * In debug builds, use a live, pref-based (browser.search.log) LOG function
216 * to allow enabling/disabling without a restart.
218 function PREF_LOG(aText) {
219 var prefB = Cc["@mozilla.org/preferences-service;1"].
220 getService(Ci.nsIPrefBranch);
221 var shouldLog = false;
222 try {
223 shouldLog = prefB.getBoolPref(BROWSER_SEARCH_PREF + "log");
224 } catch (ex) {}
226 if (shouldLog) {
227 DO_LOG(aText);
230 var LOG = PREF_LOG;
232 #else
235 * Otherwise, don't log at all by default. This can be overridden at startup
236 * by the pref, see SearchService's _init method.
238 var LOG = function(){};
240 #endif
242 function ERROR(message, resultCode) {
243 NS_ASSERT(false, SEARCH_LOG_PREFIX + message);
244 throw resultCode;
248 * Ensures an assertion is met before continuing. Should be used to indicate
249 * fatal errors.
250 * @param assertion
251 * An assertion that must be met
252 * @param message
253 * A message to display if the assertion is not met
254 * @param resultCode
255 * The NS_ERROR_* value to throw if the assertion is not met
256 * @throws resultCode
258 function ENSURE_WARN(assertion, message, resultCode) {
259 NS_ASSERT(assertion, SEARCH_LOG_PREFIX + message);
260 if (!assertion)
261 throw resultCode;
265 * Ensures an assertion is met before continuing, but does not warn the user.
266 * Used to handle normal failure conditions.
267 * @param assertion
268 * An assertion that must be met
269 * @param message
270 * A message to display if the assertion is not met
271 * @param resultCode
272 * The NS_ERROR_* value to throw if the assertion is not met
273 * @throws resultCode
275 function ENSURE(assertion, message, resultCode) {
276 if (!assertion) {
277 LOG(message);
278 throw resultCode;
283 * Ensures an argument assertion is met before continuing.
284 * @param assertion
285 * An argument assertion that must be met
286 * @param message
287 * A message to display if the assertion is not met
288 * @throws NS_ERROR_INVALID_ARG for invalid arguments
290 function ENSURE_ARG(assertion, message) {
291 ENSURE(assertion, message, Cr.NS_ERROR_INVALID_ARG);
294 function loadListener(aChannel, aEngine, aCallback) {
295 this._channel = aChannel;
296 this._bytes = [];
297 this._engine = aEngine;
298 this._callback = aCallback;
300 loadListener.prototype = {
301 _callback: null,
302 _channel: null,
303 _countRead: 0,
304 _engine: null,
305 _stream: null,
307 QueryInterface: function SRCH_loadQI(aIID) {
308 if (aIID.equals(Ci.nsISupports) ||
309 aIID.equals(Ci.nsIRequestObserver) ||
310 aIID.equals(Ci.nsIStreamListener) ||
311 aIID.equals(Ci.nsIChannelEventSink) ||
312 aIID.equals(Ci.nsIInterfaceRequestor) ||
313 aIID.equals(Ci.nsIBadCertListener2) ||
314 aIID.equals(Ci.nsISSLErrorListener) ||
315 // See FIXME comment below
316 aIID.equals(Ci.nsIHttpEventSink) ||
317 aIID.equals(Ci.nsIProgressEventSink) ||
318 false)
319 return this;
321 throw Cr.NS_ERROR_NO_INTERFACE;
324 // nsIRequestObserver
325 onStartRequest: function SRCH_loadStartR(aRequest, aContext) {
326 LOG("loadListener: Starting request: " + aRequest.name);
327 this._stream = Cc["@mozilla.org/binaryinputstream;1"].
328 createInstance(Ci.nsIBinaryInputStream);
331 onStopRequest: function SRCH_loadStopR(aRequest, aContext, aStatusCode) {
332 LOG("loadListener: Stopping request: " + aRequest.name);
334 var requestFailed = !Components.isSuccessCode(aStatusCode);
335 if (!requestFailed && (aRequest instanceof Ci.nsIHttpChannel))
336 requestFailed = !aRequest.requestSucceeded;
338 if (requestFailed || this._countRead == 0) {
339 LOG("loadListener: request failed!");
340 // send null so the callback can deal with the failure
341 this._callback(null, this._engine);
342 } else
343 this._callback(this._bytes, this._engine);
344 this._channel = null;
345 this._engine = null;
348 // nsIStreamListener
349 onDataAvailable: function SRCH_loadDAvailable(aRequest, aContext,
350 aInputStream, aOffset,
351 aCount) {
352 this._stream.setInputStream(aInputStream);
354 // Get a byte array of the data
355 this._bytes = this._bytes.concat(this._stream.readByteArray(aCount));
356 this._countRead += aCount;
359 // nsIChannelEventSink
360 onChannelRedirect: function SRCH_loadCRedirect(aOldChannel, aNewChannel,
361 aFlags) {
362 this._channel = aNewChannel;
365 // nsIInterfaceRequestor
366 getInterface: function SRCH_load_GI(aIID) {
367 return this.QueryInterface(aIID);
370 // nsIBadCertListener2
371 notifyCertProblem: function SRCH_certProblem(socketInfo, status, targetSite) {
372 return true;
375 // nsISSLErrorListener
376 notifySSLError: function SRCH_SSLError(socketInfo, error, targetSite) {
377 return true;
380 // FIXME: bug 253127
381 // nsIHttpEventSink
382 onRedirect: function (aChannel, aNewChannel) {},
383 // nsIProgressEventSink
384 onProgress: function (aRequest, aContext, aProgress, aProgressMax) {},
385 onStatus: function (aRequest, aContext, aStatus, aStatusArg) {}
390 * Used to verify a given DOM node's localName and namespaceURI.
391 * @param aElement
392 * The element to verify.
393 * @param aLocalNameArray
394 * An array of strings to compare against aElement's localName.
395 * @param aNameSpaceArray
396 * An array of strings to compare against aElement's namespaceURI.
398 * @returns false if aElement is null, or if its localName or namespaceURI
399 * does not match one of the elements in the aLocalNameArray or
400 * aNameSpaceArray arrays, respectively.
401 * @throws NS_ERROR_INVALID_ARG if aLocalNameArray or aNameSpaceArray are null.
403 function checkNameSpace(aElement, aLocalNameArray, aNameSpaceArray) {
404 ENSURE_ARG(aLocalNameArray && aNameSpaceArray, "missing aLocalNameArray or \
405 aNameSpaceArray for checkNameSpace");
406 return (aElement &&
407 (aLocalNameArray.indexOf(aElement.localName) != -1) &&
408 (aNameSpaceArray.indexOf(aElement.namespaceURI) != -1));
412 * Safely close a nsISafeOutputStream.
413 * @param aFOS
414 * The file output stream to close.
416 function closeSafeOutputStream(aFOS) {
417 if (aFOS instanceof Ci.nsISafeOutputStream) {
418 try {
419 aFOS.finish();
420 return;
421 } catch (e) { }
423 aFOS.close();
427 * Wrapper function for nsIIOService::newURI.
428 * @param aURLSpec
429 * The URL string from which to create an nsIURI.
430 * @returns an nsIURI object, or null if the creation of the URI failed.
432 function makeURI(aURLSpec, aCharset) {
433 var ios = Cc["@mozilla.org/network/io-service;1"].
434 getService(Ci.nsIIOService);
435 try {
436 return ios.newURI(aURLSpec, aCharset, null);
437 } catch (ex) { }
439 return null;
443 * Gets a directory from the directory service.
444 * @param aKey
445 * The directory service key indicating the directory to get.
447 function getDir(aKey) {
448 ENSURE_ARG(aKey, "getDir requires a directory key!");
450 var fileLocator = Cc["@mozilla.org/file/directory_service;1"].
451 getService(Ci.nsIProperties);
452 var dir = fileLocator.get(aKey, Ci.nsIFile);
453 return dir;
457 * The following two functions are essentially copied from
458 * nsInternetSearchService. They are required for backwards compatibility.
460 function queryCharsetFromCode(aCode) {
461 const codes = [];
462 codes[0] = "x-mac-roman";
463 codes[6] = "x-mac-greek";
464 codes[35] = "x-mac-turkish";
465 codes[513] = "ISO-8859-1";
466 codes[514] = "ISO-8859-2";
467 codes[517] = "ISO-8859-5";
468 codes[518] = "ISO-8859-6";
469 codes[519] = "ISO-8859-7";
470 codes[520] = "ISO-8859-8";
471 codes[521] = "ISO-8859-9";
472 codes[1049] = "IBM864";
473 codes[1280] = "windows-1252";
474 codes[1281] = "windows-1250";
475 codes[1282] = "windows-1251";
476 codes[1283] = "windows-1253";
477 codes[1284] = "windows-1254";
478 codes[1285] = "windows-1255";
479 codes[1286] = "windows-1256";
480 codes[1536] = "us-ascii";
481 codes[1584] = "GB2312";
482 codes[1585] = "x-gbk";
483 codes[1600] = "EUC-KR";
484 codes[2080] = "ISO-2022-JP";
485 codes[2096] = "ISO-2022-CN";
486 codes[2112] = "ISO-2022-KR";
487 codes[2336] = "EUC-JP";
488 codes[2352] = "GB2312";
489 codes[2353] = "x-euc-tw";
490 codes[2368] = "EUC-KR";
491 codes[2561] = "Shift_JIS";
492 codes[2562] = "KOI8-R";
493 codes[2563] = "Big5";
494 codes[2565] = "HZ-GB-2312";
496 if (codes[aCode])
497 return codes[aCode];
499 return getLocalizedPref("intl.charset.default", DEFAULT_QUERY_CHARSET);
501 function fileCharsetFromCode(aCode) {
502 const codes = [
503 "x-mac-roman", // 0
504 "Shift_JIS", // 1
505 "Big5", // 2
506 "EUC-KR", // 3
507 "X-MAC-ARABIC", // 4
508 "X-MAC-HEBREW", // 5
509 "X-MAC-GREEK", // 6
510 "X-MAC-CYRILLIC", // 7
511 "X-MAC-DEVANAGARI" , // 9
512 "X-MAC-GURMUKHI", // 10
513 "X-MAC-GUJARATI", // 11
514 "X-MAC-ORIYA", // 12
515 "X-MAC-BENGALI", // 13
516 "X-MAC-TAMIL", // 14
517 "X-MAC-TELUGU", // 15
518 "X-MAC-KANNADA", // 16
519 "X-MAC-MALAYALAM", // 17
520 "X-MAC-SINHALESE", // 18
521 "X-MAC-BURMESE", // 19
522 "X-MAC-KHMER", // 20
523 "X-MAC-THAI", // 21
524 "X-MAC-LAOTIAN", // 22
525 "X-MAC-GEORGIAN", // 23
526 "X-MAC-ARMENIAN", // 24
527 "GB2312", // 25
528 "X-MAC-TIBETAN", // 26
529 "X-MAC-MONGOLIAN", // 27
530 "X-MAC-ETHIOPIC", // 28
531 "X-MAC-CENTRALEURROMAN", // 29
532 "X-MAC-VIETNAMESE", // 30
533 "X-MAC-EXTARABIC" // 31
535 // Sherlock files have always defaulted to x-mac-roman, so do that here too
536 return codes[aCode] || codes[0];
540 * Returns a string interpretation of aBytes using aCharset, or null on
541 * failure.
543 function bytesToString(aBytes, aCharset) {
544 var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
545 createInstance(Ci.nsIScriptableUnicodeConverter);
546 LOG("bytesToString: converting using charset: " + aCharset);
548 try {
549 converter.charset = aCharset;
550 return converter.convertFromByteArray(aBytes, aBytes.length);
551 } catch (ex) {}
553 return null;
557 * Converts an array of bytes representing a Sherlock file into an array of
558 * lines representing the useful data from the file.
560 * @param aBytes
561 * The array of bytes representing the Sherlock file.
562 * @param aCharsetCode
563 * An integer value representing a character set code to be passed to
564 * fileCharsetFromCode, or null for the default Sherlock encoding.
566 function sherlockBytesToLines(aBytes, aCharsetCode) {
567 // fileCharsetFromCode returns the default encoding if aCharsetCode is null
568 var charset = fileCharsetFromCode(aCharsetCode);
570 var dataString = bytesToString(aBytes, charset);
571 ENSURE(dataString, "sherlockBytesToLines: Couldn't convert byte array!",
572 Cr.NS_ERROR_FAILURE);
574 // Split the string into lines, and filter out comments and
575 // whitespace-only lines
576 return dataString.split(NEW_LINES).filter(isUsefulLine);
580 * Gets the current value of the locale. It's possible for this preference to
581 * be localized, so we have to do a little extra work here. Similar code
582 * exists in nsHttpHandler.cpp when building the UA string.
584 function getLocale() {
585 const localePref = "general.useragent.locale";
586 var locale = getLocalizedPref(localePref);
587 if (locale)
588 return locale;
590 // Not localized
591 var prefs = Cc["@mozilla.org/preferences-service;1"].
592 getService(Ci.nsIPrefBranch);
593 return prefs.getCharPref(localePref);
597 * Wrapper for nsIPrefBranch::getComplexValue.
598 * @param aPrefName
599 * The name of the pref to get.
600 * @returns aDefault if the requested pref doesn't exist.
602 function getLocalizedPref(aPrefName, aDefault) {
603 var prefB = Cc["@mozilla.org/preferences-service;1"].
604 getService(Ci.nsIPrefBranch);
605 const nsIPLS = Ci.nsIPrefLocalizedString;
606 try {
607 return prefB.getComplexValue(aPrefName, nsIPLS).data;
608 } catch (ex) {}
610 return aDefault;
614 * Wrapper for nsIPrefBranch::setComplexValue.
615 * @param aPrefName
616 * The name of the pref to set.
618 function setLocalizedPref(aPrefName, aValue) {
619 var prefB = Cc["@mozilla.org/preferences-service;1"].
620 getService(Ci.nsIPrefBranch);
621 const nsIPLS = Ci.nsIPrefLocalizedString;
622 try {
623 var pls = Components.classes["@mozilla.org/pref-localizedstring;1"]
624 .createInstance(Ci.nsIPrefLocalizedString);
625 pls.data = aValue;
626 prefB.setComplexValue(aPrefName, nsIPLS, pls);
627 } catch (ex) {}
631 * Wrapper for nsIPrefBranch::getBoolPref.
632 * @param aPrefName
633 * The name of the pref to get.
634 * @returns aDefault if the requested pref doesn't exist.
636 function getBoolPref(aName, aDefault) {
637 var prefB = Cc["@mozilla.org/preferences-service;1"].
638 getService(Ci.nsIPrefBranch);
639 try {
640 return prefB.getBoolPref(aName);
641 } catch (ex) {
642 return aDefault;
647 * Get a unique nsIFile object with a sanitized name, based on the engine name.
648 * @param aName
649 * A name to "sanitize". Can be an empty string, in which case a random
650 * 8 character filename will be produced.
651 * @returns A nsIFile object in the user's search engines directory with a
652 * unique sanitized name.
654 function getSanitizedFile(aName) {
655 var fileName = sanitizeName(aName) + "." + XML_FILE_EXT;
656 var file = getDir(NS_APP_USER_SEARCH_DIR);
657 file.append(fileName);
658 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
659 return file;
663 * Removes all characters not in the "chars" string from aName.
665 * @returns a sanitized name to be used as a filename, or a random name
666 * if a sanitized name cannot be obtained (if aName contains
667 * no valid characters).
669 function sanitizeName(aName) {
670 const chars = "-abcdefghijklmnopqrstuvwxyz0123456789";
671 const maxLength = 60;
673 var name = aName.toLowerCase();
674 name = name.replace(/ /g, "-");
675 name = name.split("").filter(function (el) {
676 return chars.indexOf(el) != -1;
677 }).join("");
679 if (!name) {
680 // Our input had no valid characters - use a random name
681 var cl = chars.length - 1;
682 for (var i = 0; i < 8; ++i)
683 name += chars.charAt(Math.round(Math.random() * cl));
686 if (name.length > maxLength)
687 name = name.substring(0, maxLength);
689 return name;
693 * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to
694 * the state of the search service.
696 * @param aEngine
697 * The nsISearchEngine object to which the change applies.
698 * @param aVerb
699 * A verb describing the change.
701 * @see nsIBrowserSearchService.idl
703 function notifyAction(aEngine, aVerb) {
704 var os = Cc["@mozilla.org/observer-service;1"].
705 getService(Ci.nsIObserverService);
706 LOG("NOTIFY: Engine: \"" + aEngine.name + "\"; Verb: \"" + aVerb + "\"");
707 os.notifyObservers(aEngine, SEARCH_ENGINE_TOPIC, aVerb);
711 * Simple object representing a name/value pair.
713 function QueryParameter(aName, aValue) {
714 ENSURE_ARG(aName && (aValue != null),
715 "missing name or value for QueryParameter!");
717 this.name = aName;
718 this.value = aValue;
722 * Perform OpenSearch parameter substitution on aParamValue.
724 * @param aParamValue
725 * A string containing OpenSearch search parameters.
726 * @param aSearchTerms
727 * The user-provided search terms. This string will inserted into
728 * aParamValue as the value of the OS_PARAM_USER_DEFINED parameter.
729 * This value must already be escaped appropriately - it is inserted
730 * as-is.
731 * @param aQueryEncoding
732 * The value to use for the OS_PARAM_INPUT_ENCODING parameter. See
733 * definition in the OpenSearch spec.
735 * @see http://opensearch.a9.com/spec/1.1/querysyntax/#core
737 function ParamSubstitution(aParamValue, aSearchTerms, aEngine) {
738 var value = aParamValue;
740 var distributionID = MOZ_DISTRIBUTION_ID;
741 try {
742 var prefB = Cc["@mozilla.org/preferences-service;1"].
743 getService(Ci.nsIPrefBranch);
744 distributionID = prefB.getCharPref(BROWSER_SEARCH_PREF + "distributionID");
746 catch (ex) { }
748 // Custom search parameters. These are only available to default search
749 // engines.
750 if (aEngine._isDefault) {
751 value = value.replace(MOZ_PARAM_LOCALE, getLocale());
752 value = value.replace(MOZ_PARAM_DIST_ID, distributionID);
753 value = value.replace(MOZ_PARAM_OFFICIAL, MOZ_OFFICIAL);
756 // Insert the OpenSearch parameters we're confident about
757 value = value.replace(OS_PARAM_USER_DEFINED, aSearchTerms);
758 value = value.replace(OS_PARAM_INPUT_ENCODING, aEngine.queryCharset);
759 value = value.replace(OS_PARAM_LANGUAGE,
760 getLocale() || OS_PARAM_LANGUAGE_DEF);
761 value = value.replace(OS_PARAM_OUTPUT_ENCODING,
762 OS_PARAM_OUTPUT_ENCODING_DEF);
764 // Replace any optional parameters
765 value = value.replace(OS_PARAM_OPTIONAL, "");
767 // Insert any remaining required params with our default values
768 for (var i = 0; i < OS_UNSUPPORTED_PARAMS.length; ++i) {
769 value = value.replace(OS_UNSUPPORTED_PARAMS[i][0],
770 OS_UNSUPPORTED_PARAMS[i][1]);
773 return value;
777 * Creates a mozStorage statement that can be used to access the database we
778 * use to hold metadata.
780 * @param dbconn the database that the statement applies to
781 * @param sql a string specifying the sql statement that should be created
783 function createStatement (dbconn, sql) {
784 var stmt = dbconn.createStatement(sql);
785 var wrapper = Cc["@mozilla.org/storage/statement-wrapper;1"].
786 createInstance(Ci.mozIStorageStatementWrapper);
788 wrapper.initialize(stmt);
789 return wrapper;
793 * Creates an engineURL object, which holds the query URL and all parameters.
795 * @param aType
796 * A string containing the name of the MIME type of the search results
797 * returned by this URL.
798 * @param aMethod
799 * The HTTP request method. Must be a case insensitive value of either
800 * "GET" or "POST".
801 * @param aTemplate
802 * The URL to which search queries should be sent. For GET requests,
803 * must contain the string "{searchTerms}", to indicate where the user
804 * entered search terms should be inserted.
806 * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
808 * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported.
810 function EngineURL(aType, aMethod, aTemplate) {
811 ENSURE_ARG(aType && aMethod && aTemplate,
812 "missing type, method or template for EngineURL!");
814 var method = aMethod.toUpperCase();
815 var type = aType.toLowerCase();
817 ENSURE_ARG(method == "GET" || method == "POST",
818 "method passed to EngineURL must be \"GET\" or \"POST\"");
820 this.type = type;
821 this.method = method;
822 this.params = [];
824 var templateURI = makeURI(aTemplate);
825 ENSURE(templateURI, "new EngineURL: template is not a valid URI!",
826 Cr.NS_ERROR_FAILURE);
828 switch (templateURI.scheme) {
829 case "http":
830 case "https":
831 // Disable these for now, see bug 295018
832 // case "file":
833 // case "resource":
834 this.template = aTemplate;
835 break;
836 default:
837 ENSURE(false, "new EngineURL: template uses invalid scheme!",
838 Cr.NS_ERROR_FAILURE);
841 EngineURL.prototype = {
843 addParam: function SRCH_EURL_addParam(aName, aValue) {
844 this.params.push(new QueryParameter(aName, aValue));
847 getSubmission: function SRCH_EURL_getSubmission(aSearchTerms, aEngine) {
848 var url = ParamSubstitution(this.template, aSearchTerms, aEngine);
850 // Create an application/x-www-form-urlencoded representation of our params
851 // (name=value&name=value&name=value)
852 var dataString = "";
853 for (var i = 0; i < this.params.length; ++i) {
854 var param = this.params[i];
855 var value = ParamSubstitution(param.value, aSearchTerms, aEngine);
857 dataString += (i > 0 ? "&" : "") + param.name + "=" + value;
860 var postData = null;
861 if (this.method == "GET") {
862 // GET method requests have no post data, and append the encoded
863 // query string to the url...
864 if (url.indexOf("?") == -1 && dataString)
865 url += "?";
866 url += dataString;
867 } else if (this.method == "POST") {
868 // POST method requests must wrap the encoded text in a MIME
869 // stream and supply that as POSTDATA.
870 var stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
871 createInstance(Ci.nsIStringInputStream);
872 #ifdef MOZILLA_1_8_BRANCH
873 # bug 318193
874 stringStream.setData(dataString, dataString.length);
875 #else
876 stringStream.data = dataString;
877 #endif
879 postData = Cc["@mozilla.org/network/mime-input-stream;1"].
880 createInstance(Ci.nsIMIMEInputStream);
881 postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
882 postData.addContentLength = true;
883 postData.setData(stringStream);
886 return new Submission(makeURI(url), postData);
890 * Serializes the engine object to a OpenSearch Url element.
891 * @param aDoc
892 * The document to use to create the Url element.
893 * @param aElement
894 * The element to which the created Url element is appended.
896 * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
898 _serializeToElement: function SRCH_EURL_serializeToEl(aDoc, aElement) {
899 var url = aDoc.createElementNS(OPENSEARCH_NS_11, "Url");
900 url.setAttribute("type", this.type);
901 url.setAttribute("method", this.method);
902 url.setAttribute("template", this.template);
904 for (var i = 0; i < this.params.length; ++i) {
905 var param = aDoc.createElementNS(OPENSEARCH_NS_11, "Param");
906 param.setAttribute("name", this.params[i].name);
907 param.setAttribute("value", this.params[i].value);
908 url.appendChild(aDoc.createTextNode("\n "));
909 url.appendChild(param);
911 url.appendChild(aDoc.createTextNode("\n"));
912 aElement.appendChild(url);
917 * nsISearchEngine constructor.
918 * @param aLocation
919 * A nsILocalFile or nsIURI object representing the location of the
920 * search engine data file.
921 * @param aSourceDataType
922 * The data type of the file used to describe the engine. Must be either
923 * DATA_XML or DATA_TEXT.
924 * @param aIsReadOnly
925 * Boolean indicating whether the engine should be treated as read-only.
926 * Read only engines cannot be serialized to file.
928 function Engine(aLocation, aSourceDataType, aIsReadOnly) {
929 this._dataType = aSourceDataType;
930 this._readOnly = aIsReadOnly;
931 this._urls = [];
933 if (aLocation instanceof Ci.nsILocalFile) {
934 // we already have a file (e.g. loading engines from disk)
935 this._file = aLocation;
936 } else if (aLocation instanceof Ci.nsIURI) {
937 this._uri = aLocation;
938 switch (aLocation.scheme) {
939 case "https":
940 case "http":
941 case "ftp":
942 case "data":
943 case "file":
944 case "resource":
945 this._uri = aLocation;
946 break;
947 default:
948 ERROR("Invalid URI passed to the nsISearchEngine constructor",
949 Cr.NS_ERROR_INVALID_ARG);
951 } else
952 ERROR("Engine location is neither a File nor a URI object",
953 Cr.NS_ERROR_INVALID_ARG);
956 Engine.prototype = {
957 // The engine's alias.
958 _alias: null,
959 // The data describing the engine. Is either an array of bytes, for Sherlock
960 // files, or an XML document element, for XML plugins.
961 _data: null,
962 // The engine's data type. See data types (DATA_) defined above.
963 _dataType: null,
964 // Whether or not the engine is readonly.
965 _readOnly: true,
966 // The engine's description
967 _description: "",
968 // Used to store the engine to replace, if we're an update to an existing
969 // engine.
970 _engineToUpdate: null,
971 // The file from which the plugin was loaded.
972 _file: null,
973 // Set to true if the engine has a preferred icon (an icon that should not be
974 // overridden by a non-preferred icon).
975 _hasPreferredIcon: null,
976 // Whether the engine is hidden from the user.
977 _hidden: null,
978 // The engine's name.
979 _name: null,
980 // The engine type. See engine types (TYPE_) defined above.
981 _type: null,
982 // The name of the charset used to submit the search terms.
983 _queryCharset: null,
984 // A URL string pointing to the engine's search form.
985 _searchForm: null,
986 // The URI object from which the engine was retrieved.
987 // This is null for local plugins, and is used for error messages and logging.
988 _uri: null,
989 // Whether to obtain user confirmation before adding the engine. This is only
990 // used when the engine is first added to the list.
991 _confirm: false,
992 // Whether to set this as the current engine as soon as it is loaded. This
993 // is only used when the engine is first added to the list.
994 _useNow: true,
995 // Where the engine was loaded from. Can be one of: SEARCH_APP_DIR,
996 // SEARCH_PROFILE_DIR, SEARCH_IN_EXTENSION.
997 __installLocation: null,
998 // The number of days between update checks for new versions
999 _updateInterval: null,
1000 // The url to check at for a new update
1001 _updateURL: null,
1002 // The url to check for a new icon
1003 _iconUpdateURL: null,
1004 // A reference to the timer used for lazily serializing the engine to file
1005 _serializeTimer: null,
1008 * Retrieves the data from the engine's file. If the engine's dataType is
1009 * XML, the document element is placed in the engine's data field. For text
1010 * engines, the data is just read directly from file and placed as an array
1011 * of lines in the engine's data field.
1013 _initFromFile: function SRCH_ENG_initFromFile() {
1014 ENSURE(this._file && this._file.exists(),
1015 "File must exist before calling initFromFile!",
1016 Cr.NS_ERROR_UNEXPECTED);
1018 var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"].
1019 createInstance(Ci.nsIFileInputStream);
1021 fileInStream.init(this._file, MODE_RDONLY, PERMS_FILE, false);
1023 switch (this._dataType) {
1024 case SEARCH_DATA_XML:
1025 var domParser = Cc["@mozilla.org/xmlextras/domparser;1"].
1026 createInstance(Ci.nsIDOMParser);
1027 var doc = domParser.parseFromStream(fileInStream, "UTF-8",
1028 this._file.fileSize,
1029 "text/xml");
1031 this._data = doc.documentElement;
1032 break;
1033 case SEARCH_DATA_TEXT:
1034 var binaryInStream = Cc["@mozilla.org/binaryinputstream;1"].
1035 createInstance(Ci.nsIBinaryInputStream);
1036 binaryInStream.setInputStream(fileInStream);
1038 var bytes = binaryInStream.readByteArray(binaryInStream.available());
1039 this._data = bytes;
1041 break;
1042 default:
1043 ERROR("Bogus engine _dataType: \"" + this._dataType + "\"",
1044 Cr.NS_ERROR_UNEXPECTED);
1046 fileInStream.close();
1048 // Now that the data is loaded, initialize the engine object
1049 this._initFromData();
1053 * Retrieves the engine data from a URI.
1055 _initFromURI: function SRCH_ENG_initFromURI() {
1056 ENSURE_WARN(this._uri instanceof Ci.nsIURI,
1057 "Must have URI when calling _initFromURI!",
1058 Cr.NS_ERROR_UNEXPECTED);
1060 LOG("_initFromURI: Downloading engine from: \"" + this._uri.spec + "\".");
1062 var ios = Cc["@mozilla.org/network/io-service;1"].
1063 getService(Ci.nsIIOService);
1064 var chan = ios.newChannelFromURI(this._uri);
1066 if (this._engineToUpdate && (chan instanceof Ci.nsIHttpChannel)) {
1067 var lastModified = engineMetadataService.getAttr(this._engineToUpdate,
1068 "updatelastmodified");
1069 if (lastModified)
1070 chan.setRequestHeader("If-Modified-Since", lastModified, false);
1072 var listener = new loadListener(chan, this, this._onLoad);
1073 chan.notificationCallbacks = listener;
1074 chan.asyncOpen(listener, null);
1078 * Attempts to find an EngineURL object in the set of EngineURLs for
1079 * this Engine that has the given type string. (This corresponds to the
1080 * "type" attribute in the "Url" node in the OpenSearch spec.)
1081 * This method will return the first matching URL object found, or null
1082 * if no matching URL is found.
1084 * @param aType string to match the EngineURL's type attribute
1086 _getURLOfType: function SRCH_ENG__getURLOfType(aType) {
1087 for (var i = 0; i < this._urls.length; ++i) {
1088 if (this._urls[i].type == aType)
1089 return this._urls[i];
1092 return null;
1095 _confirmAddEngine: function SRCH_SVC_confirmAddEngine() {
1096 var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
1097 getService(Ci.nsIStringBundleService);
1098 var stringBundle = sbs.createBundle(SEARCH_BUNDLE);
1099 var titleMessage = stringBundle.GetStringFromName("addEngineConfirmTitle");
1101 // Display only the hostname portion of the URL.
1102 var dialogMessage =
1103 stringBundle.formatStringFromName("addEngineConfirmation",
1104 [this._name, this._uri.host], 2);
1105 var checkboxMessage = stringBundle.GetStringFromName("addEngineUseNowText");
1106 var addButtonLabel =
1107 stringBundle.GetStringFromName("addEngineAddButtonLabel");
1109 var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"].
1110 getService(Ci.nsIPromptService);
1111 var buttonFlags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
1112 (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1) +
1113 ps.BUTTON_POS_0_DEFAULT;
1115 var checked = {value: false};
1116 // confirmEx returns the index of the button that was pressed. Since "Add"
1117 // is button 0, we want to return the negation of that value.
1118 var confirm = !ps.confirmEx(null,
1119 titleMessage,
1120 dialogMessage,
1121 buttonFlags,
1122 addButtonLabel,
1123 null, null, // button 1 & 2 names not used
1124 checkboxMessage,
1125 checked);
1127 return {confirmed: confirm, useNow: checked.value};
1131 * Handle the successful download of an engine. Initializes the engine and
1132 * triggers parsing of the data. The engine is then flushed to disk. Notifies
1133 * the search service once initialization is complete.
1135 _onLoad: function SRCH_ENG_onLoad(aBytes, aEngine) {
1137 * Handle an error during the load of an engine by prompting the user to
1138 * notify him that the load failed.
1140 function onError(aErrorString, aTitleString) {
1141 if (aEngine._engineToUpdate) {
1142 // We're in an update, so just fail quietly
1143 LOG("updating " + aEngine._engineToUpdate.name + " failed");
1144 return;
1146 var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
1147 getService(Ci.nsIStringBundleService);
1149 var brandBundle = sbs.createBundle(BRAND_BUNDLE);
1150 var brandName = brandBundle.GetStringFromName("brandShortName");
1152 var searchBundle = sbs.createBundle(SEARCH_BUNDLE);
1153 var msgStringName = aErrorString || "error_loading_engine_msg2";
1154 var titleStringName = aTitleString || "error_loading_engine_title";
1155 var title = searchBundle.GetStringFromName(titleStringName);
1156 var text = searchBundle.formatStringFromName(msgStringName,
1157 [brandName, aEngine._location],
1160 var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
1161 getService(Ci.nsIWindowWatcher);
1162 ww.getNewPrompter(null).alert(title, text);
1165 if (!aBytes) {
1166 onError();
1167 return;
1170 var engineToUpdate = null;
1171 if (aEngine._engineToUpdate) {
1172 engineToUpdate = aEngine._engineToUpdate.wrappedJSObject;
1174 // Make this new engine use the old engine's file.
1175 aEngine._file = engineToUpdate._file;
1178 switch (aEngine._dataType) {
1179 case SEARCH_DATA_XML:
1180 var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
1181 createInstance(Ci.nsIDOMParser);
1182 var doc = parser.parseFromBuffer(aBytes, aBytes.length, "text/xml");
1183 aEngine._data = doc.documentElement;
1184 break;
1185 case SEARCH_DATA_TEXT:
1186 aEngine._data = aBytes;
1187 break;
1188 default:
1189 onError();
1190 LOG("_onLoad: Bogus engine _dataType: \"" + this._dataType + "\"");
1191 return;
1194 try {
1195 // Initialize the engine from the obtained data
1196 aEngine._initFromData();
1197 } catch (ex) {
1198 LOG("_onLoad: Failed to init engine!\n" + ex);
1199 // Report an error to the user
1200 onError();
1201 return;
1204 // Check to see if this is a duplicate engine. If we're confirming the
1205 // engine load, then we display a "this is a duplicate engine" prompt,
1206 // otherwise we fail silently.
1207 if (!engineToUpdate) {
1208 var ss = Cc["@mozilla.org/browser/search-service;1"].
1209 getService(Ci.nsIBrowserSearchService);
1210 if (ss.getEngineByName(aEngine.name)) {
1211 if (aEngine._confirm)
1212 onError("error_duplicate_engine_msg", "error_invalid_engine_title");
1214 LOG("_onLoad: duplicate engine found, bailing");
1215 return;
1219 // If requested, confirm the addition now that we have the title.
1220 // This property is only ever true for engines added via
1221 // nsIBrowserSearchService::addEngine.
1222 if (aEngine._confirm) {
1223 var confirmation = aEngine._confirmAddEngine();
1224 LOG("_onLoad: confirm is " + confirmation.confirmed +
1225 "; useNow is " + confirmation.useNow);
1226 if (!confirmation.confirmed)
1227 return;
1228 aEngine._useNow = confirmation.useNow;
1231 // If we don't yet have a file, get one now. The only case where we would
1232 // already have a file is if this is an update and _file was set above.
1233 if (!aEngine._file)
1234 aEngine._file = getSanitizedFile(aEngine.name);
1236 if (engineToUpdate) {
1237 // Keep track of the last modified date, so that we can make conditional
1238 // requests for future updates.
1239 engineMetadataService.setAttr(aEngine, "updatelastmodified",
1240 (new Date()).toUTCString());
1242 // Set the new engine's icon, if it doesn't yet have one.
1243 if (!aEngine._iconURI && engineToUpdate._iconURI)
1244 aEngine._iconURI = engineToUpdate._iconURI;
1246 // Clear the "use now" flag since we don't want to be changing the
1247 // current engine for an update.
1248 aEngine._useNow = false;
1251 // Write the engine to file
1252 aEngine._serializeToFile();
1254 // Notify the search service of the sucessful load. It will deal with
1255 // updates by checking aEngine._engineToUpdate.
1256 notifyAction(aEngine, SEARCH_ENGINE_LOADED);
1260 * Sets the .iconURI property of the engine.
1262 * @param aIconURL
1263 * A URI string pointing to the engine's icon. Must have a http[s],
1264 * ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be
1265 * downloaded and converted to data URIs for storage in the engine
1266 * XML files, if the engine is not readonly.
1267 * @param aIsPreferred
1268 * Whether or not this icon is to be preferred. Preferred icons can
1269 * override non-preferred icons.
1271 _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred) {
1272 // If we already have a preferred icon, and this isn't a preferred icon,
1273 // just ignore it.
1274 if (this._hasPreferredIcon && !aIsPreferred)
1275 return;
1277 var uri = makeURI(aIconURL);
1279 // Ignore bad URIs
1280 if (!uri)
1281 return;
1283 LOG("_setIcon: Setting icon url \"" + uri.spec + "\" for engine \""
1284 + this.name + "\".");
1285 // Only accept remote icons from http[s] or ftp
1286 switch (uri.scheme) {
1287 case "data":
1288 this._iconURI = uri;
1289 notifyAction(this, SEARCH_ENGINE_CHANGED);
1290 this._hasPreferredIcon = aIsPreferred;
1291 break;
1292 case "http":
1293 case "https":
1294 case "ftp":
1295 // No use downloading the icon if the engine file is read-only
1296 if (!this._readOnly) {
1297 LOG("_setIcon: Downloading icon: \"" + uri.spec +
1298 "\" for engine: \"" + this.name + "\"");
1299 var ios = Cc["@mozilla.org/network/io-service;1"].
1300 getService(Ci.nsIIOService);
1301 var chan = ios.newChannelFromURI(uri);
1303 function iconLoadCallback(aByteArray, aEngine) {
1304 // This callback may run after we've already set a preferred icon,
1305 // so check again.
1306 if (aEngine._hasPreferredIcon && !aIsPreferred)
1307 return;
1309 if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) {
1310 LOG("iconLoadCallback: load failed, or the icon was too large!");
1311 return;
1314 var str = btoa(String.fromCharCode.apply(null, aByteArray));
1315 aEngine._iconURI = makeURI(ICON_DATAURL_PREFIX + str);
1317 // The engine might not have a file yet, if it's being downloaded,
1318 // because the request for the engine file itself (_onLoad) may not
1319 // yet be complete. In that case, this change will be written to
1320 // file when _onLoad is called.
1321 if (aEngine._file)
1322 aEngine._serializeToFile();
1324 notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
1325 aEngine._hasPreferredIcon = aIsPreferred;
1328 // If we're currently acting as an "update engine", then the callback
1329 // should set the icon on the engine we're updating and not us, since
1330 // |this| might be gone by the time the callback runs.
1331 var engineToSet = this._engineToUpdate || this;
1333 var listener = new loadListener(chan, engineToSet, iconLoadCallback);
1334 chan.notificationCallbacks = listener;
1335 chan.asyncOpen(listener, null);
1337 break;
1342 * Initialize this Engine object from the collected data.
1344 _initFromData: function SRCH_ENG_initFromData() {
1346 ENSURE_WARN(this._data, "Can't init an engine with no data!",
1347 Cr.NS_ERROR_UNEXPECTED);
1349 // Find out what type of engine we are
1350 switch (this._dataType) {
1351 case SEARCH_DATA_XML:
1352 if (checkNameSpace(this._data, [MOZSEARCH_LOCALNAME],
1353 [MOZSEARCH_NS_10])) {
1355 LOG("_init: Initing MozSearch plugin from " + this._location);
1357 this._type = SEARCH_TYPE_MOZSEARCH;
1358 this._parseAsMozSearch();
1360 } else if (checkNameSpace(this._data, [OPENSEARCH_LOCALNAME],
1361 OPENSEARCH_NAMESPACES)) {
1363 LOG("_init: Initing OpenSearch plugin from " + this._location);
1365 this._type = SEARCH_TYPE_OPENSEARCH;
1366 this._parseAsOpenSearch();
1368 } else
1369 ENSURE(false, this._location + " is not a valid search plugin.",
1370 Cr.NS_ERROR_FAILURE);
1372 break;
1373 case SEARCH_DATA_TEXT:
1374 LOG("_init: Initing Sherlock plugin from " + this._location);
1376 // the only text-based format we support is Sherlock
1377 this._type = SEARCH_TYPE_SHERLOCK;
1378 this._parseAsSherlock();
1381 // No need to keep a ref to our data (which in some cases can be a document
1382 // element) past this point
1383 this._data = null;
1387 * Initialize this Engine object from a collection of metadata.
1389 _initFromMetadata: function SRCH_ENG_initMetaData(aName, aIconURL, aAlias,
1390 aDescription, aMethod,
1391 aTemplate) {
1392 ENSURE_WARN(!this._readOnly,
1393 "Can't call _initFromMetaData on a readonly engine!",
1394 Cr.NS_ERROR_FAILURE);
1396 this._urls.push(new EngineURL("text/html", aMethod, aTemplate));
1398 this._name = aName;
1399 this.alias = aAlias;
1400 this._description = aDescription;
1401 this._setIcon(aIconURL, true);
1403 this._serializeToFile();
1407 * Extracts data from an OpenSearch URL element and creates an EngineURL
1408 * object which is then added to the engine's list of URLs.
1410 * @throws NS_ERROR_FAILURE if a URL object could not be created.
1412 * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag.
1413 * @see EngineURL()
1415 _parseURL: function SRCH_ENG_parseURL(aElement) {
1416 var type = aElement.getAttribute("type");
1417 // According to the spec, method is optional, defaulting to "GET" if not
1418 // specified
1419 var method = aElement.getAttribute("method") || "GET";
1420 var template = aElement.getAttribute("template");
1422 try {
1423 var url = new EngineURL(type, method, template);
1424 } catch (ex) {
1425 LOG("_parseURL: failed to add " + template + " as a URL");
1426 throw Cr.NS_ERROR_FAILURE;
1429 for (var i = 0; i < aElement.childNodes.length; ++i) {
1430 var param = aElement.childNodes[i];
1431 if (param.localName == "Param") {
1432 try {
1433 url.addParam(param.getAttribute("name"), param.getAttribute("value"));
1434 } catch (ex) {
1435 // Ignore failure
1436 LOG("_parseURL: Url element has an invalid param");
1438 } else if (param.localName == "MozParam" &&
1439 // We only support MozParams for default search engines
1440 this._isDefault) {
1441 var value;
1442 switch (param.getAttribute("condition")) {
1443 case "defaultEngine":
1444 const defPref = BROWSER_SEARCH_PREF + "defaultenginename";
1445 var defaultPrefB = Cc["@mozilla.org/preferences-service;1"].
1446 getService(Ci.nsIPrefService).
1447 getDefaultBranch(null);
1448 const nsIPLS = Ci.nsIPrefLocalizedString;
1449 var defaultName;
1450 try {
1451 defaultName = defaultPrefB.getComplexValue(defPref, nsIPLS).data;
1452 } catch (ex) {}
1454 // If this engine was the default search engine, use the true value
1455 if (this.name == defaultName)
1456 value = param.getAttribute("trueValue");
1457 else
1458 value = param.getAttribute("falseValue");
1459 url.addParam(param.getAttribute("name"), value);
1460 break;
1462 case "pref":
1463 try {
1464 var prefB = Cc["@mozilla.org/preferences-service;1"].
1465 getService(Ci.nsIPrefBranch);
1466 value = prefB.getCharPref(BROWSER_SEARCH_PREF + "param." +
1467 param.getAttribute("pref"));
1468 url.addParam(param.getAttribute("name"), value);
1469 } catch (e) { }
1470 break;
1475 this._urls.push(url);
1479 * Get the icon from an OpenSearch Image element.
1480 * @see http://opensearch.a9.com/spec/1.1/description/#image
1482 _parseImage: function SRCH_ENG_parseImage(aElement) {
1483 LOG("_parseImage: Image textContent: \"" + aElement.textContent + "\"");
1484 if (aElement.getAttribute("width") == "16" &&
1485 aElement.getAttribute("height") == "16") {
1486 this._setIcon(aElement.textContent, true);
1490 _parseAsMozSearch: function SRCH_ENG_parseAsMoz() {
1491 //forward to the OpenSearch parser
1492 this._parseAsOpenSearch();
1496 * Extract search engine information from the collected data to initialize
1497 * the engine object.
1499 _parseAsOpenSearch: function SRCH_ENG_parseAsOS() {
1500 var doc = this._data;
1502 // The OpenSearch spec sets a default value for the input encoding.
1503 this._queryCharset = OS_PARAM_INPUT_ENCODING_DEF;
1505 for (var i = 0; i < doc.childNodes.length; ++i) {
1506 var child = doc.childNodes[i];
1507 switch (child.localName) {
1508 case "ShortName":
1509 this._name = child.textContent;
1510 break;
1511 case "Description":
1512 this._description = child.textContent;
1513 break;
1514 case "Url":
1515 try {
1516 this._parseURL(child);
1517 } catch (ex) {
1518 // Parsing of the element failed, just skip it.
1520 break;
1521 case "Image":
1522 this._parseImage(child);
1523 break;
1524 case "InputEncoding":
1525 this._queryCharset = child.textContent.toUpperCase();
1526 break;
1528 // Non-OpenSearch elements
1529 case "SearchForm":
1530 this._searchForm = child.textContent;
1531 break;
1532 case "UpdateUrl":
1533 this._updateURL = child.textContent;
1534 break;
1535 case "UpdateInterval":
1536 this._updateInterval = parseInt(child.textContent);
1537 break;
1538 case "IconUpdateUrl":
1539 this._iconUpdateURL = child.textContent;
1540 break;
1543 ENSURE(this.name && (this._urls.length > 0),
1544 "_parseAsOpenSearch: No name, or missing URL!",
1545 Cr.NS_ERROR_FAILURE);
1546 ENSURE(this.supportsResponseType(URLTYPE_SEARCH_HTML),
1547 "_parseAsOpenSearch: No text/html result type!",
1548 Cr.NS_ERROR_FAILURE);
1552 * Extract search engine information from the collected data to initialize
1553 * the engine object.
1555 _parseAsSherlock: function SRCH_ENG_parseAsSherlock() {
1557 * Trims leading and trailing whitespace from aStr.
1559 function sTrim(aStr) {
1560 return aStr.replace(/^\s+/g, "").replace(/\s+$/g, "");
1564 * Extracts one Sherlock "section" from aSource. A section is essentially
1565 * an HTML element with attributes, but each attribute must be on a new
1566 * line, by definition.
1568 * @param aLines
1569 * An array of lines from the sherlock file.
1570 * @param aSection
1571 * The name of the section (e.g. "search" or "browser"). This value
1572 * is not case sensitive.
1573 * @returns an object whose properties correspond to the section's
1574 * attributes.
1576 function getSection(aLines, aSection) {
1577 LOG("_parseAsSherlock::getSection: Sherlock lines:\n" +
1578 aLines.join("\n"));
1579 var lines = aLines;
1580 var startMark = new RegExp("^\\s*<" + aSection.toLowerCase() + "\\s*",
1581 "gi");
1582 var endMark = /\s*>\s*$/gi;
1584 var foundStart = false;
1585 var startLine, numberOfLines;
1586 // Find the beginning and end of the section
1587 for (var i = 0; i < lines.length; i++) {
1588 if (foundStart) {
1589 if (endMark.test(lines[i])) {
1590 numberOfLines = i - startLine;
1591 // Remove the end marker
1592 lines[i] = lines[i].replace(endMark, "");
1593 // If the endmarker was not the only thing on the line, include
1594 // this line in the results
1595 if (lines[i])
1596 numberOfLines++;
1597 break;
1599 } else {
1600 if (startMark.test(lines[i])) {
1601 foundStart = true;
1602 // Remove the start marker
1603 lines[i] = lines[i].replace(startMark, "");
1604 startLine = i;
1605 // If the line is empty, don't include it in the result
1606 if (!lines[i])
1607 startLine++;
1611 LOG("_parseAsSherlock::getSection: Start index: " + startLine +
1612 "\nNumber of lines: " + numberOfLines);
1613 lines = lines.splice(startLine, numberOfLines);
1614 LOG("_parseAsSherlock::getSection: Section lines:\n" +
1615 lines.join("\n"));
1617 var section = {};
1618 for (var i = 0; i < lines.length; i++) {
1619 var line = sTrim(lines[i]);
1621 var els = line.split("=");
1622 var name = sTrim(els.shift().toLowerCase());
1623 var value = sTrim(els.join("="));
1625 if (!name || !value)
1626 continue;
1628 // Strip leading and trailing whitespace, remove quotes from the
1629 // value, and remove any trailing slashes or ">" characters
1630 value = value.replace(/^["']/, "")
1631 .replace(/["']\s*[\\\/]?>?\s*$/, "") || "";
1632 value = sTrim(value);
1634 // Don't clobber existing attributes
1635 if (!(name in section))
1636 section[name] = value;
1638 return section;
1642 * Returns an array of name-value pair arrays representing the Sherlock
1643 * file's input elements. User defined inputs return USER_DEFINED
1644 * as the value. Elements are returned in the order they appear in the
1645 * source file.
1647 * Example:
1648 * <input name="foo" value="bar">
1649 * <input name="foopy" user>
1650 * Returns:
1651 * [["foo", "bar"], ["foopy", "{searchTerms}"]]
1653 * @param aLines
1654 * An array of lines from the source file.
1656 function getInputs(aLines) {
1659 * Extracts an attribute value from a given a line of text.
1660 * Example: <input value="foo" name="bar">
1661 * Extracts the string |foo| or |bar| given an input aAttr of
1662 * |value| or |name|.
1663 * Attributes may be quoted or unquoted. If unquoted, any whitespace
1664 * indicates the end of the attribute value.
1665 * Example: < value=22 33 name=44\334 >
1666 * Returns |22| for "value" and |44\334| for "name".
1668 * @param aAttr
1669 * The name of the attribute for which to obtain the value. This
1670 * value is not case sensitive.
1671 * @param aLine
1672 * The line containing the attribute.
1674 * @returns the attribute value, or an empty string if the attribute
1675 * doesn't exist.
1677 function getAttr(aAttr, aLine) {
1678 // Used to determine whether an "input" line from a Sherlock file is a
1679 // "user defined" input.
1680 const userInput = /(\s|["'=])user(\s|[>="'\/\\+]|$)/i;
1682 LOG("_parseAsSherlock::getAttr: Getting attr: \"" +
1683 aAttr + "\" for line: \"" + aLine + "\"");
1684 // We're not case sensitive, but we want to return the attribute value
1685 // in its original case, so create a copy of the source
1686 var lLine = aLine.toLowerCase();
1687 var attr = aAttr.toLowerCase();
1689 var attrStart = lLine.search(new RegExp("\\s" + attr, "i"));
1690 if (attrStart == -1) {
1692 // If this is the "user defined input" (i.e. contains the empty
1693 // "user" attribute), return our special keyword
1694 if (userInput.test(lLine) && attr == "value") {
1695 LOG("_parseAsSherlock::getAttr: Found user input!\nLine:\"" + lLine
1696 + "\"");
1697 return USER_DEFINED;
1699 // The attribute doesn't exist - ignore
1700 LOG("_parseAsSherlock::getAttr: Failed to find attribute:\nLine:\""
1701 + lLine + "\"\nAttr:\"" + attr + "\"");
1702 return "";
1705 var valueStart = lLine.indexOf("=", attrStart) + "=".length;
1706 if (valueStart == -1)
1707 return "";
1709 var quoteStart = lLine.indexOf("\"", valueStart);
1710 if (quoteStart == -1) {
1712 // Unquoted attribute, get the rest of the line, trimmed at the first
1713 // sign of whitespace. If the rest of the line is only whitespace,
1714 // returns a blank string.
1715 return lLine.substr(valueStart).replace(/\s.*$/, "");
1717 } else {
1718 // Make sure that there's only whitespace between the start of the
1719 // value and the first quote. If there is, end the attribute value at
1720 // the first sign of whitespace. This prevents us from falling into
1721 // the next attribute if this is an unquoted attribute followed by a
1722 // quoted attribute.
1723 var betweenEqualAndQuote = lLine.substring(valueStart, quoteStart);
1724 if (/\S/.test(betweenEqualAndQuote))
1725 return lLine.substr(valueStart).replace(/\s.*$/, "");
1727 // Adjust the start index to account for the opening quote
1728 valueStart = quoteStart + "\"".length;
1729 // Find the closing quote
1730 valueEnd = lLine.indexOf("\"", valueStart);
1731 // If there is no closing quote, just go to the end of the line
1732 if (valueEnd == -1)
1733 valueEnd = aLine.length;
1735 return aLine.substring(valueStart, valueEnd);
1738 var inputs = [];
1740 LOG("_parseAsSherlock::getInputs: Lines:\n" + aLines);
1741 // Filter out everything but non-inputs
1742 lines = aLines.filter(function (line) {
1743 return /^\s*<input/i.test(line);
1745 LOG("_parseAsSherlock::getInputs: Filtered lines:\n" + lines);
1747 lines.forEach(function (line) {
1748 // Strip leading/trailing whitespace and remove the surrounding markup
1749 // ("<input" and ">")
1750 line = sTrim(line).replace(/^<input/i, "").replace(/>$/, "");
1752 // If this is one of the "directional" inputs (<inputnext>/<inputprev>)
1753 const directionalInput = /^(prev|next)/i;
1754 if (directionalInput.test(line)) {
1756 // Make it look like a normal input by removing "prev" or "next"
1757 line = line.replace(directionalInput, "");
1759 // If it has a name, give it a dummy value to match previous
1760 // nsInternetSearchService behavior
1761 if (/name\s*=/i.test(line)) {
1762 line += " value=\"0\"";
1763 } else
1764 return; // Line has no name, skip it
1767 var attrName = getAttr("name", line);
1768 var attrValue = getAttr("value", line);
1769 LOG("_parseAsSherlock::getInputs: Got input:\nName:\"" + attrName +
1770 "\"\nValue:\"" + attrValue + "\"");
1771 if (attrValue)
1772 inputs.push([attrName, attrValue]);
1774 return inputs;
1777 function err(aErr) {
1778 LOG("_parseAsSherlock::err: Sherlock param error:\n" + aErr);
1779 throw Cr.NS_ERROR_FAILURE;
1782 // First try converting our byte array using the default Sherlock encoding.
1783 // If this fails, or if we find a sourceTextEncoding attribute, we need to
1784 // reconvert the byte array using the specified encoding.
1785 var sherlockLines, searchSection, sourceTextEncoding, browserSection;
1786 try {
1787 sherlockLines = sherlockBytesToLines(this._data);
1788 searchSection = getSection(sherlockLines, "search");
1789 browserSection = getSection(sherlockLines, "browser");
1790 sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]);
1791 if (sourceTextEncoding) {
1792 // Re-convert the bytes using the found sourceTextEncoding
1793 sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding);
1794 searchSection = getSection(sherlockLines, "search");
1795 browserSection = getSection(sherlockLines, "browser");
1797 } catch (ex) {
1798 // The conversion using the default charset failed. Remove any non-ascii
1799 // bytes and try to find a sourceTextEncoding.
1800 var asciiBytes = this._data.filter(function (n) {return !(0x80 & n);});
1801 var asciiString = String.fromCharCode.apply(null, asciiBytes);
1802 sherlockLines = asciiString.split(NEW_LINES).filter(isUsefulLine);
1803 searchSection = getSection(sherlockLines, "search");
1804 sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]);
1805 if (sourceTextEncoding) {
1806 sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding);
1807 searchSection = getSection(sherlockLines, "search");
1808 browserSection = getSection(sherlockLines, "browser");
1809 } else
1810 ERROR("Couldn't find a working charset", Cr.NS_ERROR_FAILURE);
1813 LOG("_parseAsSherlock: Search section:\n" + searchSection.toSource());
1815 this._name = searchSection["name"] || err("Missing name!");
1816 this._description = searchSection["description"] || "";
1817 this._queryCharset = searchSection["querycharset"] ||
1818 queryCharsetFromCode(searchSection["queryencoding"]);
1819 this._searchForm = searchSection["searchform"];
1821 this._updateInterval = parseInt(browserSection["updatecheckdays"]);
1823 this._updateURL = browserSection["update"];
1824 this._iconUpdateURL = browserSection["updateicon"];
1826 var method = (searchSection["method"] || "GET").toUpperCase();
1827 var template = searchSection["action"] || err("Missing action!");
1829 var inputs = getInputs(sherlockLines);
1830 LOG("_parseAsSherlock: Inputs:\n" + inputs.toSource());
1832 var url = null;
1834 if (method == "GET") {
1835 // Here's how we construct the input string:
1836 // <input> is first: Name Attr: Prefix Data Example:
1837 // YES EMPTY None <value> TEMPLATE<value>
1838 // YES NON-EMPTY ? <name>=<value> TEMPLATE?<name>=<value>
1839 // NO EMPTY ------------- <ignored> --------------
1840 // NO NON-EMPTY & <name>=<value> TEMPLATE?<n1>=<v1>&<n2>=<v2>
1841 for (var i = 0; i < inputs.length; i++) {
1842 var name = inputs[i][0];
1843 var value = inputs[i][1];
1844 if (i==0) {
1845 if (name == "")
1846 template += USER_DEFINED;
1847 else
1848 template += "?" + name + "=" + value;
1849 } else if (name != "")
1850 template += "&" + name + "=" + value;
1852 url = new EngineURL("text/html", method, template);
1854 } else if (method == "POST") {
1855 // Create the URL object and just add the parameters directly
1856 url = new EngineURL("text/html", method, template);
1857 for (var i = 0; i < inputs.length; i++) {
1858 var name = inputs[i][0];
1859 var value = inputs[i][1];
1860 if (name)
1861 url.addParam(name, value);
1863 } else
1864 err("Invalid method!");
1866 this._urls.push(url);
1870 * Returns an XML document object containing the search plugin information,
1871 * which can later be used to reload the engine.
1873 _serializeToElement: function SRCH_ENG_serializeToEl() {
1874 function appendTextNode(aNameSpace, aLocalName, aValue) {
1875 if (!aValue)
1876 return null;
1877 var node = doc.createElementNS(aNameSpace, aLocalName);
1878 node.appendChild(doc.createTextNode(aValue));
1879 docElem.appendChild(node);
1880 docElem.appendChild(doc.createTextNode("\n"));
1881 return node;
1884 var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
1885 createInstance(Ci.nsIDOMParser);
1887 var doc = parser.parseFromString(EMPTY_DOC, "text/xml");
1888 docElem = doc.documentElement;
1890 docElem.appendChild(doc.createTextNode("\n"));
1892 appendTextNode(OPENSEARCH_NS_11, "ShortName", this.name);
1893 appendTextNode(OPENSEARCH_NS_11, "Description", this._description);
1894 appendTextNode(OPENSEARCH_NS_11, "InputEncoding", this._queryCharset);
1896 if (this._iconURI) {
1897 var imageNode = appendTextNode(OPENSEARCH_NS_11, "Image",
1898 this._iconURI.spec);
1899 if (imageNode) {
1900 imageNode.setAttribute("width", "16");
1901 imageNode.setAttribute("height", "16");
1905 appendTextNode(MOZSEARCH_NS_10, "UpdateInterval", this._updateInterval);
1906 appendTextNode(MOZSEARCH_NS_10, "UpdateUrl", this._updateURL);
1907 appendTextNode(MOZSEARCH_NS_10, "IconUpdateUrl", this._iconUpdateURL);
1908 appendTextNode(MOZSEARCH_NS_10, "SearchForm", this._searchForm);
1910 for (var i = 0; i < this._urls.length; ++i)
1911 this._urls[i]._serializeToElement(doc, docElem);
1912 docElem.appendChild(doc.createTextNode("\n"));
1914 return doc;
1917 _lazySerializeToFile: function SRCH_ENG_serializeToFile() {
1918 if (this._serializeTimer) {
1919 // Reset the timer
1920 this._serializeTimer.delay = LAZY_SERIALIZE_DELAY;
1921 } else {
1922 this._serializeTimer = Cc["@mozilla.org/timer;1"].
1923 createInstance(Ci.nsITimer);
1924 var timerCallback = {
1925 self: this,
1926 notify: function SRCH_ENG_notify(aTimer) {
1927 try {
1928 this.self._serializeToFile();
1929 } catch (ex) {
1930 LOG("Serialization from timer callback failed:\n" + ex);
1932 this.self._serializeTimer = null;
1935 this._serializeTimer.initWithCallback(timerCallback,
1936 LAZY_SERIALIZE_DELAY,
1937 Ci.nsITimer.TYPE_ONE_SHOT);
1942 * Serializes the engine object to file.
1944 _serializeToFile: function SRCH_ENG_serializeToFile() {
1945 var file = this._file;
1946 ENSURE_WARN(!this._readOnly, "Can't serialize a read only engine!",
1947 Cr.NS_ERROR_FAILURE);
1948 ENSURE_WARN(file && file.exists(), "Can't serialize: file doesn't exist!",
1949 Cr.NS_ERROR_UNEXPECTED);
1951 var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"].
1952 createInstance(Ci.nsIFileOutputStream);
1954 // Serialize the engine first - we don't want to overwrite a good file
1955 // if this somehow fails.
1956 doc = this._serializeToElement();
1958 fos.init(file, (MODE_WRONLY | MODE_TRUNCATE), PERMS_FILE, 0);
1960 try {
1961 var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
1962 createInstance(Ci.nsIDOMSerializer);
1963 serializer.serializeToStream(doc.documentElement, fos, null);
1964 } catch (e) {
1965 LOG("_serializeToFile: Error serializing engine:\n" + e);
1968 closeSafeOutputStream(fos);
1972 * Remove the engine's file from disk. The search service calls this once it
1973 * removes the engine from its internal store. This function will throw if
1974 * the file cannot be removed.
1976 _remove: function SRCH_ENG_remove() {
1977 ENSURE(!this._readOnly, "Can't remove read only engine!",
1978 Cr.NS_ERROR_FAILURE);
1979 ENSURE(this._file && this._file.exists(),
1980 "Can't remove engine: file doesn't exist!",
1981 Cr.NS_ERROR_FILE_NOT_FOUND);
1983 this._file.remove(false);
1986 // nsISearchEngine
1987 get alias() {
1988 if (this._alias === null)
1989 this._alias = engineMetadataService.getAttr(this, "alias");
1991 return this._alias;
1993 set alias(val) {
1994 this._alias = val;
1995 engineMetadataService.setAttr(this, "alias", val);
1996 notifyAction(this, SEARCH_ENGINE_CHANGED);
1999 get description() {
2000 return this._description;
2003 get hidden() {
2004 if (this._hidden === null)
2005 this._hidden = engineMetadataService.getAttr(this, "hidden");
2006 return this._hidden;
2008 set hidden(val) {
2009 var value = !!val;
2010 if (value != this._hidden) {
2011 this._hidden = value;
2012 engineMetadataService.setAttr(this, "hidden", value);
2013 notifyAction(this, SEARCH_ENGINE_CHANGED);
2017 get iconURI() {
2018 return this._iconURI;
2021 get _iconURL() {
2022 if (!this._iconURI)
2023 return "";
2024 return this._iconURI.spec;
2027 // Where the engine is being loaded from: will return the URI's spec if the
2028 // engine is being downloaded and does not yet have a file. This is only used
2029 // for logging.
2030 get _location() {
2031 if (this._file)
2032 return this._file.path;
2034 if (this._uri)
2035 return this._uri.spec;
2037 return "";
2040 // The file that the plugin is loaded from is a unique identifier for it. We
2041 // use this as the identifier to store data in the sqlite database
2042 get _id() {
2043 ENSURE_WARN(this._file, "No _file for id!", Cr.NS_ERROR_FAILURE);
2045 if (this._isInProfile)
2046 return "[profile]/" + this._file.leafName;
2048 if (this._isInAppDir)
2049 return "[app]/" + this._file.leafName;
2051 // We're not in the profile or appdir, so this must be an extension-shipped
2052 // plugin. Use the full path.
2053 return this._file.path;
2056 get _installLocation() {
2057 ENSURE_WARN(this._file && this._file.exists(),
2058 "_installLocation: engine has no file!",
2059 Cr.NS_ERROR_FAILURE);
2061 if (this.__installLocation === null) {
2062 if (this._file.parent.equals(getDir(NS_APP_SEARCH_DIR)))
2063 this.__installLocation = SEARCH_APP_DIR;
2064 else if (this._file.parent.equals(getDir(NS_APP_USER_SEARCH_DIR)))
2065 this.__installLocation = SEARCH_PROFILE_DIR;
2066 else
2067 this.__installLocation = SEARCH_IN_EXTENSION;
2070 return this.__installLocation;
2073 get _isInAppDir() {
2074 return this._installLocation == SEARCH_APP_DIR;
2076 get _isInProfile() {
2077 return this._installLocation == SEARCH_PROFILE_DIR;
2080 get _isDefault() {
2081 // For now, our concept of a "default engine" is "one that is not in the
2082 // user's profile directory", which is currently equivalent to "is app- or
2083 // extension-shipped".
2084 return !this._isInProfile;
2087 get _hasUpdates() {
2088 // Whether or not the engine has an update URL
2089 return !!(this._updateURL || this._iconUpdateURL);
2092 get name() {
2093 return this._name;
2096 get type() {
2097 return this._type;
2100 get searchForm() {
2101 if (!this._searchForm) {
2102 // No searchForm specified in the engine definition file, use the prePath
2103 // (e.g. https://foo.com for https://foo.com/search.php?q=bar).
2104 var htmlUrl = this._getURLOfType(URLTYPE_SEARCH_HTML);
2105 ENSURE_WARN(htmlUrl, "Engine has no HTML URL!", Cr.NS_ERROR_UNEXPECTED);
2106 this._searchForm = makeURI(htmlUrl.template).prePath;
2109 return this._searchForm;
2112 get queryCharset() {
2113 if (this._queryCharset)
2114 return this._queryCharset;
2115 return this._queryCharset = queryCharsetFromCode(/* get the default */);
2118 // from nsISearchEngine
2119 addParam: function SRCH_ENG_addParam(aName, aValue, aResponseType) {
2120 ENSURE_ARG(aName && (aValue != null),
2121 "missing name or value for nsISearchEngine::addParam!");
2122 ENSURE_WARN(!this._readOnly,
2123 "called nsISearchEngine::addParam on a read-only engine!",
2124 Cr.NS_ERROR_FAILURE);
2125 if (!aResponseType)
2126 aResponseType = URLTYPE_SEARCH_HTML;
2128 var url = this._getURLOfType(aResponseType);
2130 ENSURE(url, "Engine object has no URL for response type " + aResponseType,
2131 Cr.NS_ERROR_FAILURE);
2133 url.addParam(aName, aValue);
2135 // Serialize the changes to file lazily
2136 this._lazySerializeToFile();
2139 // from nsISearchEngine
2140 getSubmission: function SRCH_ENG_getSubmission(aData, aResponseType) {
2141 if (!aResponseType)
2142 aResponseType = URLTYPE_SEARCH_HTML;
2144 var url = this._getURLOfType(aResponseType);
2146 if (!url)
2147 return null;
2149 if (!aData) {
2150 // Return a dummy submission object with our searchForm attribute
2151 return new Submission(makeURI(this.searchForm), null);
2154 LOG("getSubmission: In data: \"" + aData + "\"");
2155 var textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
2156 getService(Ci.nsITextToSubURI);
2157 var data = "";
2158 try {
2159 data = textToSubURI.ConvertAndEscape(this.queryCharset, aData);
2160 } catch (ex) {
2161 LOG("getSubmission: Falling back to default queryCharset!");
2162 data = textToSubURI.ConvertAndEscape(DEFAULT_QUERY_CHARSET, aData);
2164 LOG("getSubmission: Out data: \"" + data + "\"");
2165 return url.getSubmission(data, this);
2168 // from nsISearchEngine
2169 supportsResponseType: function SRCH_ENG_supportsResponseType(type) {
2170 return (this._getURLOfType(type) != null);
2173 // nsISupports
2174 QueryInterface: function SRCH_ENG_QI(aIID) {
2175 if (aIID.equals(Ci.nsISearchEngine) ||
2176 aIID.equals(Ci.nsISupports))
2177 return this;
2178 throw Cr.NS_ERROR_NO_INTERFACE;
2181 get wrappedJSObject() {
2182 return this;
2187 // nsISearchSubmission
2188 function Submission(aURI, aPostData) {
2189 this._uri = aURI;
2190 this._postData = aPostData;
2192 Submission.prototype = {
2193 get uri() {
2194 return this._uri;
2196 get postData() {
2197 return this._postData;
2199 QueryInterface: function SRCH_SUBM_QI(aIID) {
2200 if (aIID.equals(Ci.nsISearchSubmission) ||
2201 aIID.equals(Ci.nsISupports))
2202 return this;
2203 throw Cr.NS_ERROR_NO_INTERFACE;
2207 // nsIBrowserSearchService
2208 function SearchService() {
2209 this._init();
2211 SearchService.prototype = {
2212 _engines: { },
2213 _sortedEngines: null,
2214 // Whether or not we need to write the order of engines on shutdown. This
2215 // needs to happen anytime _sortedEngines is modified after initial startup.
2216 _needToSetOrderPrefs: false,
2218 _init: function() {
2219 var prefB = Cc["@mozilla.org/preferences-service;1"].
2220 getService(Ci.nsIPrefBranch);
2221 var shouldLog = false;
2222 try {
2223 shouldLog = prefB.getBoolPref(BROWSER_SEARCH_PREF + "log");
2224 } catch (ex) {}
2226 if (shouldLog) {
2227 // Replace the empty LOG function with the useful one
2228 LOG = DO_LOG;
2231 engineMetadataService.init();
2232 engineUpdateService.init();
2234 this._addObservers();
2236 var fileLocator = Cc["@mozilla.org/file/directory_service;1"].
2237 getService(Ci.nsIProperties);
2238 var locations = fileLocator.get(NS_APP_SEARCH_DIR_LIST,
2239 Ci.nsISimpleEnumerator);
2241 while (locations.hasMoreElements()) {
2242 var location = locations.getNext().QueryInterface(Ci.nsIFile);
2243 this._loadEngines(location);
2246 // Now that all engines are loaded, build the sorted engine list
2247 this._buildSortedEngineList();
2249 selectedEngineName = getLocalizedPref(BROWSER_SEARCH_PREF +
2250 "selectedEngine");
2251 this._currentEngine = this.getEngineByName(selectedEngineName) ||
2252 this.defaultEngine;
2255 _addEngineToStore: function SRCH_SVC_addEngineToStore(aEngine) {
2256 LOG("_addEngineToStore: Adding engine: \"" + aEngine.name + "\"");
2258 // See if there is an existing engine with the same name. However, if this
2259 // engine is updating another engine, it's allowed to have the same name.
2260 var hasSameNameAsUpdate = (aEngine._engineToUpdate &&
2261 aEngine.name == aEngine._engineToUpdate.name);
2262 if (aEngine.name in this._engines && !hasSameNameAsUpdate) {
2263 LOG("_addEngineToStore: Duplicate engine found, aborting!");
2264 return;
2267 if (aEngine._engineToUpdate) {
2268 // We need to replace engineToUpdate with the engine that just loaded.
2269 var oldEngine = aEngine._engineToUpdate;
2271 // Remove the old engine from the hash, since it's keyed by name, and our
2272 // name might change (the update might have a new name).
2273 delete this._engines[oldEngine.name];
2275 // Hack: we want to replace the old engine with the new one, but since
2276 // people may be holding refs to the nsISearchEngine objects themselves,
2277 // we'll just copy over all "private" properties (those without a getter
2278 // or setter) from one object to the other.
2279 for (var p in aEngine) {
2280 if (!(aEngine.__lookupGetter__(p) || aEngine.__lookupSetter__(p)))
2281 oldEngine[p] = aEngine[p];
2283 aEngine = oldEngine;
2284 aEngine._engineToUpdate = null;
2286 // Add the engine back
2287 this._engines[aEngine.name] = aEngine;
2288 notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
2289 } else {
2290 // Not an update, just add the new engine.
2291 this._engines[aEngine.name] = aEngine;
2292 // Only add the engine to the list of sorted engines if the initial list
2293 // has already been built (i.e. if this._sortedEngines is non-null). If
2294 // it hasn't, we're still loading engines from disk, and will build the
2295 // sorted engine list when that initial loading is done.
2296 if (this._sortedEngines) {
2297 this._sortedEngines.push(aEngine);
2298 this._needToSetOrderPrefs = true;
2300 notifyAction(aEngine, SEARCH_ENGINE_ADDED);
2303 if (aEngine._hasUpdates) {
2304 // Schedule the engine's next update, if it isn't already.
2305 if (!engineMetadataService.getAttr(aEngine, "updateexpir"))
2306 engineUpdateService.scheduleNextUpdate(aEngine);
2308 // We need to save the engine's _dataType, if this is the first time the
2309 // engine is added to the dataStore, since ._dataType isn't persisted
2310 // and will change on the next startup (since the engine will then be
2311 // XML). We need this so that we know how to load any future updates from
2312 // this engine.
2313 if (!engineMetadataService.getAttr(aEngine, "updatedatatype"))
2314 engineMetadataService.setAttr(aEngine, "updatedatatype",
2315 aEngine._dataType);
2319 _loadEngines: function SRCH_SVC_loadEngines(aDir) {
2320 LOG("_loadEngines: Searching in " + aDir.path + " for search engines.");
2322 // Check whether aDir is the user profile dir
2323 var isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR));
2325 var files = aDir.directoryEntries
2326 .QueryInterface(Ci.nsIDirectoryEnumerator);
2327 var ios = Cc["@mozilla.org/network/io-service;1"].
2328 getService(Ci.nsIIOService);
2330 while (files.hasMoreElements()) {
2331 var file = files.nextFile;
2333 // Ignore hidden and empty files, and directories
2334 if (!file.isFile() || file.fileSize == 0 || file.isHidden())
2335 continue;
2337 var fileURL = ios.newFileURI(file).QueryInterface(Ci.nsIURL);
2338 var fileExtension = fileURL.fileExtension.toLowerCase();
2339 var isWritable = isInProfile && file.isWritable();
2341 var dataType;
2342 switch (fileExtension) {
2343 case XML_FILE_EXT:
2344 dataType = SEARCH_DATA_XML;
2345 break;
2346 case SHERLOCK_FILE_EXT:
2347 dataType = SEARCH_DATA_TEXT;
2348 break;
2349 default:
2350 // Not an engine
2351 continue;
2354 var addedEngine = null;
2355 try {
2356 addedEngine = new Engine(file, dataType, !isWritable);
2357 addedEngine._initFromFile();
2358 } catch (ex) {
2359 LOG("_loadEngines: Failed to load " + file.path + "!\n" + ex);
2360 continue;
2363 if (fileExtension == SHERLOCK_FILE_EXT) {
2364 if (isWritable) {
2365 try {
2366 this._convertSherlockFile(addedEngine, fileURL.fileBaseName);
2367 } catch (ex) {
2368 LOG("_loadEngines: Failed to convert: " + fileURL.path + "\n" + ex);
2369 // The engine couldn't be converted, mark it as read-only
2370 addedEngine._readOnly = true;
2374 // If the engine still doesn't have an icon, see if we can find one
2375 if (!addedEngine._iconURI) {
2376 var icon = this._findSherlockIcon(file, fileURL.fileBaseName);
2377 if (icon)
2378 addedEngine._iconURI = ios.newFileURI(icon);
2382 this._addEngineToStore(addedEngine);
2386 _saveSortedEngineList: function SRCH_SVC_saveSortedEngineList() {
2387 // We only need to write the prefs. if something has changed.
2388 if (!this._needToSetOrderPrefs)
2389 return;
2391 // Set the useDB pref to indicate that from now on we should use the order
2392 // information stored in the database.
2393 var prefB = Cc["@mozilla.org/preferences-service;1"].
2394 getService(Ci.nsIPrefBranch);
2395 prefB.setBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", true);
2397 var engines = this._getSortedEngines(true);
2398 var values = [];
2399 var names = [];
2401 for (var i = 0; i < engines.length; ++i) {
2402 names[i] = "order";
2403 values[i] = i + 1;
2406 engineMetadataService.setAttrs(engines, names, values);
2409 _buildSortedEngineList: function SRCH_SVC_buildSortedEngineList() {
2410 var addedEngines = { };
2411 this._sortedEngines = [];
2412 var engine;
2414 // If the user has specified a custom engine order, read the order
2415 // information from the engineMetadataService instead of the default
2416 // prefs.
2417 if (getBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", false)) {
2418 for each (engine in this._engines) {
2419 var orderNumber = engineMetadataService.getAttr(engine, "order");
2421 // Since the DB isn't regularly cleared, and engine files may disappear
2422 // without us knowing, we may already have an engine in this slot. If
2423 // that happens, we just skip it - it will be added later on as an
2424 // unsorted engine. This problem will sort itself out when we call
2425 // _saveSortedEngineList at shutdown.
2426 if (orderNumber && !this._sortedEngines[orderNumber-1]) {
2427 this._sortedEngines[orderNumber-1] = engine;
2428 addedEngines[engine.name] = engine;
2429 } else {
2430 // We need to call _saveSortedEngines so this gets sorted out.
2431 this._needToSetOrderPrefs = true;
2435 // Filter out any nulls for engines that may have been removed
2436 var filteredEngines = this._sortedEngines.filter(function(a) { return !!a; });
2437 if (this._sortedEngines.length != filteredEngines.length)
2438 this._needToSetOrderPrefs = true;
2439 this._sortedEngines = filteredEngines;
2441 } else {
2442 // The DB isn't being used, so just read the engine order from the prefs
2443 var i = 0;
2444 var engineName;
2445 var prefName;
2447 try {
2448 var prefB = Cc["@mozilla.org/preferences-service;1"].
2449 getService(Ci.nsIPrefBranch);
2450 var extras =
2451 prefB.getChildList(BROWSER_SEARCH_PREF + "order.extra.", { });
2453 for each (prefName in extras) {
2454 engineName = prefB.getCharPref(prefName);
2456 engine = this._engines[engineName];
2457 if (!engine || engine.name in addedEngines)
2458 continue;
2460 this._sortedEngines.push(engine);
2461 addedEngines[engine.name] = engine;
2464 catch (e) { }
2466 while (true) {
2467 engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + (++i));
2468 if (!engineName)
2469 break;
2471 engine = this._engines[engineName];
2472 if (!engine || engine.name in addedEngines)
2473 continue;
2475 this._sortedEngines.push(engine);
2476 addedEngines[engine.name] = engine;
2480 // Array for the remaining engines, alphabetically sorted
2481 var alphaEngines = [];
2483 for each (engine in this._engines) {
2484 if (!(engine.name in addedEngines))
2485 alphaEngines.push(this._engines[engine.name]);
2487 alphaEngines = alphaEngines.sort(function (a, b) {
2488 return a.name.localeCompare(b.name);
2490 this._sortedEngines = this._sortedEngines.concat(alphaEngines);
2494 * Converts a Sherlock file and its icon into the custom XML format used by
2495 * the Search Service. Saves the engine's icon (if present) into the XML as a
2496 * data: URI and changes the extension of the source file from ".src" to
2497 * ".xml". The engine data is then written to the file as XML.
2498 * @param aEngine
2499 * The Engine object that needs to be converted.
2500 * @param aBaseName
2501 * The basename of the Sherlock file.
2502 * Example: "foo" for file "foo.src".
2504 * @throws NS_ERROR_FAILURE if the file could not be converted.
2506 * @see nsIURL::fileBaseName
2508 _convertSherlockFile: function SRCH_SVC_convertSherlock(aEngine, aBaseName) {
2509 var oldSherlockFile = aEngine._file;
2511 // Back up the old file
2512 try {
2513 var backupDir = oldSherlockFile.parent;
2514 backupDir.append("searchplugins-backup");
2516 if (!backupDir.exists())
2517 backupDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
2519 oldSherlockFile.copyTo(backupDir, null);
2520 } catch (ex) {
2521 // Just bail. Engines that can't be backed up won't be converted, but
2522 // engines that aren't converted are loaded as readonly.
2523 LOG("_convertSherlockFile: Couldn't back up " + oldSherlockFile.path +
2524 ":\n" + ex);
2525 throw Cr.NS_ERROR_FAILURE;
2528 // Rename the file, but don't clobber existing files
2529 var newXMLFile = oldSherlockFile.parent.clone();
2530 newXMLFile.append(aBaseName + "." + XML_FILE_EXT);
2532 if (newXMLFile.exists()) {
2533 // There is an existing file with this name, create a unique file
2534 newXMLFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
2537 // Rename the .src file to .xml
2538 oldSherlockFile.moveTo(null, newXMLFile.leafName);
2540 aEngine._file = newXMLFile;
2542 // Write the converted engine to disk
2543 aEngine._serializeToFile();
2545 // Update the engine's _type.
2546 aEngine._type = SEARCH_TYPE_MOZSEARCH;
2548 // See if it has a corresponding icon
2549 try {
2550 var icon = this._findSherlockIcon(aEngine._file, aBaseName);
2551 if (icon && icon.fileSize < MAX_ICON_SIZE) {
2552 // Use this as the engine's icon
2553 var bStream = Cc["@mozilla.org/binaryinputstream;1"].
2554 createInstance(Ci.nsIBinaryInputStream);
2555 var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"].
2556 createInstance(Ci.nsIFileInputStream);
2558 fileInStream.init(icon, MODE_RDONLY, PERMS_FILE, 0);
2559 bStream.setInputStream(fileInStream);
2561 var bytes = [];
2562 while (bStream.available() != 0)
2563 bytes = bytes.concat(bStream.readByteArray(bStream.available()));
2564 bStream.close();
2566 // Convert the byte array to a base64-encoded string
2567 var str = btoa(String.fromCharCode.apply(null, bytes));
2569 aEngine._iconURI = makeURI(ICON_DATAURL_PREFIX + str);
2570 LOG("_importSherlockEngine: Set sherlock iconURI to: \"" +
2571 aEngine._iconURL + "\"");
2573 // Write the engine to disk to save changes
2574 aEngine._serializeToFile();
2576 // Delete the icon now that we're sure everything's been saved
2577 icon.remove(false);
2579 } catch (ex) { LOG("_convertSherlockFile: Error setting icon:\n" + ex); }
2583 * Finds an icon associated to a given Sherlock file. Searches the provided
2584 * file's parent directory looking for files with the same base name and one
2585 * of the file extensions in SHERLOCK_ICON_EXTENSIONS.
2586 * @param aEngineFile
2587 * The Sherlock plugin file.
2588 * @param aBaseName
2589 * The basename of the Sherlock file.
2590 * Example: "foo" for file "foo.src".
2591 * @see nsIURL::fileBaseName
2593 _findSherlockIcon: function SRCH_SVC_findSherlock(aEngineFile, aBaseName) {
2594 for (var i = 0; i < SHERLOCK_ICON_EXTENSIONS.length; i++) {
2595 var icon = aEngineFile.parent.clone();
2596 icon.append(aBaseName + SHERLOCK_ICON_EXTENSIONS[i]);
2597 if (icon.exists() && icon.isFile())
2598 return icon;
2600 return null;
2604 * Get a sorted array of engines.
2605 * @param aWithHidden
2606 * True if hidden plugins should be included in the result.
2608 _getSortedEngines: function SRCH_SVC_getSorted(aWithHidden) {
2609 if (aWithHidden)
2610 return this._sortedEngines;
2612 return this._sortedEngines.filter(function (engine) {
2613 return !engine.hidden;
2617 // nsIBrowserSearchService
2618 getEngines: function SRCH_SVC_getEngines(aCount) {
2619 LOG("getEngines: getting all engines");
2620 var engines = this._getSortedEngines(true);
2621 aCount.value = engines.length;
2622 return engines;
2625 getVisibleEngines: function SRCH_SVC_getVisible(aCount) {
2626 LOG("getVisibleEngines: getting all visible engines");
2627 var engines = this._getSortedEngines(false);
2628 aCount.value = engines.length;
2629 return engines;
2632 getDefaultEngines: function SRCH_SVC_getDefault(aCount) {
2633 function isDefault(engine) {
2634 return engine._isDefault;
2636 var engines = this._sortedEngines.filter(isDefault);
2637 var engineOrder = {};
2638 var engineName;
2639 var i = 1;
2641 // Build a list of engines which we have ordering information for.
2642 // We're rebuilding the list here because _sortedEngines contain the
2643 // current order, but we want the original order.
2645 // First, look at the "browser.search.order.extra" branch.
2646 try {
2647 var prefB = Cc["@mozilla.org/preferences-service;1"].
2648 getService(Ci.nsIPrefBranch);
2649 var extras = prefB.getChildList(BROWSER_SEARCH_PREF + "order.extra.",
2650 {});
2652 for each (var prefName in extras) {
2653 engineName = prefB.getCharPref(prefName);
2655 if (!(engineName in engineOrder))
2656 engineOrder[engineName] = i++;
2658 } catch (e) {
2659 LOG("Getting extra order prefs failed: " + e);
2662 // Now look through the "browser.search.order" branch.
2663 for (var j = 1; ; j++) {
2664 engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + j);
2665 if (!engineName)
2666 break;
2668 if (!(engineName in engineOrder))
2669 engineOrder[engineName] = i++;
2672 LOG("getDefaultEngines: engineOrder: " + engineOrder.toSource());
2674 function compareEngines (a, b) {
2675 var aIdx = engineOrder[a.name];
2676 var bIdx = engineOrder[b.name];
2678 if (aIdx && bIdx)
2679 return aIdx - bIdx;
2680 if (aIdx)
2681 return -1;
2682 if (bIdx)
2683 return 1;
2685 return a.name.localeCompare(b.name);
2687 engines.sort(compareEngines);
2689 aCount.value = engines.length;
2690 return engines;
2693 getEngineByName: function SRCH_SVC_getEngineByName(aEngineName) {
2694 return this._engines[aEngineName] || null;
2697 getEngineByAlias: function SRCH_SVC_getEngineByAlias(aAlias) {
2698 for (var engineName in this._engines) {
2699 var engine = this._engines[engineName];
2700 if (engine && engine.alias == aAlias)
2701 return engine;
2703 return null;
2706 addEngineWithDetails: function SRCH_SVC_addEWD(aName, aIconURL, aAlias,
2707 aDescription, aMethod,
2708 aTemplate) {
2709 ENSURE_ARG(aName, "Invalid name passed to addEngineWithDetails!");
2710 ENSURE_ARG(aMethod, "Invalid method passed to addEngineWithDetails!");
2711 ENSURE_ARG(aTemplate, "Invalid template passed to addEngineWithDetails!");
2713 ENSURE(!this._engines[aName], "An engine with that name already exists!",
2714 Cr.NS_ERROR_FILE_ALREADY_EXISTS);
2716 var engine = new Engine(getSanitizedFile(aName), SEARCH_DATA_XML, false);
2717 engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
2718 aMethod, aTemplate);
2719 this._addEngineToStore(engine);
2722 addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL,
2723 aConfirm) {
2724 LOG("addEngine: Adding \"" + aEngineURL + "\".");
2725 try {
2726 var uri = makeURI(aEngineURL);
2727 var engine = new Engine(uri, aDataType, false);
2728 engine._initFromURI();
2729 } catch (ex) {
2730 LOG("addEngine: Error adding engine:\n" + ex);
2731 throw Cr.NS_ERROR_FAILURE;
2733 engine._setIcon(aIconURL, false);
2734 engine._confirm = aConfirm;
2737 removeEngine: function SRCH_SVC_removeEngine(aEngine) {
2738 ENSURE_ARG(aEngine, "no engine passed to removeEngine!");
2740 var engineToRemove = null;
2741 for (var e in this._engines)
2742 if (aEngine.wrappedJSObject == this._engines[e])
2743 engineToRemove = this._engines[e];
2745 ENSURE(engineToRemove, "removeEngine: Can't find engine to remove!",
2746 Cr.NS_ERROR_FILE_NOT_FOUND);
2748 if (engineToRemove == this.currentEngine)
2749 this._currentEngine = null;
2751 if (engineToRemove._readOnly) {
2752 // Just hide it (the "hidden" setter will notify) and remove its alias to
2753 // avoid future conflicts with other engines.
2754 engineToRemove.hidden = true;
2755 engineToRemove.alias = null;
2756 } else {
2757 // Cancel the lazy serialization timer if it's running
2758 if (engineToRemove._serializeTimer) {
2759 engineToRemove._serializeTimer.cancel();
2760 engineToRemove._serializeTimer = null;
2763 // Remove the engine file from disk (this might throw)
2764 engineToRemove._remove();
2765 engineToRemove._file = null;
2767 // Remove the engine from _sortedEngines
2768 var index = this._sortedEngines.indexOf(engineToRemove);
2769 ENSURE(index != -1, "Can't find engine to remove in _sortedEngines!",
2770 Cr.NS_ERROR_FAILURE);
2771 this._sortedEngines.splice(index, 1);
2773 // Remove the engine from the internal store
2774 delete this._engines[engineToRemove.name];
2776 notifyAction(engineToRemove, SEARCH_ENGINE_REMOVED);
2778 // Since we removed an engine, we need to update the preferences.
2779 this._needToSetOrderPrefs = true;
2783 moveEngine: function SRCH_SVC_moveEngine(aEngine, aNewIndex) {
2784 ENSURE_ARG((aNewIndex < this._sortedEngines.length) && (aNewIndex >= 0),
2785 "SRCH_SVC_moveEngine: Index out of bounds!");
2786 ENSURE_ARG(aEngine instanceof Ci.nsISearchEngine,
2787 "SRCH_SVC_moveEngine: Invalid engine passed to moveEngine!");
2788 ENSURE(!aEngine.hidden, "moveEngine: Can't move a hidden engine!",
2789 Cr.NS_ERROR_FAILURE);
2791 var engine = aEngine.wrappedJSObject;
2793 var currentIndex = this._sortedEngines.indexOf(engine);
2794 ENSURE(currentIndex != -1, "moveEngine: Can't find engine to move!",
2795 Cr.NS_ERROR_UNEXPECTED);
2797 // Our callers only take into account non-hidden engines when calculating
2798 // aNewIndex, but we need to move it in the array of all engines, so we
2799 // need to adjust aNewIndex accordingly. To do this, we count the number
2800 // of hidden engines in the list before the engine that we're taking the
2801 // place of. We do this by first finding newIndexEngine (the engine that
2802 // we were supposed to replace) and then iterating through the complete
2803 // engine list until we reach it, increasing aNewIndex for each hidden
2804 // engine we find on our way there.
2806 // This could be further simplified by having our caller pass in
2807 // newIndexEngine directly instead of aNewIndex.
2808 var newIndexEngine = this._getSortedEngines(false)[aNewIndex];
2809 ENSURE(newIndexEngine, "moveEngine: Can't find engine to replace!",
2810 Cr.NS_ERROR_UNEXPECTED);
2812 for (var i = 0; i < this._sortedEngines.length; ++i) {
2813 if (newIndexEngine == this._sortedEngines[i])
2814 break;
2815 if (this._sortedEngines[i].hidden)
2816 aNewIndex++;
2819 if (currentIndex == aNewIndex)
2820 return; // nothing to do!
2822 // Move the engine
2823 var movedEngine = this._sortedEngines.splice(currentIndex, 1)[0];
2824 this._sortedEngines.splice(aNewIndex, 0, movedEngine);
2826 notifyAction(engine, SEARCH_ENGINE_CHANGED);
2828 // Since we moved an engine, we need to update the preferences.
2829 this._needToSetOrderPrefs = true;
2832 restoreDefaultEngines: function SRCH_SVC_resetDefaultEngines() {
2833 for each (var e in this._engines) {
2834 // Unhide all default engines
2835 if (e.hidden && e._isDefault)
2836 e.hidden = false;
2840 get defaultEngine() {
2841 const defPref = BROWSER_SEARCH_PREF + "defaultenginename";
2842 // Get the default engine - this pref should always exist, but the engine
2843 // might be hidden
2844 this._defaultEngine = this.getEngineByName(getLocalizedPref(defPref, ""));
2845 if (!this._defaultEngine || this._defaultEngine.hidden)
2846 this._defaultEngine = this._getSortedEngines(false)[0] || null;
2847 return this._defaultEngine;
2850 get currentEngine() {
2851 if (!this._currentEngine || this._currentEngine.hidden)
2852 this._currentEngine = this.defaultEngine;
2853 return this._currentEngine;
2855 set currentEngine(val) {
2856 ENSURE_ARG(val instanceof Ci.nsISearchEngine,
2857 "Invalid argument passed to currentEngine setter");
2859 var newCurrentEngine = this.getEngineByName(val.name);
2860 ENSURE(newCurrentEngine, "Can't find engine in store!",
2861 Cr.NS_ERROR_UNEXPECTED);
2863 this._currentEngine = newCurrentEngine;
2865 var currentEnginePref = BROWSER_SEARCH_PREF + "selectedEngine";
2867 var prefB = Cc["@mozilla.org/preferences-service;1"].
2868 getService(Ci.nsIPrefService).QueryInterface(Ci.nsIPrefBranch);
2870 if (this._currentEngine == this.defaultEngine) {
2871 if (prefB.prefHasUserValue(currentEnginePref))
2872 prefB.clearUserPref(currentEnginePref);
2874 else {
2875 setLocalizedPref(currentEnginePref, this._currentEngine.name);
2878 notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT);
2881 // nsIObserver
2882 observe: function SRCH_SVC_observe(aEngine, aTopic, aVerb) {
2883 switch (aTopic) {
2884 case SEARCH_ENGINE_TOPIC:
2885 if (aVerb == SEARCH_ENGINE_LOADED) {
2886 var engine = aEngine.QueryInterface(Ci.nsISearchEngine);
2887 LOG("nsSearchService::observe: Done installation of " + engine.name
2888 + ".");
2889 this._addEngineToStore(engine.wrappedJSObject);
2890 if (engine.wrappedJSObject._useNow) {
2891 LOG("nsSearchService::observe: setting current");
2892 this.currentEngine = aEngine;
2895 break;
2896 case QUIT_APPLICATION_TOPIC:
2897 this._removeObservers();
2898 this._saveSortedEngineList();
2899 break;
2903 _addObservers: function SRCH_SVC_addObservers() {
2904 var os = Cc["@mozilla.org/observer-service;1"].
2905 getService(Ci.nsIObserverService);
2906 os.addObserver(this, SEARCH_ENGINE_TOPIC, false);
2907 os.addObserver(this, QUIT_APPLICATION_TOPIC, false);
2910 _removeObservers: function SRCH_SVC_removeObservers() {
2911 var os = Cc["@mozilla.org/observer-service;1"].
2912 getService(Ci.nsIObserverService);
2913 os.removeObserver(this, SEARCH_ENGINE_TOPIC);
2914 os.removeObserver(this, QUIT_APPLICATION_TOPIC);
2917 QueryInterface: function SRCH_SVC_QI(aIID) {
2918 if (aIID.equals(Ci.nsIBrowserSearchService) ||
2919 aIID.equals(Ci.nsIObserver) ||
2920 aIID.equals(Ci.nsISupports))
2921 return this;
2922 throw Cr.NS_ERROR_NO_INTERFACE;
2926 var engineMetadataService = {
2927 init: function epsInit() {
2928 var engineDataTable = "id INTEGER PRIMARY KEY, engineid STRING, name STRING, value STRING";
2929 var file = getDir(NS_APP_USER_PROFILE_50_DIR);
2930 file.append("search.sqlite");
2931 var dbService = Cc["@mozilla.org/storage/service;1"].
2932 getService(Ci.mozIStorageService);
2933 try {
2934 this.mDB = dbService.openDatabase(file);
2935 } catch (ex) {
2936 if (ex.result == 0x8052000b) { /* NS_ERROR_FILE_CORRUPTED */
2937 // delete and try again
2938 file.remove(false);
2939 this.mDB = dbService.openDatabase(file);
2940 } else {
2941 throw ex;
2945 try {
2946 this.mDB.createTable("engine_data", engineDataTable);
2947 } catch (ex) {
2948 // Fails if the table already exists, which is fine
2951 this.mGetData = createStatement (
2952 this.mDB,
2953 "SELECT value FROM engine_data WHERE engineid = :engineid AND name = :name");
2954 this.mDeleteData = createStatement (
2955 this.mDB,
2956 "DELETE FROM engine_data WHERE engineid = :engineid AND name = :name");
2957 this.mInsertData = createStatement (
2958 this.mDB,
2959 "INSERT INTO engine_data (engineid, name, value) " +
2960 "VALUES (:engineid, :name, :value)");
2962 getAttr: function epsGetAttr(engine, name) {
2963 // attr names must be lower case
2964 name = name.toLowerCase();
2966 var stmt = this.mGetData;
2967 stmt.reset();
2968 var pp = stmt.params;
2969 pp.engineid = engine._id;
2970 pp.name = name;
2972 var value = null;
2973 if (stmt.step())
2974 value = stmt.row.value;
2975 stmt.reset();
2976 return value;
2979 setAttr: function epsSetAttr(engine, name, value) {
2980 // attr names must be lower case
2981 name = name.toLowerCase();
2983 this.mDB.beginTransaction();
2985 var pp = this.mDeleteData.params;
2986 pp.engineid = engine._id;
2987 pp.name = name;
2988 this.mDeleteData.step();
2989 this.mDeleteData.reset();
2991 pp = this.mInsertData.params;
2992 pp.engineid = engine._id;
2993 pp.name = name;
2994 pp.value = value;
2995 this.mInsertData.step();
2996 this.mInsertData.reset();
2998 this.mDB.commitTransaction();
3001 setAttrs: function epsSetAttrs(engines, names, values) {
3002 this.mDB.beginTransaction();
3004 for (var i = 0; i < engines.length; i++) {
3005 // attr names must be lower case
3006 var name = names[i].toLowerCase();
3008 var pp = this.mDeleteData.params;
3009 pp.engineid = engines[i]._id;
3010 pp.name = names[i];
3011 this.mDeleteData.step();
3012 this.mDeleteData.reset();
3014 pp = this.mInsertData.params;
3015 pp.engineid = engines[i]._id;
3016 pp.name = names[i];
3017 pp.value = values[i];
3018 this.mInsertData.step();
3019 this.mInsertData.reset();
3022 this.mDB.commitTransaction();
3025 deleteEngineData: function epsDelData(engine, name) {
3026 // attr names must be lower case
3027 name = name.toLowerCase();
3029 var pp = this.mDeleteData.params;
3030 pp.engineid = engine._id;
3031 pp.name = name;
3032 this.mDeleteData.step();
3033 this.mDeleteData.reset();
3037 const SEARCH_UPDATE_LOG_PREFIX = "*** Search update: ";
3040 * Outputs aText to the JavaScript console as well as to stdout, if the search
3041 * logging pref (browser.search.update.log) is set to true.
3043 function ULOG(aText) {
3044 var prefB = Cc["@mozilla.org/preferences-service;1"].
3045 getService(Ci.nsIPrefBranch);
3046 var shouldLog = false;
3047 try {
3048 shouldLog = prefB.getBoolPref(BROWSER_SEARCH_PREF + "update.log");
3049 } catch (ex) {}
3051 if (shouldLog) {
3052 dump(SEARCH_UPDATE_LOG_PREFIX + aText + "\n");
3053 var consoleService = Cc["@mozilla.org/consoleservice;1"].
3054 getService(Ci.nsIConsoleService);
3055 consoleService.logStringMessage(aText);
3059 var engineUpdateService = {
3060 init: function eus_init() {
3061 var tm = Cc["@mozilla.org/updates/timer-manager;1"].
3062 getService(Ci.nsIUpdateTimerManager);
3063 // figure out how often to check for any expired engines
3064 var prefB = Cc["@mozilla.org/preferences-service;1"].
3065 getService(Ci.nsIPrefBranch);
3066 var interval = prefB.getIntPref(BROWSER_SEARCH_PREF + "updateinterval");
3068 // Interval is stored in hours
3069 var seconds = interval * 3600;
3070 tm.registerTimer("search-engine-update-timer", engineUpdateService,
3071 seconds);
3074 scheduleNextUpdate: function eus_scheduleNextUpdate(aEngine) {
3075 var interval = aEngine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL;
3076 var milliseconds = interval * 86400000; // |interval| is in days
3077 engineMetadataService.setAttr(aEngine, "updateexpir",
3078 Date.now() + milliseconds);
3081 notify: function eus_Notify(aTimer) {
3082 ULOG("notify called");
3084 if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true))
3085 return;
3087 // Our timer has expired, but unfortunately, we can't get any data from it.
3088 // Therefore, we need to walk our engine-list, looking for expired engines
3089 var searchService = Cc["@mozilla.org/browser/search-service;1"].
3090 getService(Ci.nsIBrowserSearchService);
3091 var currentTime = Date.now();
3092 ULOG("currentTime: " + currentTime);
3093 for each (engine in searchService.getEngines({})) {
3094 engine = engine.wrappedJSObject;
3095 if (!engine._hasUpdates || engine._readOnly)
3096 continue;
3098 ULOG("checking " + engine.name);
3100 var expirTime = engineMetadataService.getAttr(engine, "updateexpir");
3101 var updateURL = engine._updateURL;
3102 var iconUpdateURL = engine._iconUpdateURL;
3103 ULOG("expirTime: " + expirTime + "\nupdateURL: " + updateURL +
3104 "\niconUpdateURL: " + iconUpdateURL);
3106 var engineExpired = expirTime <= currentTime;
3108 if (!expirTime || !engineExpired) {
3109 ULOG("skipping engine");
3110 continue;
3113 ULOG(engine.name + " has expired");
3115 var testEngine = null;
3117 var updateURI = makeURI(updateURL);
3118 if (updateURI) {
3119 var dataType = engineMetadataService.getAttr(engine, "updatedatatype")
3120 if (!dataType) {
3121 ULOG("No loadtype to update engine!");
3122 continue;
3125 testEngine = new Engine(updateURI, dataType, false);
3126 testEngine._engineToUpdate = engine;
3127 testEngine._initFromURI();
3128 } else
3129 ULOG("invalid updateURI");
3131 if (iconUpdateURL) {
3132 // If we're updating the engine too, use the new engine object,
3133 // otherwise use the existing engine object.
3134 (testEngine || engine)._setIcon(iconUpdateURL, true);
3137 // Schedule the next update
3138 this.scheduleNextUpdate(engine);
3140 } // end engine iteration
3144 const kClassID = Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}");
3145 const kClassName = "Browser Search Service";
3146 const kContractID = "@mozilla.org/browser/search-service;1";
3148 // nsIFactory
3149 const kFactory = {
3150 createInstance: function (outer, iid) {
3151 if (outer != null)
3152 throw Cr.NS_ERROR_NO_AGGREGATION;
3153 return (new SearchService()).QueryInterface(iid);
3157 // nsIModule
3158 const gModule = {
3159 registerSelf: function (componentManager, fileSpec, location, type) {
3160 componentManager.QueryInterface(Ci.nsIComponentRegistrar);
3161 componentManager.registerFactoryLocation(kClassID,
3162 kClassName,
3163 kContractID,
3164 fileSpec, location, type);
3167 unregisterSelf: function(componentManager, fileSpec, location) {
3168 componentManager.QueryInterface(Ci.nsIComponentRegistrar);
3169 componentManager.unregisterFactoryLocation(kClassID, fileSpec);
3172 getClassObject: function (componentManager, cid, iid) {
3173 if (!cid.equals(kClassID))
3174 throw Cr.NS_ERROR_NO_INTERFACE;
3175 if (!iid.equals(Ci.nsIFactory))
3176 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
3177 return kFactory;
3180 canUnload: function (componentManager) {
3181 return true;
3185 function NSGetModule(componentManager, fileSpec) {
3186 return gModule;
3189 #include ../../../toolkit/content/debug.js