michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", michael@0: "resource://gre/modules/AsyncShutdown.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", michael@0: "resource://gre/modules/DeferredTask.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", michael@0: "resource://gre/modules/TelemetryStopwatch.jsm"); michael@0: michael@0: // A text encoder to UTF8, used whenever we commit the michael@0: // engine metadata to disk. michael@0: XPCOMUtils.defineLazyGetter(this, "gEncoder", michael@0: function() { michael@0: return new TextEncoder(); michael@0: }); michael@0: michael@0: const PERMS_FILE = 0644; michael@0: const PERMS_DIRECTORY = 0755; michael@0: michael@0: const MODE_RDONLY = 0x01; michael@0: const MODE_WRONLY = 0x02; michael@0: const MODE_CREATE = 0x08; michael@0: const MODE_APPEND = 0x10; michael@0: const MODE_TRUNCATE = 0x20; michael@0: michael@0: // Directory service keys michael@0: const NS_APP_SEARCH_DIR_LIST = "SrchPluginsDL"; michael@0: const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns"; michael@0: const NS_APP_SEARCH_DIR = "SrchPlugns"; michael@0: const NS_APP_USER_PROFILE_50_DIR = "ProfD"; michael@0: michael@0: // Search engine "locations". If this list is changed, be sure to update michael@0: // the engine's _isDefault function accordingly. michael@0: const SEARCH_APP_DIR = 1; michael@0: const SEARCH_PROFILE_DIR = 2; michael@0: const SEARCH_IN_EXTENSION = 3; michael@0: const SEARCH_JAR = 4; michael@0: michael@0: // See documentation in nsIBrowserSearchService.idl. michael@0: const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified"; michael@0: const QUIT_APPLICATION_TOPIC = "quit-application"; michael@0: michael@0: const SEARCH_ENGINE_REMOVED = "engine-removed"; michael@0: const SEARCH_ENGINE_ADDED = "engine-added"; michael@0: const SEARCH_ENGINE_CHANGED = "engine-changed"; michael@0: const SEARCH_ENGINE_LOADED = "engine-loaded"; michael@0: const SEARCH_ENGINE_CURRENT = "engine-current"; michael@0: const SEARCH_ENGINE_DEFAULT = "engine-default"; michael@0: michael@0: // The following constants are left undocumented in nsIBrowserSearchService.idl michael@0: // For the moment, they are meant for testing/debugging purposes only. michael@0: michael@0: /** michael@0: * Topic used for events involving the service itself. michael@0: */ michael@0: const SEARCH_SERVICE_TOPIC = "browser-search-service"; michael@0: michael@0: /** michael@0: * Sent whenever metadata is fully written to disk. michael@0: */ michael@0: const SEARCH_SERVICE_METADATA_WRITTEN = "write-metadata-to-disk-complete"; michael@0: michael@0: /** michael@0: * Sent whenever the cache is fully written to disk. michael@0: */ michael@0: const SEARCH_SERVICE_CACHE_WRITTEN = "write-cache-to-disk-complete"; michael@0: michael@0: const SEARCH_TYPE_MOZSEARCH = Ci.nsISearchEngine.TYPE_MOZSEARCH; michael@0: const SEARCH_TYPE_OPENSEARCH = Ci.nsISearchEngine.TYPE_OPENSEARCH; michael@0: const SEARCH_TYPE_SHERLOCK = Ci.nsISearchEngine.TYPE_SHERLOCK; michael@0: michael@0: const SEARCH_DATA_XML = Ci.nsISearchEngine.DATA_XML; michael@0: const SEARCH_DATA_TEXT = Ci.nsISearchEngine.DATA_TEXT; michael@0: michael@0: // Delay for lazy serialization (ms) michael@0: const LAZY_SERIALIZE_DELAY = 100; michael@0: michael@0: // Delay for batching invalidation of the JSON cache (ms) michael@0: const CACHE_INVALIDATION_DELAY = 1000; michael@0: michael@0: // Current cache version. This should be incremented if the format of the cache michael@0: // file is modified. michael@0: const CACHE_VERSION = 7; michael@0: michael@0: const ICON_DATAURL_PREFIX = "data:image/x-icon;base64,"; michael@0: michael@0: const NEW_LINES = /(\r\n|\r|\n)/; michael@0: michael@0: // Set an arbitrary cap on the maximum icon size. Without this, large icons can michael@0: // cause big delays when loading them at startup. michael@0: const MAX_ICON_SIZE = 10000; michael@0: michael@0: // Default charset to use for sending search parameters. ISO-8859-1 is used to michael@0: // match previous nsInternetSearchService behavior. michael@0: const DEFAULT_QUERY_CHARSET = "ISO-8859-1"; michael@0: michael@0: const SEARCH_BUNDLE = "chrome://global/locale/search/search.properties"; michael@0: const BRAND_BUNDLE = "chrome://branding/locale/brand.properties"; michael@0: michael@0: const OPENSEARCH_NS_10 = "http://a9.com/-/spec/opensearch/1.0/"; michael@0: const OPENSEARCH_NS_11 = "http://a9.com/-/spec/opensearch/1.1/"; michael@0: michael@0: // Although the specification at http://opensearch.a9.com/spec/1.1/description/ michael@0: // gives the namespace names defined above, many existing OpenSearch engines michael@0: // are using the following versions. We therefore allow either. michael@0: const OPENSEARCH_NAMESPACES = [ michael@0: OPENSEARCH_NS_11, OPENSEARCH_NS_10, michael@0: "http://a9.com/-/spec/opensearchdescription/1.1/", michael@0: "http://a9.com/-/spec/opensearchdescription/1.0/" michael@0: ]; michael@0: michael@0: const OPENSEARCH_LOCALNAME = "OpenSearchDescription"; michael@0: michael@0: const MOZSEARCH_NS_10 = "http://www.mozilla.org/2006/browser/search/"; michael@0: const MOZSEARCH_LOCALNAME = "SearchPlugin"; michael@0: michael@0: const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json"; michael@0: const URLTYPE_SEARCH_HTML = "text/html"; michael@0: const URLTYPE_OPENSEARCH = "application/opensearchdescription+xml"; michael@0: michael@0: // Empty base document used to serialize engines to file. michael@0: const EMPTY_DOC = "\n" + michael@0: "<" + MOZSEARCH_LOCALNAME + michael@0: " xmlns=\"" + MOZSEARCH_NS_10 + "\"" + michael@0: " xmlns:os=\"" + OPENSEARCH_NS_11 + "\"" + michael@0: "/>"; michael@0: michael@0: const BROWSER_SEARCH_PREF = "browser.search."; michael@0: michael@0: const USER_DEFINED = "{searchTerms}"; michael@0: michael@0: // Custom search parameters michael@0: #ifdef MOZ_OFFICIAL_BRANDING michael@0: const MOZ_OFFICIAL = "official"; michael@0: #else michael@0: const MOZ_OFFICIAL = "unofficial"; michael@0: #endif michael@0: #expand const MOZ_DISTRIBUTION_ID = __MOZ_DISTRIBUTION_ID__; michael@0: michael@0: const MOZ_PARAM_LOCALE = /\{moz:locale\}/g; michael@0: const MOZ_PARAM_DIST_ID = /\{moz:distributionID\}/g; michael@0: const MOZ_PARAM_OFFICIAL = /\{moz:official\}/g; michael@0: michael@0: // Supported OpenSearch parameters michael@0: // See http://opensearch.a9.com/spec/1.1/querysyntax/#core michael@0: const OS_PARAM_USER_DEFINED = /\{searchTerms\??\}/g; michael@0: const OS_PARAM_INPUT_ENCODING = /\{inputEncoding\??\}/g; michael@0: const OS_PARAM_LANGUAGE = /\{language\??\}/g; michael@0: const OS_PARAM_OUTPUT_ENCODING = /\{outputEncoding\??\}/g; michael@0: michael@0: // Default values michael@0: const OS_PARAM_LANGUAGE_DEF = "*"; michael@0: const OS_PARAM_OUTPUT_ENCODING_DEF = "UTF-8"; michael@0: const OS_PARAM_INPUT_ENCODING_DEF = "UTF-8"; michael@0: michael@0: // "Unsupported" OpenSearch parameters. For example, we don't support michael@0: // page-based results, so if the engine requires that we send the "page index" michael@0: // parameter, we'll always send "1". michael@0: const OS_PARAM_COUNT = /\{count\??\}/g; michael@0: const OS_PARAM_START_INDEX = /\{startIndex\??\}/g; michael@0: const OS_PARAM_START_PAGE = /\{startPage\??\}/g; michael@0: michael@0: // Default values michael@0: const OS_PARAM_COUNT_DEF = "20"; // 20 results michael@0: const OS_PARAM_START_INDEX_DEF = "1"; // start at 1st result michael@0: const OS_PARAM_START_PAGE_DEF = "1"; // 1st page michael@0: michael@0: // Optional parameter michael@0: const OS_PARAM_OPTIONAL = /\{(?:\w+:)?\w+\?\}/g; michael@0: michael@0: // A array of arrays containing parameters that we don't fully support, and michael@0: // their default values. We will only send values for these parameters if michael@0: // required, since our values are just really arbitrary "guesses" that should michael@0: // give us the output we want. michael@0: var OS_UNSUPPORTED_PARAMS = [ michael@0: [OS_PARAM_COUNT, OS_PARAM_COUNT_DEF], michael@0: [OS_PARAM_START_INDEX, OS_PARAM_START_INDEX_DEF], michael@0: [OS_PARAM_START_PAGE, OS_PARAM_START_PAGE_DEF], michael@0: ]; michael@0: michael@0: // The default engine update interval, in days. This is only used if an engine michael@0: // specifies an updateURL, but not an updateInterval. michael@0: const SEARCH_DEFAULT_UPDATE_INTERVAL = 7; michael@0: michael@0: // Returns false for whitespace-only or commented out lines in a michael@0: // Sherlock file, true otherwise. michael@0: function isUsefulLine(aLine) { michael@0: return !(/^\s*($|#)/i.test(aLine)); michael@0: } michael@0: michael@0: this.__defineGetter__("FileUtils", function() { michael@0: delete this.FileUtils; michael@0: Components.utils.import("resource://gre/modules/FileUtils.jsm"); michael@0: return FileUtils; michael@0: }); michael@0: michael@0: this.__defineGetter__("NetUtil", function() { michael@0: delete this.NetUtil; michael@0: Components.utils.import("resource://gre/modules/NetUtil.jsm"); michael@0: return NetUtil; michael@0: }); michael@0: michael@0: this.__defineGetter__("gChromeReg", function() { michael@0: delete this.gChromeReg; michael@0: return this.gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. michael@0: getService(Ci.nsIChromeRegistry); michael@0: }); michael@0: michael@0: /** michael@0: * Prefixed to all search debug output. michael@0: */ michael@0: const SEARCH_LOG_PREFIX = "*** Search: "; michael@0: michael@0: /** michael@0: * Outputs aText to the JavaScript console as well as to stdout. michael@0: */ michael@0: function DO_LOG(aText) { michael@0: dump(SEARCH_LOG_PREFIX + aText + "\n"); michael@0: Services.console.logStringMessage(aText); michael@0: } michael@0: michael@0: #ifdef DEBUG michael@0: /** michael@0: * In debug builds, use a live, pref-based (browser.search.log) LOG function michael@0: * to allow enabling/disabling without a restart. michael@0: */ michael@0: function PREF_LOG(aText) { michael@0: if (getBoolPref(BROWSER_SEARCH_PREF + "log", false)) michael@0: DO_LOG(aText); michael@0: } michael@0: var LOG = PREF_LOG; michael@0: michael@0: #else michael@0: michael@0: /** michael@0: * Otherwise, don't log at all by default. This can be overridden at startup michael@0: * by the pref, see SearchService's _init method. michael@0: */ michael@0: var LOG = function(){}; michael@0: michael@0: #endif michael@0: michael@0: /** michael@0: * Presents an assertion dialog in non-release builds and throws. michael@0: * @param message michael@0: * A message to display michael@0: * @param resultCode michael@0: * The NS_ERROR_* value to throw. michael@0: * @throws resultCode michael@0: */ michael@0: function ERROR(message, resultCode) { michael@0: NS_ASSERT(false, SEARCH_LOG_PREFIX + message); michael@0: throw Components.Exception(message, resultCode); michael@0: } michael@0: michael@0: /** michael@0: * Logs the failure message (if browser.search.log is enabled) and throws. michael@0: * @param message michael@0: * A message to display michael@0: * @param resultCode michael@0: * The NS_ERROR_* value to throw. michael@0: * @throws resultCode or NS_ERROR_INVALID_ARG if resultCode isn't specified. michael@0: */ michael@0: function FAIL(message, resultCode) { michael@0: LOG(message); michael@0: throw Components.Exception(message, resultCode || Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: /** michael@0: * Truncates big blobs of (data-)URIs to console-friendly sizes michael@0: * @param str michael@0: * String to tone down michael@0: * @param len michael@0: * Maximum length of the string to return. Defaults to the length of a tweet. michael@0: */ michael@0: function limitURILength(str, len) { michael@0: len = len || 140; michael@0: if (str.length > len) michael@0: return str.slice(0, len) + "..."; michael@0: return str; michael@0: } michael@0: michael@0: /** michael@0: * Utilities for dealing with promises and Task.jsm michael@0: */ michael@0: const TaskUtils = { michael@0: /** michael@0: * Add logging to a promise. michael@0: * michael@0: * @param {Promise} promise michael@0: * @return {Promise} A promise behaving as |promise|, but with additional michael@0: * logging in case of uncaught error. michael@0: */ michael@0: captureErrors: function captureErrors(promise) { michael@0: return promise.then( michael@0: null, michael@0: function onError(reason) { michael@0: LOG("Uncaught asynchronous error: " + reason + " at\n" + reason.stack); michael@0: throw reason; michael@0: } michael@0: ); michael@0: }, michael@0: /** michael@0: * Spawn a new Task from a generator. michael@0: * michael@0: * This function behaves as |Task.spawn|, with the exception that it michael@0: * adds logging in case of uncaught error. For more information, see michael@0: * the documentation of |Task.jsm|. michael@0: * michael@0: * @param {generator} gen Some generator. michael@0: * @return {Promise} A promise built from |gen|, with the same semantics michael@0: * as |Task.spawn(gen)|. michael@0: */ michael@0: spawn: function spawn(gen) { michael@0: return this.captureErrors(Task.spawn(gen)); michael@0: }, michael@0: /** michael@0: * Execute a mozIStorage statement asynchronously, wrapping the michael@0: * result in a promise. michael@0: * michael@0: * @param {mozIStorageStaement} statement A statement to be executed michael@0: * asynchronously. The semantics are the same as these of |statement.execute|. michael@0: * @param {function*} onResult A callback, called for each successive result. michael@0: * michael@0: * @return {Promise} A promise, resolved successfully if |statement.execute| michael@0: * succeeds, rejected if it fails. michael@0: */ michael@0: executeStatement: function executeStatement(statement, onResult) { michael@0: let deferred = Promise.defer(); michael@0: onResult = onResult || function() {}; michael@0: statement.executeAsync({ michael@0: handleResult: onResult, michael@0: handleError: function handleError(aError) { michael@0: deferred.reject(aError); michael@0: }, michael@0: handleCompletion: function handleCompletion(aReason) { michael@0: statement.finalize(); michael@0: // Note that, in case of error, deferred.reject(aError) michael@0: // has already been called by this point, so the call to michael@0: // |deferred.resolve| is simply ignored. michael@0: deferred.resolve(aReason); michael@0: } michael@0: }); michael@0: return deferred.promise; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Ensures an assertion is met before continuing. Should be used to indicate michael@0: * fatal errors. michael@0: * @param assertion michael@0: * An assertion that must be met michael@0: * @param message michael@0: * A message to display if the assertion is not met michael@0: * @param resultCode michael@0: * The NS_ERROR_* value to throw if the assertion is not met michael@0: * @throws resultCode michael@0: */ michael@0: function ENSURE_WARN(assertion, message, resultCode) { michael@0: NS_ASSERT(assertion, SEARCH_LOG_PREFIX + message); michael@0: if (!assertion) michael@0: throw Components.Exception(message, resultCode); michael@0: } michael@0: michael@0: function loadListener(aChannel, aEngine, aCallback) { michael@0: this._channel = aChannel; michael@0: this._bytes = []; michael@0: this._engine = aEngine; michael@0: this._callback = aCallback; michael@0: } michael@0: loadListener.prototype = { michael@0: _callback: null, michael@0: _channel: null, michael@0: _countRead: 0, michael@0: _engine: null, michael@0: _stream: null, michael@0: michael@0: QueryInterface: function SRCH_loadQI(aIID) { michael@0: if (aIID.equals(Ci.nsISupports) || michael@0: aIID.equals(Ci.nsIRequestObserver) || michael@0: aIID.equals(Ci.nsIStreamListener) || michael@0: aIID.equals(Ci.nsIChannelEventSink) || michael@0: aIID.equals(Ci.nsIInterfaceRequestor) || michael@0: // See FIXME comment below michael@0: aIID.equals(Ci.nsIHttpEventSink) || michael@0: aIID.equals(Ci.nsIProgressEventSink) || michael@0: false) michael@0: return this; michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: // nsIRequestObserver michael@0: onStartRequest: function SRCH_loadStartR(aRequest, aContext) { michael@0: LOG("loadListener: Starting request: " + aRequest.name); michael@0: this._stream = Cc["@mozilla.org/binaryinputstream;1"]. michael@0: createInstance(Ci.nsIBinaryInputStream); michael@0: }, michael@0: michael@0: onStopRequest: function SRCH_loadStopR(aRequest, aContext, aStatusCode) { michael@0: LOG("loadListener: Stopping request: " + aRequest.name); michael@0: michael@0: var requestFailed = !Components.isSuccessCode(aStatusCode); michael@0: if (!requestFailed && (aRequest instanceof Ci.nsIHttpChannel)) michael@0: requestFailed = !aRequest.requestSucceeded; michael@0: michael@0: if (requestFailed || this._countRead == 0) { michael@0: LOG("loadListener: request failed!"); michael@0: // send null so the callback can deal with the failure michael@0: this._callback(null, this._engine); michael@0: } else michael@0: this._callback(this._bytes, this._engine); michael@0: this._channel = null; michael@0: this._engine = null; michael@0: }, michael@0: michael@0: // nsIStreamListener michael@0: onDataAvailable: function SRCH_loadDAvailable(aRequest, aContext, michael@0: aInputStream, aOffset, michael@0: aCount) { michael@0: this._stream.setInputStream(aInputStream); michael@0: michael@0: // Get a byte array of the data michael@0: this._bytes = this._bytes.concat(this._stream.readByteArray(aCount)); michael@0: this._countRead += aCount; michael@0: }, michael@0: michael@0: // nsIChannelEventSink michael@0: asyncOnChannelRedirect: function SRCH_loadCRedirect(aOldChannel, aNewChannel, michael@0: aFlags, callback) { michael@0: this._channel = aNewChannel; michael@0: callback.onRedirectVerifyCallback(Components.results.NS_OK); michael@0: }, michael@0: michael@0: // nsIInterfaceRequestor michael@0: getInterface: function SRCH_load_GI(aIID) { michael@0: return this.QueryInterface(aIID); michael@0: }, michael@0: michael@0: // FIXME: bug 253127 michael@0: // nsIHttpEventSink michael@0: onRedirect: function (aChannel, aNewChannel) {}, michael@0: // nsIProgressEventSink michael@0: onProgress: function (aRequest, aContext, aProgress, aProgressMax) {}, michael@0: onStatus: function (aRequest, aContext, aStatus, aStatusArg) {} michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Used to verify a given DOM node's localName and namespaceURI. michael@0: * @param aElement michael@0: * The element to verify. michael@0: * @param aLocalNameArray michael@0: * An array of strings to compare against aElement's localName. michael@0: * @param aNameSpaceArray michael@0: * An array of strings to compare against aElement's namespaceURI. michael@0: * michael@0: * @returns false if aElement is null, or if its localName or namespaceURI michael@0: * does not match one of the elements in the aLocalNameArray or michael@0: * aNameSpaceArray arrays, respectively. michael@0: * @throws NS_ERROR_INVALID_ARG if aLocalNameArray or aNameSpaceArray are null. michael@0: */ michael@0: function checkNameSpace(aElement, aLocalNameArray, aNameSpaceArray) { michael@0: if (!aLocalNameArray || !aNameSpaceArray) michael@0: FAIL("missing aLocalNameArray or aNameSpaceArray for checkNameSpace"); michael@0: return (aElement && michael@0: (aLocalNameArray.indexOf(aElement.localName) != -1) && michael@0: (aNameSpaceArray.indexOf(aElement.namespaceURI) != -1)); michael@0: } michael@0: michael@0: /** michael@0: * Safely close a nsISafeOutputStream. michael@0: * @param aFOS michael@0: * The file output stream to close. michael@0: */ michael@0: function closeSafeOutputStream(aFOS) { michael@0: if (aFOS instanceof Ci.nsISafeOutputStream) { michael@0: try { michael@0: aFOS.finish(); michael@0: return; michael@0: } catch (e) { } michael@0: } michael@0: aFOS.close(); michael@0: } michael@0: michael@0: /** michael@0: * Wrapper function for nsIIOService::newURI. michael@0: * @param aURLSpec michael@0: * The URL string from which to create an nsIURI. michael@0: * @returns an nsIURI object, or null if the creation of the URI failed. michael@0: */ michael@0: function makeURI(aURLSpec, aCharset) { michael@0: try { michael@0: return NetUtil.newURI(aURLSpec, aCharset); michael@0: } catch (ex) { } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Gets a directory from the directory service. michael@0: * @param aKey michael@0: * The directory service key indicating the directory to get. michael@0: */ michael@0: function getDir(aKey, aIFace) { michael@0: if (!aKey) michael@0: FAIL("getDir requires a directory key!"); michael@0: michael@0: return Services.dirsvc.get(aKey, aIFace || Ci.nsIFile); michael@0: } michael@0: michael@0: /** michael@0: * The following two functions are essentially copied from michael@0: * nsInternetSearchService. They are required for backwards compatibility. michael@0: */ michael@0: function queryCharsetFromCode(aCode) { michael@0: const codes = []; michael@0: codes[0] = "macintosh"; michael@0: codes[6] = "x-mac-greek"; michael@0: codes[35] = "x-mac-turkish"; michael@0: codes[513] = "ISO-8859-1"; michael@0: codes[514] = "ISO-8859-2"; michael@0: codes[517] = "ISO-8859-5"; michael@0: codes[518] = "ISO-8859-6"; michael@0: codes[519] = "ISO-8859-7"; michael@0: codes[520] = "ISO-8859-8"; michael@0: codes[521] = "ISO-8859-9"; michael@0: codes[1280] = "windows-1252"; michael@0: codes[1281] = "windows-1250"; michael@0: codes[1282] = "windows-1251"; michael@0: codes[1283] = "windows-1253"; michael@0: codes[1284] = "windows-1254"; michael@0: codes[1285] = "windows-1255"; michael@0: codes[1286] = "windows-1256"; michael@0: codes[1536] = "us-ascii"; michael@0: codes[1584] = "GB2312"; michael@0: codes[1585] = "gbk"; michael@0: codes[1600] = "EUC-KR"; michael@0: codes[2080] = "ISO-2022-JP"; michael@0: codes[2096] = "ISO-2022-CN"; michael@0: codes[2112] = "ISO-2022-KR"; michael@0: codes[2336] = "EUC-JP"; michael@0: codes[2352] = "GB2312"; michael@0: codes[2353] = "x-euc-tw"; michael@0: codes[2368] = "EUC-KR"; michael@0: codes[2561] = "Shift_JIS"; michael@0: codes[2562] = "KOI8-R"; michael@0: codes[2563] = "Big5"; michael@0: codes[2565] = "HZ-GB-2312"; michael@0: michael@0: if (codes[aCode]) michael@0: return codes[aCode]; michael@0: michael@0: // Don't bother being fancy about what to return in the failure case. michael@0: return "windows-1252"; michael@0: } michael@0: function fileCharsetFromCode(aCode) { michael@0: const codes = [ michael@0: "macintosh", // 0 michael@0: "Shift_JIS", // 1 michael@0: "Big5", // 2 michael@0: "EUC-KR", // 3 michael@0: "X-MAC-ARABIC", // 4 michael@0: "X-MAC-HEBREW", // 5 michael@0: "X-MAC-GREEK", // 6 michael@0: "X-MAC-CYRILLIC", // 7 michael@0: "X-MAC-DEVANAGARI" , // 9 michael@0: "X-MAC-GURMUKHI", // 10 michael@0: "X-MAC-GUJARATI", // 11 michael@0: "X-MAC-ORIYA", // 12 michael@0: "X-MAC-BENGALI", // 13 michael@0: "X-MAC-TAMIL", // 14 michael@0: "X-MAC-TELUGU", // 15 michael@0: "X-MAC-KANNADA", // 16 michael@0: "X-MAC-MALAYALAM", // 17 michael@0: "X-MAC-SINHALESE", // 18 michael@0: "X-MAC-BURMESE", // 19 michael@0: "X-MAC-KHMER", // 20 michael@0: "X-MAC-THAI", // 21 michael@0: "X-MAC-LAOTIAN", // 22 michael@0: "X-MAC-GEORGIAN", // 23 michael@0: "X-MAC-ARMENIAN", // 24 michael@0: "GB2312", // 25 michael@0: "X-MAC-TIBETAN", // 26 michael@0: "X-MAC-MONGOLIAN", // 27 michael@0: "X-MAC-ETHIOPIC", // 28 michael@0: "X-MAC-CENTRALEURROMAN", // 29 michael@0: "X-MAC-VIETNAMESE", // 30 michael@0: "X-MAC-EXTARABIC" // 31 michael@0: ]; michael@0: // Sherlock files have always defaulted to macintosh, so do that here too michael@0: return codes[aCode] || codes[0]; michael@0: } michael@0: michael@0: /** michael@0: * Returns a string interpretation of aBytes using aCharset, or null on michael@0: * failure. michael@0: */ michael@0: function bytesToString(aBytes, aCharset) { michael@0: var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. michael@0: createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: LOG("bytesToString: converting using charset: " + aCharset); michael@0: michael@0: try { michael@0: converter.charset = aCharset; michael@0: return converter.convertFromByteArray(aBytes, aBytes.length); michael@0: } catch (ex) {} michael@0: michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Converts an array of bytes representing a Sherlock file into an array of michael@0: * lines representing the useful data from the file. michael@0: * michael@0: * @param aBytes michael@0: * The array of bytes representing the Sherlock file. michael@0: * @param aCharsetCode michael@0: * An integer value representing a character set code to be passed to michael@0: * fileCharsetFromCode, or null for the default Sherlock encoding. michael@0: */ michael@0: function sherlockBytesToLines(aBytes, aCharsetCode) { michael@0: // fileCharsetFromCode returns the default encoding if aCharsetCode is null michael@0: var charset = fileCharsetFromCode(aCharsetCode); michael@0: michael@0: var dataString = bytesToString(aBytes, charset); michael@0: if (!dataString) michael@0: FAIL("sherlockBytesToLines: Couldn't convert byte array!", Cr.NS_ERROR_FAILURE); michael@0: michael@0: // Split the string into lines, and filter out comments and michael@0: // whitespace-only lines michael@0: return dataString.split(NEW_LINES).filter(isUsefulLine); michael@0: } michael@0: michael@0: /** michael@0: * Gets the current value of the locale. It's possible for this preference to michael@0: * be localized, so we have to do a little extra work here. Similar code michael@0: * exists in nsHttpHandler.cpp when building the UA string. michael@0: */ michael@0: function getLocale() { michael@0: const localePref = "general.useragent.locale"; michael@0: var locale = getLocalizedPref(localePref); michael@0: if (locale) michael@0: return locale; michael@0: michael@0: // Not localized michael@0: return Services.prefs.getCharPref(localePref); michael@0: } michael@0: michael@0: /** michael@0: * Wrapper for nsIPrefBranch::getComplexValue. michael@0: * @param aPrefName michael@0: * The name of the pref to get. michael@0: * @returns aDefault if the requested pref doesn't exist. michael@0: */ michael@0: function getLocalizedPref(aPrefName, aDefault) { michael@0: const nsIPLS = Ci.nsIPrefLocalizedString; michael@0: try { michael@0: return Services.prefs.getComplexValue(aPrefName, nsIPLS).data; michael@0: } catch (ex) {} michael@0: michael@0: return aDefault; michael@0: } michael@0: michael@0: /** michael@0: * Wrapper for nsIPrefBranch::setComplexValue. michael@0: * @param aPrefName michael@0: * The name of the pref to set. michael@0: */ michael@0: function setLocalizedPref(aPrefName, aValue) { michael@0: const nsIPLS = Ci.nsIPrefLocalizedString; michael@0: try { michael@0: var pls = Components.classes["@mozilla.org/pref-localizedstring;1"] michael@0: .createInstance(Ci.nsIPrefLocalizedString); michael@0: pls.data = aValue; michael@0: Services.prefs.setComplexValue(aPrefName, nsIPLS, pls); michael@0: } catch (ex) {} michael@0: } michael@0: michael@0: /** michael@0: * Wrapper for nsIPrefBranch::getBoolPref. michael@0: * @param aPrefName michael@0: * The name of the pref to get. michael@0: * @returns aDefault if the requested pref doesn't exist. michael@0: */ michael@0: function getBoolPref(aName, aDefault) { michael@0: try { michael@0: return Services.prefs.getBoolPref(aName); michael@0: } catch (ex) { michael@0: return aDefault; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Get a unique nsIFile object with a sanitized name, based on the engine name. michael@0: * @param aName michael@0: * A name to "sanitize". Can be an empty string, in which case a random michael@0: * 8 character filename will be produced. michael@0: * @returns A nsIFile object in the user's search engines directory with a michael@0: * unique sanitized name. michael@0: */ michael@0: function getSanitizedFile(aName) { michael@0: var fileName = sanitizeName(aName) + ".xml"; michael@0: var file = getDir(NS_APP_USER_SEARCH_DIR); michael@0: file.append(fileName); michael@0: file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); michael@0: return file; michael@0: } michael@0: michael@0: /** michael@0: * @return a sanitized name to be used as a filename, or a random name michael@0: * if a sanitized name cannot be obtained (if aName contains michael@0: * no valid characters). michael@0: */ michael@0: function sanitizeName(aName) { michael@0: const maxLength = 60; michael@0: const minLength = 1; michael@0: var name = aName.toLowerCase(); michael@0: name = name.replace(/\s+/g, "-"); michael@0: name = name.replace(/[^-a-z0-9]/g, ""); michael@0: michael@0: // Use a random name if our input had no valid characters. michael@0: if (name.length < minLength) michael@0: name = Math.random().toString(36).replace(/^.*\./, ''); michael@0: michael@0: // Force max length. michael@0: return name.substring(0, maxLength); michael@0: } michael@0: michael@0: /** michael@0: * Retrieve a pref from the search param branch. michael@0: * michael@0: * @param prefName michael@0: * The name of the pref. michael@0: **/ michael@0: function getMozParamPref(prefName) michael@0: Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "param." + prefName); michael@0: michael@0: /** michael@0: * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to michael@0: * the state of the search service. michael@0: * michael@0: * @param aEngine michael@0: * The nsISearchEngine object to which the change applies. michael@0: * @param aVerb michael@0: * A verb describing the change. michael@0: * michael@0: * @see nsIBrowserSearchService.idl michael@0: */ michael@0: let gInitialized = false; michael@0: function notifyAction(aEngine, aVerb) { michael@0: if (gInitialized) { michael@0: LOG("NOTIFY: Engine: \"" + aEngine.name + "\"; Verb: \"" + aVerb + "\""); michael@0: Services.obs.notifyObservers(aEngine, SEARCH_ENGINE_TOPIC, aVerb); michael@0: } michael@0: } michael@0: michael@0: function parseJsonFromStream(aInputStream) { michael@0: const json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); michael@0: const data = json.decodeFromStream(aInputStream, aInputStream.available()); michael@0: return data; michael@0: } michael@0: michael@0: /** michael@0: * Simple object representing a name/value pair. michael@0: */ michael@0: function QueryParameter(aName, aValue, aPurpose) { michael@0: if (!aName || (aValue == null)) michael@0: FAIL("missing name or value for QueryParameter!"); michael@0: michael@0: this.name = aName; michael@0: this.value = aValue; michael@0: this.purpose = aPurpose; michael@0: } michael@0: michael@0: /** michael@0: * Perform OpenSearch parameter substitution on aParamValue. michael@0: * michael@0: * @param aParamValue michael@0: * A string containing OpenSearch search parameters. michael@0: * @param aSearchTerms michael@0: * The user-provided search terms. This string will inserted into michael@0: * aParamValue as the value of the OS_PARAM_USER_DEFINED parameter. michael@0: * This value must already be escaped appropriately - it is inserted michael@0: * as-is. michael@0: * @param aEngine michael@0: * The engine which owns the string being acted on. michael@0: * michael@0: * @see http://opensearch.a9.com/spec/1.1/querysyntax/#core michael@0: */ michael@0: function ParamSubstitution(aParamValue, aSearchTerms, aEngine) { michael@0: var value = aParamValue; michael@0: michael@0: var distributionID = MOZ_DISTRIBUTION_ID; michael@0: try { michael@0: distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID"); michael@0: } michael@0: catch (ex) { } michael@0: var official = MOZ_OFFICIAL; michael@0: try { michael@0: if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official")) michael@0: official = "official"; michael@0: else michael@0: official = "unofficial"; michael@0: } michael@0: catch (ex) { } michael@0: michael@0: // Custom search parameters. These are only available to default search michael@0: // engines. michael@0: if (aEngine._isDefault) { michael@0: value = value.replace(MOZ_PARAM_LOCALE, getLocale()); michael@0: value = value.replace(MOZ_PARAM_DIST_ID, distributionID); michael@0: value = value.replace(MOZ_PARAM_OFFICIAL, official); michael@0: } michael@0: michael@0: // Insert the OpenSearch parameters we're confident about michael@0: value = value.replace(OS_PARAM_USER_DEFINED, aSearchTerms); michael@0: value = value.replace(OS_PARAM_INPUT_ENCODING, aEngine.queryCharset); michael@0: value = value.replace(OS_PARAM_LANGUAGE, michael@0: getLocale() || OS_PARAM_LANGUAGE_DEF); michael@0: value = value.replace(OS_PARAM_OUTPUT_ENCODING, michael@0: OS_PARAM_OUTPUT_ENCODING_DEF); michael@0: michael@0: // Replace any optional parameters michael@0: value = value.replace(OS_PARAM_OPTIONAL, ""); michael@0: michael@0: // Insert any remaining required params with our default values michael@0: for (var i = 0; i < OS_UNSUPPORTED_PARAMS.length; ++i) { michael@0: value = value.replace(OS_UNSUPPORTED_PARAMS[i][0], michael@0: OS_UNSUPPORTED_PARAMS[i][1]); michael@0: } michael@0: michael@0: return value; michael@0: } michael@0: michael@0: /** michael@0: * Creates an engineURL object, which holds the query URL and all parameters. michael@0: * michael@0: * @param aType michael@0: * A string containing the name of the MIME type of the search results michael@0: * returned by this URL. michael@0: * @param aMethod michael@0: * The HTTP request method. Must be a case insensitive value of either michael@0: * "GET" or "POST". michael@0: * @param aTemplate michael@0: * The URL to which search queries should be sent. For GET requests, michael@0: * must contain the string "{searchTerms}", to indicate where the user michael@0: * entered search terms should be inserted. michael@0: * @param aResultDomain michael@0: * The root domain for this URL. Defaults to the template's host. michael@0: * michael@0: * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag michael@0: * michael@0: * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported. michael@0: */ michael@0: function EngineURL(aType, aMethod, aTemplate, aResultDomain) { michael@0: if (!aType || !aMethod || !aTemplate) michael@0: FAIL("missing type, method or template for EngineURL!"); michael@0: michael@0: var method = aMethod.toUpperCase(); michael@0: var type = aType.toLowerCase(); michael@0: michael@0: if (method != "GET" && method != "POST") michael@0: FAIL("method passed to EngineURL must be \"GET\" or \"POST\""); michael@0: michael@0: this.type = type; michael@0: this.method = method; michael@0: this.params = []; michael@0: this.rels = []; michael@0: // Don't serialize expanded mozparams michael@0: this.mozparams = {}; michael@0: michael@0: var templateURI = makeURI(aTemplate); michael@0: if (!templateURI) michael@0: FAIL("new EngineURL: template is not a valid URI!", Cr.NS_ERROR_FAILURE); michael@0: michael@0: switch (templateURI.scheme) { michael@0: case "http": michael@0: case "https": michael@0: // Disable these for now, see bug 295018 michael@0: // case "file": michael@0: // case "resource": michael@0: this.template = aTemplate; michael@0: break; michael@0: default: michael@0: FAIL("new EngineURL: template uses invalid scheme!", Cr.NS_ERROR_FAILURE); michael@0: } michael@0: michael@0: // If no resultDomain was specified in the engine definition file, use the michael@0: // host from the template. michael@0: this.resultDomain = aResultDomain || templateURI.host; michael@0: // We never want to return a "www." prefix, so eventually strip it. michael@0: if (this.resultDomain.startsWith("www.")) { michael@0: this.resultDomain = this.resultDomain.substr(4); michael@0: } michael@0: } michael@0: EngineURL.prototype = { michael@0: michael@0: addParam: function SRCH_EURL_addParam(aName, aValue, aPurpose) { michael@0: this.params.push(new QueryParameter(aName, aValue, aPurpose)); michael@0: }, michael@0: michael@0: // Note: This method requires that aObj has a unique name or the previous MozParams entry with michael@0: // that name will be overwritten. michael@0: _addMozParam: function SRCH_EURL__addMozParam(aObj) { michael@0: aObj.mozparam = true; michael@0: this.mozparams[aObj.name] = aObj; michael@0: }, michael@0: michael@0: reevalMozParams: function(engine) { michael@0: for (let param of this.params) { michael@0: let mozparam = this.mozparams[param.name]; michael@0: if (mozparam && mozparam.positionDependent) { michael@0: // the condition is a string in the form of "topN", extract N as int michael@0: let positionStr = mozparam.condition.slice("top".length); michael@0: let position = parseInt(positionStr, 10); michael@0: let engines; michael@0: try { michael@0: // This will throw if we're not initialized yet (which shouldn't happen), just michael@0: // ignore and move on with the false Value (checking isInitialized also throws) michael@0: // XXX michael@0: engines = Services.search.getVisibleEngines({}); michael@0: } catch (ex) { michael@0: LOG("reevalMozParams called before search service initialization!?"); michael@0: break; michael@0: } michael@0: let index = engines.map((e) => e.wrappedJSObject).indexOf(engine.wrappedJSObject); michael@0: let isTopN = index > -1 && (index + 1) <= position; michael@0: param.value = isTopN ? mozparam.trueValue : mozparam.falseValue; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: getSubmission: function SRCH_EURL_getSubmission(aSearchTerms, aEngine, aPurpose) { michael@0: this.reevalMozParams(aEngine); michael@0: michael@0: var url = ParamSubstitution(this.template, aSearchTerms, aEngine); michael@0: // Default to an empty string if the purpose is not provided so that default purpose params michael@0: // (purpose="") work consistently rather than having to define "null" and "" purposes. michael@0: var purpose = aPurpose || ""; michael@0: michael@0: // Create an application/x-www-form-urlencoded representation of our params michael@0: // (name=value&name=value&name=value) michael@0: var dataString = ""; michael@0: for (var i = 0; i < this.params.length; ++i) { michael@0: var param = this.params[i]; michael@0: michael@0: // If this parameter has a purpose, only add it if the purpose matches michael@0: if (param.purpose !== undefined && param.purpose != purpose) michael@0: continue; michael@0: michael@0: var value = ParamSubstitution(param.value, aSearchTerms, aEngine); michael@0: michael@0: dataString += (i > 0 ? "&" : "") + param.name + "=" + value; michael@0: } michael@0: michael@0: var postData = null; michael@0: if (this.method == "GET") { michael@0: // GET method requests have no post data, and append the encoded michael@0: // query string to the url... michael@0: if (url.indexOf("?") == -1 && dataString) michael@0: url += "?"; michael@0: url += dataString; michael@0: } else if (this.method == "POST") { michael@0: // POST method requests must wrap the encoded text in a MIME michael@0: // stream and supply that as POSTDATA. michael@0: var stringStream = Cc["@mozilla.org/io/string-input-stream;1"]. michael@0: createInstance(Ci.nsIStringInputStream); michael@0: stringStream.data = dataString; michael@0: michael@0: postData = Cc["@mozilla.org/network/mime-input-stream;1"]. michael@0: createInstance(Ci.nsIMIMEInputStream); michael@0: postData.addHeader("Content-Type", "application/x-www-form-urlencoded"); michael@0: postData.addContentLength = true; michael@0: postData.setData(stringStream); michael@0: } michael@0: michael@0: return new Submission(makeURI(url), postData); michael@0: }, michael@0: michael@0: _hasRelation: function SRC_EURL__hasRelation(aRel) michael@0: this.rels.some(function(e) e == aRel.toLowerCase()), michael@0: michael@0: _initWithJSON: function SRC_EURL__initWithJSON(aJson, aEngine) { michael@0: if (!aJson.params) michael@0: return; michael@0: michael@0: this.rels = aJson.rels; michael@0: michael@0: for (let i = 0; i < aJson.params.length; ++i) { michael@0: let param = aJson.params[i]; michael@0: if (param.mozparam) { michael@0: if (param.condition == "defaultEngine") { michael@0: if (aEngine._isDefaultEngine()) michael@0: this.addParam(param.name, param.trueValue); michael@0: else michael@0: this.addParam(param.name, param.falseValue); michael@0: } else if (param.condition == "pref") { michael@0: let value = getMozParamPref(param.pref); michael@0: this.addParam(param.name, value); michael@0: } michael@0: this._addMozParam(param); michael@0: } michael@0: else michael@0: this.addParam(param.name, param.value, param.purpose); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates a JavaScript object that represents this URL. michael@0: * @returns An object suitable for serialization as JSON. michael@0: **/ michael@0: _serializeToJSON: function SRCH_EURL__serializeToJSON() { michael@0: var json = { michael@0: template: this.template, michael@0: rels: this.rels, michael@0: resultDomain: this.resultDomain michael@0: }; michael@0: michael@0: if (this.type != URLTYPE_SEARCH_HTML) michael@0: json.type = this.type; michael@0: if (this.method != "GET") michael@0: json.method = this.method; michael@0: michael@0: function collapseMozParams(aParam) michael@0: this.mozparams[aParam.name] || aParam; michael@0: json.params = this.params.map(collapseMozParams, this); michael@0: michael@0: return json; michael@0: }, michael@0: michael@0: /** michael@0: * Serializes the engine object to a OpenSearch Url element. michael@0: * @param aDoc michael@0: * The document to use to create the Url element. michael@0: * @param aElement michael@0: * The element to which the created Url element is appended. michael@0: * michael@0: * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag michael@0: */ michael@0: _serializeToElement: function SRCH_EURL_serializeToEl(aDoc, aElement) { michael@0: var url = aDoc.createElementNS(OPENSEARCH_NS_11, "Url"); michael@0: url.setAttribute("type", this.type); michael@0: url.setAttribute("method", this.method); michael@0: url.setAttribute("template", this.template); michael@0: if (this.rels.length) michael@0: url.setAttribute("rel", this.rels.join(" ")); michael@0: if (this.resultDomain) michael@0: url.setAttribute("resultDomain", this.resultDomain); michael@0: michael@0: for (var i = 0; i < this.params.length; ++i) { michael@0: var param = aDoc.createElementNS(OPENSEARCH_NS_11, "Param"); michael@0: param.setAttribute("name", this.params[i].name); michael@0: param.setAttribute("value", this.params[i].value); michael@0: url.appendChild(aDoc.createTextNode("\n ")); michael@0: url.appendChild(param); michael@0: } michael@0: url.appendChild(aDoc.createTextNode("\n")); michael@0: aElement.appendChild(url); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * nsISearchEngine constructor. michael@0: * @param aLocation michael@0: * A nsILocalFile or nsIURI object representing the location of the michael@0: * search engine data file. michael@0: * @param aSourceDataType michael@0: * The data type of the file used to describe the engine. Must be either michael@0: * DATA_XML or DATA_TEXT. michael@0: * @param aIsReadOnly michael@0: * Boolean indicating whether the engine should be treated as read-only. michael@0: * Read only engines cannot be serialized to file. michael@0: */ michael@0: function Engine(aLocation, aSourceDataType, aIsReadOnly) { michael@0: this._dataType = aSourceDataType; michael@0: this._readOnly = aIsReadOnly; michael@0: this._urls = []; michael@0: michael@0: if (aLocation.type) { michael@0: if (aLocation.type == "filePath") michael@0: this._file = aLocation.value; michael@0: else if (aLocation.type == "uri") michael@0: this._uri = aLocation.value; michael@0: } else if (aLocation instanceof Ci.nsILocalFile) { michael@0: // we already have a file (e.g. loading engines from disk) michael@0: this._file = aLocation; michael@0: } else if (aLocation instanceof Ci.nsIURI) { michael@0: switch (aLocation.scheme) { michael@0: case "https": michael@0: case "http": michael@0: case "ftp": michael@0: case "data": michael@0: case "file": michael@0: case "resource": michael@0: case "chrome": michael@0: this._uri = aLocation; michael@0: break; michael@0: default: michael@0: ERROR("Invalid URI passed to the nsISearchEngine constructor", michael@0: Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: } else michael@0: ERROR("Engine location is neither a File nor a URI object", michael@0: Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: Engine.prototype = { michael@0: // The engine's alias (can be null). Initialized to |undefined| to indicate michael@0: // not-initialized-from-engineMetadataService. michael@0: _alias: undefined, michael@0: // A distribution-unique identifier for the engine. Either null or set michael@0: // when loaded. See getter. michael@0: _identifier: undefined, michael@0: // The data describing the engine. Is either an array of bytes, for Sherlock michael@0: // files, or an XML document element, for XML plugins. michael@0: _data: null, michael@0: // The engine's data type. See data types (DATA_) defined above. michael@0: _dataType: null, michael@0: // Whether or not the engine is readonly. michael@0: _readOnly: true, michael@0: // The engine's description michael@0: _description: "", michael@0: // Used to store the engine to replace, if we're an update to an existing michael@0: // engine. michael@0: _engineToUpdate: null, michael@0: // The file from which the plugin was loaded. michael@0: __file: null, michael@0: get _file() { michael@0: if (this.__file && !(this.__file instanceof Ci.nsILocalFile)) { michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); michael@0: file.persistentDescriptor = this.__file; michael@0: return this.__file = file; michael@0: } michael@0: return this.__file; michael@0: }, michael@0: set _file(aValue) { michael@0: this.__file = aValue; michael@0: }, michael@0: // Set to true if the engine has a preferred icon (an icon that should not be michael@0: // overridden by a non-preferred icon). michael@0: _hasPreferredIcon: null, michael@0: // Whether the engine is hidden from the user. michael@0: _hidden: null, michael@0: // The engine's name. michael@0: _name: null, michael@0: // The engine type. See engine types (TYPE_) defined above. michael@0: _type: null, michael@0: // The name of the charset used to submit the search terms. michael@0: _queryCharset: null, michael@0: // The engine's raw SearchForm value (URL string pointing to a search form). michael@0: __searchForm: null, michael@0: get _searchForm() { michael@0: return this.__searchForm; michael@0: }, michael@0: set _searchForm(aValue) { michael@0: if (/^https?:/i.test(aValue)) michael@0: this.__searchForm = aValue; michael@0: else michael@0: LOG("_searchForm: Invalid URL dropped for " + this._name || michael@0: "the current engine"); michael@0: }, michael@0: // The URI object from which the engine was retrieved. michael@0: // This is null for engines loaded from disk, but present for engines loaded michael@0: // from chrome:// URIs. michael@0: __uri: null, michael@0: get _uri() { michael@0: if (this.__uri && !(this.__uri instanceof Ci.nsIURI)) michael@0: this.__uri = makeURI(this.__uri); michael@0: michael@0: return this.__uri; michael@0: }, michael@0: set _uri(aValue) { michael@0: this.__uri = aValue; michael@0: }, michael@0: // Whether to obtain user confirmation before adding the engine. This is only michael@0: // used when the engine is first added to the list. michael@0: _confirm: false, michael@0: // Whether to set this as the current engine as soon as it is loaded. This michael@0: // is only used when the engine is first added to the list. michael@0: _useNow: false, michael@0: // A function to be invoked when this engine object's addition completes (or michael@0: // fails). Only used for installation via addEngine. michael@0: _installCallback: null, michael@0: // Where the engine was loaded from. Can be one of: SEARCH_APP_DIR, michael@0: // SEARCH_PROFILE_DIR, SEARCH_IN_EXTENSION. michael@0: __installLocation: null, michael@0: // The number of days between update checks for new versions michael@0: _updateInterval: null, michael@0: // The url to check at for a new update michael@0: _updateURL: null, michael@0: // The url to check for a new icon michael@0: _iconUpdateURL: null, michael@0: /* Deferred serialization task. */ michael@0: _lazySerializeTask: null, michael@0: michael@0: /** michael@0: * Retrieves the data from the engine's file. If the engine's dataType is michael@0: * XML, the document element is placed in the engine's data field. For text michael@0: * engines, the data is just read directly from file and placed as an array michael@0: * of lines in the engine's data field. michael@0: */ michael@0: _initFromFile: function SRCH_ENG_initFromFile() { michael@0: if (!this._file || !this._file.exists()) michael@0: FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: michael@0: fileInStream.init(this._file, MODE_RDONLY, PERMS_FILE, false); michael@0: michael@0: if (this._dataType == SEARCH_DATA_XML) { michael@0: var domParser = Cc["@mozilla.org/xmlextras/domparser;1"]. michael@0: createInstance(Ci.nsIDOMParser); michael@0: var doc = domParser.parseFromStream(fileInStream, "UTF-8", michael@0: this._file.fileSize, michael@0: "text/xml"); michael@0: michael@0: this._data = doc.documentElement; michael@0: } else { michael@0: ERROR("Unsuppored engine _dataType in _initFromFile: \"" + michael@0: this._dataType + "\"", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: } michael@0: fileInStream.close(); michael@0: michael@0: // Now that the data is loaded, initialize the engine object michael@0: this._initFromData(); michael@0: }, michael@0: michael@0: /** michael@0: * Retrieves the data from the engine's file asynchronously. If the engine's michael@0: * dataType is XML, the document element is placed in the engine's data field. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if initializing from michael@0: * data succeeds, rejected if it fails. michael@0: */ michael@0: _asyncInitFromFile: function SRCH_ENG__asyncInitFromFile() { michael@0: return TaskUtils.spawn(function() { michael@0: if (!this._file || !(yield OS.File.exists(this._file.path))) michael@0: FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: if (this._dataType == SEARCH_DATA_XML) { michael@0: let fileURI = NetUtil.ioService.newFileURI(this._file); michael@0: yield this._retrieveSearchXMLData(fileURI.spec); michael@0: } else { michael@0: ERROR("Unsuppored engine _dataType in _initFromFile: \"" + michael@0: this._dataType + "\"", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: } michael@0: michael@0: // Now that the data is loaded, initialize the engine object michael@0: this._initFromData(); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Retrieves the engine data from a URI. Initializes the engine, flushes to michael@0: * disk, and notifies the search service once initialization is complete. michael@0: */ michael@0: _initFromURIAndLoad: function SRCH_ENG_initFromURIAndLoad() { michael@0: ENSURE_WARN(this._uri instanceof Ci.nsIURI, michael@0: "Must have URI when calling _initFromURIAndLoad!", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: LOG("_initFromURIAndLoad: Downloading engine from: \"" + this._uri.spec + "\"."); michael@0: michael@0: var chan = NetUtil.ioService.newChannelFromURI(this._uri); michael@0: michael@0: if (this._engineToUpdate && (chan instanceof Ci.nsIHttpChannel)) { michael@0: var lastModified = engineMetadataService.getAttr(this._engineToUpdate, michael@0: "updatelastmodified"); michael@0: if (lastModified) michael@0: chan.setRequestHeader("If-Modified-Since", lastModified, false); michael@0: } michael@0: var listener = new loadListener(chan, this, this._onLoad); michael@0: chan.notificationCallbacks = listener; michael@0: chan.asyncOpen(listener, null); michael@0: }, michael@0: michael@0: /** michael@0: * Retrieves the engine data from a URI asynchronously and initializes it. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if retrieveing data michael@0: * succeeds. michael@0: */ michael@0: _asyncInitFromURI: function SRCH_ENG__asyncInitFromURI() { michael@0: return TaskUtils.spawn(function() { michael@0: LOG("_asyncInitFromURI: Loading engine from: \"" + this._uri.spec + "\"."); michael@0: yield this._retrieveSearchXMLData(this._uri.spec); michael@0: // Now that the data is loaded, initialize the engine object michael@0: this._initFromData(); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Retrieves the engine data for a given URI asynchronously. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if retrieveing data michael@0: * succeeds. michael@0: */ michael@0: _retrieveSearchXMLData: function SRCH_ENG__retrieveSearchXMLData(aURL) { michael@0: let deferred = Promise.defer(); michael@0: let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. michael@0: createInstance(Ci.nsIXMLHttpRequest); michael@0: request.overrideMimeType("text/xml"); michael@0: request.onload = (aEvent) => { michael@0: let responseXML = aEvent.target.responseXML; michael@0: this._data = responseXML.documentElement; michael@0: deferred.resolve(); michael@0: }; michael@0: request.onerror = function(aEvent) { michael@0: deferred.resolve(); michael@0: }; michael@0: request.open("GET", aURL, true); michael@0: request.send(); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _initFromURISync: function SRCH_ENG_initFromURISync() { michael@0: ENSURE_WARN(this._uri instanceof Ci.nsIURI, michael@0: "Must have URI when calling _initFromURISync!", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: ENSURE_WARN(this._uri.schemeIs("chrome"), "_initFromURISync called for non-chrome URI", michael@0: Cr.NS_ERROR_FAILURE); michael@0: michael@0: LOG("_initFromURISync: Loading engine from: \"" + this._uri.spec + "\"."); michael@0: michael@0: var chan = NetUtil.ioService.newChannelFromURI(this._uri); michael@0: michael@0: var stream = chan.open(); michael@0: var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. michael@0: createInstance(Ci.nsIDOMParser); michael@0: var doc = parser.parseFromStream(stream, "UTF-8", stream.available(), "text/xml"); michael@0: michael@0: this._data = doc.documentElement; michael@0: michael@0: // Now that the data is loaded, initialize the engine object michael@0: this._initFromData(); michael@0: }, michael@0: michael@0: /** michael@0: * Attempts to find an EngineURL object in the set of EngineURLs for michael@0: * this Engine that has the given type string. (This corresponds to the michael@0: * "type" attribute in the "Url" node in the OpenSearch spec.) michael@0: * This method will return the first matching URL object found, or null michael@0: * if no matching URL is found. michael@0: * michael@0: * @param aType string to match the EngineURL's type attribute michael@0: * @param aRel [optional] only return URLs that with this rel value michael@0: */ michael@0: _getURLOfType: function SRCH_ENG__getURLOfType(aType, aRel) { michael@0: for (var i = 0; i < this._urls.length; ++i) { michael@0: if (this._urls[i].type == aType && (!aRel || this._urls[i]._hasRelation(aRel))) michael@0: return this._urls[i]; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: _confirmAddEngine: function SRCH_SVC_confirmAddEngine() { michael@0: var stringBundle = Services.strings.createBundle(SEARCH_BUNDLE); michael@0: var titleMessage = stringBundle.GetStringFromName("addEngineConfirmTitle"); michael@0: michael@0: // Display only the hostname portion of the URL. michael@0: var dialogMessage = michael@0: stringBundle.formatStringFromName("addEngineConfirmation", michael@0: [this._name, this._uri.host], 2); michael@0: var checkboxMessage = null; michael@0: if (!getBoolPref(BROWSER_SEARCH_PREF + "noCurrentEngine", false)) michael@0: checkboxMessage = stringBundle.GetStringFromName("addEngineAsCurrentText"); michael@0: michael@0: var addButtonLabel = michael@0: stringBundle.GetStringFromName("addEngineAddButtonLabel"); michael@0: michael@0: var ps = Services.prompt; michael@0: var buttonFlags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) + michael@0: (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1) + michael@0: ps.BUTTON_POS_0_DEFAULT; michael@0: michael@0: var checked = {value: false}; michael@0: // confirmEx returns the index of the button that was pressed. Since "Add" michael@0: // is button 0, we want to return the negation of that value. michael@0: var confirm = !ps.confirmEx(null, michael@0: titleMessage, michael@0: dialogMessage, michael@0: buttonFlags, michael@0: addButtonLabel, michael@0: null, null, // button 1 & 2 names not used michael@0: checkboxMessage, michael@0: checked); michael@0: michael@0: return {confirmed: confirm, useNow: checked.value}; michael@0: }, michael@0: michael@0: /** michael@0: * Handle the successful download of an engine. Initializes the engine and michael@0: * triggers parsing of the data. The engine is then flushed to disk. Notifies michael@0: * the search service once initialization is complete. michael@0: */ michael@0: _onLoad: function SRCH_ENG_onLoad(aBytes, aEngine) { michael@0: /** michael@0: * Handle an error during the load of an engine by notifying the engine's michael@0: * error callback, if any. michael@0: */ michael@0: function onError(errorCode = Ci.nsISearchInstallCallback.ERROR_UNKNOWN_FAILURE) { michael@0: // Notify the callback of the failure michael@0: if (aEngine._installCallback) { michael@0: aEngine._installCallback(errorCode); michael@0: } michael@0: } michael@0: michael@0: function promptError(strings = {}, error = undefined) { michael@0: onError(error); michael@0: michael@0: if (aEngine._engineToUpdate) { michael@0: // We're in an update, so just fail quietly michael@0: LOG("updating " + aEngine._engineToUpdate.name + " failed"); michael@0: return; michael@0: } michael@0: var brandBundle = Services.strings.createBundle(BRAND_BUNDLE); michael@0: var brandName = brandBundle.GetStringFromName("brandShortName"); michael@0: michael@0: var searchBundle = Services.strings.createBundle(SEARCH_BUNDLE); michael@0: var msgStringName = strings.error || "error_loading_engine_msg2"; michael@0: var titleStringName = strings.title || "error_loading_engine_title"; michael@0: var title = searchBundle.GetStringFromName(titleStringName); michael@0: var text = searchBundle.formatStringFromName(msgStringName, michael@0: [brandName, aEngine._location], michael@0: 2); michael@0: michael@0: Services.ww.getNewPrompter(null).alert(title, text); michael@0: } michael@0: michael@0: if (!aBytes) { michael@0: promptError(); michael@0: return; michael@0: } michael@0: michael@0: var engineToUpdate = null; michael@0: if (aEngine._engineToUpdate) { michael@0: engineToUpdate = aEngine._engineToUpdate.wrappedJSObject; michael@0: michael@0: // Make this new engine use the old engine's file. michael@0: aEngine._file = engineToUpdate._file; michael@0: } michael@0: michael@0: switch (aEngine._dataType) { michael@0: case SEARCH_DATA_XML: michael@0: var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. michael@0: createInstance(Ci.nsIDOMParser); michael@0: var doc = parser.parseFromBuffer(aBytes, aBytes.length, "text/xml"); michael@0: aEngine._data = doc.documentElement; michael@0: break; michael@0: case SEARCH_DATA_TEXT: michael@0: aEngine._data = aBytes; michael@0: break; michael@0: default: michael@0: promptError(); michael@0: LOG("_onLoad: Bogus engine _dataType: \"" + this._dataType + "\""); michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: // Initialize the engine from the obtained data michael@0: aEngine._initFromData(); michael@0: } catch (ex) { michael@0: LOG("_onLoad: Failed to init engine!\n" + ex); michael@0: // Report an error to the user michael@0: promptError(); michael@0: return; michael@0: } michael@0: michael@0: // Check that when adding a new engine (e.g., not updating an michael@0: // existing one), a duplicate engine does not already exist. michael@0: if (!engineToUpdate) { michael@0: if (Services.search.getEngineByName(aEngine.name)) { michael@0: // If we're confirming the engine load, then display a "this is a michael@0: // duplicate engine" prompt; otherwise, fail silently. michael@0: if (aEngine._confirm) { michael@0: promptError({ error: "error_duplicate_engine_msg", michael@0: title: "error_invalid_engine_title" michael@0: }, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE); michael@0: } else { michael@0: onError(Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE); michael@0: } michael@0: LOG("_onLoad: duplicate engine found, bailing"); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // If requested, confirm the addition now that we have the title. michael@0: // This property is only ever true for engines added via michael@0: // nsIBrowserSearchService::addEngine. michael@0: if (aEngine._confirm) { michael@0: var confirmation = aEngine._confirmAddEngine(); michael@0: LOG("_onLoad: confirm is " + confirmation.confirmed + michael@0: "; useNow is " + confirmation.useNow); michael@0: if (!confirmation.confirmed) { michael@0: onError(); michael@0: return; michael@0: } michael@0: aEngine._useNow = confirmation.useNow; michael@0: } michael@0: michael@0: // If we don't yet have a file, get one now. The only case where we would michael@0: // already have a file is if this is an update and _file was set above. michael@0: if (!aEngine._file) michael@0: aEngine._file = getSanitizedFile(aEngine.name); michael@0: michael@0: if (engineToUpdate) { michael@0: // Keep track of the last modified date, so that we can make conditional michael@0: // requests for future updates. michael@0: engineMetadataService.setAttr(aEngine, "updatelastmodified", michael@0: (new Date()).toUTCString()); michael@0: michael@0: // If we're updating an app-shipped engine, ensure that the updateURLs michael@0: // are the same. michael@0: if (engineToUpdate._isInAppDir) { michael@0: let oldUpdateURL = engineToUpdate._updateURL; michael@0: let newUpdateURL = aEngine._updateURL; michael@0: let oldSelfURL = engineToUpdate._getURLOfType(URLTYPE_OPENSEARCH, "self"); michael@0: if (oldSelfURL) { michael@0: oldUpdateURL = oldSelfURL.template; michael@0: let newSelfURL = aEngine._getURLOfType(URLTYPE_OPENSEARCH, "self"); michael@0: if (!newSelfURL) { michael@0: LOG("_onLoad: updateURL missing in updated engine for " + michael@0: aEngine.name + " aborted"); michael@0: onError(); michael@0: return; michael@0: } michael@0: newUpdateURL = newSelfURL.template; michael@0: } michael@0: michael@0: if (oldUpdateURL != newUpdateURL) { michael@0: LOG("_onLoad: updateURLs do not match! Update of " + aEngine.name + " aborted"); michael@0: onError(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Set the new engine's icon, if it doesn't yet have one. michael@0: if (!aEngine._iconURI && engineToUpdate._iconURI) michael@0: aEngine._iconURI = engineToUpdate._iconURI; michael@0: } michael@0: michael@0: // Write the engine to file. For readOnly engines, they'll be stored in the michael@0: // cache following the notification below. michael@0: if (!aEngine._readOnly) michael@0: aEngine._serializeToFile(); michael@0: michael@0: // Notify the search service of the successful load. It will deal with michael@0: // updates by checking aEngine._engineToUpdate. michael@0: notifyAction(aEngine, SEARCH_ENGINE_LOADED); michael@0: michael@0: // Notify the callback if needed michael@0: if (aEngine._installCallback) { michael@0: aEngine._installCallback(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates a key by serializing an object that contains the icon's width michael@0: * and height. michael@0: * michael@0: * @param aWidth michael@0: * Width of the icon. michael@0: * @param aHeight michael@0: * Height of the icon. michael@0: * @returns key string michael@0: */ michael@0: _getIconKey: function SRCH_ENG_getIconKey(aWidth, aHeight) { michael@0: let keyObj = { michael@0: width: aWidth, michael@0: height: aHeight michael@0: }; michael@0: michael@0: return JSON.stringify(keyObj); michael@0: }, michael@0: michael@0: /** michael@0: * Add an icon to the icon map used by getIconURIBySize() and getIcons(). michael@0: * michael@0: * @param aWidth michael@0: * Width of the icon. michael@0: * @param aHeight michael@0: * Height of the icon. michael@0: * @param aURISpec michael@0: * String with the icon's URI. michael@0: */ michael@0: _addIconToMap: function SRCH_ENG_addIconToMap(aWidth, aHeight, aURISpec) { michael@0: // Use an object instead of a Map() because it needs to be serializable. michael@0: this._iconMapObj = this._iconMapObj || {}; michael@0: let key = this._getIconKey(aWidth, aHeight); michael@0: this._iconMapObj[key] = aURISpec; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the .iconURI property of the engine. If both aWidth and aHeight are michael@0: * provided an entry will be added to _iconMapObj that will enable accessing michael@0: * icon's data through getIcons() and getIconURIBySize() APIs. michael@0: * michael@0: * @param aIconURL michael@0: * A URI string pointing to the engine's icon. Must have a http[s], michael@0: * ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be michael@0: * downloaded and converted to data URIs for storage in the engine michael@0: * XML files, if the engine is not readonly. michael@0: * @param aIsPreferred michael@0: * Whether or not this icon is to be preferred. Preferred icons can michael@0: * override non-preferred icons. michael@0: * @param aWidth (optional) michael@0: * Width of the icon. michael@0: * @param aHeight (optional) michael@0: * Height of the icon. michael@0: */ michael@0: _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred, aWidth, aHeight) { michael@0: var uri = makeURI(aIconURL); michael@0: michael@0: // Ignore bad URIs michael@0: if (!uri) michael@0: return; michael@0: michael@0: LOG("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \"" michael@0: + this.name + "\"."); michael@0: // Only accept remote icons from http[s] or ftp michael@0: switch (uri.scheme) { michael@0: case "data": michael@0: if (!this._hasPreferredIcon || aIsPreferred) { michael@0: this._iconURI = uri; michael@0: notifyAction(this, SEARCH_ENGINE_CHANGED); michael@0: this._hasPreferredIcon = aIsPreferred; michael@0: } michael@0: michael@0: if (aWidth && aHeight) { michael@0: this._addIconToMap(aWidth, aHeight, aIconURL) michael@0: } michael@0: break; michael@0: case "http": michael@0: case "https": michael@0: case "ftp": michael@0: // No use downloading the icon if the engine file is read-only michael@0: if (!this._readOnly || michael@0: getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) { michael@0: LOG("_setIcon: Downloading icon: \"" + uri.spec + michael@0: "\" for engine: \"" + this.name + "\""); michael@0: var chan = NetUtil.ioService.newChannelFromURI(uri); michael@0: michael@0: function iconLoadCallback(aByteArray, aEngine) { michael@0: // This callback may run after we've already set a preferred icon, michael@0: // so check again. michael@0: if (aEngine._hasPreferredIcon && !aIsPreferred) michael@0: return; michael@0: michael@0: if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) { michael@0: LOG("iconLoadCallback: load failed, or the icon was too large!"); michael@0: return; michael@0: } michael@0: michael@0: var str = btoa(String.fromCharCode.apply(null, aByteArray)); michael@0: let dataURL = ICON_DATAURL_PREFIX + str; michael@0: aEngine._iconURI = makeURI(dataURL); michael@0: michael@0: if (aWidth && aHeight) { michael@0: aEngine._addIconToMap(aWidth, aHeight, dataURL) michael@0: } michael@0: michael@0: // The engine might not have a file yet, if it's being downloaded, michael@0: // because the request for the engine file itself (_onLoad) may not michael@0: // yet be complete. In that case, this change will be written to michael@0: // file when _onLoad is called. For readonly engines, we'll store michael@0: // the changes in the cache once notified below. michael@0: if (aEngine._file && !aEngine._readOnly) michael@0: aEngine._serializeToFile(); michael@0: michael@0: notifyAction(aEngine, SEARCH_ENGINE_CHANGED); michael@0: aEngine._hasPreferredIcon = aIsPreferred; michael@0: } michael@0: michael@0: // If we're currently acting as an "update engine", then the callback michael@0: // should set the icon on the engine we're updating and not us, since michael@0: // |this| might be gone by the time the callback runs. michael@0: var engineToSet = this._engineToUpdate || this; michael@0: michael@0: var listener = new loadListener(chan, engineToSet, iconLoadCallback); michael@0: chan.notificationCallbacks = listener; michael@0: chan.asyncOpen(listener, null); michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Initialize this Engine object from the collected data. michael@0: */ michael@0: _initFromData: function SRCH_ENG_initFromData() { michael@0: ENSURE_WARN(this._data, "Can't init an engine with no data!", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: // Find out what type of engine we are michael@0: switch (this._dataType) { michael@0: case SEARCH_DATA_XML: michael@0: if (checkNameSpace(this._data, [MOZSEARCH_LOCALNAME], michael@0: [MOZSEARCH_NS_10])) { michael@0: michael@0: LOG("_init: Initing MozSearch plugin from " + this._location); michael@0: michael@0: this._type = SEARCH_TYPE_MOZSEARCH; michael@0: this._parseAsMozSearch(); michael@0: michael@0: } else if (checkNameSpace(this._data, [OPENSEARCH_LOCALNAME], michael@0: OPENSEARCH_NAMESPACES)) { michael@0: michael@0: LOG("_init: Initing OpenSearch plugin from " + this._location); michael@0: michael@0: this._type = SEARCH_TYPE_OPENSEARCH; michael@0: this._parseAsOpenSearch(); michael@0: michael@0: } else michael@0: FAIL(this._location + " is not a valid search plugin.", Cr.NS_ERROR_FAILURE); michael@0: michael@0: break; michael@0: case SEARCH_DATA_TEXT: michael@0: LOG("_init: Initing Sherlock plugin from " + this._location); michael@0: michael@0: // the only text-based format we support is Sherlock michael@0: this._type = SEARCH_TYPE_SHERLOCK; michael@0: this._parseAsSherlock(); michael@0: } michael@0: michael@0: // No need to keep a ref to our data (which in some cases can be a document michael@0: // element) past this point michael@0: this._data = null; michael@0: }, michael@0: michael@0: /** michael@0: * Initialize this Engine object from a collection of metadata. michael@0: */ michael@0: _initFromMetadata: function SRCH_ENG_initMetaData(aName, aIconURL, aAlias, michael@0: aDescription, aMethod, michael@0: aTemplate) { michael@0: ENSURE_WARN(!this._readOnly, michael@0: "Can't call _initFromMetaData on a readonly engine!", michael@0: Cr.NS_ERROR_FAILURE); michael@0: michael@0: this._urls.push(new EngineURL(URLTYPE_SEARCH_HTML, aMethod, aTemplate)); michael@0: michael@0: this._name = aName; michael@0: this.alias = aAlias; michael@0: this._description = aDescription; michael@0: this._setIcon(aIconURL, true); michael@0: michael@0: this._serializeToFile(); michael@0: }, michael@0: michael@0: /** michael@0: * Extracts data from an OpenSearch URL element and creates an EngineURL michael@0: * object which is then added to the engine's list of URLs. michael@0: * michael@0: * @throws NS_ERROR_FAILURE if a URL object could not be created. michael@0: * michael@0: * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag. michael@0: * @see EngineURL() michael@0: */ michael@0: _parseURL: function SRCH_ENG_parseURL(aElement) { michael@0: var type = aElement.getAttribute("type"); michael@0: // According to the spec, method is optional, defaulting to "GET" if not michael@0: // specified michael@0: var method = aElement.getAttribute("method") || "GET"; michael@0: var template = aElement.getAttribute("template"); michael@0: var resultDomain = aElement.getAttribute("resultdomain"); michael@0: michael@0: try { michael@0: var url = new EngineURL(type, method, template, resultDomain); michael@0: } catch (ex) { michael@0: FAIL("_parseURL: failed to add " + template + " as a URL", michael@0: Cr.NS_ERROR_FAILURE); michael@0: } michael@0: michael@0: if (aElement.hasAttribute("rel")) michael@0: url.rels = aElement.getAttribute("rel").toLowerCase().split(/\s+/); michael@0: michael@0: for (var i = 0; i < aElement.childNodes.length; ++i) { michael@0: var param = aElement.childNodes[i]; michael@0: if (param.localName == "Param") { michael@0: try { michael@0: url.addParam(param.getAttribute("name"), param.getAttribute("value")); michael@0: } catch (ex) { michael@0: // Ignore failure michael@0: LOG("_parseURL: Url element has an invalid param"); michael@0: } michael@0: } else if (param.localName == "MozParam" && michael@0: // We only support MozParams for default search engines michael@0: this._isDefault) { michael@0: var value; michael@0: let condition = param.getAttribute("condition"); michael@0: michael@0: // MozParams must have a condition to be valid michael@0: if (!condition) { michael@0: let engineLoc = this._location; michael@0: let paramName = param.getAttribute("name"); michael@0: LOG("_parseURL: MozParam (" + paramName + ") without a condition attribute found parsing engine: " + engineLoc); michael@0: continue; michael@0: } michael@0: michael@0: switch (condition) { michael@0: case "purpose": michael@0: url.addParam(param.getAttribute("name"), michael@0: param.getAttribute("value"), michael@0: param.getAttribute("purpose")); michael@0: // _addMozParam is not needed here since it can be serialized fine without. _addMozParam michael@0: // also requires a unique "name" which is not normally the case when @purpose is used. michael@0: break; michael@0: case "defaultEngine": michael@0: // If this engine was the default search engine, use the true value michael@0: if (this._isDefaultEngine()) michael@0: value = param.getAttribute("trueValue"); michael@0: else michael@0: value = param.getAttribute("falseValue"); michael@0: url.addParam(param.getAttribute("name"), value); michael@0: url._addMozParam({"name": param.getAttribute("name"), michael@0: "falseValue": param.getAttribute("falseValue"), michael@0: "trueValue": param.getAttribute("trueValue"), michael@0: "condition": "defaultEngine"}); michael@0: break; michael@0: michael@0: case "pref": michael@0: try { michael@0: value = getMozParamPref(param.getAttribute("pref"), value); michael@0: url.addParam(param.getAttribute("name"), value); michael@0: url._addMozParam({"pref": param.getAttribute("pref"), michael@0: "name": param.getAttribute("name"), michael@0: "condition": "pref"}); michael@0: } catch (e) { } michael@0: break; michael@0: default: michael@0: if (condition && condition.startsWith("top")) { michael@0: url.addParam(param.getAttribute("name"), param.getAttribute("falseValue")); michael@0: let mozparam = {"name": param.getAttribute("name"), michael@0: "falseValue": param.getAttribute("falseValue"), michael@0: "trueValue": param.getAttribute("trueValue"), michael@0: "condition": condition, michael@0: "positionDependent": true}; michael@0: url._addMozParam(mozparam); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: this._urls.push(url); michael@0: }, michael@0: michael@0: _isDefaultEngine: function SRCH_ENG__isDefaultEngine() { michael@0: let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF); michael@0: let nsIPLS = Ci.nsIPrefLocalizedString; michael@0: let defaultEngine; michael@0: try { michael@0: defaultEngine = defaultPrefB.getComplexValue("defaultenginename", nsIPLS).data; michael@0: } catch (ex) {} michael@0: return this.name == defaultEngine; michael@0: }, michael@0: michael@0: /** michael@0: * Get the icon from an OpenSearch Image element. michael@0: * @see http://opensearch.a9.com/spec/1.1/description/#image michael@0: */ michael@0: _parseImage: function SRCH_ENG_parseImage(aElement) { michael@0: LOG("_parseImage: Image textContent: \"" + limitURILength(aElement.textContent) + "\""); michael@0: michael@0: let width = parseInt(aElement.getAttribute("width"), 10); michael@0: let height = parseInt(aElement.getAttribute("height"), 10); michael@0: let isPrefered = width == 16 && height == 16; michael@0: michael@0: if (isNaN(width) || isNaN(height) || width <= 0 || height <=0) { michael@0: LOG("OpenSearch image element must have positive width and height."); michael@0: return; michael@0: } michael@0: michael@0: this._setIcon(aElement.textContent, isPrefered, width, height); michael@0: }, michael@0: michael@0: _parseAsMozSearch: function SRCH_ENG_parseAsMoz() { michael@0: //forward to the OpenSearch parser michael@0: this._parseAsOpenSearch(); michael@0: }, michael@0: michael@0: /** michael@0: * Extract search engine information from the collected data to initialize michael@0: * the engine object. michael@0: */ michael@0: _parseAsOpenSearch: function SRCH_ENG_parseAsOS() { michael@0: var doc = this._data; michael@0: michael@0: // The OpenSearch spec sets a default value for the input encoding. michael@0: this._queryCharset = OS_PARAM_INPUT_ENCODING_DEF; michael@0: michael@0: for (var i = 0; i < doc.childNodes.length; ++i) { michael@0: var child = doc.childNodes[i]; michael@0: switch (child.localName) { michael@0: case "ShortName": michael@0: this._name = child.textContent; michael@0: break; michael@0: case "Description": michael@0: this._description = child.textContent; michael@0: break; michael@0: case "Url": michael@0: try { michael@0: this._parseURL(child); michael@0: } catch (ex) { michael@0: // Parsing of the element failed, just skip it. michael@0: LOG("_parseAsOpenSearch: failed to parse URL child: " + ex); michael@0: } michael@0: break; michael@0: case "Image": michael@0: this._parseImage(child); michael@0: break; michael@0: case "InputEncoding": michael@0: this._queryCharset = child.textContent.toUpperCase(); michael@0: break; michael@0: michael@0: // Non-OpenSearch elements michael@0: case "SearchForm": michael@0: this._searchForm = child.textContent; michael@0: break; michael@0: case "UpdateUrl": michael@0: this._updateURL = child.textContent; michael@0: break; michael@0: case "UpdateInterval": michael@0: this._updateInterval = parseInt(child.textContent); michael@0: break; michael@0: case "IconUpdateUrl": michael@0: this._iconUpdateURL = child.textContent; michael@0: break; michael@0: } michael@0: } michael@0: if (!this.name || (this._urls.length == 0)) michael@0: FAIL("_parseAsOpenSearch: No name, or missing URL!", Cr.NS_ERROR_FAILURE); michael@0: if (!this.supportsResponseType(URLTYPE_SEARCH_HTML)) michael@0: FAIL("_parseAsOpenSearch: No text/html result type!", Cr.NS_ERROR_FAILURE); michael@0: }, michael@0: michael@0: /** michael@0: * Extract search engine information from the collected data to initialize michael@0: * the engine object. michael@0: */ michael@0: _parseAsSherlock: function SRCH_ENG_parseAsSherlock() { michael@0: /** michael@0: * Extracts one Sherlock "section" from aSource. A section is essentially michael@0: * an HTML element with attributes, but each attribute must be on a new michael@0: * line, by definition. michael@0: * michael@0: * @param aLines michael@0: * An array of lines from the sherlock file. michael@0: * @param aSection michael@0: * The name of the section (e.g. "search" or "browser"). This value michael@0: * is not case sensitive. michael@0: * @returns an object whose properties correspond to the section's michael@0: * attributes. michael@0: */ michael@0: function getSection(aLines, aSection) { michael@0: LOG("_parseAsSherlock::getSection: Sherlock lines:\n" + michael@0: aLines.join("\n")); michael@0: var lines = aLines; michael@0: var startMark = new RegExp("^\\s*<" + aSection.toLowerCase() + "\\s*", michael@0: "gi"); michael@0: var endMark = /\s*>\s*$/gi; michael@0: michael@0: var foundStart = false; michael@0: var startLine, numberOfLines; michael@0: // Find the beginning and end of the section michael@0: for (var i = 0; i < lines.length; i++) { michael@0: if (foundStart) { michael@0: if (endMark.test(lines[i])) { michael@0: numberOfLines = i - startLine; michael@0: // Remove the end marker michael@0: lines[i] = lines[i].replace(endMark, ""); michael@0: // If the endmarker was not the only thing on the line, include michael@0: // this line in the results michael@0: if (lines[i]) michael@0: numberOfLines++; michael@0: break; michael@0: } michael@0: } else { michael@0: if (startMark.test(lines[i])) { michael@0: foundStart = true; michael@0: // Remove the start marker michael@0: lines[i] = lines[i].replace(startMark, ""); michael@0: startLine = i; michael@0: // If the line is empty, don't include it in the result michael@0: if (!lines[i]) michael@0: startLine++; michael@0: } michael@0: } michael@0: } michael@0: LOG("_parseAsSherlock::getSection: Start index: " + startLine + michael@0: "\nNumber of lines: " + numberOfLines); michael@0: lines = lines.splice(startLine, numberOfLines); michael@0: LOG("_parseAsSherlock::getSection: Section lines:\n" + michael@0: lines.join("\n")); michael@0: michael@0: var section = {}; michael@0: for (var i = 0; i < lines.length; i++) { michael@0: var line = lines[i].trim(); michael@0: michael@0: var els = line.split("="); michael@0: var name = els.shift().trim().toLowerCase(); michael@0: var value = els.join("=").trim(); michael@0: michael@0: if (!name || !value) michael@0: continue; michael@0: michael@0: // Strip leading and trailing whitespace, remove quotes from the michael@0: // value, and remove any trailing slashes or ">" characters michael@0: value = value.replace(/^["']/, "") michael@0: .replace(/["']\s*[\\\/]?>?\s*$/, "") || ""; michael@0: value = value.trim(); michael@0: michael@0: // Don't clobber existing attributes michael@0: if (!(name in section)) michael@0: section[name] = value; michael@0: } michael@0: return section; michael@0: } michael@0: michael@0: /** michael@0: * Returns an array of name-value pair arrays representing the Sherlock michael@0: * file's input elements. User defined inputs return USER_DEFINED michael@0: * as the value. Elements are returned in the order they appear in the michael@0: * source file. michael@0: * michael@0: * Example: michael@0: * michael@0: * michael@0: * Returns: michael@0: * [["foo", "bar"], ["foopy", "{searchTerms}"]] michael@0: * michael@0: * @param aLines michael@0: * An array of lines from the source file. michael@0: */ michael@0: function getInputs(aLines) { michael@0: michael@0: /** michael@0: * Extracts an attribute value from a given a line of text. michael@0: * Example: michael@0: * Extracts the string |foo| or |bar| given an input aAttr of michael@0: * |value| or |name|. michael@0: * Attributes may be quoted or unquoted. If unquoted, any whitespace michael@0: * indicates the end of the attribute value. michael@0: * Example: < value=22 33 name=44\334 > michael@0: * Returns |22| for "value" and |44\334| for "name". michael@0: * michael@0: * @param aAttr michael@0: * The name of the attribute for which to obtain the value. This michael@0: * value is not case sensitive. michael@0: * @param aLine michael@0: * The line containing the attribute. michael@0: * michael@0: * @returns the attribute value, or an empty string if the attribute michael@0: * doesn't exist. michael@0: */ michael@0: function getAttr(aAttr, aLine) { michael@0: // Used to determine whether an "input" line from a Sherlock file is a michael@0: // "user defined" input. michael@0: const userInput = /(\s|["'=])user(\s|[>="'\/\\+]|$)/i; michael@0: michael@0: LOG("_parseAsSherlock::getAttr: Getting attr: \"" + michael@0: aAttr + "\" for line: \"" + aLine + "\""); michael@0: // We're not case sensitive, but we want to return the attribute value michael@0: // in its original case, so create a copy of the source michael@0: var lLine = aLine.toLowerCase(); michael@0: var attr = aAttr.toLowerCase(); michael@0: michael@0: var attrStart = lLine.search(new RegExp("\\s" + attr, "i")); michael@0: if (attrStart == -1) { michael@0: michael@0: // If this is the "user defined input" (i.e. contains the empty michael@0: // "user" attribute), return our special keyword michael@0: if (userInput.test(lLine) && attr == "value") { michael@0: LOG("_parseAsSherlock::getAttr: Found user input!\nLine:\"" + lLine michael@0: + "\""); michael@0: return USER_DEFINED; michael@0: } michael@0: // The attribute doesn't exist - ignore michael@0: LOG("_parseAsSherlock::getAttr: Failed to find attribute:\nLine:\"" michael@0: + lLine + "\"\nAttr:\"" + attr + "\""); michael@0: return ""; michael@0: } michael@0: michael@0: var valueStart = lLine.indexOf("=", attrStart) + "=".length; michael@0: if (valueStart == -1) michael@0: return ""; michael@0: michael@0: var quoteStart = lLine.indexOf("\"", valueStart); michael@0: if (quoteStart == -1) { michael@0: michael@0: // Unquoted attribute, get the rest of the line, trimmed at the first michael@0: // sign of whitespace. If the rest of the line is only whitespace, michael@0: // returns a blank string. michael@0: return lLine.substr(valueStart).replace(/\s.*$/, ""); michael@0: michael@0: } else { michael@0: // Make sure that there's only whitespace between the start of the michael@0: // value and the first quote. If there is, end the attribute value at michael@0: // the first sign of whitespace. This prevents us from falling into michael@0: // the next attribute if this is an unquoted attribute followed by a michael@0: // quoted attribute. michael@0: var betweenEqualAndQuote = lLine.substring(valueStart, quoteStart); michael@0: if (/\S/.test(betweenEqualAndQuote)) michael@0: return lLine.substr(valueStart).replace(/\s.*$/, ""); michael@0: michael@0: // Adjust the start index to account for the opening quote michael@0: valueStart = quoteStart + "\"".length; michael@0: // Find the closing quote michael@0: var valueEnd = lLine.indexOf("\"", valueStart); michael@0: // If there is no closing quote, just go to the end of the line michael@0: if (valueEnd == -1) michael@0: valueEnd = aLine.length; michael@0: } michael@0: return aLine.substring(valueStart, valueEnd); michael@0: } michael@0: michael@0: var inputs = []; michael@0: michael@0: LOG("_parseAsSherlock::getInputs: Lines:\n" + aLines); michael@0: // Filter out everything but non-inputs michael@0: let lines = aLines.filter(function (line) { michael@0: return /^\s*") michael@0: line = line.trim().replace(/^$/, ""); michael@0: michael@0: // If this is one of the "directional" inputs (/) michael@0: const directionalInput = /^(prev|next)/i; michael@0: if (directionalInput.test(line)) { michael@0: michael@0: // Make it look like a normal input by removing "prev" or "next" michael@0: line = line.replace(directionalInput, ""); michael@0: michael@0: // If it has a name, give it a dummy value to match previous michael@0: // nsInternetSearchService behavior michael@0: if (/name\s*=/i.test(line)) { michael@0: line += " value=\"0\""; michael@0: } else michael@0: return; // Line has no name, skip it michael@0: } michael@0: michael@0: var attrName = getAttr("name", line); michael@0: var attrValue = getAttr("value", line); michael@0: LOG("_parseAsSherlock::getInputs: Got input:\nName:\"" + attrName + michael@0: "\"\nValue:\"" + attrValue + "\""); michael@0: if (attrValue) michael@0: inputs.push([attrName, attrValue]); michael@0: }); michael@0: return inputs; michael@0: } michael@0: michael@0: function err(aErr) { michael@0: FAIL("_parseAsSherlock::err: Sherlock param error:\n" + aErr, michael@0: Cr.NS_ERROR_FAILURE); michael@0: } michael@0: michael@0: // First try converting our byte array using the default Sherlock encoding. michael@0: // If this fails, or if we find a sourceTextEncoding attribute, we need to michael@0: // reconvert the byte array using the specified encoding. michael@0: var sherlockLines, searchSection, sourceTextEncoding, browserSection; michael@0: try { michael@0: sherlockLines = sherlockBytesToLines(this._data); michael@0: searchSection = getSection(sherlockLines, "search"); michael@0: browserSection = getSection(sherlockLines, "browser"); michael@0: sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]); michael@0: if (sourceTextEncoding) { michael@0: // Re-convert the bytes using the found sourceTextEncoding michael@0: sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding); michael@0: searchSection = getSection(sherlockLines, "search"); michael@0: browserSection = getSection(sherlockLines, "browser"); michael@0: } michael@0: } catch (ex) { michael@0: // The conversion using the default charset failed. Remove any non-ascii michael@0: // bytes and try to find a sourceTextEncoding. michael@0: var asciiBytes = this._data.filter(function (n) {return !(0x80 & n);}); michael@0: var asciiString = String.fromCharCode.apply(null, asciiBytes); michael@0: sherlockLines = asciiString.split(NEW_LINES).filter(isUsefulLine); michael@0: searchSection = getSection(sherlockLines, "search"); michael@0: sourceTextEncoding = parseInt(searchSection["sourcetextencoding"]); michael@0: if (sourceTextEncoding) { michael@0: sherlockLines = sherlockBytesToLines(this._data, sourceTextEncoding); michael@0: searchSection = getSection(sherlockLines, "search"); michael@0: browserSection = getSection(sherlockLines, "browser"); michael@0: } else michael@0: ERROR("Couldn't find a working charset", Cr.NS_ERROR_FAILURE); michael@0: } michael@0: michael@0: LOG("_parseAsSherlock: Search section:\n" + searchSection.toSource()); michael@0: michael@0: this._name = searchSection["name"] || err("Missing name!"); michael@0: this._description = searchSection["description"] || ""; michael@0: this._queryCharset = searchSection["querycharset"] || michael@0: queryCharsetFromCode(searchSection["queryencoding"]); michael@0: this._searchForm = searchSection["searchform"]; michael@0: michael@0: this._updateInterval = parseInt(browserSection["updatecheckdays"]); michael@0: michael@0: this._updateURL = browserSection["update"]; michael@0: this._iconUpdateURL = browserSection["updateicon"]; michael@0: michael@0: var method = (searchSection["method"] || "GET").toUpperCase(); michael@0: var template = searchSection["action"] || err("Missing action!"); michael@0: michael@0: var inputs = getInputs(sherlockLines); michael@0: LOG("_parseAsSherlock: Inputs:\n" + inputs.toSource()); michael@0: michael@0: var url = null; michael@0: michael@0: if (method == "GET") { michael@0: // Here's how we construct the input string: michael@0: // is first: Name Attr: Prefix Data Example: michael@0: // YES EMPTY None TEMPLATE michael@0: // YES NON-EMPTY ? = TEMPLATE?= michael@0: // NO EMPTY ------------- -------------- michael@0: // NO NON-EMPTY & = TEMPLATE?=&= michael@0: for (var i = 0; i < inputs.length; i++) { michael@0: var name = inputs[i][0]; michael@0: var value = inputs[i][1]; michael@0: if (i==0) { michael@0: if (name == "") michael@0: template += USER_DEFINED; michael@0: else michael@0: template += "?" + name + "=" + value; michael@0: } else if (name != "") michael@0: template += "&" + name + "=" + value; michael@0: } michael@0: url = new EngineURL(URLTYPE_SEARCH_HTML, method, template); michael@0: michael@0: } else if (method == "POST") { michael@0: // Create the URL object and just add the parameters directly michael@0: url = new EngineURL(URLTYPE_SEARCH_HTML, method, template); michael@0: for (var i = 0; i < inputs.length; i++) { michael@0: var name = inputs[i][0]; michael@0: var value = inputs[i][1]; michael@0: if (name) michael@0: url.addParam(name, value); michael@0: } michael@0: } else michael@0: err("Invalid method!"); michael@0: michael@0: this._urls.push(url); michael@0: }, michael@0: michael@0: /** michael@0: * Init from a JSON record. michael@0: **/ michael@0: _initWithJSON: function SRCH_ENG__initWithJSON(aJson) { michael@0: this.__id = aJson._id; michael@0: this._name = aJson._name; michael@0: this._description = aJson.description; michael@0: if (aJson._hasPreferredIcon == undefined) michael@0: this._hasPreferredIcon = true; michael@0: else michael@0: this._hasPreferredIcon = false; michael@0: this._hidden = aJson._hidden; michael@0: this._type = aJson.type || SEARCH_TYPE_MOZSEARCH; michael@0: this._queryCharset = aJson.queryCharset || DEFAULT_QUERY_CHARSET; michael@0: this.__searchForm = aJson.__searchForm; michael@0: this.__installLocation = aJson._installLocation || SEARCH_APP_DIR; michael@0: this._updateInterval = aJson._updateInterval || null; michael@0: this._updateURL = aJson._updateURL || null; michael@0: this._iconUpdateURL = aJson._iconUpdateURL || null; michael@0: if (aJson._readOnly == undefined) michael@0: this._readOnly = true; michael@0: else michael@0: this._readOnly = false; michael@0: this._iconURI = makeURI(aJson._iconURL); michael@0: this._iconMapObj = aJson._iconMapObj; michael@0: for (let i = 0; i < aJson._urls.length; ++i) { michael@0: let url = aJson._urls[i]; michael@0: let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML, michael@0: url.method || "GET", url.template, michael@0: url.resultDomain); michael@0: engineURL._initWithJSON(url, this); michael@0: this._urls.push(engineURL); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates a JavaScript object that represents this engine. michael@0: * @param aFilter michael@0: * Whether or not to filter out common default values. Recommended for michael@0: * use with _initWithJSON(). michael@0: * @returns An object suitable for serialization as JSON. michael@0: **/ michael@0: _serializeToJSON: function SRCH_ENG__serializeToJSON(aFilter) { michael@0: var json = { michael@0: _id: this._id, michael@0: _name: this._name, michael@0: _hidden: this.hidden, michael@0: description: this.description, michael@0: __searchForm: this.__searchForm, michael@0: _iconURL: this._iconURL, michael@0: _iconMapObj: this._iconMapObj, michael@0: _urls: [url._serializeToJSON() for each(url in this._urls)] michael@0: }; michael@0: michael@0: if (this._file instanceof Ci.nsILocalFile) michael@0: json.filePath = this._file.persistentDescriptor; michael@0: if (this._uri) michael@0: json._url = this._uri.spec; michael@0: if (this._installLocation != SEARCH_APP_DIR || !aFilter) michael@0: json._installLocation = this._installLocation; michael@0: if (this._updateInterval || !aFilter) michael@0: json._updateInterval = this._updateInterval; michael@0: if (this._updateURL || !aFilter) michael@0: json._updateURL = this._updateURL; michael@0: if (this._iconUpdateURL || !aFilter) michael@0: json._iconUpdateURL = this._iconUpdateURL; michael@0: if (!this._hasPreferredIcon || !aFilter) michael@0: json._hasPreferredIcon = this._hasPreferredIcon; michael@0: if (this.type != SEARCH_TYPE_MOZSEARCH || !aFilter) michael@0: json.type = this.type; michael@0: if (this.queryCharset != DEFAULT_QUERY_CHARSET || !aFilter) michael@0: json.queryCharset = this.queryCharset; michael@0: if (this._dataType != SEARCH_DATA_XML || !aFilter) michael@0: json._dataType = this._dataType; michael@0: if (!this._readOnly || !aFilter) michael@0: json._readOnly = this._readOnly; michael@0: michael@0: return json; michael@0: }, michael@0: michael@0: /** michael@0: * Returns an XML document object containing the search plugin information, michael@0: * which can later be used to reload the engine. michael@0: */ michael@0: _serializeToElement: function SRCH_ENG_serializeToEl() { michael@0: function appendTextNode(aNameSpace, aLocalName, aValue) { michael@0: if (!aValue) michael@0: return null; michael@0: var node = doc.createElementNS(aNameSpace, aLocalName); michael@0: node.appendChild(doc.createTextNode(aValue)); michael@0: docElem.appendChild(node); michael@0: docElem.appendChild(doc.createTextNode("\n")); michael@0: return node; michael@0: } michael@0: michael@0: var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. michael@0: createInstance(Ci.nsIDOMParser); michael@0: michael@0: var doc = parser.parseFromString(EMPTY_DOC, "text/xml"); michael@0: var docElem = doc.documentElement; michael@0: michael@0: docElem.appendChild(doc.createTextNode("\n")); michael@0: michael@0: appendTextNode(OPENSEARCH_NS_11, "ShortName", this.name); michael@0: appendTextNode(OPENSEARCH_NS_11, "Description", this._description); michael@0: appendTextNode(OPENSEARCH_NS_11, "InputEncoding", this._queryCharset); michael@0: michael@0: if (this._iconURI) { michael@0: var imageNode = appendTextNode(OPENSEARCH_NS_11, "Image", michael@0: this._iconURI.spec); michael@0: if (imageNode) { michael@0: imageNode.setAttribute("width", "16"); michael@0: imageNode.setAttribute("height", "16"); michael@0: } michael@0: } michael@0: michael@0: appendTextNode(MOZSEARCH_NS_10, "UpdateInterval", this._updateInterval); michael@0: appendTextNode(MOZSEARCH_NS_10, "UpdateUrl", this._updateURL); michael@0: appendTextNode(MOZSEARCH_NS_10, "IconUpdateUrl", this._iconUpdateURL); michael@0: appendTextNode(MOZSEARCH_NS_10, "SearchForm", this._searchForm); michael@0: michael@0: for (var i = 0; i < this._urls.length; ++i) michael@0: this._urls[i]._serializeToElement(doc, docElem); michael@0: docElem.appendChild(doc.createTextNode("\n")); michael@0: michael@0: return doc; michael@0: }, michael@0: michael@0: get lazySerializeTask() { michael@0: if (!this._lazySerializeTask) { michael@0: let task = function taskCallback() { michael@0: this._serializeToFile(); michael@0: }.bind(this); michael@0: this._lazySerializeTask = new DeferredTask(task, LAZY_SERIALIZE_DELAY); michael@0: } michael@0: michael@0: return this._lazySerializeTask; michael@0: }, michael@0: michael@0: /** michael@0: * Serializes the engine object to file. michael@0: */ michael@0: _serializeToFile: function SRCH_ENG_serializeToFile() { michael@0: var file = this._file; michael@0: ENSURE_WARN(!this._readOnly, "Can't serialize a read only engine!", michael@0: Cr.NS_ERROR_FAILURE); michael@0: ENSURE_WARN(file && file.exists(), "Can't serialize: file doesn't exist!", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"]. michael@0: createInstance(Ci.nsIFileOutputStream); michael@0: michael@0: // Serialize the engine first - we don't want to overwrite a good file michael@0: // if this somehow fails. michael@0: var doc = this._serializeToElement(); michael@0: michael@0: fos.init(file, (MODE_WRONLY | MODE_TRUNCATE), PERMS_FILE, 0); michael@0: michael@0: try { michael@0: var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"]. michael@0: createInstance(Ci.nsIDOMSerializer); michael@0: serializer.serializeToStream(doc.documentElement, fos, null); michael@0: } catch (e) { michael@0: LOG("_serializeToFile: Error serializing engine:\n" + e); michael@0: } michael@0: michael@0: closeSafeOutputStream(fos); michael@0: michael@0: Services.obs.notifyObservers(file.clone(), SEARCH_SERVICE_TOPIC, michael@0: "write-engine-to-disk-complete"); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the engine's file from disk. The search service calls this once it michael@0: * removes the engine from its internal store. This function will throw if michael@0: * the file cannot be removed. michael@0: */ michael@0: _remove: function SRCH_ENG_remove() { michael@0: if (this._readOnly) michael@0: FAIL("Can't remove read only engine!", Cr.NS_ERROR_FAILURE); michael@0: if (!this._file || !this._file.exists()) michael@0: FAIL("Can't remove engine: file doesn't exist!", Cr.NS_ERROR_FILE_NOT_FOUND); michael@0: michael@0: this._file.remove(false); michael@0: }, michael@0: michael@0: // nsISearchEngine michael@0: get alias() { michael@0: if (this._alias === undefined) michael@0: this._alias = engineMetadataService.getAttr(this, "alias"); michael@0: michael@0: return this._alias; michael@0: }, michael@0: set alias(val) { michael@0: this._alias = val; michael@0: engineMetadataService.setAttr(this, "alias", val); michael@0: notifyAction(this, SEARCH_ENGINE_CHANGED); michael@0: }, michael@0: michael@0: /** michael@0: * Return the built-in identifier of app-provided engines. michael@0: * michael@0: * Note that this identifier is substantially similar to _id, with the michael@0: * following exceptions: michael@0: * michael@0: * * There is no trailing file extension. michael@0: * * There is no [app] prefix. michael@0: * michael@0: * @return a string identifier, or null. michael@0: */ michael@0: get identifier() { michael@0: if (this._identifier !== undefined) { michael@0: return this._identifier; michael@0: } michael@0: michael@0: // No identifier if If the engine isn't app-provided michael@0: if (!this._isInAppDir && !this._isInJAR) { michael@0: return this._identifier = null; michael@0: } michael@0: michael@0: let leaf = this._getLeafName(); michael@0: ENSURE_WARN(leaf, "identifier: app-provided engine has no leafName"); michael@0: michael@0: // Strip file extension. michael@0: let ext = leaf.lastIndexOf("."); michael@0: if (ext == -1) { michael@0: return this._identifier = leaf; michael@0: } michael@0: return this._identifier = leaf.substring(0, ext); michael@0: }, michael@0: michael@0: get description() { michael@0: return this._description; michael@0: }, michael@0: michael@0: get hidden() { michael@0: if (this._hidden === null) michael@0: this._hidden = engineMetadataService.getAttr(this, "hidden") || false; michael@0: return this._hidden; michael@0: }, michael@0: set hidden(val) { michael@0: var value = !!val; michael@0: if (value != this._hidden) { michael@0: this._hidden = value; michael@0: engineMetadataService.setAttr(this, "hidden", value); michael@0: notifyAction(this, SEARCH_ENGINE_CHANGED); michael@0: } michael@0: }, michael@0: michael@0: get iconURI() { michael@0: if (this._iconURI) michael@0: return this._iconURI; michael@0: return null; michael@0: }, michael@0: michael@0: get _iconURL() { michael@0: if (!this._iconURI) michael@0: return ""; michael@0: return this._iconURI.spec; michael@0: }, michael@0: michael@0: // Where the engine is being loaded from: will return the URI's spec if the michael@0: // engine is being downloaded and does not yet have a file. This is only used michael@0: // for logging and error messages. michael@0: get _location() { michael@0: if (this._file) michael@0: return this._file.path; michael@0: michael@0: if (this._uri) michael@0: return this._uri.spec; michael@0: michael@0: return ""; michael@0: }, michael@0: michael@0: /** michael@0: * @return the leaf name of the filename or URI of this plugin, michael@0: * or null if no file or URI is known. michael@0: */ michael@0: _getLeafName: function () { michael@0: if (this._file) { michael@0: return this._file.leafName; michael@0: } michael@0: if (this._uri && this._uri instanceof Ci.nsIURL) { michael@0: return this._uri.fileName; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: // The file that the plugin is loaded from is a unique identifier for it. We michael@0: // use this as the identifier to store data in the sqlite database michael@0: __id: null, michael@0: get _id() { michael@0: if (this.__id) { michael@0: return this.__id; michael@0: } michael@0: michael@0: let leafName = this._getLeafName(); michael@0: michael@0: // Treat engines loaded from JARs the same way we treat app shipped michael@0: // engines. michael@0: // Theoretically, these could also come from extensions, but there's no michael@0: // real way for extensions to register their chrome locations at the michael@0: // moment, so let's not deal with that case. michael@0: // This means we're vulnerable to conflicts if a file loaded from a JAR michael@0: // has the same filename as a file loaded from the app dir, but with a michael@0: // different engine name. People using the JAR functionality should be michael@0: // careful not to do that! michael@0: if (this._isInAppDir || this._isInJAR) { michael@0: // App dir and JAR engines should always have leafNames michael@0: ENSURE_WARN(leafName, "_id: no leafName for appDir or JAR engine", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: return this.__id = "[app]/" + leafName; michael@0: } michael@0: michael@0: if (this._isInProfile) { michael@0: ENSURE_WARN(leafName, "_id: no leafName for profile engine", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: return this.__id = "[profile]/" + leafName; michael@0: } michael@0: michael@0: // If the engine isn't a JAR engine, it should have a file. michael@0: ENSURE_WARN(this._file, "_id: no _file for non-JAR engine", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: // We're not in the profile or appdir, so this must be an extension-shipped michael@0: // plugin. Use the full filename. michael@0: return this.__id = this._file.path; michael@0: }, michael@0: michael@0: get _installLocation() { michael@0: if (this.__installLocation === null) { michael@0: if (!this._file) { michael@0: ENSURE_WARN(this._uri, "Engines without files must have URIs", michael@0: Cr.NS_ERROR_UNEXPECTED); michael@0: this.__installLocation = SEARCH_JAR; michael@0: } michael@0: else if (this._file.parent.equals(getDir(NS_APP_SEARCH_DIR))) michael@0: this.__installLocation = SEARCH_APP_DIR; michael@0: else if (this._file.parent.equals(getDir(NS_APP_USER_SEARCH_DIR))) michael@0: this.__installLocation = SEARCH_PROFILE_DIR; michael@0: else michael@0: this.__installLocation = SEARCH_IN_EXTENSION; michael@0: } michael@0: michael@0: return this.__installLocation; michael@0: }, michael@0: michael@0: get _isInJAR() { michael@0: return this._installLocation == SEARCH_JAR; michael@0: }, michael@0: get _isInAppDir() { michael@0: return this._installLocation == SEARCH_APP_DIR; michael@0: }, michael@0: get _isInProfile() { michael@0: return this._installLocation == SEARCH_PROFILE_DIR; michael@0: }, michael@0: michael@0: get _isDefault() { michael@0: // For now, our concept of a "default engine" is "one that is not in the michael@0: // user's profile directory", which is currently equivalent to "is app- or michael@0: // extension-shipped". michael@0: return !this._isInProfile; michael@0: }, michael@0: michael@0: get _hasUpdates() { michael@0: // Whether or not the engine has an update URL michael@0: let selfURL = this._getURLOfType(URLTYPE_OPENSEARCH, "self"); michael@0: return !!(this._updateURL || this._iconUpdateURL || selfURL); michael@0: }, michael@0: michael@0: get name() { michael@0: return this._name; michael@0: }, michael@0: michael@0: get type() { michael@0: return this._type; michael@0: }, michael@0: michael@0: get searchForm() { michael@0: // First look for a michael@0: var searchFormURL = this._getURLOfType(URLTYPE_SEARCH_HTML, "searchform"); michael@0: if (searchFormURL) { michael@0: let submission = searchFormURL.getSubmission("", this); michael@0: michael@0: // If the rel=searchform URL is not type="get" (i.e. has postData), michael@0: // ignore it, since we can only return a URL. michael@0: if (!submission.postData) michael@0: return submission.uri.spec; michael@0: } michael@0: michael@0: if (!this._searchForm) { michael@0: // No SearchForm specified in the engine definition file, use the prePath michael@0: // (e.g. https://foo.com for https://foo.com/search.php?q=bar). michael@0: var htmlUrl = this._getURLOfType(URLTYPE_SEARCH_HTML); michael@0: ENSURE_WARN(htmlUrl, "Engine has no HTML URL!", Cr.NS_ERROR_UNEXPECTED); michael@0: this._searchForm = makeURI(htmlUrl.template).prePath; michael@0: } michael@0: michael@0: return ParamSubstitution(this._searchForm, "", this); michael@0: }, michael@0: michael@0: get queryCharset() { michael@0: if (this._queryCharset) michael@0: return this._queryCharset; michael@0: return this._queryCharset = queryCharsetFromCode(/* get the default */); michael@0: }, michael@0: michael@0: // from nsISearchEngine michael@0: addParam: function SRCH_ENG_addParam(aName, aValue, aResponseType) { michael@0: if (!aName || (aValue == null)) michael@0: FAIL("missing name or value for nsISearchEngine::addParam!"); michael@0: ENSURE_WARN(!this._readOnly, michael@0: "called nsISearchEngine::addParam on a read-only engine!", michael@0: Cr.NS_ERROR_FAILURE); michael@0: if (!aResponseType) michael@0: aResponseType = URLTYPE_SEARCH_HTML; michael@0: michael@0: var url = this._getURLOfType(aResponseType); michael@0: if (!url) michael@0: FAIL("Engine object has no URL for response type " + aResponseType, michael@0: Cr.NS_ERROR_FAILURE); michael@0: michael@0: url.addParam(aName, aValue); michael@0: michael@0: // Serialize the changes to file lazily michael@0: this.lazySerializeTask.arm(); michael@0: }, michael@0: michael@0: #ifdef ANDROID michael@0: get _defaultMobileResponseType() { michael@0: let type = URLTYPE_SEARCH_HTML; michael@0: michael@0: let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); michael@0: let isTablet = sysInfo.get("tablet"); michael@0: if (isTablet && this.supportsResponseType("application/x-moz-tabletsearch")) { michael@0: // Check for a tablet-specific search URL override michael@0: type = "application/x-moz-tabletsearch"; michael@0: } else if (!isTablet && this.supportsResponseType("application/x-moz-phonesearch")) { michael@0: // Check for a phone-specific search URL override michael@0: type = "application/x-moz-phonesearch"; michael@0: } michael@0: michael@0: delete this._defaultMobileResponseType; michael@0: return this._defaultMobileResponseType = type; michael@0: }, michael@0: #endif michael@0: michael@0: // from nsISearchEngine michael@0: getSubmission: function SRCH_ENG_getSubmission(aData, aResponseType, aPurpose) { michael@0: #ifdef ANDROID michael@0: if (!aResponseType) { michael@0: aResponseType = this._defaultMobileResponseType; michael@0: } michael@0: #endif michael@0: if (!aResponseType) { michael@0: aResponseType = URLTYPE_SEARCH_HTML; michael@0: } michael@0: michael@0: var url = this._getURLOfType(aResponseType); michael@0: michael@0: if (!url) michael@0: return null; michael@0: michael@0: if (!aData) { michael@0: // Return a dummy submission object with our searchForm attribute michael@0: return new Submission(makeURI(this.searchForm), null); michael@0: } michael@0: michael@0: LOG("getSubmission: In data: \"" + aData + "\"; Purpose: \"" + aPurpose + "\""); michael@0: var textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"]. michael@0: getService(Ci.nsITextToSubURI); michael@0: var data = ""; michael@0: try { michael@0: data = textToSubURI.ConvertAndEscape(this.queryCharset, aData); michael@0: } catch (ex) { michael@0: LOG("getSubmission: Falling back to default queryCharset!"); michael@0: data = textToSubURI.ConvertAndEscape(DEFAULT_QUERY_CHARSET, aData); michael@0: } michael@0: LOG("getSubmission: Out data: \"" + data + "\""); michael@0: return url.getSubmission(data, this, aPurpose); michael@0: }, michael@0: michael@0: // from nsISearchEngine michael@0: supportsResponseType: function SRCH_ENG_supportsResponseType(type) { michael@0: return (this._getURLOfType(type) != null); michael@0: }, michael@0: michael@0: // from nsISearchEngine michael@0: getResultDomain: function SRCH_ENG_getResultDomain(aResponseType) { michael@0: #ifdef ANDROID michael@0: if (!aResponseType) { michael@0: aResponseType = this._defaultMobileResponseType; michael@0: } michael@0: #endif michael@0: if (!aResponseType) { michael@0: aResponseType = URLTYPE_SEARCH_HTML; michael@0: } michael@0: michael@0: LOG("getResultDomain: responseType: \"" + aResponseType + "\""); michael@0: michael@0: let url = this._getURLOfType(aResponseType); michael@0: if (url) michael@0: return url.resultDomain; michael@0: return ""; michael@0: }, michael@0: michael@0: // nsISupports michael@0: QueryInterface: function SRCH_ENG_QI(aIID) { michael@0: if (aIID.equals(Ci.nsISearchEngine) || michael@0: aIID.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: get wrappedJSObject() { michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a string with the URL to an engine's icon matching both width and michael@0: * height. Returns null if icon with specified dimensions is not found. michael@0: * michael@0: * @param width michael@0: * Width of the requested icon. michael@0: * @param height michael@0: * Height of the requested icon. michael@0: */ michael@0: getIconURLBySize: function SRCH_ENG_getIconURLBySize(aWidth, aHeight) { michael@0: if (!this._iconMapObj) michael@0: return null; michael@0: michael@0: let key = this._getIconKey(aWidth, aHeight); michael@0: if (key in this._iconMapObj) { michael@0: return this._iconMapObj[key]; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Gets an array of all available icons. Each entry is an object with michael@0: * width, height and url properties. width and height are numeric and michael@0: * represent the icon's dimensions. url is a string with the URL for michael@0: * the icon. michael@0: */ michael@0: getIcons: function SRCH_ENG_getIcons() { michael@0: let result = []; michael@0: michael@0: if (!this._iconMapObj) michael@0: return result; michael@0: michael@0: for (let key of Object.keys(this._iconMapObj)) { michael@0: let iconSize = JSON.parse(key); michael@0: result.push({ michael@0: width: iconSize.width, michael@0: height: iconSize.height, michael@0: url: this._iconMapObj[key] michael@0: }); michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: }; michael@0: michael@0: // nsISearchSubmission michael@0: function Submission(aURI, aPostData = null) { michael@0: this._uri = aURI; michael@0: this._postData = aPostData; michael@0: } michael@0: Submission.prototype = { michael@0: get uri() { michael@0: return this._uri; michael@0: }, michael@0: get postData() { michael@0: return this._postData; michael@0: }, michael@0: QueryInterface: function SRCH_SUBM_QI(aIID) { michael@0: if (aIID.equals(Ci.nsISearchSubmission) || michael@0: aIID.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: } michael@0: michael@0: function executeSoon(func) { michael@0: Services.tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: michael@0: /** michael@0: * Check for sync initialization has completed or not. michael@0: * michael@0: * @param {aPromise} A promise. michael@0: * michael@0: * @returns the value returned by the invoked method. michael@0: * @throws NS_ERROR_ALREADY_INITIALIZED if sync initialization has completed. michael@0: */ michael@0: function checkForSyncCompletion(aPromise) { michael@0: return aPromise.then(function(aValue) { michael@0: if (gInitialized) { michael@0: throw Components.Exception("Synchronous fallback was called and has " + michael@0: "finished so no need to pursue asynchronous " + michael@0: "initialization", michael@0: Cr.NS_ERROR_ALREADY_INITIALIZED); michael@0: } michael@0: return aValue; michael@0: }); michael@0: } michael@0: michael@0: // nsIBrowserSearchService michael@0: function SearchService() { michael@0: // Replace empty LOG function with the useful one if the log pref is set. michael@0: if (getBoolPref(BROWSER_SEARCH_PREF + "log", false)) michael@0: LOG = DO_LOG; michael@0: michael@0: this._initObservers = Promise.defer(); michael@0: } michael@0: michael@0: SearchService.prototype = { michael@0: classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"), michael@0: michael@0: // The current status of initialization. Note that it does not determine if michael@0: // initialization is complete, only if an error has been encountered so far. michael@0: _initRV: Cr.NS_OK, michael@0: michael@0: // The boolean indicates that the initialization has started or not. michael@0: _initStarted: null, michael@0: michael@0: // If initialization has not been completed yet, perform synchronous michael@0: // initialization. michael@0: // Throws in case of initialization error. michael@0: _ensureInitialized: function SRCH_SVC__ensureInitialized() { michael@0: if (gInitialized) { michael@0: if (!Components.isSuccessCode(this._initRV)) { michael@0: LOG("_ensureInitialized: failure"); michael@0: throw this._initRV; michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let warning = michael@0: "Search service falling back to synchronous initialization. " + michael@0: "This is generally the consequence of an add-on using a deprecated " + michael@0: "search service API."; michael@0: // Bug 785487 - Disable warning until our own callers are fixed. michael@0: //Deprecated.warning(warning, "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIBrowserSearchService#async_warning"); michael@0: LOG(warning); michael@0: michael@0: engineMetadataService.syncInit(); michael@0: this._syncInit(); michael@0: if (!Components.isSuccessCode(this._initRV)) { michael@0: throw this._initRV; michael@0: } michael@0: }, michael@0: michael@0: // Synchronous implementation of the initializer. michael@0: // Used by |_ensureInitialized| as a fallback if initialization is not michael@0: // complete. michael@0: _syncInit: function SRCH_SVC__syncInit() { michael@0: LOG("_syncInit start"); michael@0: this._initStarted = true; michael@0: try { michael@0: this._syncLoadEngines(); michael@0: } catch (ex) { michael@0: this._initRV = Cr.NS_ERROR_FAILURE; michael@0: LOG("_syncInit: failure loading engines: " + ex); michael@0: } michael@0: this._addObservers(); michael@0: michael@0: gInitialized = true; michael@0: michael@0: this._initObservers.resolve(this._initRV); michael@0: michael@0: Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete"); michael@0: michael@0: LOG("_syncInit end"); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronous implementation of the initializer. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if the initialization michael@0: * succeeds. michael@0: */ michael@0: _asyncInit: function SRCH_SVC__asyncInit() { michael@0: return TaskUtils.spawn(function() { michael@0: LOG("_asyncInit start"); michael@0: try { michael@0: yield checkForSyncCompletion(this._asyncLoadEngines()); michael@0: } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) { michael@0: this._initRV = Cr.NS_ERROR_FAILURE; michael@0: LOG("_asyncInit: failure loading engines: " + ex); michael@0: } michael@0: this._addObservers(); michael@0: gInitialized = true; michael@0: this._initObservers.resolve(this._initRV); michael@0: Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete"); michael@0: LOG("_asyncInit: Completed _asyncInit"); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: michael@0: _engines: { }, michael@0: __sortedEngines: null, michael@0: get _sortedEngines() { michael@0: if (!this.__sortedEngines) michael@0: return this._buildSortedEngineList(); michael@0: return this.__sortedEngines; michael@0: }, michael@0: michael@0: // Get the original Engine object that belongs to the defaultenginename pref michael@0: // of the default branch. michael@0: get _originalDefaultEngine() { michael@0: let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF); michael@0: let nsIPLS = Ci.nsIPrefLocalizedString; michael@0: let defaultEngine; michael@0: try { michael@0: defaultEngine = defaultPrefB.getComplexValue("defaultenginename", nsIPLS).data; michael@0: } catch (ex) { michael@0: // If the default pref is invalid (e.g. an add-on set it to a bogus value) michael@0: // getEngineByName will just return null, which is the best we can do. michael@0: } michael@0: return this.getEngineByName(defaultEngine); michael@0: }, michael@0: michael@0: _buildCache: function SRCH_SVC__buildCache() { michael@0: if (!getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) michael@0: return; michael@0: michael@0: TelemetryStopwatch.start("SEARCH_SERVICE_BUILD_CACHE_MS"); michael@0: let cache = {}; michael@0: let locale = getLocale(); michael@0: let buildID = Services.appinfo.platformBuildID; michael@0: michael@0: // Allows us to force a cache refresh should the cache format change. michael@0: cache.version = CACHE_VERSION; michael@0: // We don't want to incur the costs of stat()ing each plugin on every michael@0: // startup when the only (supported) time they will change is during michael@0: // runtime (where we refresh for changes through the API) and app updates michael@0: // (where the buildID is obviously going to change). michael@0: // Extension-shipped plugins are the only exception to this, but their michael@0: // directories are blown away during updates, so we'll detect their changes. michael@0: cache.buildID = buildID; michael@0: cache.locale = locale; michael@0: michael@0: cache.directories = {}; michael@0: michael@0: function getParent(engine) { michael@0: if (engine._file) michael@0: return engine._file.parent; michael@0: michael@0: let uri = engine._uri; michael@0: if (!uri.schemeIs("chrome")) { michael@0: LOG("getParent: engine URI must be a chrome URI if it has no file"); michael@0: return null; michael@0: } michael@0: michael@0: // use the underlying JAR file, for chrome URIs michael@0: try { michael@0: uri = gChromeReg.convertChromeURL(uri); michael@0: if (uri instanceof Ci.nsINestedURI) michael@0: uri = uri.innermostURI; michael@0: uri.QueryInterface(Ci.nsIFileURL) michael@0: michael@0: return uri.file; michael@0: } catch (ex) { michael@0: LOG("getParent: couldn't map chrome:// URI to a file: " + ex) michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: for each (let engine in this._engines) { michael@0: let parent = getParent(engine); michael@0: if (!parent) { michael@0: LOG("Error: no parent for engine " + engine._location + ", failing to cache it"); michael@0: michael@0: continue; michael@0: } michael@0: michael@0: let cacheKey = parent.path; michael@0: if (!cache.directories[cacheKey]) { michael@0: let cacheEntry = {}; michael@0: cacheEntry.lastModifiedTime = parent.lastModifiedTime; michael@0: cacheEntry.engines = []; michael@0: cache.directories[cacheKey] = cacheEntry; michael@0: } michael@0: cache.directories[cacheKey].engines.push(engine._serializeToJSON(true)); michael@0: } michael@0: michael@0: try { michael@0: LOG("_buildCache: Writing to cache file."); michael@0: let path = OS.Path.join(OS.Constants.Path.profileDir, "search.json"); michael@0: let data = gEncoder.encode(JSON.stringify(cache)); michael@0: let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp"}); michael@0: michael@0: promise.then( michael@0: function onSuccess() { michael@0: Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, SEARCH_SERVICE_CACHE_WRITTEN); michael@0: }, michael@0: function onError(e) { michael@0: LOG("_buildCache: failure during writeAtomic: " + e); michael@0: } michael@0: ); michael@0: } catch (ex) { michael@0: LOG("_buildCache: Could not write to cache file: " + ex); michael@0: } michael@0: TelemetryStopwatch.finish("SEARCH_SERVICE_BUILD_CACHE_MS"); michael@0: }, michael@0: michael@0: _syncLoadEngines: function SRCH_SVC__syncLoadEngines() { michael@0: LOG("_syncLoadEngines: start"); michael@0: // See if we have a cache file so we don't have to parse a bunch of XML. michael@0: let cache = {}; michael@0: let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true); michael@0: if (cacheEnabled) { michael@0: let cacheFile = getDir(NS_APP_USER_PROFILE_50_DIR); michael@0: cacheFile.append("search.json"); michael@0: if (cacheFile.exists()) michael@0: cache = this._readCacheFile(cacheFile); michael@0: } michael@0: michael@0: let loadDirs = []; michael@0: let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator); michael@0: while (locations.hasMoreElements()) { michael@0: let dir = locations.getNext().QueryInterface(Ci.nsIFile); michael@0: if (dir.directoryEntries.hasMoreElements()) michael@0: loadDirs.push(dir); michael@0: } michael@0: michael@0: let loadFromJARs = getBoolPref(BROWSER_SEARCH_PREF + "loadFromJars", false); michael@0: let chromeURIs = []; michael@0: let chromeFiles = []; michael@0: if (loadFromJARs) michael@0: [chromeFiles, chromeURIs] = this._findJAREngines(); michael@0: michael@0: let toLoad = chromeFiles.concat(loadDirs); michael@0: michael@0: function modifiedDir(aDir) { michael@0: return (!cache.directories || !cache.directories[aDir.path] || michael@0: cache.directories[aDir.path].lastModifiedTime != aDir.lastModifiedTime); michael@0: } michael@0: michael@0: function notInCachePath(aPathToLoad) michael@0: cachePaths.indexOf(aPathToLoad.path) == -1; michael@0: michael@0: let buildID = Services.appinfo.platformBuildID; michael@0: let cachePaths = [path for (path in cache.directories)]; michael@0: michael@0: let rebuildCache = !cache.directories || michael@0: cache.version != CACHE_VERSION || michael@0: cache.locale != getLocale() || michael@0: cache.buildID != buildID || michael@0: cachePaths.length != toLoad.length || michael@0: toLoad.some(notInCachePath) || michael@0: toLoad.some(modifiedDir); michael@0: michael@0: if (!cacheEnabled || rebuildCache) { michael@0: LOG("_loadEngines: Absent or outdated cache. Loading engines from disk."); michael@0: loadDirs.forEach(this._loadEnginesFromDir, this); michael@0: michael@0: this._loadFromChromeURLs(chromeURIs); michael@0: michael@0: if (cacheEnabled) michael@0: this._buildCache(); michael@0: return; michael@0: } michael@0: michael@0: LOG("_loadEngines: loading from cache directories"); michael@0: for each (let dir in cache.directories) michael@0: this._loadEnginesFromCache(dir); michael@0: michael@0: LOG("_loadEngines: done"); michael@0: }, michael@0: michael@0: /** michael@0: * Loads engines asynchronously. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if loading data michael@0: * succeeds. michael@0: */ michael@0: _asyncLoadEngines: function SRCH_SVC__asyncLoadEngines() { michael@0: return TaskUtils.spawn(function() { michael@0: LOG("_asyncLoadEngines: start"); michael@0: // See if we have a cache file so we don't have to parse a bunch of XML. michael@0: let cache = {}; michael@0: let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true); michael@0: if (cacheEnabled) { michael@0: let cacheFilePath = OS.Path.join(OS.Constants.Path.profileDir, "search.json"); michael@0: cache = yield checkForSyncCompletion(this._asyncReadCacheFile(cacheFilePath)); michael@0: } michael@0: michael@0: // Add all the non-empty directories of NS_APP_SEARCH_DIR_LIST to michael@0: // loadDirs. michael@0: let loadDirs = []; michael@0: let locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator); michael@0: while (locations.hasMoreElements()) { michael@0: let dir = locations.getNext().QueryInterface(Ci.nsIFile); michael@0: let iterator = new OS.File.DirectoryIterator(dir.path, michael@0: { winPattern: "*.xml" }); michael@0: try { michael@0: // Add dir to loadDirs if it contains any files. michael@0: yield checkForSyncCompletion(iterator.next()); michael@0: loadDirs.push(dir); michael@0: } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) { michael@0: // Catch for StopIteration exception. michael@0: } finally { michael@0: iterator.close(); michael@0: } michael@0: } michael@0: michael@0: let loadFromJARs = getBoolPref(BROWSER_SEARCH_PREF + "loadFromJars", false); michael@0: let chromeURIs = []; michael@0: let chromeFiles = []; michael@0: if (loadFromJARs) { michael@0: Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "find-jar-engines"); michael@0: [chromeFiles, chromeURIs] = michael@0: yield checkForSyncCompletion(this._asyncFindJAREngines()); michael@0: } michael@0: michael@0: let toLoad = chromeFiles.concat(loadDirs); michael@0: function hasModifiedDir(aList) { michael@0: return TaskUtils.spawn(function() { michael@0: let modifiedDir = false; michael@0: michael@0: for (let dir of aList) { michael@0: if (!cache.directories || !cache.directories[dir.path]) { michael@0: modifiedDir = true; michael@0: break; michael@0: } michael@0: michael@0: let info = yield OS.File.stat(dir.path); michael@0: if (cache.directories[dir.path].lastModifiedTime != michael@0: info.lastModificationDate.getTime()) { michael@0: modifiedDir = true; michael@0: break; michael@0: } michael@0: } michael@0: throw new Task.Result(modifiedDir); michael@0: }); michael@0: } michael@0: michael@0: function notInCachePath(aPathToLoad) michael@0: cachePaths.indexOf(aPathToLoad.path) == -1; michael@0: michael@0: let buildID = Services.appinfo.platformBuildID; michael@0: let cachePaths = [path for (path in cache.directories)]; michael@0: michael@0: let rebuildCache = !cache.directories || michael@0: cache.version != CACHE_VERSION || michael@0: cache.locale != getLocale() || michael@0: cache.buildID != buildID || michael@0: cachePaths.length != toLoad.length || michael@0: toLoad.some(notInCachePath) || michael@0: (yield checkForSyncCompletion(hasModifiedDir(toLoad))); michael@0: michael@0: if (!cacheEnabled || rebuildCache) { michael@0: LOG("_asyncLoadEngines: Absent or outdated cache. Loading engines from disk."); michael@0: let engines = []; michael@0: for (let loadDir of loadDirs) { michael@0: let enginesFromDir = michael@0: yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir)); michael@0: engines = engines.concat(enginesFromDir); michael@0: } michael@0: let enginesFromURLs = michael@0: yield checkForSyncCompletion(this._asyncLoadFromChromeURLs(chromeURIs)); michael@0: engines = engines.concat(enginesFromURLs); michael@0: michael@0: for (let engine of engines) { michael@0: this._addEngineToStore(engine); michael@0: } michael@0: if (cacheEnabled) michael@0: this._buildCache(); michael@0: return; michael@0: } michael@0: michael@0: LOG("_asyncLoadEngines: loading from cache directories"); michael@0: for each (let dir in cache.directories) michael@0: this._loadEnginesFromCache(dir); michael@0: michael@0: LOG("_asyncLoadEngines: done"); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: _readCacheFile: function SRCH_SVC__readCacheFile(aFile) { michael@0: let stream = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); michael@0: michael@0: try { michael@0: stream.init(aFile, MODE_RDONLY, PERMS_FILE, 0); michael@0: return json.decodeFromStream(stream, stream.available()); michael@0: } catch (ex) { michael@0: LOG("_readCacheFile: Error reading cache file: " + ex); michael@0: } finally { michael@0: stream.close(); michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Read from a given cache file asynchronously. michael@0: * michael@0: * @param aPath the file path. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if retrieveing data michael@0: * succeeds. michael@0: */ michael@0: _asyncReadCacheFile: function SRCH_SVC__asyncReadCacheFile(aPath) { michael@0: return TaskUtils.spawn(function() { michael@0: let json; michael@0: try { michael@0: let bytes = yield OS.File.read(aPath); michael@0: json = JSON.parse(new TextDecoder().decode(bytes)); michael@0: } catch (ex) { michael@0: LOG("_asyncReadCacheFile: Error reading cache file: " + ex); michael@0: json = {}; michael@0: } michael@0: throw new Task.Result(json); michael@0: }); michael@0: }, michael@0: michael@0: _batchTask: null, michael@0: get batchTask() { michael@0: if (!this._batchTask) { michael@0: let task = function taskCallback() { michael@0: LOG("batchTask: Invalidating engine cache"); michael@0: this._buildCache(); michael@0: }.bind(this); michael@0: this._batchTask = new DeferredTask(task, CACHE_INVALIDATION_DELAY); michael@0: } michael@0: return this._batchTask; michael@0: }, michael@0: michael@0: _addEngineToStore: function SRCH_SVC_addEngineToStore(aEngine) { michael@0: LOG("_addEngineToStore: Adding engine: \"" + aEngine.name + "\""); michael@0: michael@0: // See if there is an existing engine with the same name. However, if this michael@0: // engine is updating another engine, it's allowed to have the same name. michael@0: var hasSameNameAsUpdate = (aEngine._engineToUpdate && michael@0: aEngine.name == aEngine._engineToUpdate.name); michael@0: if (aEngine.name in this._engines && !hasSameNameAsUpdate) { michael@0: LOG("_addEngineToStore: Duplicate engine found, aborting!"); michael@0: return; michael@0: } michael@0: michael@0: if (aEngine._engineToUpdate) { michael@0: // We need to replace engineToUpdate with the engine that just loaded. michael@0: var oldEngine = aEngine._engineToUpdate; michael@0: michael@0: // Remove the old engine from the hash, since it's keyed by name, and our michael@0: // name might change (the update might have a new name). michael@0: delete this._engines[oldEngine.name]; michael@0: michael@0: // Hack: we want to replace the old engine with the new one, but since michael@0: // people may be holding refs to the nsISearchEngine objects themselves, michael@0: // we'll just copy over all "private" properties (those without a getter michael@0: // or setter) from one object to the other. michael@0: for (var p in aEngine) { michael@0: if (!(aEngine.__lookupGetter__(p) || aEngine.__lookupSetter__(p))) michael@0: oldEngine[p] = aEngine[p]; michael@0: } michael@0: aEngine = oldEngine; michael@0: aEngine._engineToUpdate = null; michael@0: michael@0: // Add the engine back michael@0: this._engines[aEngine.name] = aEngine; michael@0: notifyAction(aEngine, SEARCH_ENGINE_CHANGED); michael@0: } else { michael@0: // Not an update, just add the new engine. michael@0: this._engines[aEngine.name] = aEngine; michael@0: // Only add the engine to the list of sorted engines if the initial list michael@0: // has already been built (i.e. if this.__sortedEngines is non-null). If michael@0: // it hasn't, we're loading engines from disk and the sorted engine list michael@0: // will be built once we need it. michael@0: if (this.__sortedEngines) { michael@0: this.__sortedEngines.push(aEngine); michael@0: this._saveSortedEngineList(); michael@0: } michael@0: notifyAction(aEngine, SEARCH_ENGINE_ADDED); michael@0: } michael@0: michael@0: if (aEngine._hasUpdates) { michael@0: // Schedule the engine's next update, if it isn't already. michael@0: if (!engineMetadataService.getAttr(aEngine, "updateexpir")) michael@0: engineUpdateService.scheduleNextUpdate(aEngine); michael@0: michael@0: // We need to save the engine's _dataType, if this is the first time the michael@0: // engine is added to the dataStore, since ._dataType isn't persisted michael@0: // and will change on the next startup (since the engine will then be michael@0: // XML). We need this so that we know how to load any future updates from michael@0: // this engine. michael@0: if (!engineMetadataService.getAttr(aEngine, "updatedatatype")) michael@0: engineMetadataService.setAttr(aEngine, "updatedatatype", michael@0: aEngine._dataType); michael@0: } michael@0: }, michael@0: michael@0: _loadEnginesFromCache: function SRCH_SVC__loadEnginesFromCache(aDir) { michael@0: let engines = aDir.engines; michael@0: LOG("_loadEnginesFromCache: Loading from cache. " + engines.length + " engines to load."); michael@0: for (let i = 0; i < engines.length; i++) { michael@0: let json = engines[i]; michael@0: michael@0: try { michael@0: let engine; michael@0: if (json.filePath) michael@0: engine = new Engine({type: "filePath", value: json.filePath}, json._dataType, michael@0: json._readOnly); michael@0: else if (json._url) michael@0: engine = new Engine({type: "uri", value: json._url}, json._dataType, json._readOnly); michael@0: michael@0: engine._initWithJSON(json); michael@0: this._addEngineToStore(engine); michael@0: } catch (ex) { michael@0: LOG("Failed to load " + engines[i]._name + " from cache: " + ex); michael@0: LOG("Engine JSON: " + engines[i].toSource()); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _loadEnginesFromDir: function SRCH_SVC__loadEnginesFromDir(aDir) { michael@0: LOG("_loadEnginesFromDir: Searching in " + aDir.path + " for search engines."); michael@0: michael@0: // Check whether aDir is the user profile dir michael@0: var isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR)); michael@0: michael@0: var files = aDir.directoryEntries michael@0: .QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: michael@0: while (files.hasMoreElements()) { michael@0: var file = files.nextFile; michael@0: michael@0: // Ignore hidden and empty files, and directories michael@0: if (!file.isFile() || file.fileSize == 0 || file.isHidden()) michael@0: continue; michael@0: michael@0: var fileURL = NetUtil.ioService.newFileURI(file).QueryInterface(Ci.nsIURL); michael@0: var fileExtension = fileURL.fileExtension.toLowerCase(); michael@0: var isWritable = isInProfile && file.isWritable(); michael@0: michael@0: if (fileExtension != "xml") { michael@0: // Not an engine michael@0: continue; michael@0: } michael@0: michael@0: var addedEngine = null; michael@0: try { michael@0: addedEngine = new Engine(file, SEARCH_DATA_XML, !isWritable); michael@0: addedEngine._initFromFile(); michael@0: } catch (ex) { michael@0: LOG("_loadEnginesFromDir: Failed to load " + file.path + "!\n" + ex); michael@0: continue; michael@0: } michael@0: michael@0: this._addEngineToStore(addedEngine); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Loads engines from a given directory asynchronously. michael@0: * michael@0: * @param aDir the directory. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if retrieveing data michael@0: * succeeds. michael@0: */ michael@0: _asyncLoadEnginesFromDir: function SRCH_SVC__asyncLoadEnginesFromDir(aDir) { michael@0: LOG("_asyncLoadEnginesFromDir: Searching in " + aDir.path + " for search engines."); michael@0: michael@0: // Check whether aDir is the user profile dir michael@0: let isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR)); michael@0: let iterator = new OS.File.DirectoryIterator(aDir.path); michael@0: return TaskUtils.spawn(function() { michael@0: let osfiles = yield iterator.nextBatch(); michael@0: iterator.close(); michael@0: michael@0: let engines = []; michael@0: for (let osfile of osfiles) { michael@0: if (osfile.isDir || osfile.isSymLink) michael@0: continue; michael@0: michael@0: let fileInfo = yield OS.File.stat(osfile.path); michael@0: if (fileInfo.size == 0) michael@0: continue; michael@0: michael@0: let parts = osfile.path.split("."); michael@0: if (parts.length <= 1 || (parts.pop()).toLowerCase() != "xml") { michael@0: // Not an engine michael@0: continue; michael@0: } michael@0: michael@0: let addedEngine = null; michael@0: try { michael@0: let file = new FileUtils.File(osfile.path); michael@0: let isWritable = isInProfile; michael@0: addedEngine = new Engine(file, SEARCH_DATA_XML, !isWritable); michael@0: yield checkForSyncCompletion(addedEngine._asyncInitFromFile()); michael@0: } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) { michael@0: LOG("_asyncLoadEnginesFromDir: Failed to load " + osfile.path + "!\n" + ex); michael@0: continue; michael@0: } michael@0: engines.push(addedEngine); michael@0: } michael@0: throw new Task.Result(engines); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: _loadFromChromeURLs: function SRCH_SVC_loadFromChromeURLs(aURLs) { michael@0: aURLs.forEach(function (url) { michael@0: try { michael@0: LOG("_loadFromChromeURLs: loading engine from chrome url: " + url); michael@0: michael@0: let engine = new Engine(makeURI(url), SEARCH_DATA_XML, true); michael@0: michael@0: engine._initFromURISync(); michael@0: michael@0: this._addEngineToStore(engine); michael@0: } catch (ex) { michael@0: LOG("_loadFromChromeURLs: failed to load engine: " + ex); michael@0: } michael@0: }, this); michael@0: }, michael@0: michael@0: /** michael@0: * Loads engines from Chrome URLs asynchronously. michael@0: * michael@0: * @param aURLs a list of URLs. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if loading data michael@0: * succeeds. michael@0: */ michael@0: _asyncLoadFromChromeURLs: function SRCH_SVC__asyncLoadFromChromeURLs(aURLs) { michael@0: return TaskUtils.spawn(function() { michael@0: let engines = []; michael@0: for (let url of aURLs) { michael@0: try { michael@0: LOG("_asyncLoadFromChromeURLs: loading engine from chrome url: " + url); michael@0: let engine = new Engine(NetUtil.newURI(url), SEARCH_DATA_XML, true); michael@0: yield checkForSyncCompletion(engine._asyncInitFromURI()); michael@0: engines.push(engine); michael@0: } catch (ex if ex.result != Cr.NS_ERROR_ALREADY_INITIALIZED) { michael@0: LOG("_asyncLoadFromChromeURLs: failed to load engine: " + ex); michael@0: } michael@0: } michael@0: throw new Task.Result(engines); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: _findJAREngines: function SRCH_SVC_findJAREngines() { michael@0: LOG("_findJAREngines: looking for engines in JARs") michael@0: michael@0: let rootURIPref = "" michael@0: try { michael@0: rootURIPref = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "jarURIs"); michael@0: } catch (ex) {} michael@0: michael@0: if (!rootURIPref) { michael@0: LOG("_findJAREngines: no JAR URIs were specified"); michael@0: michael@0: return [[], []]; michael@0: } michael@0: michael@0: let rootURIs = rootURIPref.split(","); michael@0: let uris = []; michael@0: let chromeFiles = []; michael@0: michael@0: rootURIs.forEach(function (root) { michael@0: // Find the underlying JAR file for this chrome package (_loadEngines uses michael@0: // it to determine whether it needs to invalidate the cache) michael@0: let chromeFile; michael@0: try { michael@0: let chromeURI = gChromeReg.convertChromeURL(makeURI(root)); michael@0: let fileURI = chromeURI; // flat packaging michael@0: while (fileURI instanceof Ci.nsIJARURI) michael@0: fileURI = fileURI.JARFile; // JAR packaging michael@0: fileURI.QueryInterface(Ci.nsIFileURL); michael@0: chromeFile = fileURI.file; michael@0: } catch (ex) { michael@0: LOG("_findJAREngines: failed to get chromeFile for " + root + ": " + ex); michael@0: } michael@0: michael@0: if (!chromeFile) michael@0: return; michael@0: michael@0: chromeFiles.push(chromeFile); michael@0: michael@0: // Read list.txt from the chrome package to find the engines we need to michael@0: // load michael@0: let listURL = root + "list.txt"; michael@0: let names = []; michael@0: try { michael@0: let chan = NetUtil.ioService.newChannelFromURI(makeURI(listURL)); michael@0: let sis = Cc["@mozilla.org/scriptableinputstream;1"]. michael@0: createInstance(Ci.nsIScriptableInputStream); michael@0: sis.init(chan.open()); michael@0: let list = sis.read(sis.available()); michael@0: names = list.split("\n").filter(function (n) !!n); michael@0: } catch (ex) { michael@0: LOG("_findJAREngines: failed to retrieve list.txt from " + listURL + ": " + ex); michael@0: michael@0: return; michael@0: } michael@0: michael@0: names.forEach(function (n) uris.push(root + n + ".xml")); michael@0: }); michael@0: michael@0: return [chromeFiles, uris]; michael@0: }, michael@0: michael@0: /** michael@0: * Loads jar engines asynchronously. michael@0: * michael@0: * @returns {Promise} A promise, resolved successfully if finding jar engines michael@0: * succeeds. michael@0: */ michael@0: _asyncFindJAREngines: function SRCH_SVC__asyncFindJAREngines() { michael@0: return TaskUtils.spawn(function() { michael@0: LOG("_asyncFindJAREngines: looking for engines in JARs") michael@0: michael@0: let rootURIPref = ""; michael@0: try { michael@0: rootURIPref = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "jarURIs"); michael@0: } catch (ex) {} michael@0: michael@0: if (!rootURIPref) { michael@0: LOG("_asyncFindJAREngines: no JAR URIs were specified"); michael@0: throw new Task.Result([[], []]); michael@0: } michael@0: michael@0: let rootURIs = rootURIPref.split(","); michael@0: let uris = []; michael@0: let chromeFiles = []; michael@0: michael@0: for (let root of rootURIs) { michael@0: // Find the underlying JAR file for this chrome package (_loadEngines uses michael@0: // it to determine whether it needs to invalidate the cache) michael@0: let chromeFile; michael@0: try { michael@0: let chromeURI = gChromeReg.convertChromeURL(makeURI(root)); michael@0: let fileURI = chromeURI; // flat packaging michael@0: while (fileURI instanceof Ci.nsIJARURI) michael@0: fileURI = fileURI.JARFile; // JAR packaging michael@0: fileURI.QueryInterface(Ci.nsIFileURL); michael@0: chromeFile = fileURI.file; michael@0: } catch (ex) { michael@0: LOG("_asyncFindJAREngines: failed to get chromeFile for " + root + ": " + ex); michael@0: } michael@0: michael@0: if (!chromeFile) { michael@0: return; michael@0: } michael@0: michael@0: chromeFiles.push(chromeFile); michael@0: michael@0: // Read list.txt from the chrome package to find the engines we need to michael@0: // load michael@0: let listURL = root + "list.txt"; michael@0: let deferred = Promise.defer(); michael@0: let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. michael@0: createInstance(Ci.nsIXMLHttpRequest); michael@0: request.onload = function(aEvent) { michael@0: deferred.resolve(aEvent.target.responseText); michael@0: }; michael@0: request.onerror = function(aEvent) { michael@0: LOG("_asyncFindJAREngines: failed to retrieve list.txt from " + listURL); michael@0: deferred.resolve(""); michael@0: }; michael@0: request.open("GET", NetUtil.newURI(listURL).spec, true); michael@0: request.send(); michael@0: let list = yield deferred.promise; michael@0: michael@0: let names = []; michael@0: names = list.split("\n").filter(function (n) !!n); michael@0: names.forEach(function (n) uris.push(root + n + ".xml")); michael@0: } michael@0: throw new Task.Result([chromeFiles, uris]); michael@0: }); michael@0: }, michael@0: michael@0: michael@0: _saveSortedEngineList: function SRCH_SVC_saveSortedEngineList() { michael@0: LOG("SRCH_SVC_saveSortedEngineList: starting"); michael@0: michael@0: // Set the useDB pref to indicate that from now on we should use the order michael@0: // information stored in the database. michael@0: Services.prefs.setBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", true); michael@0: michael@0: var engines = this._getSortedEngines(true); michael@0: michael@0: let instructions = []; michael@0: for (var i = 0; i < engines.length; ++i) { michael@0: instructions.push( michael@0: {key: "order", michael@0: value: i+1, michael@0: engine: engines[i] michael@0: }); michael@0: } michael@0: michael@0: engineMetadataService.setAttrs(instructions); michael@0: LOG("SRCH_SVC_saveSortedEngineList: done"); michael@0: }, michael@0: michael@0: _buildSortedEngineList: function SRCH_SVC_buildSortedEngineList() { michael@0: LOG("_buildSortedEngineList: building list"); michael@0: var addedEngines = { }; michael@0: this.__sortedEngines = []; michael@0: var engine; michael@0: michael@0: // If the user has specified a custom engine order, read the order michael@0: // information from the engineMetadataService instead of the default michael@0: // prefs. michael@0: if (getBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", false)) { michael@0: LOG("_buildSortedEngineList: using db for order"); michael@0: michael@0: // Flag to keep track of whether or not we need to call _saveSortedEngineList. michael@0: let needToSaveEngineList = false; michael@0: michael@0: for each (engine in this._engines) { michael@0: var orderNumber = engineMetadataService.getAttr(engine, "order"); michael@0: michael@0: // Since the DB isn't regularly cleared, and engine files may disappear michael@0: // without us knowing, we may already have an engine in this slot. If michael@0: // that happens, we just skip it - it will be added later on as an michael@0: // unsorted engine. michael@0: if (orderNumber && !this.__sortedEngines[orderNumber-1]) { michael@0: this.__sortedEngines[orderNumber-1] = engine; michael@0: addedEngines[engine.name] = engine; michael@0: } else { michael@0: // We need to call _saveSortedEngineList so this gets sorted out. michael@0: needToSaveEngineList = true; michael@0: } michael@0: } michael@0: michael@0: // Filter out any nulls for engines that may have been removed michael@0: var filteredEngines = this.__sortedEngines.filter(function(a) { return !!a; }); michael@0: if (this.__sortedEngines.length != filteredEngines.length) michael@0: needToSaveEngineList = true; michael@0: this.__sortedEngines = filteredEngines; michael@0: michael@0: if (needToSaveEngineList) michael@0: this._saveSortedEngineList(); michael@0: } else { michael@0: // The DB isn't being used, so just read the engine order from the prefs michael@0: var i = 0; michael@0: var engineName; michael@0: var prefName; michael@0: michael@0: try { michael@0: var extras = michael@0: Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra."); michael@0: michael@0: for each (prefName in extras) { michael@0: engineName = Services.prefs.getCharPref(prefName); michael@0: michael@0: engine = this._engines[engineName]; michael@0: if (!engine || engine.name in addedEngines) michael@0: continue; michael@0: michael@0: this.__sortedEngines.push(engine); michael@0: addedEngines[engine.name] = engine; michael@0: } michael@0: } michael@0: catch (e) { } michael@0: michael@0: while (true) { michael@0: engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + (++i)); michael@0: if (!engineName) michael@0: break; michael@0: michael@0: engine = this._engines[engineName]; michael@0: if (!engine || engine.name in addedEngines) michael@0: continue; michael@0: michael@0: this.__sortedEngines.push(engine); michael@0: addedEngines[engine.name] = engine; michael@0: } michael@0: } michael@0: michael@0: // Array for the remaining engines, alphabetically sorted michael@0: var alphaEngines = []; michael@0: michael@0: for each (engine in this._engines) { michael@0: if (!(engine.name in addedEngines)) michael@0: alphaEngines.push(this._engines[engine.name]); michael@0: } michael@0: alphaEngines = alphaEngines.sort(function (a, b) { michael@0: return a.name.localeCompare(b.name); michael@0: }); michael@0: return this.__sortedEngines = this.__sortedEngines.concat(alphaEngines); michael@0: }, michael@0: michael@0: /** michael@0: * Get a sorted array of engines. michael@0: * @param aWithHidden michael@0: * True if hidden plugins should be included in the result. michael@0: */ michael@0: _getSortedEngines: function SRCH_SVC_getSorted(aWithHidden) { michael@0: if (aWithHidden) michael@0: return this._sortedEngines; michael@0: michael@0: return this._sortedEngines.filter(function (engine) { michael@0: return !engine.hidden; michael@0: }); michael@0: }, michael@0: michael@0: _setEngineByPref: function SRCH_SVC_setEngineByPref(aEngineType, aPref) { michael@0: this._ensureInitialized(); michael@0: let newEngine = this.getEngineByName(getLocalizedPref(aPref, "")); michael@0: if (!newEngine) michael@0: FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: this[aEngineType] = newEngine; michael@0: }, michael@0: michael@0: // nsIBrowserSearchService michael@0: init: function SRCH_SVC_init(observer) { michael@0: LOG("SearchService.init"); michael@0: let self = this; michael@0: if (!this._initStarted) { michael@0: TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS"); michael@0: this._initStarted = true; michael@0: TaskUtils.spawn(function task() { michael@0: try { michael@0: yield checkForSyncCompletion(engineMetadataService.init()); michael@0: // Complete initialization by calling asynchronous initializer. michael@0: yield self._asyncInit(); michael@0: TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS"); michael@0: } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) { michael@0: // No need to pursue asynchronous because synchronous fallback was michael@0: // called and has finished. michael@0: TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS"); michael@0: } catch (ex) { michael@0: self._initObservers.reject(ex); michael@0: TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS"); michael@0: } michael@0: }); michael@0: } michael@0: if (observer) { michael@0: TaskUtils.captureErrors(this._initObservers.promise.then( michael@0: function onSuccess() { michael@0: observer.onInitComplete(self._initRV); michael@0: }, michael@0: function onError(aReason) { michael@0: Components.utils.reportError("Internal error while initializing SearchService: " + aReason); michael@0: observer.onInitComplete(Components.results.NS_ERROR_UNEXPECTED); michael@0: } michael@0: )); michael@0: } michael@0: }, michael@0: michael@0: get isInitialized() { michael@0: return gInitialized; michael@0: }, michael@0: michael@0: getEngines: function SRCH_SVC_getEngines(aCount) { michael@0: this._ensureInitialized(); michael@0: LOG("getEngines: getting all engines"); michael@0: var engines = this._getSortedEngines(true); michael@0: aCount.value = engines.length; michael@0: return engines; michael@0: }, michael@0: michael@0: getVisibleEngines: function SRCH_SVC_getVisible(aCount) { michael@0: this._ensureInitialized(); michael@0: LOG("getVisibleEngines: getting all visible engines"); michael@0: var engines = this._getSortedEngines(false); michael@0: aCount.value = engines.length; michael@0: return engines; michael@0: }, michael@0: michael@0: getDefaultEngines: function SRCH_SVC_getDefault(aCount) { michael@0: this._ensureInitialized(); michael@0: function isDefault(engine) { michael@0: return engine._isDefault; michael@0: }; michael@0: var engines = this._sortedEngines.filter(isDefault); michael@0: var engineOrder = {}; michael@0: var engineName; michael@0: var i = 1; michael@0: michael@0: // Build a list of engines which we have ordering information for. michael@0: // We're rebuilding the list here because _sortedEngines contain the michael@0: // current order, but we want the original order. michael@0: michael@0: // First, look at the "browser.search.order.extra" branch. michael@0: try { michael@0: var extras = Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra."); michael@0: michael@0: for each (var prefName in extras) { michael@0: engineName = Services.prefs.getCharPref(prefName); michael@0: michael@0: if (!(engineName in engineOrder)) michael@0: engineOrder[engineName] = i++; michael@0: } michael@0: } catch (e) { michael@0: LOG("Getting extra order prefs failed: " + e); michael@0: } michael@0: michael@0: // Now look through the "browser.search.order" branch. michael@0: for (var j = 1; ; j++) { michael@0: engineName = getLocalizedPref(BROWSER_SEARCH_PREF + "order." + j); michael@0: if (!engineName) michael@0: break; michael@0: michael@0: if (!(engineName in engineOrder)) michael@0: engineOrder[engineName] = i++; michael@0: } michael@0: michael@0: LOG("getDefaultEngines: engineOrder: " + engineOrder.toSource()); michael@0: michael@0: function compareEngines (a, b) { michael@0: var aIdx = engineOrder[a.name]; michael@0: var bIdx = engineOrder[b.name]; michael@0: michael@0: if (aIdx && bIdx) michael@0: return aIdx - bIdx; michael@0: if (aIdx) michael@0: return -1; michael@0: if (bIdx) michael@0: return 1; michael@0: michael@0: return a.name.localeCompare(b.name); michael@0: } michael@0: engines.sort(compareEngines); michael@0: michael@0: aCount.value = engines.length; michael@0: return engines; michael@0: }, michael@0: michael@0: getEngineByName: function SRCH_SVC_getEngineByName(aEngineName) { michael@0: this._ensureInitialized(); michael@0: return this._engines[aEngineName] || null; michael@0: }, michael@0: michael@0: getEngineByAlias: function SRCH_SVC_getEngineByAlias(aAlias) { michael@0: this._ensureInitialized(); michael@0: for (var engineName in this._engines) { michael@0: var engine = this._engines[engineName]; michael@0: if (engine && engine.alias == aAlias) michael@0: return engine; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: addEngineWithDetails: function SRCH_SVC_addEWD(aName, aIconURL, aAlias, michael@0: aDescription, aMethod, michael@0: aTemplate) { michael@0: this._ensureInitialized(); michael@0: if (!aName) michael@0: FAIL("Invalid name passed to addEngineWithDetails!"); michael@0: if (!aMethod) michael@0: FAIL("Invalid method passed to addEngineWithDetails!"); michael@0: if (!aTemplate) michael@0: FAIL("Invalid template passed to addEngineWithDetails!"); michael@0: if (this._engines[aName]) michael@0: FAIL("An engine with that name already exists!", Cr.NS_ERROR_FILE_ALREADY_EXISTS); michael@0: michael@0: var engine = new Engine(getSanitizedFile(aName), SEARCH_DATA_XML, false); michael@0: engine._initFromMetadata(aName, aIconURL, aAlias, aDescription, michael@0: aMethod, aTemplate); michael@0: this._addEngineToStore(engine); michael@0: this.batchTask.disarm(); michael@0: this.batchTask.arm(); michael@0: }, michael@0: michael@0: addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL, michael@0: aConfirm, aCallback) { michael@0: LOG("addEngine: Adding \"" + aEngineURL + "\"."); michael@0: this._ensureInitialized(); michael@0: try { michael@0: var uri = makeURI(aEngineURL); michael@0: var engine = new Engine(uri, aDataType, false); michael@0: if (aCallback) { michael@0: engine._installCallback = function (errorCode) { michael@0: try { michael@0: if (errorCode == null) michael@0: aCallback.onSuccess(engine); michael@0: else michael@0: aCallback.onError(errorCode); michael@0: } catch (ex) { michael@0: Cu.reportError("Error invoking addEngine install callback: " + ex); michael@0: } michael@0: // Clear the reference to the callback now that it's been invoked. michael@0: engine._installCallback = null; michael@0: }; michael@0: } michael@0: engine._initFromURIAndLoad(); michael@0: } catch (ex) { michael@0: // Drop the reference to the callback, if set michael@0: if (engine) michael@0: engine._installCallback = null; michael@0: FAIL("addEngine: Error adding engine:\n" + ex, Cr.NS_ERROR_FAILURE); michael@0: } michael@0: engine._setIcon(aIconURL, false); michael@0: engine._confirm = aConfirm; michael@0: }, michael@0: michael@0: removeEngine: function SRCH_SVC_removeEngine(aEngine) { michael@0: this._ensureInitialized(); michael@0: if (!aEngine) michael@0: FAIL("no engine passed to removeEngine!"); michael@0: michael@0: var engineToRemove = null; michael@0: for (var e in this._engines) { michael@0: if (aEngine.wrappedJSObject == this._engines[e]) michael@0: engineToRemove = this._engines[e]; michael@0: } michael@0: michael@0: if (!engineToRemove) michael@0: FAIL("removeEngine: Can't find engine to remove!", Cr.NS_ERROR_FILE_NOT_FOUND); michael@0: michael@0: if (engineToRemove == this.currentEngine) { michael@0: this._currentEngine = null; michael@0: } michael@0: michael@0: if (engineToRemove == this.defaultEngine) { michael@0: this._defaultEngine = null; michael@0: } michael@0: michael@0: if (engineToRemove._readOnly) { michael@0: // Just hide it (the "hidden" setter will notify) and remove its alias to michael@0: // avoid future conflicts with other engines. michael@0: engineToRemove.hidden = true; michael@0: engineToRemove.alias = null; michael@0: } else { michael@0: // Cancel the serialized task if it's pending. Since the task is a michael@0: // synchronous function, we don't need to wait on the "finalize" method. michael@0: if (engineToRemove._lazySerializeTask) { michael@0: engineToRemove._lazySerializeTask.disarm(); michael@0: engineToRemove._lazySerializeTask = null; michael@0: } michael@0: michael@0: // Remove the engine file from disk (this might throw) michael@0: engineToRemove._remove(); michael@0: engineToRemove._file = null; michael@0: michael@0: // Remove the engine from _sortedEngines michael@0: var index = this._sortedEngines.indexOf(engineToRemove); michael@0: if (index == -1) michael@0: FAIL("Can't find engine to remove in _sortedEngines!", Cr.NS_ERROR_FAILURE); michael@0: this.__sortedEngines.splice(index, 1); michael@0: michael@0: // Remove the engine from the internal store michael@0: delete this._engines[engineToRemove.name]; michael@0: michael@0: notifyAction(engineToRemove, SEARCH_ENGINE_REMOVED); michael@0: michael@0: // Since we removed an engine, we need to update the preferences. michael@0: this._saveSortedEngineList(); michael@0: } michael@0: }, michael@0: michael@0: moveEngine: function SRCH_SVC_moveEngine(aEngine, aNewIndex) { michael@0: this._ensureInitialized(); michael@0: if ((aNewIndex > this._sortedEngines.length) || (aNewIndex < 0)) michael@0: FAIL("SRCH_SVC_moveEngine: Index out of bounds!"); michael@0: if (!(aEngine instanceof Ci.nsISearchEngine)) michael@0: FAIL("SRCH_SVC_moveEngine: Invalid engine passed to moveEngine!"); michael@0: if (aEngine.hidden) michael@0: FAIL("moveEngine: Can't move a hidden engine!", Cr.NS_ERROR_FAILURE); michael@0: michael@0: var engine = aEngine.wrappedJSObject; michael@0: michael@0: var currentIndex = this._sortedEngines.indexOf(engine); michael@0: if (currentIndex == -1) michael@0: FAIL("moveEngine: Can't find engine to move!", Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: // Our callers only take into account non-hidden engines when calculating michael@0: // aNewIndex, but we need to move it in the array of all engines, so we michael@0: // need to adjust aNewIndex accordingly. To do this, we count the number michael@0: // of hidden engines in the list before the engine that we're taking the michael@0: // place of. We do this by first finding newIndexEngine (the engine that michael@0: // we were supposed to replace) and then iterating through the complete michael@0: // engine list until we reach it, increasing aNewIndex for each hidden michael@0: // engine we find on our way there. michael@0: // michael@0: // This could be further simplified by having our caller pass in michael@0: // newIndexEngine directly instead of aNewIndex. michael@0: var newIndexEngine = this._getSortedEngines(false)[aNewIndex]; michael@0: if (!newIndexEngine) michael@0: FAIL("moveEngine: Can't find engine to replace!", Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: for (var i = 0; i < this._sortedEngines.length; ++i) { michael@0: if (newIndexEngine == this._sortedEngines[i]) michael@0: break; michael@0: if (this._sortedEngines[i].hidden) michael@0: aNewIndex++; michael@0: } michael@0: michael@0: if (currentIndex == aNewIndex) michael@0: return; // nothing to do! michael@0: michael@0: // Move the engine michael@0: var movedEngine = this.__sortedEngines.splice(currentIndex, 1)[0]; michael@0: this.__sortedEngines.splice(aNewIndex, 0, movedEngine); michael@0: michael@0: notifyAction(engine, SEARCH_ENGINE_CHANGED); michael@0: michael@0: // Since we moved an engine, we need to update the preferences. michael@0: this._saveSortedEngineList(); michael@0: }, michael@0: michael@0: restoreDefaultEngines: function SRCH_SVC_resetDefaultEngines() { michael@0: this._ensureInitialized(); michael@0: for each (var e in this._engines) { michael@0: // Unhide all default engines michael@0: if (e.hidden && e._isDefault) michael@0: e.hidden = false; michael@0: } michael@0: }, michael@0: michael@0: get defaultEngine() { michael@0: this._ensureInitialized(); michael@0: if (!this._defaultEngine) { michael@0: let defPref = BROWSER_SEARCH_PREF + "defaultenginename"; michael@0: let defaultEngine = this.getEngineByName(getLocalizedPref(defPref, "")) michael@0: if (!defaultEngine) michael@0: defaultEngine = this._getSortedEngines(false)[0] || null; michael@0: this._defaultEngine = defaultEngine; michael@0: } michael@0: if (this._defaultEngine.hidden) michael@0: return this._getSortedEngines(false)[0]; michael@0: return this._defaultEngine; michael@0: }, michael@0: michael@0: set defaultEngine(val) { michael@0: this._ensureInitialized(); michael@0: // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers), michael@0: // and sometimes we get raw Engine JS objects (callers in this file), so michael@0: // handle both. michael@0: if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine)) michael@0: FAIL("Invalid argument passed to defaultEngine setter"); michael@0: michael@0: let newDefaultEngine = this.getEngineByName(val.name); michael@0: if (!newDefaultEngine) michael@0: FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: if (newDefaultEngine == this._defaultEngine) michael@0: return; michael@0: michael@0: this._defaultEngine = newDefaultEngine; michael@0: michael@0: // Set a flag to keep track that this setter was called properly, not by michael@0: // setting the pref alone. michael@0: this._changingDefaultEngine = true; michael@0: let defPref = BROWSER_SEARCH_PREF + "defaultenginename"; michael@0: // If we change the default engine in the future, that change should impact michael@0: // users who have switched away from and then back to the build's "default" michael@0: // engine. So clear the user pref when the defaultEngine is set to the michael@0: // build's default engine, so that the defaultEngine getter falls back to michael@0: // whatever the default is. michael@0: if (this._defaultEngine == this._originalDefaultEngine) { michael@0: Services.prefs.clearUserPref(defPref); michael@0: } michael@0: else { michael@0: setLocalizedPref(defPref, this._defaultEngine.name); michael@0: } michael@0: this._changingDefaultEngine = false; michael@0: michael@0: notifyAction(this._defaultEngine, SEARCH_ENGINE_DEFAULT); michael@0: }, michael@0: michael@0: get currentEngine() { michael@0: this._ensureInitialized(); michael@0: if (!this._currentEngine) { michael@0: let selectedEngine = getLocalizedPref(BROWSER_SEARCH_PREF + michael@0: "selectedEngine"); michael@0: this._currentEngine = this.getEngineByName(selectedEngine); michael@0: } michael@0: michael@0: if (!this._currentEngine || this._currentEngine.hidden) michael@0: this._currentEngine = this.defaultEngine; michael@0: return this._currentEngine; michael@0: }, michael@0: michael@0: set currentEngine(val) { michael@0: this._ensureInitialized(); michael@0: // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers), michael@0: // and sometimes we get raw Engine JS objects (callers in this file), so michael@0: // handle both. michael@0: if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine)) michael@0: FAIL("Invalid argument passed to currentEngine setter"); michael@0: michael@0: var newCurrentEngine = this.getEngineByName(val.name); michael@0: if (!newCurrentEngine) michael@0: FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED); michael@0: michael@0: if (newCurrentEngine == this._currentEngine) michael@0: return; michael@0: michael@0: this._currentEngine = newCurrentEngine; michael@0: michael@0: var currentEnginePref = BROWSER_SEARCH_PREF + "selectedEngine"; michael@0: michael@0: // Set a flag to keep track that this setter was called properly, not by michael@0: // setting the pref alone. michael@0: this._changingCurrentEngine = true; michael@0: // If we change the default engine in the future, that change should impact michael@0: // users who have switched away from and then back to the build's "default" michael@0: // engine. So clear the user pref when the currentEngine is set to the michael@0: // build's default engine, so that the currentEngine getter falls back to michael@0: // whatever the default is. michael@0: if (this._currentEngine == this._originalDefaultEngine) { michael@0: Services.prefs.clearUserPref(currentEnginePref); michael@0: } michael@0: else { michael@0: setLocalizedPref(currentEnginePref, this._currentEngine.name); michael@0: } michael@0: this._changingCurrentEngine = false; michael@0: michael@0: notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT); michael@0: }, michael@0: michael@0: // nsIObserver michael@0: observe: function SRCH_SVC_observe(aEngine, aTopic, aVerb) { michael@0: switch (aTopic) { michael@0: case SEARCH_ENGINE_TOPIC: michael@0: switch (aVerb) { michael@0: case SEARCH_ENGINE_LOADED: michael@0: var engine = aEngine.QueryInterface(Ci.nsISearchEngine); michael@0: LOG("nsSearchService::observe: Done installation of " + engine.name michael@0: + "."); michael@0: this._addEngineToStore(engine.wrappedJSObject); michael@0: if (engine.wrappedJSObject._useNow) { michael@0: LOG("nsSearchService::observe: setting current"); michael@0: this.currentEngine = aEngine; michael@0: } michael@0: this.batchTask.disarm(); michael@0: this.batchTask.arm(); michael@0: break; michael@0: case SEARCH_ENGINE_CHANGED: michael@0: case SEARCH_ENGINE_REMOVED: michael@0: this.batchTask.disarm(); michael@0: this.batchTask.arm(); michael@0: break; michael@0: } michael@0: break; michael@0: michael@0: case QUIT_APPLICATION_TOPIC: michael@0: this._removeObservers(); michael@0: break; michael@0: michael@0: case "nsPref:changed": michael@0: let currPref = BROWSER_SEARCH_PREF + "selectedEngine"; michael@0: let defPref = BROWSER_SEARCH_PREF + "defaultenginename"; michael@0: if (aVerb == currPref && !this._changingCurrentEngine) { michael@0: this._setEngineByPref("currentEngine", currPref); michael@0: } else if (aVerb == defPref && !this._changingDefaultEngine) { michael@0: this._setEngineByPref("defaultEngine", defPref); michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: // nsITimerCallback michael@0: notify: function SRCH_SVC_notify(aTimer) { michael@0: LOG("_notify: checking for updates"); michael@0: michael@0: if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true)) michael@0: return; michael@0: michael@0: // Our timer has expired, but unfortunately, we can't get any data from it. michael@0: // Therefore, we need to walk our engine-list, looking for expired engines michael@0: var currentTime = Date.now(); michael@0: LOG("currentTime: " + currentTime); michael@0: for each (engine in this._engines) { michael@0: engine = engine.wrappedJSObject; michael@0: if (!engine._hasUpdates) michael@0: continue; michael@0: michael@0: LOG("checking " + engine.name); michael@0: michael@0: var expirTime = engineMetadataService.getAttr(engine, "updateexpir"); michael@0: LOG("expirTime: " + expirTime + "\nupdateURL: " + engine._updateURL + michael@0: "\niconUpdateURL: " + engine._iconUpdateURL); michael@0: michael@0: var engineExpired = expirTime <= currentTime; michael@0: michael@0: if (!expirTime || !engineExpired) { michael@0: LOG("skipping engine"); michael@0: continue; michael@0: } michael@0: michael@0: LOG(engine.name + " has expired"); michael@0: michael@0: engineUpdateService.update(engine); michael@0: michael@0: // Schedule the next update michael@0: engineUpdateService.scheduleNextUpdate(engine); michael@0: michael@0: } // end engine iteration michael@0: }, michael@0: michael@0: _addObservers: function SRCH_SVC_addObservers() { michael@0: Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, false); michael@0: Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC, false); michael@0: Services.prefs.addObserver(BROWSER_SEARCH_PREF + "defaultenginename", this, false); michael@0: Services.prefs.addObserver(BROWSER_SEARCH_PREF + "selectedEngine", this, false); michael@0: michael@0: AsyncShutdown.profileBeforeChange.addBlocker( michael@0: "Search service: shutting down", michael@0: () => Task.spawn(function () { michael@0: if (this._batchTask) { michael@0: yield this._batchTask.finalize().then(null, Cu.reportError); michael@0: } michael@0: yield engineMetadataService.finalize(); michael@0: }.bind(this)) michael@0: ); michael@0: }, michael@0: michael@0: _removeObservers: function SRCH_SVC_removeObservers() { michael@0: Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC); michael@0: Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC); michael@0: Services.prefs.removeObserver(BROWSER_SEARCH_PREF + "defaultenginename", this); michael@0: Services.prefs.removeObserver(BROWSER_SEARCH_PREF + "selectedEngine", this); michael@0: }, michael@0: michael@0: QueryInterface: function SRCH_SVC_QI(aIID) { michael@0: if (aIID.equals(Ci.nsIBrowserSearchService) || michael@0: aIID.equals(Ci.nsIObserver) || michael@0: aIID.equals(Ci.nsITimerCallback) || michael@0: aIID.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }; michael@0: michael@0: var engineMetadataService = { michael@0: _jsonFile: OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json"), michael@0: michael@0: /** michael@0: * Possible values for |_initState|. michael@0: * michael@0: * We have two paths to perform initialization: a default asynchronous michael@0: * path and a fallback synchronous path that can interrupt the async michael@0: * path. For this reason, initialization is actually something of a michael@0: * finite state machine, represented with the following states: michael@0: * michael@0: * @enum michael@0: */ michael@0: _InitStates: { michael@0: NOT_STARTED: "NOT_STARTED" michael@0: /**Initialization has not started*/, michael@0: FINISHED_SUCCESS: "FINISHED_SUCCESS" michael@0: /**Setup complete, with a success*/ michael@0: }, michael@0: michael@0: /** michael@0: * The latest step completed by initialization. One of |InitStates| michael@0: * michael@0: * @type {engineMetadataService._InitStates} michael@0: */ michael@0: _initState: null, michael@0: michael@0: // A promise fulfilled once initialization is complete michael@0: _initializer: null, michael@0: michael@0: /** michael@0: * Asynchronous initializer michael@0: * michael@0: * Note: In the current implementation, initialization never fails. michael@0: */ michael@0: init: function epsInit() { michael@0: if (!this._initializer) { michael@0: // Launch asynchronous initialization michael@0: let initializer = this._initializer = Promise.defer(); michael@0: TaskUtils.spawn((function task_init() { michael@0: LOG("metadata init: starting"); michael@0: switch (this._initState) { michael@0: case engineMetadataService._InitStates.NOT_STARTED: michael@0: // 1. Load json file if it exists michael@0: try { michael@0: let contents = yield OS.File.read(this._jsonFile); michael@0: if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) { michael@0: // No need to pursue asynchronous initialization, michael@0: // synchronous fallback was called and has finished. michael@0: return; michael@0: } michael@0: this._store = JSON.parse(new TextDecoder().decode(contents)); michael@0: } catch (ex) { michael@0: if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) { michael@0: // No need to pursue asynchronous initialization, michael@0: // synchronous fallback was called and has finished. michael@0: return; michael@0: } michael@0: // Couldn't load json, use an empty store michael@0: LOG("metadata init: could not load JSON file " + ex); michael@0: this._store = {}; michael@0: } michael@0: break; michael@0: michael@0: default: michael@0: throw new Error("metadata init: invalid state " + this._initState); michael@0: } michael@0: michael@0: this._initState = this._InitStates.FINISHED_SUCCESS; michael@0: LOG("metadata init: complete"); michael@0: }).bind(this)).then( michael@0: // 3. Inform any observers michael@0: function onSuccess() { michael@0: initializer.resolve(); michael@0: }, michael@0: function onError() { michael@0: initializer.reject(); michael@0: } michael@0: ); michael@0: } michael@0: return TaskUtils.captureErrors(this._initializer.promise); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronous implementation of initializer michael@0: * michael@0: * This initializer is able to pick wherever the async initializer michael@0: * is waiting. The asynchronous initializer is expected to stop michael@0: * if it detects that the synchronous initializer has completed michael@0: * initialization. michael@0: */ michael@0: syncInit: function epsSyncInit() { michael@0: LOG("metadata syncInit start"); michael@0: if (this._initState == engineMetadataService._InitStates.FINISHED_SUCCESS) { michael@0: return; michael@0: } michael@0: switch (this._initState) { michael@0: case engineMetadataService._InitStates.NOT_STARTED: michael@0: let jsonFile = new FileUtils.File(this._jsonFile); michael@0: // 1. Load json file if it exists michael@0: if (jsonFile.exists()) { michael@0: try { michael@0: let uri = Services.io.newFileURI(jsonFile); michael@0: let stream = Services.io.newChannelFromURI(uri).open(); michael@0: this._store = parseJsonFromStream(stream); michael@0: } catch (x) { michael@0: LOG("metadata syncInit: could not load JSON file " + x); michael@0: this._store = {}; michael@0: } michael@0: } else { michael@0: LOG("metadata syncInit: using an empty store"); michael@0: this._store = {}; michael@0: } michael@0: michael@0: this._initState = this._InitStates.FINISHED_SUCCESS; michael@0: break; michael@0: michael@0: default: michael@0: throw new Error("metadata syncInit: invalid state " + this._initState); michael@0: } michael@0: michael@0: // 3. Inform any observers michael@0: if (this._initializer) { michael@0: this._initializer.resolve(); michael@0: } else { michael@0: this._initializer = Promise.resolve(); michael@0: } michael@0: LOG("metadata syncInit end"); michael@0: }, michael@0: michael@0: getAttr: function epsGetAttr(engine, name) { michael@0: let record = this._store[engine._id]; michael@0: if (!record) { michael@0: return null; michael@0: } michael@0: michael@0: // attr names must be lower case michael@0: let aName = name.toLowerCase(); michael@0: if (!record[aName]) michael@0: return null; michael@0: return record[aName]; michael@0: }, michael@0: michael@0: _setAttr: function epsSetAttr(engine, name, value) { michael@0: // attr names must be lower case michael@0: name = name.toLowerCase(); michael@0: let db = this._store; michael@0: let record = db[engine._id]; michael@0: if (!record) { michael@0: record = db[engine._id] = {}; michael@0: } michael@0: if (!record[name] || (record[name] != value)) { michael@0: record[name] = value; michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Set one metadata attribute for an engine. michael@0: * michael@0: * If an actual change has taken place, the attribute is committed michael@0: * automatically (and lazily), using this._commit. michael@0: * michael@0: * @param {nsISearchEngine} engine The engine to update. michael@0: * @param {string} key The name of the attribute. Case-insensitive. In michael@0: * the current implementation, this _must not_ conflict with properties michael@0: * of |Object|. michael@0: * @param {*} value A value to store. michael@0: */ michael@0: setAttr: function epsSetAttr(engine, key, value) { michael@0: if (this._setAttr(engine, key, value)) { michael@0: this._commit(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Bulk set metadata attributes for a number of engines. michael@0: * michael@0: * If actual changes have taken place, the store is committed michael@0: * automatically (and lazily), using this._commit. michael@0: * michael@0: * @param {Array.<{engine: nsISearchEngine, key: string, value: *}>} changes michael@0: * The list of changes to effect. See |setAttr| for the documentation of michael@0: * |engine|, |key|, |value|. michael@0: */ michael@0: setAttrs: function epsSetAttrs(changes) { michael@0: let self = this; michael@0: let changed = false; michael@0: changes.forEach(function(change) { michael@0: changed |= self._setAttr(change.engine, change.key, change.value); michael@0: }); michael@0: if (changed) { michael@0: this._commit(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Flush any waiting write. michael@0: */ michael@0: finalize: function () this._lazyWriter ? this._lazyWriter.finalize() michael@0: : Promise.resolve(), michael@0: michael@0: /** michael@0: * Commit changes to disk, asynchronously. michael@0: * michael@0: * Calls to this function are actually delayed by LAZY_SERIALIZE_DELAY michael@0: * (= 100ms). If the function is called again before the expiration of michael@0: * the delay, commits are merged and the function is again delayed by michael@0: * the same amount of time. michael@0: * michael@0: * @param aStore is an optional parameter specifying the object to serialize. michael@0: * If not specified, this._store is used. michael@0: */ michael@0: _commit: function epsCommit(aStore) { michael@0: LOG("metadata _commit: start"); michael@0: let store = aStore || this._store; michael@0: if (!store) { michael@0: LOG("metadata _commit: nothing to do"); michael@0: return; michael@0: } michael@0: michael@0: if (!this._lazyWriter) { michael@0: LOG("metadata _commit: initializing lazy writer"); michael@0: function writeCommit() { michael@0: LOG("metadata writeCommit: start"); michael@0: let data = gEncoder.encode(JSON.stringify(store)); michael@0: let path = engineMetadataService._jsonFile; michael@0: LOG("metadata writeCommit: path " + path); michael@0: let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" }); michael@0: promise = promise.then( michael@0: function onSuccess() { michael@0: Services.obs.notifyObservers(null, michael@0: SEARCH_SERVICE_TOPIC, michael@0: SEARCH_SERVICE_METADATA_WRITTEN); michael@0: LOG("metadata writeCommit: done"); michael@0: } michael@0: ); michael@0: // Use our error logging instead of the default one. michael@0: return TaskUtils.captureErrors(promise).then(null, () => {}); michael@0: } michael@0: this._lazyWriter = new DeferredTask(writeCommit, LAZY_SERIALIZE_DELAY); michael@0: } michael@0: LOG("metadata _commit: (re)setting timer"); michael@0: this._lazyWriter.disarm(); michael@0: this._lazyWriter.arm(); michael@0: }, michael@0: _lazyWriter: null michael@0: }; michael@0: michael@0: engineMetadataService._initState = engineMetadataService._InitStates.NOT_STARTED; michael@0: michael@0: const SEARCH_UPDATE_LOG_PREFIX = "*** Search update: "; michael@0: michael@0: /** michael@0: * Outputs aText to the JavaScript console as well as to stdout, if the search michael@0: * logging pref (browser.search.update.log) is set to true. michael@0: */ michael@0: function ULOG(aText) { michael@0: if (getBoolPref(BROWSER_SEARCH_PREF + "update.log", false)) { michael@0: dump(SEARCH_UPDATE_LOG_PREFIX + aText + "\n"); michael@0: Services.console.logStringMessage(aText); michael@0: } michael@0: } michael@0: michael@0: var engineUpdateService = { michael@0: scheduleNextUpdate: function eus_scheduleNextUpdate(aEngine) { michael@0: var interval = aEngine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL; michael@0: var milliseconds = interval * 86400000; // |interval| is in days michael@0: engineMetadataService.setAttr(aEngine, "updateexpir", michael@0: Date.now() + milliseconds); michael@0: }, michael@0: michael@0: update: function eus_Update(aEngine) { michael@0: let engine = aEngine.wrappedJSObject; michael@0: ULOG("update called for " + aEngine._name); michael@0: if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true) || !engine._hasUpdates) michael@0: return; michael@0: michael@0: // We use the cache to store updated app engines, so refuse to update if the michael@0: // cache is disabled. michael@0: if (engine._readOnly && michael@0: !getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) michael@0: return; michael@0: michael@0: let testEngine = null; michael@0: let updateURL = engine._getURLOfType(URLTYPE_OPENSEARCH); michael@0: let updateURI = (updateURL && updateURL._hasRelation("self")) ? michael@0: updateURL.getSubmission("", engine).uri : michael@0: makeURI(engine._updateURL); michael@0: if (updateURI) { michael@0: if (engine._isDefault && !updateURI.schemeIs("https")) { michael@0: ULOG("Invalid scheme for default engine update"); michael@0: return; michael@0: } michael@0: michael@0: let dataType = engineMetadataService.getAttr(engine, "updatedatatype"); michael@0: if (!dataType) { michael@0: ULOG("No loadtype to update engine!"); michael@0: return; michael@0: } michael@0: michael@0: ULOG("updating " + engine.name + " from " + updateURI.spec); michael@0: testEngine = new Engine(updateURI, dataType, false); michael@0: testEngine._engineToUpdate = engine; michael@0: testEngine._initFromURIAndLoad(); michael@0: } else michael@0: ULOG("invalid updateURI"); michael@0: michael@0: if (engine._iconUpdateURL) { michael@0: // If we're updating the engine too, use the new engine object, michael@0: // otherwise use the existing engine object. michael@0: (testEngine || engine)._setIcon(engine._iconUpdateURL, true); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SearchService]); michael@0: michael@0: #include ../../../toolkit/modules/debug.js