string_hashset: removed
[conkeror.git] / modules / utils.js
blob04108c017ecefe145a169fead4ce29bd84679d25
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2011 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 require("io");
12 // Put the string on the clipboard
13 function writeToClipboard (str) {
14     var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
15         .getService(Ci.nsIClipboardHelper);
16     gClipboardHelper.copyString(str);
20 function makeURLAbsolute (base, url) {
21     // Construct nsIURL.
22     var ioService = Cc["@mozilla.org/network/io-service;1"]
23         .getService(Ci.nsIIOService);
24     var baseURI  = ioService.newURI(base, null, null);
25     return ioService.newURI(baseURI.resolve(url), null, null).spec;
29 function make_file (path) {
30     if (path instanceof Ci.nsILocalFile)
31         return path;
32     if (path == "~")
33         return get_home_directory();
34     if (WINDOWS)
35         path = path.replace("/", "\\", "g");
36     if ((POSIX && path.substring(0,2) == "~/") ||
37         (WINDOWS && path.substring(0,2) == "~\\"))
38     {
39         var f = get_home_directory();
40         f.appendRelativePath(path.substring(2));
41     } else {
42         f = Cc["@mozilla.org/file/local;1"]
43             .createInstance(Ci.nsILocalFile);
44         f.initWithPath(path);
45     }
46     return f;
50 function make_file_from_chrome (url) {
51     var crs = Cc['@mozilla.org/chrome/chrome-registry;1']
52         .getService(Ci.nsIChromeRegistry);
53     var file = crs.convertChromeURL(make_uri(url));
54     return make_file(file.path);
57 function get_document_content_disposition (document_o) {
58     var content_disposition = null;
59     try {
60         content_disposition = document_o.defaultView
61             .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
62             .getInterface(Components.interfaces.nsIDOMWindowUtils)
63             .getDocumentMetadata("content-disposition");
64     } catch (e) { }
65     return content_disposition;
69 function set_focus_no_scroll (window, element) {
70     window.document.commandDispatcher.suppressFocusScroll = true;
71     element.focus();
72     window.document.commandDispatcher.suppressFocusScroll = false;
75 function do_repeatedly_positive (func, n) {
76     var args = Array.prototype.slice.call(arguments, 2);
77     while (n-- > 0)
78         func.apply(null, args);
81 function do_repeatedly (func, n, positive_args, negative_args) {
82     if (n < 0)
83         do func.apply(null, negative_args); while (++n < 0);
84     else
85         while (n-- > 0) func.apply(null, positive_args);
89 /**
90  * Given a node, returns its position relative to the document.
91  *
92  * @param node The node to get the position of.
93  * @return An object with properties "x" and "y" representing its offset from
94  *         the left and top of the document, respectively.
95  */
96 function abs_point (node) {
97     var orig = node;
98     var pt = {};
99     try {
100         pt.x = node.offsetLeft;
101         pt.y = node.offsetTop;
102         // find imagemap's coordinates
103         if (node.tagName == "AREA") {
104             var coords = node.getAttribute("coords").split(",");
105             pt.x += Number(coords[0]);
106             pt.y += Number(coords[1]);
107         }
109         node = node.offsetParent;
110         // Sometimes this fails, so just return what we got.
112         while (node.tagName != "BODY") {
113             pt.x += node.offsetLeft;
114             pt.y += node.offsetTop;
115             node = node.offsetParent;
116         }
117     } catch(e) {
118 //      node = orig;
119 //      while (node.tagName != "BODY") {
120 //          alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
121 //          node = node.offsetParent;
122 //      }
123     }
124     return pt;
128 const XHTML_NS = "http://www.w3.org/1999/xhtml";
129 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
130 const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
131 const XLINK_NS = "http://www.w3.org/1999/xlink";
132 const SVG_NS = "http://www.w3.org/2000/svg";
134 function create_XUL (window, tag_name) {
135     return window.document.createElementNS(XUL_NS, tag_name);
139 /* Used in calls to XPath evaluate */
140 function xpath_lookup_namespace (prefix) {
141     return {
142         xhtml: XHTML_NS,
143         m: MATHML_NS,
144         xul: XUL_NS,
145         svg: SVG_NS
146     }[prefix] || null;
149 function method_caller (obj, func) {
150     return function () {
151         func.apply(obj, arguments);
152     };
156 function get_window_from_frame (frame) {
157     try {
158         var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
159             .getInterface(Ci.nsIWebNavigation)
160             .QueryInterface(Ci.nsIDocShellTreeItem)
161             .rootTreeItem
162             .QueryInterface(Ci.nsIInterfaceRequestor)
163             .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
164         /* window is now an XPCSafeJSObjectWrapper */
165         window.escape_wrapper(function (w) { window = w; });
166         /* window is now completely unwrapped */
167         return window;
168     } catch (e) {
169         return null;
170     }
173 function get_buffer_from_frame (window, frame) {
174     var count = window.buffers.count;
175     for (var i = 0; i < count; ++i) {
176         var b = window.buffers.get_buffer(i);
177         if (b.top_frame == frame.top)
178             return b;
179     }
180     return null;
184 function dom_generator (document, ns) {
185     this.document = document;
186     this.ns = ns;
188 dom_generator.prototype = {
189     constructor: dom_generator,
190     element: function (tag, parent) {
191         var node = this.document.createElementNS(this.ns, tag);
192         var i = 1;
193         if (parent != null && (parent instanceof Ci.nsIDOMNode)) {
194             parent.appendChild(node);
195             i = 2;
196         }
197         for (var nargs = arguments.length; i < nargs; i += 2)
198             node.setAttribute(arguments[i], arguments[i+1]);
199         return node;
200     },
202     text: function (str, parent) {
203         var node = this.document.createTextNode(str);
204         if (parent)
205             parent.appendChild(node);
206         return node;
207     },
210     stylesheet_link: function (href, parent) {
211         var node = this.element("link");
212         node.setAttribute("rel", "stylesheet");
213         node.setAttribute("type", "text/css");
214         node.setAttribute("href", href);
215         if (parent)
216             parent.appendChild(node);
217         return node;
218     },
221     add_stylesheet: function (url) {
222         var head = this.document.documentElement.firstChild;
223         this.stylesheet_link(url, head);
224     }
228  * Generates a QueryInterface function suitable for an implemenation
229  * of an XPCOM interface.  Unlike XPCOMUtils, this uses the Function
230  * constructor to generate a slightly more efficient version.  The
231  * arguments can be either Strings or elements of
232  * Components.interfaces.
233  */
234 function generate_QI () {
235     var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
236     var fstr = "if(" +
237         Array.prototype.map.call(args, function (x) {
238             return "iid.equals(Components.interfaces." + x + ")";
239         })
240         .join("||") +
241         ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
242     return new Function("iid", fstr);
246 function abort (str) {
247     var e = new Error(str);
248     e.__proto__ = abort.prototype;
249     return e;
251 abort.prototype.__proto__ = Error.prototype;
254 function get_temporary_file (name) {
255     if (name == null)
256         name = "temp.txt";
257     var file = file_locator_service.get("TmpD", Ci.nsIFile);
258     file.append(name);
259     // Create the file now to ensure that no exploits are possible
260     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
261     return file;
265 /* FIXME: This should be moved somewhere else, perhaps. */
266 function create_info_panel (window, panel_class, row_arr) {
267     /* Show information panel above minibuffer */
269     var g = new dom_generator(window.document, XUL_NS);
271     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
272     var grid = g.element("grid", p);
273     var cols = g.element("columns", grid);
274     g.element("column", cols, "flex", "0");
275     g.element("column", cols, "flex", "1");
277     var rows = g.element("rows", grid);
278     var row;
280     for each (let [row_class, row_label, row_value] in row_arr) {
281         row = g.element("row", rows, "class", row_class);
282         g.element("label", row,
283                   "value", row_label,
284                   "class", "panel-row-label");
285         g.element("label", row,
286                   "value", row_value,
287                   "class", "panel-row-value",
288                   "crop", "end");
289     }
290     window.minibuffer.insert_before(p);
292     p.destroy = function () {
293         this.parentNode.removeChild(this);
294     };
296     return p;
301  * Paste from the X primary selection, unless the system doesn't support a
302  * primary selection, in which case fall back to the clipboard.
303  */
304 function read_from_x_primary_selection () {
305     // Get clipboard.
306     let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
307         .getService(Components.interfaces.nsIClipboard);
309     // Fall back to global clipboard if the system doesn't support a selection
310     let which_clipboard = clipboard.supportsSelectionClipboard() ?
311         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
313     let flavors = ["text/unicode"];
315     // Don't barf if there's nothing on the clipboard
316     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
317         return "";
319     // Create transferable that will transfer the text.
320     let trans = Components.classes["@mozilla.org/widget/transferable;1"]
321         .createInstance(Components.interfaces.nsITransferable);
323     for each (let flavor in flavors) {
324         trans.addDataFlavor(flavor);
325     }
326     clipboard.getData(trans, which_clipboard);
328     var data_flavor = {};
329     var data = {};
330     var dataLen = {};
331     trans.getAnyTransferData(data_flavor, data, dataLen);
333     if (data) {
334         data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
335         let data_length = dataLen.value;
336         if (data_flavor.value == "text/unicode")
337             data_length = dataLen.value / 2;
338         return data.data.substring(0, data_length);
339     } else {
340         return "";
341     }
345 function predicate_alist_match (alist, key) {
346     for each (let i in alist) {
347         if (i[0] instanceof RegExp) {
348             if (i[0].exec(key))
349                 return i[1];
350         } else if (i[0](key))
351             return i[1];
352     }
353     return undefined;
357 function get_meta_title (doc) {
358     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
359                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
360     if (title && title.stringValue)
361         return title.stringValue;
362     return null;
366 function queue () {
367     this.input = [];
368     this.output = [];
370 queue.prototype = {
371     constructor: queue,
372     get length () {
373         return this.input.length + this.output.length;
374     },
375     push: function (x) {
376         this.input[this.input.length] = x;
377     },
378     pop: function (x) {
379         let l = this.output.length;
380         if (!l) {
381             l = this.input.length;
382             if (!l)
383                 return undefined;
384             this.output = this.input.reverse();
385             this.input = [];
386             let x = this.output[l];
387             this.output.length--;
388             return x;
389         }
390     }
393 function frame_iterator (root_frame, start_with) {
394     var q = new queue, x;
395     if (start_with) {
396         x = start_with;
397         do {
398             yield x;
399             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
400                 q.push(x.frames[i]);
401         } while ((x = q.pop()));
402     }
403     x = root_frame;
404     do {
405         if (x == start_with)
406             continue;
407         yield x;
408         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
409             q.push(x.frames[i]);
410     } while ((x = q.pop()));
413 function xml_http_request () {
414     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
415         .createInstance(Ci.nsIXMLHttpRequest)
416         .QueryInterface(Ci.nsIJSXMLHttpRequest)
417         .QueryInterface(Ci.nsIDOMEventTarget);
420 var xml_http_request_load_listener = {
421   // nsIBadCertListener2
422   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
423     return true;
424   },
426   // nsISSLErrorListener
427   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
428     return true;
429   },
431   // nsIInterfaceRequestor
432   getInterface: function SSLL_getInterface (iid) {
433     return this.QueryInterface(iid);
434   },
436   // nsISupports
437   //
438   // FIXME: array comprehension used here to hack around the lack of
439   // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
440   // make it a simple generateQI when xulrunner is more stable.
441   QueryInterface: XPCOMUtils.generateQI(
442       [i for each (i in [Ci.nsIBadCertListener2,
443                          Ci.nsISSLErrorListener,
444                          Ci.nsIInterfaceRequestor])
445        if (i)])
450  * Coroutine interface for sending an HTTP request and waiting for the
451  * response. (This includes so-called "AJAX" requests.)
453  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
455  * The request URI is obtained from this argument. In addition, if the
456  * load spec specifies post data, a POST request is made instead of a
457  * GET request, and the post data included in the load spec is
458  * sent. Specifically, the request_mime_type and raw_post_data
459  * properties of the load spec are used.
461  * @param $user (optional) HTTP user name to include in the request headers
462  * @param $password (optional) HTTP password to include in the request headers
464  * @param $override_mime_type (optional) Force the response to be interpreted
465  *                            as having the specified MIME type.  This is only
466  *                            really useful for forcing the MIME type to be
467  *                            text/xml or something similar, such that it is
468  *                            automatically parsed into a DOM document.
469  * @param $headers (optional) an array of [name,value] pairs (each specified as
470  *                 a two-element array) specifying additional headers to add to
471  *                 the request.
473  * @returns After the request completes (either successfully or with an error),
474  *          the nsIXMLHttpRequest object is returned.  Its responseText (for any
475  *          arbitrary document) or responseXML (if the response type is an XML
476  *          content type) properties can be accessed to examine the response
477  *          document.
479  * If an exception is thrown to the continutation (which can be obtained by the
480  * caller by calling yield CONTINUATION prior to calling this function) while the
481  * request is in progress (i.e. before this coroutine returns), the request will
482  * be aborted, and the exception will be propagated to the caller.
484  **/
485 define_keywords("$user", "$password", "$override_mime_type", "$headers");
486 function send_http_request (lspec) {
487     // why do we get warnings in jsconsole unless we initialize the
488     // following keywords?
489     keywords(arguments, $user = undefined, $password = undefined,
490              $override_mime_type = undefined, $headers = undefined);
491     if (! (lspec instanceof load_spec))
492         lspec = load_spec(lspec);
493     var req = xml_http_request();
494     var cc = yield CONTINUATION;
495     var aborting = false;
496     req.onreadystatechange = function send_http_request__onreadystatechange () {
497         if (req.readyState != 4)
498             return;
499         if (aborting)
500             return;
501         cc();
502     };
504     if (arguments.$override_mime_type)
505         req.overrideMimeType(arguments.$override_mime_type);
507     var post_data = load_spec_raw_post_data(lspec);
509     var method = post_data ? "POST" : "GET";
511     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
512     req.channel.notificationCallbacks = xml_http_request_load_listener;
514     for each (let [name,value] in arguments.$headers) {
515         req.setRequestHeader(name, value);
516     }
518     if (post_data) {
519         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
520         req.send(post_data);
521     } else
522         req.send(null);
524     try {
525         yield SUSPEND;
526     } catch (e) {
527         aborting = true;
528         req.abort();
529         throw e;
530     }
532     // Let the caller access the status and reponse data
533     yield co_return(req);
538  * scroll_selection_into_view takes an editable element, and scrolls it so
539  * that the selection (or insertion point) are visible.
540  */
541 function scroll_selection_into_view (field) {
542     if (field.namespaceURI == XUL_NS)
543         field = field.inputField;
544     try {
545         field.QueryInterface(Ci.nsIDOMNSEditableElement)
546             .editor
547             .selectionController
548             .scrollSelectionIntoView(
549                 Ci.nsISelectionController.SELECTION_NORMAL,
550                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
551                 true);
552     } catch (e) {
553         // we'll get here for richedit fields
554     }
559  * build_url_regexp builds a regular expression to match URLs for a given
560  * web site.
562  * Both the $domain and $path arguments can be either regexps, in
563  * which case they will be matched as is, or strings, in which case
564  * they will be matched literally.
566  * $tlds specifies a list of valid top-level-domains to match, and
567  * defaults to .com. Useful for when e.g. foo.org and foo.com are the
568  * same.
570  * If $allow_www is true, www.domain.tld will also be allowed.
571  */
572 define_keywords("$domain", "$path", "$tlds", "$allow_www");
573 function build_url_regexp () {
574     function regexp_to_string (obj) {
575         if (typeof obj == "object" && "source" in obj)
576             return obj.source;
577         return quotemeta(obj);
578     }
580     keywords(arguments, $path = "", $tlds = ["com"], $allow_www = false);
581     var domain = regexp_to_string(arguments.$domain);
582     if (arguments.$allow_www) {
583         domain = "(?:www\.)?" + domain;
584     }
585     var path = regexp_to_string(arguments.$path);
586     var tlds = arguments.$tlds;
587     var regexp = "^https?://" + domain + "\\." + choice_regexp(tlds) + "/" + path;
588     return new RegExp(regexp);
592 function compute_up_url (uri) {
593     uri = uri.clone().QueryInterface(Ci.nsIURL);
594     for each (var p in ["ref", "query", "param", "fileName"]) {
595         if (uri[p] != "") {
596             uri[p] = "";
597             return uri.spec;
598         }
599     }
600     return uri.resolve("..");
604 function url_path_trim (url) {
605     var uri = make_uri(url);
606     uri.spec = url;
607     uri.path = "";
608     return uri.spec;
611 /* possibly_valid_url returns true if the string might be a valid
612  * thing to pass to nsIWebNavigation.loadURI.  Currently just checks
613  * that there's no whitespace in the middle and that it's not entirely
614  * whitespace.
615  */
616 function possibly_valid_url (url) {
617     return !(/\S\s+\S/.test(url)) && !(/^\s*$/.test(url));
622  * Convenience function for making simple XPath lookups in a document.
624  * @param doc The document to look in.
625  * @param exp The XPath expression to search for.
626  * @return The XPathResult object representing the set of found nodes.
627  */
628 function xpath_lookup (doc, exp) {
629     return doc.evaluate(exp, doc, null, Ci.nsIDOMXPathResult.ANY_TYPE, null);
633 /* get_contents_synchronously returns the contents of the given
634  * url (string or nsIURI) as a string on success, or null on failure.
635  */
636 function get_contents_synchronously (url) {
637     var ioService=Cc["@mozilla.org/network/io-service;1"]
638         .getService(Ci.nsIIOService);
639     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
640         .getService(Ci.nsIScriptableInputStream);
641     var channel;
642     var input;
643     try {
644         if (url instanceof Ci.nsIURI)
645             channel = ioService.newChannelFromURI(url);
646         else
647             channel = ioService.newChannel(url, null, null);
648         input=channel.open();
649     } catch (e) {
650         return null;
651     }
652     scriptableStream.init(input);
653     var str=scriptableStream.read(input.available());
654     scriptableStream.close();
655     input.close();
656     return str;
661  * dom_add_class adds a css class to the given dom node.
662  */
663 function dom_add_class (node, cssclass) {
664     if (node.className) {
665         var cs = node.className.split(" ");
666         if (cs.indexOf(cssclass) != -1)
667             return;
668         cs.push(cssclass);
669         node.className = cs.join(" ");
670     } else
671         node.className = cssclass;
675  * dom_remove_class removes the given css class from the given dom node.
676  */
677 function dom_remove_class (node, cssclass) {
678     if (! node.className)
679         return;
680     var classes = node.className.split(" ");
681     classes = classes.filter(function (x) { return x != cssclass; });
682     node.className = classes.join(" ");
687  * dom_node_flash adds the given cssclass to the node for a brief interval.
688  * this class can be styled, to create a flashing effect.
689  */
690 function dom_node_flash (node, cssclass) {
691     dom_add_class(node, cssclass);
692     call_after_timeout(
693         function () {
694             dom_remove_class(node, cssclass);
695         },
696         400);
701  * data is an an alist (array of 2 element arrays) where each pair is a key
702  * and a value.
704  * The return type is a mime input stream that can be passed as postData to
705  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
706  * of this function is of the correct type for the `post_data' field of a
707  * load_spec.
708  */
709 function make_post_data (data) {
710     data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
711             for each (pair in data)].join('&');
712     data = string_input_stream(data);
713     return mime_input_stream(
714         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
719  * Centers the viewport around a given element.
721  * @param win  The window to scroll.
722  * @param elem The element arund which we put the viewport.
723  */
724 function center_in_viewport (win, elem) {
725     let point = abs_point(elem);
727     point.x -= win.innerWidth / 2;
728     point.y -= win.innerHeight / 2;
730     win.scrollTo(point.x, point.y);
735  * Simple predicate returns true if elem is an nsIDOMNode or
736  * nsIDOMWindow.
737  */
738 function dom_node_or_window_p (elem) {
739     if (elem instanceof Ci.nsIDOMNode)
740         return true;
741     if (elem instanceof Ci.nsIDOMWindow)
742         return true;
743     return false;
747  * Given a hook name, a buffer and a function, waits until the buffer document
748  * has fully loaded, then calls the function with the buffer as its only
749  * argument.
751  * @param {String} The hook name.
752  * @param {buffer} The buffer.
753  * @param {function} The function to call with the buffer as its argument once
754  *                   the buffer has loaded.
755  */
756 function do_when (hook, buffer, fun) {
757     if (buffer.browser.webProgress.isLoadingDocument)
758         add_hook.call(buffer, hook, fun);
759     else
760         fun(buffer);
765  * evaluate string s as javascript in the 'this' scope in which evaluate
766  * is called.
767  */
768 function evaluate (s) {
769     try {
770         var obs = Cc["@mozilla.org/observer-service;1"]
771             .getService(Ci.nsIObserverService);
772         obs.notifyObservers(null, "startupcache-invalidate", null);
773         var temp = get_temporary_file("conkeror-evaluate.tmp.js");
774         write_text_file(temp, s);
775         var url = make_uri(temp).spec;
776         return load_url(url, this);
777     } finally {
778         if (temp && temp.exists())
779             temp.remove(false);
780     }
785  * set_protocol_handler takes a protocol and a handler spec.  If the
786  * handler is true, Mozilla will (try to) handle this protocol internally.
787  * If the handler null, the user will be prompted for a handler when a
788  * resource of this protocol is requested.  If the handler is an nsIFile,
789  * the program it gives will be launched with the url as an argument.  If
790  * the handler is a string, it will be interpreted as an URL template for
791  * a web service and the sequence '%s' within it will be replaced by the
792  * url-encoded url.
793  */
794 function set_protocol_handler (protocol, handler) {
795     var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
796         .getService(Ci.nsIExternalProtocolService);
797     var info = eps.getProtocolHandlerInfo(protocol);
798     var expose_pref = "network.protocol-handler.expose."+protocol;
799     if (handler == true) {
800         // internal handling
801         clear_default_pref(expose_pref);
802     } else if (handler) {
803         // external handling
804         if (handler instanceof Ci.nsIFile) {
805             var h = Cc["@mozilla.org/uriloader/local-handler-app;1"]
806                 .createInstance(Ci.nsILocalHandlerApp);
807             h.executable = handler;
808         } else if (typeof handler == "string") {
809             h = Cc["@mozilla.org/uriloader/web-handler-app;1"]
810                 .createInstance(Ci.nsIWebHandlerApp);
811             var uri = make_uri(handler);
812             h.name = uri.host;
813             h.uriTemplate = handler;
814         }
815         info.alwaysAskBeforeHandling = false;
816         info.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
817         info.possibleApplicationHandlers.clear();
818         info.possibleApplicationHandlers.appendElement(h, false);
819         info.preferredApplicationHandler = h;
820         session_pref(expose_pref, false);
821     } else {
822         // prompt
823         info.alwaysAskBeforeHandling = true;
824         info.preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
825         session_pref(expose_pref, false);
826     }
827     var hs = Cc["@mozilla.org/uriloader/handler-service;1"]
828         .getService(Ci.nsIHandlerService);
829     hs.store(info);
832 provide("utils");