# ***** BEGIN LICENSE BLOCK ***** # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is the Browser Search Service. # # The Initial Developer of the Original Code is # Google Inc. # Portions created by the Initial Developer are Copyright (C) 2005 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Ben Goodger (Original author) # Gavin Sharp # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** const Ci = Components.interfaces; const Cc = Components.classes; const Cr = Components.results; const PERMS_FILE = 0644; const PERMS_DIRECTORY = 0755; const MODE_RDONLY = 0x01; const MODE_WRONLY = 0x02; const MODE_CREATE = 0x08; const MODE_APPEND = 0x10; const MODE_TRUNCATE = 0x20; const NS_APP_SEARCH_DIR_LIST = "SrchPluginsDL"; const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns"; // See documentation in nsIBrowserSearchService.idl. const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified"; const XPCOM_SHUTDOWN_TOPIC = "xpcom-shutdown"; const SEARCH_ENGINE_REMOVED = "engine-removed"; const SEARCH_ENGINE_ADDED = "engine-added"; const SEARCH_ENGINE_CHANGED = "engine-changed"; const SEARCH_ENGINE_LOADED = "engine-loaded"; const SEARCH_ENGINE_CURRENT = "engine-current"; const SEARCH_TYPE_MOZSEARCH = Ci.nsISearchEngine.TYPE_MOZSEARCH; const SEARCH_TYPE_OPENSEARCH = Ci.nsISearchEngine.TYPE_OPENSEARCH; const SEARCH_TYPE_SHERLOCK = Ci.nsISearchEngine.TYPE_SHERLOCK; const SEARCH_DATA_XML = Ci.nsISearchEngine.DATA_XML; const SEARCH_DATA_TEXT = Ci.nsISearchEngine.DATA_TEXT; // File extensions for search plugin description files const XML_FILE_EXT = "xml"; const SHERLOCK_FILE_EXT = "src"; const ICON_DATAURL_PREFIX = "data:image/x-icon;base64,"; // Supported extensions for Sherlock plugin icons const SHERLOCK_ICON_EXTENSIONS = [".gif", ".jpg", ".jpeg", ".png"]; // Set an arbitrary cap on the maximum icon size. Without this, large icons can // cause big delays when loading them at startup. const MAX_ICON_SIZE = 10000; // Default charset to use for sending search parameters. This is used to match // previous nsInternetSearchService behavior. const DEFAULT_QUERY_CHARSET = "ISO-8859-1"; const SEARCH_BUNDLE = "chrome://browser/locale/search.properties"; const BRAND_BUNDLE = "chrome://branding/locale/brand.properties"; const kOpenSearchNS_10 = "http://a9.com/-/spec/opensearchdescription/1.0/"; const kOpenSearchNS_11 = "http://a9.com/-/spec/opensearchdescription/1.1/"; const kOpenSearchLocalName = "OpenSearchDescription"; const kMozSearchNS_10 = "http://www.mozilla.org/2006/browser/search/"; const kMozSearchLocalName = "SearchPlugin"; // Empty base document used to serialize engines to file. const EMPTY_DOC = "\n" + "<" + kMozSearchLocalName + " xmlns=\"" + kMozSearchNS_10 + "\"" + " xmlns:os=\"" + kOpenSearchNS_11 + "\"" + "/>"; const BROWSER_SEARCH_PREF = "browser.search."; // Unsupported search parameters. // XXX We do use inputEncoding - should consider having it available. This // would require doing multiple parameter substition, so just having // searchTerms is sufficient for now. const kIllegalWords = /(\{count\})|(\{startIndex\})|(\{startPage\})|(\{language\})|(\{outputEncoding\})|(\{inputEncoding\})/; const kValidWords = /\{searchTerms\}/gi; const kUserDefined = "{searchTerms}"; // Returns false for whitespace-only or commented out lines in a // Sherlock file, true otherwise. function isUsefulLine(aLine) { return !(/^\s*($|#)/i.test(aLine)); } /** * Used to determine whether an "input" line from a Sherlock file is a "user * defined" input. That is, check for the string "user", preceded by either * whitespace or a quote, followed by any of ">", "=", """, "'", whitespace, * a slash, "+", or EOL. */ const kIsUserInput = /(\s|["'=])user(\s|[>="'\/\\+]|$)/i; /** * Prefixed to all search debug output. */ const SEARCH_LOG_PREFIX = "*** Search: "; /** * Outputs aText to the JavaScript console as well as to stdout, if the search * logging pref (browser.search.log) is set to true. */ function LOG(aText) { var prefB = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); var shouldLog = false; try { shouldLog = prefB.getBoolPref(BROWSER_SEARCH_PREF + "log"); } catch (ex) {} if (shouldLog) { dump(SEARCH_LOG_PREFIX + aText + "\n"); var consoleService = Cc["@mozilla.org/consoleservice;1"]. getService(Ci.nsIConsoleService); consoleService.logStringMessage(aText); } } function ERROR(message, resultCode) { NS_ASSERT(false, SEARCH_LOG_PREFIX + message); throw resultCode; } /** * Ensures an assertion is met before continuing. Should be used to indicate * fatal errors. * @param assertion * An assertion that must be met * @param message * A message to display if the assertion is not met * @param resultCode * The NS_ERROR_* value to throw if the assertion is not met * @throws resultCode */ function ENSURE_WARN(assertion, message, resultCode) { NS_ASSERT(assertion, SEARCH_LOG_PREFIX + message); if (!assertion) throw resultCode; } /** * Ensures an assertion is met before continuing, but does not warn the user. * Used to handle normal failure conditions. * @param assertion * An assertion that must be met * @param message * A message to display if the assertion is not met * @param resultCode * The NS_ERROR_* value to throw if the assertion is not met * @throws resultCode */ function ENSURE(assertion, message, resultCode) { if (!assertion) { LOG(message); throw resultCode; } } /** * Ensures an argument assertion is met before continuing. * @param assertion * An argument assertion that must be met * @param message * A message to display if the assertion is not met * @throws NS_ERROR_INVALID_ARG for invalid arguments */ function ENSURE_ARG(assertion, message) { ENSURE(assertion, message, Cr.NS_ERROR_INVALID_ARG); } //XXX Bug 326854: no btoa for components, use our own /** * Encodes an array of bytes into a string using the base 64 encoding scheme. * @param aBytes * An array of bytes to encode. */ function b64(aBytes) { const B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var out = "", bits = 0, i, j; while (aBytes.length >= 3) { bits = 0; for (i=0; i<3; i++) { bits <<= 8; bits |= aBytes[i]; } for (j=18; j>=0; j-=6) out += B64_CHARS[(bits>>j) & 0x3F]; aBytes.splice(0, 3); } switch (aBytes.length) { case 2: out += B64_CHARS[(aBytes[0]>>2) & 0x3F]; out += B64_CHARS[((aBytes[0] & 0x03) << 4) | ((aBytes[1] >> 4) & 0x0F)]; out += B64_CHARS[((aBytes[1] & 0x0F) << 2)]; out += "="; break; case 1: out += B64_CHARS[(aBytes[0]>>2) & 0x3F]; out += B64_CHARS[(aBytes[0] & 0x03) << 4]; out += "=="; break; } return out; } function iconLoadListener(aChannel, aEngine) { this._countRead = 0; this._channel = aChannel; this._bytes = [], this._engine = aEngine.QueryInterface(Ci.nsISearchEngine); } iconLoadListener.prototype = { _channel: null, _countRead: 0, _engine: null, _stream: null, QueryInterface: function SRCH_iconLoad_QI(aIID) { if (aIID.equals(Ci.nsISupports) || aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsIStreamListener) || aIID.equals(Ci.nsIChannelEventSink) || aIID.equals(Ci.nsIInterfaceRequestor) || // See XXX comment below aIID.equals(Ci.nsIHttpEventSink) || aIID.equals(Ci.nsIProgressEventSink) || false) return this; throw Cr.NS_ERROR_NO_INTERFACE; }, // nsIRequestObserver onStartRequest: function SRCH_iconLoadStartR(aRequest, aContext) { LOG("iconLoadListener: Starting icon request."); this._stream = Cc["@mozilla.org/binaryinputstream;1"]. createInstance(Ci.nsIBinaryInputStream); }, onStopRequest: function SRCH_iconLoadStopR(aRequest, aContext, aStatusCode) { LOG("iconLoadListener: Stopping icon request."); var httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel); if ((httpChannel && httpChannel.requestSucceeded) && Components.isSuccessCode(aStatusCode) && this._countRead > 0) { if (this._countRead < MAX_ICON_SIZE) { var str = b64(this._bytes); this._engine._iconURI = makeURI(ICON_DATAURL_PREFIX + str); // The engine might not have a file yet, if it's being downloaded, // because the request for the engine file itself (_onLoad) may not yet // have occured. In that case, this change will be written to file when // _onLoad is called. if (this._engine._file) this._engine._serializeToFile(); notifyAction(this._engine, SEARCH_ENGINE_CHANGED); } } this._channel = null; this._engine = null; }, // nsIStreamListener onDataAvailable: function SRCH_iconLoadDAvailable(aRequest, aContext, aInputStream, aOffset, aCount) { this._stream.setInputStream(aInputStream); // Get a byte array of the data this._bytes = this._bytes.concat(this._stream.readByteArray(aCount)); this._countRead += aCount; }, // nsIChannelEventSink onChannelRedirect: function SRCH_iconLoadCRedirect(aOldChannel, aNewChannel, aFlags) { this._channel = aNewChannel; }, // nsIInterfaceRequestor getInterface: function SRCH_iconLoad_GI(aIID) { return this.QueryInterface(aIID); }, // XXX bug 253127 // nsIHttpEventSink onRedirect: function (aChannel, aNewChannel) {}, // nsIProgressEventSink onProgress: function (aRequest, aContext, aProgress, aProgressMax) {}, onStatus: function (aRequest, aContext, aStatus, aStatusArg) {} } /** * Used to verify a given DOM node's localName and namespaceURI. * @param aElement * The element to verify. * @param aLocalNameArray * An array of strings to compare against aElement's localName. * @param aNameSpaceArray * An array of strings to compare against aElement's namespaceURI. * * @returns false if aElement is null, or if it's localName or namespaceURI * does not match one of the elements in the aLocalNameArray or * aNameSpaceArray arrays, respectively. * @throws NS_ERROR_INVALID_ARG if aLocalNameArray or aNameSpaceArray are null. */ function checkNameSpace(aElement, aLocalNameArray, aNameSpaceArray) { ENSURE_ARG(aLocalNameArray && aNameSpaceArray, "missing aLocalNameArray or \ aNameSpaceArray for checkNameSpace"); return (aElement && (aLocalNameArray.indexOf(aElement.localName) != -1) && (aNameSpaceArray.indexOf(aElement.namespaceURI) != -1)); } /** * Safely close a nsISafeOutputStream. * @param aFOS * The file output stream to close. */ function closeSafeOutputStream(aFOS) { if (aFOS instanceof Ci.nsISafeOutputStream) { try { aFOS.finish(); return; } catch (e) { } } aFOS.close(); } /** * Wrapper function for nsIIOService::newURI. * @param aURLSpec * The URL string from which to create an nsIURI. * @returns an nsIURI object, or null if the creation of the URI failed. */ function makeURI(aURLSpec) { var ios = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); try { return ios.newURI(aURLSpec, null, null); } catch (ex) { } return null; } /** * Gets a directory from the directory service. * @param aKey * The directory service key indicating the directory to get. */ function getDir(aKey) { ENSURE_ARG(aKey, "getDir requires a directory key!"); var fileLocator = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); var dir = fileLocator.get(aKey, Ci.nsIFile); return dir; } // This isn't a full list - this is just copied over from // nsInternetSearchService to maintain backwards compat with Firefox 1.0.x const kCharsetCodes = []; kCharsetCodes[0] = "x-mac-roman"; kCharsetCodes[6] = "x-mac-greek"; kCharsetCodes[35] = "x-mac-turkish"; kCharsetCodes[513] = "ISO-8859-1"; kCharsetCodes[514] = "ISO-8859-2"; kCharsetCodes[517] = "ISO-8859-5"; kCharsetCodes[518] = "ISO-8859-6"; kCharsetCodes[519] = "ISO-8859-7"; kCharsetCodes[520] = "ISO-8859-8"; kCharsetCodes[521] = "ISO-8859-9"; kCharsetCodes[1049] = "IBM864"; kCharsetCodes[1280] = "windows-1252"; kCharsetCodes[1281] = "windows-1250"; kCharsetCodes[1282] = "windows-1251"; kCharsetCodes[1283] = "windows-1253"; kCharsetCodes[1284] = "windows-1254"; kCharsetCodes[1285] = "windows-1255"; kCharsetCodes[1286] = "windows-1256"; kCharsetCodes[1536] = "us-ascii"; kCharsetCodes[1584] = "GB2312"; kCharsetCodes[1585] = "x-gbk"; kCharsetCodes[1600] = "EUC-KR"; kCharsetCodes[2080] = "ISO-2022-JP"; kCharsetCodes[2096] = "ISO-2022-CN"; kCharsetCodes[2112] = "ISO-2022-KR"; kCharsetCodes[2336] = "EUC-JP"; kCharsetCodes[2352] = "GB2312"; kCharsetCodes[2353] = "x-euc-tw"; kCharsetCodes[2368] = "EUC-KR"; kCharsetCodes[2561] = "Shift_JIS"; kCharsetCodes[2562] = "KOI8-R"; kCharsetCodes[2563] = "Big5"; kCharsetCodes[2565] = "HZ-GB-2312"; /** * Gets a character set name from a given code. * @param aCode * One of the codes from the kCharsetCodes table, representing the * requested charset. * @returns the requested character set name, or the default character set name * if it doesn't exist. */ function getCharSetFromCode(aCode) { if (kCharsetCodes[aCode]) return kCharsetCodes[aCode]; return getLocalizedPref("intl.charset.default", DEFAULT_QUERY_CHARSET); } /** * Wrapper for nsIPrefBranch::getComplexValue. * @param aPrefName * The name of the pref to get. * @returns aDefault if the requested pref doesn't exist. */ function getLocalizedPref(aPrefName, aDefault) { var prefB = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); const nsIPLS = Ci.nsIPrefLocalizedString; try { return prefB.getComplexValue(aPrefName, nsIPLS).data; } catch (ex) {} return aDefault; } /** * Wrapper for nsIPrefBranch::setComplexValue. * @param aPrefName * The name of the pref to set. * @param aValue * The value of the pref. */ function setLocalizedPref(aPrefName, aValue) { var prefB = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); var pls = Cc["@mozilla.org/pref-localizedstring;1"]. createInstance(Ci.nsIPrefLocalizedString); pls.data = aValue; prefB.setComplexValue(aPrefName, Ci.nsIPrefLocalizedString, pls); } /** * Wrapper for nsIPrefBranch::setBoolPref. * @param aPrefName * The name of the pref to set. * @param aValue * The value of the pref. */ function setBoolPref(aName, aVal) { var prefB = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); prefB.setBoolPref(aName, aVal); } /** * Wrapper for nsIPrefBranch::getBoolPref. * @param aPrefName * The name of the pref to get. * @returns aDefault if the requested pref doesn't exist. */ function getBoolPref(aName, aDefault) { var prefB = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); try { return prefB.getBoolPref(aName); } catch (ex) { return aDefault; } } /** * Get a unique nsIFile object with a sanitized name, based on the engine name. * @param aName * A name to "sanitize". Can be an empty string, in which case a random * 8 character filename will be produced. * @param aExt * A file extension to use for the file. If not provided, defaults to * XML_FILE_EXT. * @returns A nsIFile object in the user's search engines directory with a * unique sanitized name. */ function getSanitizedFile(aName) { var fileName = sanitizeName(aName) + "." + XML_FILE_EXT; var file = getDir(NS_APP_USER_SEARCH_DIR); file.append(fileName); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); return file; } /** * Removes all characters not in the "chars" string from aName. * * @returns a sanitized name to be used as a filename, or a random name if a * sanitized name cannot be obtained (if aName contains no valid * characters). */ function sanitizeName(aName) { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; var name = aName.toLowerCase() .split("").filter(function (el) { return chars.indexOf(el) != -1; }).join(""); if (!name) { // Our input had no valid characters - use a random name var cl = chars.length; for (var i = 0; i < 8; ++i) name += chars.charAt(Math.round(Math.random() * cl)); } return name; } /** * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to * the state of the search service. * * @param aEngine * The nsISearchEngine object to which the change applies. * @param aVerb * A verb describing the change. * * @see nsIBrowserSearchService.idl */ function notifyAction(aEngine, aVerb) { var os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); LOG("NOTIFY: Engine: \"" + aEngine.name + "\"; Verb: \"" + aVerb + "\""); os.notifyObservers(aEngine, SEARCH_ENGINE_TOPIC, aVerb); } /** * Simple object representing a name/value pair. * @throws NS_ERROR_NOT_IMPLEMENTED if the provided value includes unsupported * parameters. * @see kIllegalWords. */ function QueryParameter(aName, aValue) { ENSURE_ARG(aName && aValue, "missing name or value for QueryParameter!"); ENSURE(!kIllegalWords.test(aValue), "Illegal value while creating a QueryParameter", Cr.NS_ERROR_NOT_IMPLEMENTED); this.name = aName; this.value = aValue; } /** * Creates an engineURL object, which holds the query URL and all parameters. * * @param aType * A string containing the name of the MIME type of the search results * returned by this URL. * @param aMethod * The HTTP request method. Must be a case insensitive value of either * "GET" or "POST". * @param aTemplate * The URL to which search queries should be sent. For GET requests, * must contain the string "{searchTerms}", to indicate where the user * entered search terms should be inserted. * * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag * * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported, or if aTemplate * includes unsupported parameters. * * @see kIllegalWords. */ function EngineURL(aType, aMethod, aTemplate) { ENSURE_ARG(aType && aMethod && aTemplate, "missing type, method or template for EngineURL!"); var method = aMethod.toUpperCase(); var type = aType.toLowerCase(); ENSURE_ARG(method == "GET" || method == "POST", "method passed to EngineURL must be \"GET\" or \"POST\""); ENSURE(type == "text/html", "EngineURLs must be of type text/html!", Cr.NS_ERROR_NOT_IMPLEMENTED); ENSURE(!kIllegalWords.test(aTemplate), "Invalid URL parameter!", Cr.NS_ERROR_NOT_IMPLEMENTED); this.type = type; this.method = method; this.template = aTemplate; this.params = []; } EngineURL.prototype = { addParam: function SRCH_EURL_addParam(aName, aValue) { this.params.push(new QueryParameter(aName, aValue)); }, getSubmission: function SRCH_EURL_getSubmission(aData) { /** * From an array of QueryParameter objects, generates a string in the * application/x-www-form-urlencoded format: * name=value&name=value&name=value... * @param aParams * An array of QueryParameter objects * @param aData * Data to be substituted into parameter values using the * |kValidWords| regexp * @returns A string of encoded param names and values in * application/x-www-form-urlencoded format. */ function makeQueryString(aParams, aData) { var str = ""; for (var i = 0; i < aParams.length; ++i) { var param = aParams[i]; var value = param.value.replace(kValidWords, aData); str += (i > 0 ? "&" : "") + param.name + "=" + value; } return str; } var url = this.template.replace(kValidWords, aData); var postData = null; var dataString = makeQueryString(this.params, aData); if (this.method == "GET") { // GET method requests have no post data, and append the encoded // text to the url... if (url.indexOf("?") == -1 && dataString) url += "?"; url += dataString; } else if (this.method == "POST") { // POST method requests must wrap the encoded text in a MIME // stream and supply that as POSTDATA. var stringStream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); #ifdef MOZILLA_1_8_BRANCH # bug 318193 stringStream.setData(dataString, dataString.length); #else stringStream.data = dataString; #endif postData = Cc["@mozilla.org/network/mime-input-stream;1"]. createInstance(Ci.nsIMIMEInputStream); postData.addHeader("Content-Type", "application/x-www-form-urlencoded"); postData.addContentLength = true; postData.setData(stringStream); } return new Submission(makeURI(url), postData); }, /** * Serializes the engine object to a OpenSearch Url element. * @param aDoc * The document to use to create the Url element. * @param aElement * The element to which the created Url element is appended. * * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag */ _serializeToElement: function SRCH_EURL_serializeToEl(aDoc, aElement) { var url = aDoc.createElementNS(kOpenSearchNS_11, "Url"); url.setAttribute("type", this.type); url.setAttribute("method", this.method); url.setAttribute("template", this.template); for (var i = 0; i < this.params.length; ++i) { var param = aDoc.createElementNS(kOpenSearchNS_11, "Param"); param.setAttribute("name", this.params[i].name); param.setAttribute("value", this.params[i].value); url.appendChild(aDoc.createTextNode("\n ")); url.appendChild(param); } url.appendChild(aDoc.createTextNode("\n")); aElement.appendChild(url); } }; /** * nsISearchEngine constructor. * @param aLocation * A nsILocalFile or nsIURI object representing the location of the * search engine data file. * @param aSourceDataType * The data type of the file used to describe the engine. Must be either * DATA_XML or DATA_TEXT. * @param aIsReadOnly * Boolean indicating whether the engine should be treated as read-only. * Read only engines cannot be serialized to file. */ function Engine(aLocation, aSourceDataType, aIsReadOnly) { this._dataType = aSourceDataType; this._readOnly = aIsReadOnly; this._urls = []; if (aLocation instanceof Ci.nsILocalFile) { // we already have a file (e.g. loading engines from disk) this._file = aLocation; } else if (aLocation instanceof Ci.nsIURI) { this._uri = aLocation; switch (aLocation.scheme) { case "https": case "http": case "data": case "file": case "resource": this._uri = aLocation; break; default: ERROR("Invalid URI passed to the nsISearchEngine constructor", Cr.NS_ERROR_INVALID_ARG); } } else ERROR("Engine location is neither a File nor a URI object", Cr.NS_ERROR_INVALID_ARG); } Engine.prototype = { // The engine's alias. _alias: null, // The data describing the engine. Is either an array of lines, for Sherlock // files, or an XML document element, for XML plugins. _data: null, // The engine's data type. See data types (DATA_) defined above. _dataType: null, // Whether or not the engine is readonly. _readOnly: true, // The engine's description _description: "", // The file from which the plugin was loaded. _file: null, // Whether the engine is hidden from the user. _hidden: null, // The XMLHTTPRequest object used to download the engine. // (null for engines loaded from disk) _req: null, // The engine's name. _name: null, // The engine type. See engine types (TYPE_) defined above. _type: null, // The name of the charset used to submit the search terms. _queryCharset: null, // The URI object from which the engine was retrieved. // This is null for local plugins, and is only used for error messages and // logging. _uri: null, /** * Retrieves the data from the engine's file. If the engine's dataType is * XML, the document element is placed in the engine's data field. For text * engines, the data is just read directly from file and placed as an array * of lines in the engine's data field. */ _initFromFile: function SRCH_ENG_initFromFile() { ENSURE(this._file && this._file.exists(), "File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED); var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"]. createInstance(Ci.nsIFileInputStream); fileInStream.init(this._file, MODE_RDONLY, PERMS_FILE, false); switch (this._dataType) { case SEARCH_DATA_XML: var domParser = Cc["@mozilla.org/xmlextras/domparser;1"]. createInstance(Ci.nsIDOMParser); var doc = domParser.parseFromStream(fileInStream, "UTF-8", this._file.fileSize, "text/xml"); this._data = doc.documentElement; break; case SEARCH_DATA_TEXT: fileInStream.QueryInterface(Ci.nsILineInputStream); var line = { value: "" }; var more = false; var lines = []; do { more = fileInStream.readLine(line); // Filter out comments and whitespace-only lines if (isUsefulLine(line.value)) lines.push(line.value); } while (more); this._data = lines; break; default: ERROR("Bogus engine _dataType: \"" + this._dataType + "\"", Cr.NS_ERROR_UNEXPECTED); } fileInStream.close(); // Now that the data is loaded, initialize the engine object this._initFromData(); }, /** * Retrieves the engine data from a URI. * @param aURI * The URL to transfer from. */ _initFromURI: function SRCH_ENG_initFromURI() { ENSURE_WARN(this._uri instanceof Ci.nsIURI, "Must have URI when calling _initFromURI!", Cr.NS_ERROR_UNEXPECTED); LOG("_initFromURI: Downloading engine from: \"" + this._uri.spec + "\"."); var mimeType = ""; switch (this._dataType) { case SEARCH_DATA_XML: mimeType = "text/xml"; break; case SEARCH_DATA_TEXT: mimeType = "text/plain"; break; default: ERROR("Bogus engine _dataType: \"" + this._dataType + "\"", Cr.NS_ERROR_UNEXPECTED); } this._req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. createInstance(Ci.nsIXMLHttpRequest); this._req.open("GET", this._uri.spec, true); this._req.overrideMimeType(mimeType); this._req.setRequestHeader("Cache-Control", "no-cache"); var self = this; this._req.send(null); this._req.onerror = function (event) { self._onError(event); }; this._req.onload = function (event) { self._onLoad(event); }; }, /** * Handle an error during the load of an engine by prompting the user to * notify him that the load failed. */ _onError: function SRCH_ENG_onError(aEvent) { var sbs = Cc["@mozilla.org/intl/stringbundle;1"]. getService(Ci.nsIStringBundleService); var searchBundle = sbs.createBundle(SEARCH_BUNDLE); var brandBundle = sbs.createBundle(BRAND_BUNDLE); var brandName = brandBundle.GetStringFromName("brandShortName"); var title = searchBundle.GetStringFromName("error_loading_engine_title"); var text = searchBundle.formatStringFromName("error_loading_engine_msg", [brandName, this._location], 2); var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. getService(Ci.nsIWindowWatcher); ww.getNewPrompter(null).alert(title, text); }, /** * Handle the successful download of an engine. Initializes the engine and * triggers parsing of the data. The engine is then flushed to disk. Notifies * the search service once initialization is complete. */ _onLoad: function SRCH_ENG_onLoad(aEvent) { var httpChannel = this._req.channel.QueryInterface(Ci.nsIHttpChannel); if (this._req.readyState != 4 || !httpChannel.requestSucceeded) { this._onError(); LOG("_onLoad: Request for " + this._location + " failed!"); return; } switch (this._dataType) { case SEARCH_DATA_XML: this._data = this._req.responseXML.documentElement; break; case SEARCH_DATA_TEXT: this._data = this._req.responseText.split(/(\r\n|\n\r|\r|\n)/); // Filter out comments and whitespace-only lines. this._data.filter(isUsefulLine); break; default: this._onError(); LOG("_onLoad: Bogus engine _dataType: \"" + this._dataType + "\""); return; } try { // Initialize the engine from the obtained data this._initFromData(); } catch (ex) { // Report an error to the user LOG("_onLoad: Failed to init engine!\n" + ex); this._onError(); return; } // Write the engine to file this._serializeToFile(); // Notify the search service of the sucessful load notifyAction(this, SEARCH_ENGINE_LOADED); }, /** * Sets the .iconURI property of the engine. * * @param aIconURL * A URI string pointing to the engine's icon. Must have a http[s] or * data scheme. Icons with HTTP[S] schemes will be downloaded and * converted to data URIs for storage in the engine XML files, if * the engine is not readonly. */ _setIcon: function SRCH_ENG_setIcon(aIconURL) { var uri = makeURI(aIconURL); // Ignore bad URIs if (!uri) return; LOG("_setIcon: Setting icon url \"" + uri.spec + "\" for engine \"" + this.name + "\"."); // Only accept remote icons from http[s] switch (uri.scheme) { case "data": this._iconURI = uri; break; case "http": case "https": // No use downloading the icon if the engine file is read-only // XXX could store the data: URI in a pref... ew? if (!this._readOnly) { LOG("_setIcon: Downloading icon: \"" + uri.spec + "\" for engine: \"" + this.name + "\""); var ios = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); var chan = ios.newChannelFromURI(uri); var listener = new iconLoadListener(chan, this); chan.notificationCallbacks = listener; chan.asyncOpen(listener, null); } break; } }, /** * Initialize this Engine object from the collected data. */ _initFromData: function SRCH_ENG_initFromData() { ENSURE_WARN(this._data, "Can't init an engine with no data!", Cr.NS_ERROR_UNEXPECTED); // Find out what type of engine we are switch (this._dataType) { case SEARCH_DATA_XML: if (checkNameSpace(this._data, [kMozSearchLocalName], [kMozSearchNS_10])) { LOG("_init: Initing MozSearch plugin from " + this._location); this._type = SEARCH_TYPE_MOZSEARCH; this._parseAsMozSearch(); } else if (checkNameSpace(this._data, [kOpenSearchLocalName], [kOpenSearchNS_11, kOpenSearchNS_10])) { LOG("_init: Initing OpenSearch plugin from " + this._location); this._type = SEARCH_TYPE_OPENSEARCH; this._parseAsOpenSearch(); } else ENSURE(false, this._location + " is not a valid search plugin.", Cr.NS_ERROR_FAILURE); break; case SEARCH_DATA_TEXT: LOG("_init: Initing Sherlock plugin from " + this._location); // the only text-based format we support is Sherlock this._type = SEARCH_TYPE_SHERLOCK; this._parseAsSherlock(); } // If we don't yet have a file (i.e. we instantiated an engine object from // passed in URL), get one now if (!this._file) this._file = getSanitizedFile(this.name); // Generate a unique ID for this engine. Use the name of the engine, URI // encoded because pref names can only contain certain characters this._pref = BROWSER_SEARCH_PREF + "engine." + encodeURIComponent(this.name) + "."; }, /** * Initialize this Engine object from a collection of metadata. */ _initFromMetadata: function SRCH_ENG_initMetaData(aName, aIconURL, aAlias, aDescription, aMethod, aTemplate) { ENSURE_WARN(!this._readOnly, "Can't call _initFromMetaData on a readonly engine!", Cr.NS_ERROR_FAILURE); this._name = aName; this._alias = aAlias; this._description = aDescription; this._setIcon(aIconURL); this._urls.push(new EngineURL("text/html", aMethod, aTemplate)); this._serializeToFile(); }, /** * Extracts data from an OpenSearch URL element and creates an EngineURL * object which is then added to the engine's list of URLs. * * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag. * @see EngineURL() */ _parseURL: function SRCH_ENG_parseURL(aElement) { var type = aElement.getAttribute("type"); var method = aElement.getAttribute("method"); var template = aElement.getAttribute("template"); var url = new EngineURL(type, method, template); for (var i = 0; i < aElement.childNodes.length; ++i) { var param = aElement.childNodes[i]; if (param.localName == "Param") url.addParam(param.getAttribute("name"), param.getAttribute("value")); } this._urls.push(url); }, /** * Get the icon from an OpenSearch Image element. * @see http://opensearch.a9.com/spec/1.1/description/#image */ _parseImage: function SRCH_ENG_parseImage(aElement) { LOG("_parseImage: Image textContent: \"" + aElement.textContent + "\""); if (aElement.getAttribute("width") == "16" && aElement.getAttribute("height") == "16") { this._setIcon(aElement.textContent); } }, _parseAsMozSearch: function SRCH_ENG_parseAsMoz() { //XXX for now, just forward to the OpenSearch parser this._parseAsOpenSearch(); }, /** * Extract search engine information from the collected data to initialize * the engine object. */ _parseAsOpenSearch: function SRCH_ENG_parseAsOS() { var doc = this._data; for (var i = 0; i < doc.childNodes.length; ++i) { var child = doc.childNodes[i]; switch (child.localName) { case "ShortName": this._name = child.textContent; break; case "Description": this._description = child.textContent; break; case "Url": this._parseURL(child); break; case "Image": this._parseImage(child); break; case "Alias": this._alias = child.textContent; break; case "InputEncoding": this._queryCharset = child.textContent.toUpperCase(); break; } } ENSURE(this.name && (this._urls.length > 0), "_parseAsOpenSearch: No name, or missing URL!", Cr.NS_ERROR_FAILURE); }, /** * Extract search engine information from the collected data to initialize * the engine object. */ _parseAsSherlock: function SRCH_ENG_parseAsSherlock() { /** * Trims leading and trailing whitespace from aStr. */ function sTrim(aStr) { return aStr.replace(/^\s+/g, "").replace(/\s+$/g, ""); } /** * Extracts one Sherlock "section" from aSource. A section is essentially * an HTML element with attributes, but each attribute must be on a new * line, by definition. * * @param aLines * An array of lines from the sherlock file. * @param aSection * The name of the section (e.g. "search" or "browser"). This value * is not case sensitive. * @returns an object whose properties correspond to the section's * attributes. */ function getSection(aLines, aSection) { LOG("_parseAsSherlock::getSection: Sherlock lines:\n" + aLines.join("\n")); var lines = aLines; var startMark = new RegExp("^\\s*<" + aSection.toLowerCase() + "\\s*", "gi"); var endMark = /\s*>\s*$/gi; var foundStart = false; var startLine, numberOfLines; // Find the beginning and end of the section for (var i=0; i" characters value = value.replace(/^["']/, "") .replace(/["']\s*[\\\/]?>?\s*$/, "") || ""; value = sTrim(value); // Don't clobber existing attributes if (!(name in section)) section[name] = value; } return section; } /** * Returns an array of name-value pair arrays representing the Sherlock * file's input elements. User defined inputs return kUserDefined as the * value. Elements are returned in the order they appear in the source * file. * * Example: * * * Returns: * [["foo", "bar"], ["foopy", "{searchTerms}"]] * * @param aLines * An array of lines from the source file. */ function getInputs(aLines) { /** * Extracts an attribute value from a given a line of text. * Example: * Extracts the string |foo| or |bar| given an input aAttr of * |value| or |name|. * Attributes may be quoted or unquoted. If unquoted, any whitespace * indicates the end of the attribute value. * Example: < value=22 33 name=44\334 > * Returns |22| for "value" and |44\334| for "name". * * @param aAttr * The name of the attribute for which to obtain the value. This * value is not case sensitive. * @param aLine * The line containing the attribute. * * @returns the attribute value, or an empty string if the attribute * doesn't exist. */ function getAttr(aAttr, aLine) { LOG("_parseAsSherlock::getAttr: Getting attr: \"" + aAttr + "\" for line: \"" + aLine + "\""); // We're not case sensitive, but we want to return the attribute value // in it's original case, so create a copy of the source var lLine = aLine.toLowerCase(); var attr = aAttr.toLowerCase(); var attrStart = lLine.search(new RegExp("\\s" + attr, "i")); if (attrStart == -1) { // If this is the "user defined input" (i.e. contains the empty // "user" attribute), return our special keyword if (kIsUserInput.test(lLine) && attr == "value") { LOG("_parseAsSherlock::getAttr: Found user input!\nLine:\"" + lLine + "\""); return kUserDefined; } // The attribute doesn't exist - ignore LOG("_parseAsSherlock::getAttr: Failed to find attribute:\nLine:\"" + lLine + "\"\nAttr:\"" + attr + "\""); return ""; } var valueStart = lLine.indexOf("=", attrStart) + "=".length; if (valueStart == -1) return ""; var quoteStart = lLine.indexOf("\"", valueStart); if (quoteStart == -1) { // Unquoted attribute, get the rest of the line, trimmed at the first // sign of whitespace. If the rest of the line is only whitespace, // returns a blank string. return lLine.substr(valueStart).replace(/\s.*$/, ""); } else { // Make sure that there's only whitespace between the start of the // value and the first quote. If there is, end the attribute value at // the first sign of whitespace. This prevents us from falling into // the next attribute if this is an unquoted attribute followed by a // quoted attribute. var betweenEqualAndQuote = lLine.substring(valueStart, quoteStart); if (/\S/.test(betweenEqualAndQuote)) return lLine.substr(valueStart).replace(/\s.*$/, ""); // Adjust the start index to account for the opening quote valueStart = quoteStart + "\"".length; // Find the closing quote valueEnd = lLine.indexOf("\"", valueStart); // If there is no closing quote, just go to the end of the line if (valueEnd == -1) valueEnd = aLine.length; } return aLine.substring(valueStart, valueEnd); } var inputs = []; LOG("_parseAsSherlock::getInputs: Lines:\n" + aLines); // Filter out everything but non-inputs lines = aLines.filter(function (line) { return /^\s*") line = sTrim(line).replace(/^$/, ""); // If this is one of the "directional" inputs (/) const directionalInput = /^(prev|next)/i; if (directionalInput.test(line)) { // Make it look like a normal input by removing "prev" or "next" line = line.replace(directionalInput, ""); // If it has a name, give it a dummy value to match previous // nsInternetSearchService behavior if (/name\s*=/i.test(line)) { line += " value=\"0\""; } else return; // Line has no name, skip it } var attrName = getAttr("name", line); var attrValue = getAttr("value", line); LOG("_parseAsSherlock::getInputs: Got input:\nName:\"" + attrName + "\"\nValue:\"" + attrValue + "\""); if (attrValue) inputs.push([attrName, attrValue]); }); return inputs; } function err(aErr) { LOG("_parseAsSherlock::err: Sherlock param error:\n" + aErr); throw Cr.NS_ERROR_FAILURE; } var searchSection = getSection(this._data, "search"); LOG("_parseAsSherlock: Search section:\n" + searchSection.toSource()); this._name = searchSection["name"] || err("Missing name!"); this._description = searchSection["description"] || ""; this._queryCharset = searchSection["querycharset"] || getCharSetFromCode(searchSection["queryencoding"]); // XXX should this really fall back to GET? var method = (searchSection["method"] || "GET").toUpperCase(); var template = searchSection["action"] || err("Missing action!"); var inputs = getInputs(this._data); LOG("_parseAsSherlock: Inputs:\n" + inputs.toSource()); var url = null; if (method == "GET") { // Here's how we construct the input string: // is first: Name Attr: Prefix Data Example: // YES EMPTY None TEMPLATE // YES NON-EMPTY ? = TEMPLATE?= // NO EMPTY ------------- -------------- // NO NON-EMPTY & = TEMPLATE?=&= for (var i=0; i