Support Debian's upcoming xulrunner-1.9.1 packages
[conkeror.git] / modules / search-engine.js
blob51f8ffa677f526f9dbcf049ef39a8d03e7c4fdcd
1 /**
2  * (C) Copyright 2008 Jeremy Maitin-Shepard
3  *
4  * Use, modification, and distribution are subject to the terms specified in the
5  * COPYING file.
6 **/
8 require("utils.js");
10 var search_engines = new string_hashmap();
12 function search_engine_parse_error(msg) {
13     var e = new Error(msg);
14     e.__proto__ = search_engine_parse_error.prototype;
15     return e;
17 search_engine_parse_error.prototype.__proto__ = Error.prototype;
19 function search_engine() {
20     this.urls = new string_hashmap();
23 function search_engine_url(type, method, template) {
24     if (!method || !type || !template)
25         throw search_engine_parse_error("Missing method, type, or template for search engine URL");
26     method = method.toUpperCase();
27     type = type.toUpperCase();
28     if (method != "GET" && method != "POST")
29         throw search_engine_parse_error("Invalid method");
30     var template_uri = make_uri(template);
31     switch (template_uri.scheme) {
32     case "http":
33     case "https":
34         break;
35     default:
36         throw search_engine_parse_error("URL template has invalid scheme.");
37         break;
38     }
39     this.type = type;
40     this.method = method;
41     this.template = template;
42     this.params =  [];
44 search_engine_url.prototype.add_param = function search_engine_url__add_param(name, value) {
45     this.params.push({name: name, value: value});
48 function load_search_engines_in_directory(dir) {
49     var files = null;
50     try {
51         files = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
53         while (files.hasMoreElements()) {
54             var file = files.nextFile;
56             if (!file.isFile())
57                 continue;
59             try {
60                 load_search_engine_from_file(file);
61             } catch (e) {
62                 dumpln("WARNING: Failed to load search engine from file: " + file.path);
63                 dump_error(e);
64             }
65         }
66     } catch (e) {
67         // FIXME: maybe have a better error message
68         dump_error(e);
69     } finally {
70         if (files)
71             files.close();
72     }
75 function load_search_engine_from_file(file) {
76     var file_istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
77     file_istream.init(file, MODE_RDONLY, 0644, false);
78     var dom_parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
79     var doc = dom_parser.parseFromStream(file_istream, "UTF-8", file.fileSize, "text/xml");
81     var eng = parse_search_engine_from_dom_node(doc.documentElement);
82     search_engines.put(file.leafName, eng);
85 // Supported OpenSearch parameters
86 // http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax
87 const OPENSEARCH_PARAM_USER_DEFINED    = /\{searchTerms\??\}/g;
88 const OPENSEARCH_PARAM_INPUT_ENCODING  = /\{inputEncoding\??\}/g;
89 const OPENSEARCH_PARAM_LANGUAGE        = /\{language\??\}/g;
90 const OPENSEARCH_PARAM_OUTPUT_ENCODING = /\{outputEncoding\??\}/g;
92 // Default values
93 const OPENSEARCH_PARAM_LANGUAGE_DEF         = "*";
94 const OPENSEARCH_PARAM_OUTPUT_ENCODING_DEF  = "UTF-8";
95 const OPENSEARCH_PARAM_INPUT_ENCODING_DEF   = "UTF-8";
97 // "Unsupported" OpenSearch parameters. For example, we don't support
98 // page-based results, so if the engine requires that we send the "page index"
99 // parameter, we'll always send "1".
100 const OPENSEARCH_PARAM_COUNT        = /\{count\??\}/g;
101 const OPENSEARCH_PARAM_START_INDEX  = /\{startIndex\??\}/g;
102 const OPENSEARCH_PARAM_START_PAGE   = /\{startPage\??\}/g;
104 // Default values
105 const OPENSEARCH_PARAM_COUNT_DEF        = "20"; // 20 results
106 const OPENSEARCH_PARAM_START_INDEX_DEF  = "1";  // start at 1st result
107 const OPENSEARCH_PARAM_START_PAGE_DEF   = "1";  // 1st page
109 // Optional parameter
110 const OPENSEARCH_PARAM_OPTIONAL     = /\{(?:\w+:)?\w+\?\}/g;
112 // A array of arrays containing parameters that we don't fully support, and
113 // their default values. We will only send values for these parameters if
114 // required, since our values are just really arbitrary "guesses" that should
115 // give us the output we want.
116 var OPENSEARCH_UNSUPPORTED_PARAMS = [
117   [OPENSEARCH_PARAM_COUNT, OPENSEARCH_PARAM_COUNT_DEF],
118   [OPENSEARCH_PARAM_START_INDEX, OPENSEARCH_PARAM_START_INDEX_DEF],
119   [OPENSEARCH_PARAM_START_PAGE, OPENSEARCH_PARAM_START_PAGE_DEF]
123 function parse_search_engine_from_dom_node(node) {
124     var eng = new search_engine();
125     eng.query_charset = OPENSEARCH_PARAM_INPUT_ENCODING_DEF;
127     for each (let child in node.childNodes) {
128         switch (child.localName) {
129         case "ShortName":
130             eng.name = child.textContent;
131             break;
132         case "Description":
133             eng.description = child.textContent;
134             break;
135         case "Url":
136             try {
137                 let type = child.getAttribute("type");
138                 let method = child.getAttribute("method") || "GET";
139                 let template = child.getAttribute("template");
141                 let engine_url = new search_engine_url(type, method, template);
142                 for each (let p in child.childNodes) {
143                     if (p.localName == "Param") {
144                         let name = p.getAttribute("name");
145                         let value = p.getAttribute("value");
146                         if (name && value)
147                             engine_url.add_param(name, value);
148                     }
149                 }
150                 eng.urls.put(type, engine_url);
151             } catch (e) {
152                 // Skip this element if parsing fails
153             }
154             break;
155         case "InputEncoding":
156             eng.query_charset = child.textContent.toUpperCase();
157             break;
158         }
159     }
160     return eng;
163 search_engine.prototype.supports_response_type = function (type) {
164     return this.urls.contains(type);
168  * Returns null if the result mime_type isn't supported.  The string
169  * search_terms will be escaped by this function.
170  */
171 search_engine.prototype.get_query_load_spec = function search_engine__get_query_load_spec(search_terms, type) {
172     if (type == null)
173         type = "text/html";
174     var url = this.urls.get(type);
175     if (!url)
176         return null;
177     search_terms = encodeURIComponent(search_terms);
178     var eng = this;
180     function substitute(value) {
181         // Insert the OpenSearch parameters we're confident about
182         value = value.replace(OPENSEARCH_PARAM_USER_DEFINED, search_terms);
183         value = value.replace(OPENSEARCH_PARAM_INPUT_ENCODING, eng.query_charset);
184         value = value.replace(OPENSEARCH_PARAM_LANGUAGE,
185                               get_locale() || OPENSEARCH_PARAM_LANGUAGE_DEF);
186         value = value.replace(OPENSEARCH_PARAM_OUTPUT_ENCODING,
187                               OPENSEARCH_PARAM_OUTPUT_ENCODING_DEF);
189         // Replace any optional parameters
190         value = value.replace(OPENSEARCH_PARAM_OPTIONAL, "");
192         // Insert any remaining required params with our default values
193         for (let i = 0; i < OPENSEARCH_UNSUPPORTED_PARAMS.length; ++i) {
194             value = value.replace(OPENSEARCH_UNSUPPORTED_PARAMS[i][0],
195                                   OPENSEARCH_UNSUPPORTED_PARAMS[i][1]);
196         }
198         return value;
199     }
201     var url_string = substitute(url.template);
203     var data = url.params.map(function (p) (p.name + "=" + substitute(p.value))).join("&");
205     if (url.method == "GET") {
206         if (data.length > 0) {
207             if (url_string.indexOf("?") == -1)
208                 url_string += "?";
209             else
210                 url_string += "&";
211             url_string += data;
212         }
213         return load_spec({uri: url_string});
214     } else {
215         return load_spec({uri: url_string, raw_post_data: data,
216                                request_mime_type: "application/x-www-form-urlencoded"});
217     }
220 search_engine.prototype.__defineGetter__("completer", function () {
221     const response_type_json = "application/x-suggestions+json";
222     const response_type_xml = "application/x-suggestions+xml";
224     var eng = this;
225     if (this.supports_response_type(response_type_xml)) {
226         return function (input, pos, conservative) {
227             if (pos == 0 && conservative)
228                 yield co_return(undefined);
229             let str = input.substring(0,pos);
230             try {
231                 let lspec = eng.get_query_load_spec(str, response_type_xml);
232                 let result = yield send_http_request(lspec);
233                 let doc = result.responseXML;
234                 let data = [];
235                 if (doc) {
236                     let elems = doc.getElementsByTagName("CompleteSuggestion");
237                     for (let i = 0; i < elems.length; ++i) {
238                         let node = elems[i];
239                         let name = node.firstChild.getAttribute("data");
240                         let desc = node.lastChild.getAttribute("int");
241                         if (name && desc)
242                             data.push([name,desc]);
243                     }
244                     delete doc;
245                     delete elem;
246                     delete result;
247                     delete lspec;
248                     let c = { count: data.length,
249                               get_string: function (i) data[i][0],
250                               get_description: function (i) data[i][1] + " results",
251                               get_input_state: function (i) [data[i][0]]
252                             };
253                     yield co_return(c);
254                 }
255             } catch (e) {
256                 yield co_return(null);
257             }
258         };
259     } else if (JSON && this.supports_response_type(response_type_json)) {
260         return function (input, pos, conservative) {
261             if (pos == 0 && conservative)
262                 yield co_return(undefined);
263             let str = input.substring(0,pos);
264             try {
265                 let lspec = eng.get_query_load_spec(str, response_type_json);
266                 let result = yield send_http_request(lspec);
267                 let data = JSON.decode(result.responseText);
268                 delete result;
269                 delete lspec;
271                 if (!(data instanceof Array &&
272                       data.length >= 2 &&
273                       typeof(data[0]) == "string" &&
274                       data[0] == str &&
275                       data[1] instanceof Array &&
276                       (data[2] == null || (data[2] instanceof Array))))
277                     yield co_return(null);
278                 if (data[2] && data[2].length != data[1].length)
279                     data[2] = null;
280                 let c = { count: data[1].length,
281                           get_string: function (i) String(data[1][i]),
282                           get_description: (data[2] != null ? (function (i) String(data[2][i])) : null),
283                           get_input_state: function (i) [String(data[1][i])]
284                         };
285                 yield co_return(c);
286             } catch (e) {
287                 yield co_return(null);
288             }
289         };
290     } else {
291         return null;
292     }
296  * Guess the url of a home page to correspond with the search engine.
297  * Take the text/html url for the search engine, trim off the path and
298  * any "search." prefix on the domain.
299  * This works for all the provided search engines.
300  */
301 function search_engine_get_homepage(search_engine) {
302     var url = search_engine.urls.get("text/html");
303     if (!url)
304         return null;
305     url = url_path_trim(url.template);
306     url = url.replace("//search.", "//");
307     return url;
310 // Load search engines from default directories
312     let dir = file_locator.get("CurProcD", Ci.nsIFile);
313     dir.append("search-engines");
314     if (dir.exists() && dir.isDirectory())
315         load_search_engines_in_directory(dir);
317     dir = file_locator.get("ProfD", Ci.nsIFile);
318     dir.append("search-engines");
319     if (dir.exists() && dir.isDirectory())
320         load_search_engines_in_directory(dir);
323 define_keywords("$alternative");
324 function define_search_engine_webjump(search_engine_name, key) {
325     keywords(arguments);
326     var eng = search_engines.get(search_engine_name);
327     let alternative = arguments.$alternative;
329     if (key == null)
330         key = search_engine_name;
331     if (alternative == null)
332         alternative = search_engine_get_homepage(eng);
334     define_webjump(key,
335                    function (arg) {
336                        return eng.get_query_load_spec(arg);
337                    },
338                    $alternative = alternative,
339                    $description = eng.description,
340                    $completer = eng.completer);
343 define_search_engine_webjump("google.xml", "google");
344 define_search_engine_webjump("mozilla-bugzilla.xml", "bugzilla");
345 define_search_engine_webjump("wikipedia.xml", "wikipedia");
346 define_search_engine_webjump("answers.xml", "answers");
347 define_search_engine_webjump("yahoo.xml", "yahoo");
348 define_search_engine_webjump("creativecommons.xml", "creativecommons");
349 define_search_engine_webjump("eBay.xml", "ebay");