2 * (C) Copyright 2008 Jeremy Maitin-Shepard
4 * Use, modification, and distribution are subject to the terms specified in the
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;
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
) {
36 throw search_engine_parse_error("URL template has invalid scheme.");
41 this.template
= template
;
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
) {
51 files
= dir
.directoryEntries
.QueryInterface(Ci
.nsIDirectoryEnumerator
);
53 while (files
.hasMoreElements()) {
54 var file
= files
.nextFile
;
60 load_search_engine_from_file(file
);
62 dumpln("WARNING: Failed to load search engine from file: " + file
.path
);
67 // FIXME: maybe have a better error message
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;
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;
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
) {
130 eng
.name
= child
.textContent
;
133 eng
.description
= child
.textContent
;
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");
147 engine_url
.add_param(name
, value
);
150 eng
.urls
.put(type
, engine_url
);
152 // Skip this element if parsing fails
155 case "InputEncoding":
156 eng
.query_charset
= child
.textContent
.toUpperCase();
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.
171 search_engine
.prototype.get_query_load_spec
= function search_engine__get_query_load_spec(search_terms
, type
) {
174 var url
= this.urls
.get(type
);
177 search_terms
= encodeURIComponent(search_terms
);
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]);
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)
213 return load_spec({uri
: url_string
});
215 return load_spec({uri
: url_string
, raw_post_data
: data
,
216 request_mime_type
: "application/x-www-form-urlencoded"});
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";
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
);
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
;
236 let elems
= doc
.getElementsByTagName("CompleteSuggestion");
237 for (let i
= 0; i
< elems
.length
; ++i
) {
239 let name
= node
.firstChild
.getAttribute("data");
240 let desc
= node
.lastChild
.getAttribute("int");
242 data
.push([name
,desc
]);
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]]
256 yield co_return(null);
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
);
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
);
271 if (!(data
instanceof Array
&&
273 typeof(data
[0]) == "string" &&
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
)
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
])]
287 yield co_return(null);
295 // Load search engines from default directories
297 let dir
= file_locator
.get("CurProcD", Ci
.nsIFile
);
298 dir
.append("search-engines");
299 if (dir
.exists() && dir
.isDirectory())
300 load_search_engines_in_directory(dir
);
302 dir
= file_locator
.get("ProfD", Ci
.nsIFile
);
303 dir
.append("search-engines");
304 if (dir
.exists() && dir
.isDirectory())
305 load_search_engines_in_directory(dir
);
308 function define_search_engine_webjump(search_engine_name
, key
) {
309 var eng
= search_engines
.get(search_engine_name
);
312 key
= search_engine_name
;
316 return eng
.get_query_load_spec(arg
);
318 $description
= eng
.description
,
319 $completer
= eng
.completer
);
322 define_search_engine_webjump("google.xml", "google");
323 define_search_engine_webjump("mozilla-bugzilla.xml", "bugzilla");
324 define_search_engine_webjump("amazondotcom.xml", "amazon");
325 define_search_engine_webjump("wikipedia.xml", "wikipedia");
326 define_search_engine_webjump("answers.xml", "answers");
327 define_search_engine_webjump("yahoo.xml", "yahoo");
328 define_search_engine_webjump("creativecommons.xml", "creativecommons");
329 define_search_engine_webjump("eBay.xml", "ebay");