string_hashmap: removed
[conkeror/arlinius.git] / modules / utils.js
blob90d11ec56bac961401882c3e130e2f0e229b5b15
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 function string_hashset () {}
13 string_hashset.prototype = {
14     constructor: string_hashset,
16     add: function (s) {
17         this["-" + s] = true;
18     },
20     contains: function (s) {
21         return (("-" + s) in this);
22     },
24     remove: function (s) {
25         delete this["-" + s];
26     },
28     for_each: function (f) {
29         for (var i in this) {
30             if (i[0] == "-")
31                 f(i.slice(1));
32         }
33     },
35     iterator: function () {
36         for (let k in this) {
37             if (i[0] == "-")
38                 yield i.slice(1);
39         }
40     }
44 // Put the string on the clipboard
45 function writeToClipboard (str) {
46     var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
47         .getService(Ci.nsIClipboardHelper);
48     gClipboardHelper.copyString(str);
52 function makeURLAbsolute (base, url) {
53     // Construct nsIURL.
54     var ioService = Cc["@mozilla.org/network/io-service;1"]
55         .getService(Ci.nsIIOService);
56     var baseURI  = ioService.newURI(base, null, null);
57     return ioService.newURI(baseURI.resolve(url), null, null).spec;
61 function make_file (path) {
62     if (path instanceof Ci.nsILocalFile)
63         return path;
64     if (path == "~")
65         return get_home_directory();
66     if (WINDOWS)
67         path = path.replace("/", "\\", "g");
68     if ((POSIX && path.substring(0,2) == "~/") ||
69         (WINDOWS && path.substring(0,2) == "~\\"))
70     {
71         var f = get_home_directory();
72         f.appendRelativePath(path.substring(2));
73     } else {
74         f = Cc["@mozilla.org/file/local;1"]
75             .createInstance(Ci.nsILocalFile);
76         f.initWithPath(path);
77     }
78     return f;
82 function make_file_from_chrome (url) {
83     var crs = Cc['@mozilla.org/chrome/chrome-registry;1']
84         .getService(Ci.nsIChromeRegistry);
85     var file = crs.convertChromeURL(make_uri(url));
86     return make_file(file.path);
89 function get_document_content_disposition (document_o) {
90     var content_disposition = null;
91     try {
92         content_disposition = document_o.defaultView
93             .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
94             .getInterface(Components.interfaces.nsIDOMWindowUtils)
95             .getDocumentMetadata("content-disposition");
96     } catch (e) { }
97     return content_disposition;
101 function set_focus_no_scroll (window, element) {
102     window.document.commandDispatcher.suppressFocusScroll = true;
103     element.focus();
104     window.document.commandDispatcher.suppressFocusScroll = false;
107 function do_repeatedly_positive (func, n) {
108     var args = Array.prototype.slice.call(arguments, 2);
109     while (n-- > 0)
110         func.apply(null, args);
113 function do_repeatedly (func, n, positive_args, negative_args) {
114     if (n < 0)
115         do func.apply(null, negative_args); while (++n < 0);
116     else
117         while (n-- > 0) func.apply(null, positive_args);
122  * Given a node, returns its position relative to the document.
124  * @param node The node to get the position of.
125  * @return An object with properties "x" and "y" representing its offset from
126  *         the left and top of the document, respectively.
127  */
128 function abs_point (node) {
129     var orig = node;
130     var pt = {};
131     try {
132         pt.x = node.offsetLeft;
133         pt.y = node.offsetTop;
134         // find imagemap's coordinates
135         if (node.tagName == "AREA") {
136             var coords = node.getAttribute("coords").split(",");
137             pt.x += Number(coords[0]);
138             pt.y += Number(coords[1]);
139         }
141         node = node.offsetParent;
142         // Sometimes this fails, so just return what we got.
144         while (node.tagName != "BODY") {
145             pt.x += node.offsetLeft;
146             pt.y += node.offsetTop;
147             node = node.offsetParent;
148         }
149     } catch(e) {
150 //      node = orig;
151 //      while (node.tagName != "BODY") {
152 //          alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
153 //          node = node.offsetParent;
154 //      }
155     }
156     return pt;
160 const XHTML_NS = "http://www.w3.org/1999/xhtml";
161 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
162 const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
163 const XLINK_NS = "http://www.w3.org/1999/xlink";
164 const SVG_NS = "http://www.w3.org/2000/svg";
166 function create_XUL (window, tag_name) {
167     return window.document.createElementNS(XUL_NS, tag_name);
171 /* Used in calls to XPath evaluate */
172 function xpath_lookup_namespace (prefix) {
173     return {
174         xhtml: XHTML_NS,
175         m: MATHML_NS,
176         xul: XUL_NS,
177         svg: SVG_NS
178     }[prefix] || null;
181 function method_caller (obj, func) {
182     return function () {
183         func.apply(obj, arguments);
184     };
188 function get_window_from_frame (frame) {
189     try {
190         var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
191             .getInterface(Ci.nsIWebNavigation)
192             .QueryInterface(Ci.nsIDocShellTreeItem)
193             .rootTreeItem
194             .QueryInterface(Ci.nsIInterfaceRequestor)
195             .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
196         /* window is now an XPCSafeJSObjectWrapper */
197         window.escape_wrapper(function (w) { window = w; });
198         /* window is now completely unwrapped */
199         return window;
200     } catch (e) {
201         return null;
202     }
205 function get_buffer_from_frame (window, frame) {
206     var count = window.buffers.count;
207     for (var i = 0; i < count; ++i) {
208         var b = window.buffers.get_buffer(i);
209         if (b.top_frame == frame.top)
210             return b;
211     }
212     return null;
216 function dom_generator (document, ns) {
217     this.document = document;
218     this.ns = ns;
220 dom_generator.prototype = {
221     constructor: dom_generator,
222     element: function (tag, parent) {
223         var node = this.document.createElementNS(this.ns, tag);
224         var i = 1;
225         if (parent != null && (parent instanceof Ci.nsIDOMNode)) {
226             parent.appendChild(node);
227             i = 2;
228         }
229         for (var nargs = arguments.length; i < nargs; i += 2)
230             node.setAttribute(arguments[i], arguments[i+1]);
231         return node;
232     },
234     text: function (str, parent) {
235         var node = this.document.createTextNode(str);
236         if (parent)
237             parent.appendChild(node);
238         return node;
239     },
242     stylesheet_link: function (href, parent) {
243         var node = this.element("link");
244         node.setAttribute("rel", "stylesheet");
245         node.setAttribute("type", "text/css");
246         node.setAttribute("href", href);
247         if (parent)
248             parent.appendChild(node);
249         return node;
250     },
253     add_stylesheet: function (url) {
254         var head = this.document.documentElement.firstChild;
255         this.stylesheet_link(url, head);
256     }
260  * Generates a QueryInterface function suitable for an implemenation
261  * of an XPCOM interface.  Unlike XPCOMUtils, this uses the Function
262  * constructor to generate a slightly more efficient version.  The
263  * arguments can be either Strings or elements of
264  * Components.interfaces.
265  */
266 function generate_QI () {
267     var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
268     var fstr = "if(" +
269         Array.prototype.map.call(args, function (x) {
270             return "iid.equals(Components.interfaces." + x + ")";
271         })
272         .join("||") +
273         ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
274     return new Function("iid", fstr);
278 function abort (str) {
279     var e = new Error(str);
280     e.__proto__ = abort.prototype;
281     return e;
283 abort.prototype.__proto__ = Error.prototype;
286 function get_temporary_file (name) {
287     if (name == null)
288         name = "temp.txt";
289     var file = file_locator_service.get("TmpD", Ci.nsIFile);
290     file.append(name);
291     // Create the file now to ensure that no exploits are possible
292     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
293     return file;
297 /* FIXME: This should be moved somewhere else, perhaps. */
298 function create_info_panel (window, panel_class, row_arr) {
299     /* Show information panel above minibuffer */
301     var g = new dom_generator(window.document, XUL_NS);
303     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
304     var grid = g.element("grid", p);
305     var cols = g.element("columns", grid);
306     g.element("column", cols, "flex", "0");
307     g.element("column", cols, "flex", "1");
309     var rows = g.element("rows", grid);
310     var row;
312     for each (let [row_class, row_label, row_value] in row_arr) {
313         row = g.element("row", rows, "class", row_class);
314         g.element("label", row,
315                   "value", row_label,
316                   "class", "panel-row-label");
317         g.element("label", row,
318                   "value", row_value,
319                   "class", "panel-row-value",
320                   "crop", "end");
321     }
322     window.minibuffer.insert_before(p);
324     p.destroy = function () {
325         this.parentNode.removeChild(this);
326     };
328     return p;
333  * Paste from the X primary selection, unless the system doesn't support a
334  * primary selection, in which case fall back to the clipboard.
335  */
336 function read_from_x_primary_selection () {
337     // Get clipboard.
338     let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
339         .getService(Components.interfaces.nsIClipboard);
341     // Fall back to global clipboard if the system doesn't support a selection
342     let which_clipboard = clipboard.supportsSelectionClipboard() ?
343         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
345     let flavors = ["text/unicode"];
347     // Don't barf if there's nothing on the clipboard
348     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
349         return "";
351     // Create transferable that will transfer the text.
352     let trans = Components.classes["@mozilla.org/widget/transferable;1"]
353         .createInstance(Components.interfaces.nsITransferable);
355     for each (let flavor in flavors) {
356         trans.addDataFlavor(flavor);
357     }
358     clipboard.getData(trans, which_clipboard);
360     var data_flavor = {};
361     var data = {};
362     var dataLen = {};
363     trans.getAnyTransferData(data_flavor, data, dataLen);
365     if (data) {
366         data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
367         let data_length = dataLen.value;
368         if (data_flavor.value == "text/unicode")
369             data_length = dataLen.value / 2;
370         return data.data.substring(0, data_length);
371     } else {
372         return "";
373     }
377 function predicate_alist_match (alist, key) {
378     for each (let i in alist) {
379         if (i[0] instanceof RegExp) {
380             if (i[0].exec(key))
381                 return i[1];
382         } else if (i[0](key))
383             return i[1];
384     }
385     return undefined;
389 function get_meta_title (doc) {
390     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
391                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
392     if (title && title.stringValue)
393         return title.stringValue;
394     return null;
398 function queue () {
399     this.input = [];
400     this.output = [];
402 queue.prototype = {
403     constructor: queue,
404     get length () {
405         return this.input.length + this.output.length;
406     },
407     push: function (x) {
408         this.input[this.input.length] = x;
409     },
410     pop: function (x) {
411         let l = this.output.length;
412         if (!l) {
413             l = this.input.length;
414             if (!l)
415                 return undefined;
416             this.output = this.input.reverse();
417             this.input = [];
418             let x = this.output[l];
419             this.output.length--;
420             return x;
421         }
422     }
425 function frame_iterator (root_frame, start_with) {
426     var q = new queue, x;
427     if (start_with) {
428         x = start_with;
429         do {
430             yield x;
431             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
432                 q.push(x.frames[i]);
433         } while ((x = q.pop()));
434     }
435     x = root_frame;
436     do {
437         if (x == start_with)
438             continue;
439         yield x;
440         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
441             q.push(x.frames[i]);
442     } while ((x = q.pop()));
445 function xml_http_request () {
446     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
447         .createInstance(Ci.nsIXMLHttpRequest)
448         .QueryInterface(Ci.nsIJSXMLHttpRequest)
449         .QueryInterface(Ci.nsIDOMEventTarget);
452 var xml_http_request_load_listener = {
453   // nsIBadCertListener2
454   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
455     return true;
456   },
458   // nsISSLErrorListener
459   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
460     return true;
461   },
463   // nsIInterfaceRequestor
464   getInterface: function SSLL_getInterface (iid) {
465     return this.QueryInterface(iid);
466   },
468   // nsISupports
469   //
470   // FIXME: array comprehension used here to hack around the lack of
471   // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
472   // make it a simple generateQI when xulrunner is more stable.
473   QueryInterface: XPCOMUtils.generateQI(
474       [i for each (i in [Ci.nsIBadCertListener2,
475                          Ci.nsISSLErrorListener,
476                          Ci.nsIInterfaceRequestor])
477        if (i)])
482  * Coroutine interface for sending an HTTP request and waiting for the
483  * response. (This includes so-called "AJAX" requests.)
485  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
487  * The request URI is obtained from this argument. In addition, if the
488  * load spec specifies post data, a POST request is made instead of a
489  * GET request, and the post data included in the load spec is
490  * sent. Specifically, the request_mime_type and raw_post_data
491  * properties of the load spec are used.
493  * @param $user (optional) HTTP user name to include in the request headers
494  * @param $password (optional) HTTP password to include in the request headers
496  * @param $override_mime_type (optional) Force the response to be interpreted
497  *                            as having the specified MIME type.  This is only
498  *                            really useful for forcing the MIME type to be
499  *                            text/xml or something similar, such that it is
500  *                            automatically parsed into a DOM document.
501  * @param $headers (optional) an array of [name,value] pairs (each specified as
502  *                 a two-element array) specifying additional headers to add to
503  *                 the request.
505  * @returns After the request completes (either successfully or with an error),
506  *          the nsIXMLHttpRequest object is returned.  Its responseText (for any
507  *          arbitrary document) or responseXML (if the response type is an XML
508  *          content type) properties can be accessed to examine the response
509  *          document.
511  * If an exception is thrown to the continutation (which can be obtained by the
512  * caller by calling yield CONTINUATION prior to calling this function) while the
513  * request is in progress (i.e. before this coroutine returns), the request will
514  * be aborted, and the exception will be propagated to the caller.
516  **/
517 define_keywords("$user", "$password", "$override_mime_type", "$headers");
518 function send_http_request (lspec) {
519     // why do we get warnings in jsconsole unless we initialize the
520     // following keywords?
521     keywords(arguments, $user = undefined, $password = undefined,
522              $override_mime_type = undefined, $headers = undefined);
523     if (! (lspec instanceof load_spec))
524         lspec = load_spec(lspec);
525     var req = xml_http_request();
526     var cc = yield CONTINUATION;
527     var aborting = false;
528     req.onreadystatechange = function send_http_request__onreadystatechange () {
529         if (req.readyState != 4)
530             return;
531         if (aborting)
532             return;
533         cc();
534     };
536     if (arguments.$override_mime_type)
537         req.overrideMimeType(arguments.$override_mime_type);
539     var post_data = load_spec_raw_post_data(lspec);
541     var method = post_data ? "POST" : "GET";
543     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
544     req.channel.notificationCallbacks = xml_http_request_load_listener;
546     for each (let [name,value] in arguments.$headers) {
547         req.setRequestHeader(name, value);
548     }
550     if (post_data) {
551         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
552         req.send(post_data);
553     } else
554         req.send(null);
556     try {
557         yield SUSPEND;
558     } catch (e) {
559         aborting = true;
560         req.abort();
561         throw e;
562     }
564     // Let the caller access the status and reponse data
565     yield co_return(req);
570  * scroll_selection_into_view takes an editable element, and scrolls it so
571  * that the selection (or insertion point) are visible.
572  */
573 function scroll_selection_into_view (field) {
574     if (field.namespaceURI == XUL_NS)
575         field = field.inputField;
576     try {
577         field.QueryInterface(Ci.nsIDOMNSEditableElement)
578             .editor
579             .selectionController
580             .scrollSelectionIntoView(
581                 Ci.nsISelectionController.SELECTION_NORMAL,
582                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
583                 true);
584     } catch (e) {
585         // we'll get here for richedit fields
586     }
591  * build_url_regexp builds a regular expression to match URLs for a given
592  * web site.
594  * Both the $domain and $path arguments can be either regexps, in
595  * which case they will be matched as is, or strings, in which case
596  * they will be matched literally.
598  * $tlds specifies a list of valid top-level-domains to match, and
599  * defaults to .com. Useful for when e.g. foo.org and foo.com are the
600  * same.
602  * If $allow_www is true, www.domain.tld will also be allowed.
603  */
604 define_keywords("$domain", "$path", "$tlds", "$allow_www");
605 function build_url_regexp () {
606     function regexp_to_string (obj) {
607         if (typeof obj == "object" && "source" in obj)
608             return obj.source;
609         return quotemeta(obj);
610     }
612     keywords(arguments, $path = "", $tlds = ["com"], $allow_www = false);
613     var domain = regexp_to_string(arguments.$domain);
614     if (arguments.$allow_www) {
615         domain = "(?:www\.)?" + domain;
616     }
617     var path = regexp_to_string(arguments.$path);
618     var tlds = arguments.$tlds;
619     var regexp = "^https?://" + domain + "\\." + choice_regexp(tlds) + "/" + path;
620     return new RegExp(regexp);
624 function compute_up_url (uri) {
625     uri = uri.clone().QueryInterface(Ci.nsIURL);
626     for each (var p in ["ref", "query", "param", "fileName"]) {
627         if (uri[p] != "") {
628             uri[p] = "";
629             return uri.spec;
630         }
631     }
632     return uri.resolve("..");
636 function url_path_trim (url) {
637     var uri = make_uri(url);
638     uri.spec = url;
639     uri.path = "";
640     return uri.spec;
643 /* possibly_valid_url returns true if the string might be a valid
644  * thing to pass to nsIWebNavigation.loadURI.  Currently just checks
645  * that there's no whitespace in the middle and that it's not entirely
646  * whitespace.
647  */
648 function possibly_valid_url (url) {
649     return !(/\S\s+\S/.test(url)) && !(/^\s*$/.test(url));
654  * Convenience function for making simple XPath lookups in a document.
656  * @param doc The document to look in.
657  * @param exp The XPath expression to search for.
658  * @return The XPathResult object representing the set of found nodes.
659  */
660 function xpath_lookup (doc, exp) {
661     return doc.evaluate(exp, doc, null, Ci.nsIDOMXPathResult.ANY_TYPE, null);
665 /* get_contents_synchronously returns the contents of the given
666  * url (string or nsIURI) as a string on success, or null on failure.
667  */
668 function get_contents_synchronously (url) {
669     var ioService=Cc["@mozilla.org/network/io-service;1"]
670         .getService(Ci.nsIIOService);
671     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
672         .getService(Ci.nsIScriptableInputStream);
673     var channel;
674     var input;
675     try {
676         if (url instanceof Ci.nsIURI)
677             channel = ioService.newChannelFromURI(url);
678         else
679             channel = ioService.newChannel(url, null, null);
680         input=channel.open();
681     } catch (e) {
682         return null;
683     }
684     scriptableStream.init(input);
685     var str=scriptableStream.read(input.available());
686     scriptableStream.close();
687     input.close();
688     return str;
693  * dom_add_class adds a css class to the given dom node.
694  */
695 function dom_add_class (node, cssclass) {
696     if (node.className) {
697         var cs = node.className.split(" ");
698         if (cs.indexOf(cssclass) != -1)
699             return;
700         cs.push(cssclass);
701         node.className = cs.join(" ");
702     } else
703         node.className = cssclass;
707  * dom_remove_class removes the given css class from the given dom node.
708  */
709 function dom_remove_class (node, cssclass) {
710     if (! node.className)
711         return;
712     var classes = node.className.split(" ");
713     classes = classes.filter(function (x) { return x != cssclass; });
714     node.className = classes.join(" ");
719  * dom_node_flash adds the given cssclass to the node for a brief interval.
720  * this class can be styled, to create a flashing effect.
721  */
722 function dom_node_flash (node, cssclass) {
723     dom_add_class(node, cssclass);
724     call_after_timeout(
725         function () {
726             dom_remove_class(node, cssclass);
727         },
728         400);
733  * data is an an alist (array of 2 element arrays) where each pair is a key
734  * and a value.
736  * The return type is a mime input stream that can be passed as postData to
737  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
738  * of this function is of the correct type for the `post_data' field of a
739  * load_spec.
740  */
741 function make_post_data (data) {
742     data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
743             for each (pair in data)].join('&');
744     data = string_input_stream(data);
745     return mime_input_stream(
746         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
751  * Centers the viewport around a given element.
753  * @param win  The window to scroll.
754  * @param elem The element arund which we put the viewport.
755  */
756 function center_in_viewport (win, elem) {
757     let point = abs_point(elem);
759     point.x -= win.innerWidth / 2;
760     point.y -= win.innerHeight / 2;
762     win.scrollTo(point.x, point.y);
767  * Simple predicate returns true if elem is an nsIDOMNode or
768  * nsIDOMWindow.
769  */
770 function dom_node_or_window_p (elem) {
771     if (elem instanceof Ci.nsIDOMNode)
772         return true;
773     if (elem instanceof Ci.nsIDOMWindow)
774         return true;
775     return false;
779  * Given a hook name, a buffer and a function, waits until the buffer document
780  * has fully loaded, then calls the function with the buffer as its only
781  * argument.
783  * @param {String} The hook name.
784  * @param {buffer} The buffer.
785  * @param {function} The function to call with the buffer as its argument once
786  *                   the buffer has loaded.
787  */
788 function do_when (hook, buffer, fun) {
789     if (buffer.browser.webProgress.isLoadingDocument)
790         add_hook.call(buffer, hook, fun);
791     else
792         fun(buffer);
797  * evaluate string s as javascript in the 'this' scope in which evaluate
798  * is called.
799  */
800 function evaluate (s) {
801     try {
802         var obs = Cc["@mozilla.org/observer-service;1"]
803             .getService(Ci.nsIObserverService);
804         obs.notifyObservers(null, "startupcache-invalidate", null);
805         var temp = get_temporary_file("conkeror-evaluate.tmp.js");
806         write_text_file(temp, s);
807         var url = make_uri(temp).spec;
808         return load_url(url, this);
809     } finally {
810         if (temp && temp.exists())
811             temp.remove(false);
812     }
817  * set_protocol_handler takes a protocol and a handler spec.  If the
818  * handler is true, Mozilla will (try to) handle this protocol internally.
819  * If the handler null, the user will be prompted for a handler when a
820  * resource of this protocol is requested.  If the handler is an nsIFile,
821  * the program it gives will be launched with the url as an argument.  If
822  * the handler is a string, it will be interpreted as an URL template for
823  * a web service and the sequence '%s' within it will be replaced by the
824  * url-encoded url.
825  */
826 function set_protocol_handler (protocol, handler) {
827     var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
828         .getService(Ci.nsIExternalProtocolService);
829     var info = eps.getProtocolHandlerInfo(protocol);
830     var expose_pref = "network.protocol-handler.expose."+protocol;
831     if (handler == true) {
832         // internal handling
833         clear_default_pref(expose_pref);
834     } else if (handler) {
835         // external handling
836         if (handler instanceof Ci.nsIFile) {
837             var h = Cc["@mozilla.org/uriloader/local-handler-app;1"]
838                 .createInstance(Ci.nsILocalHandlerApp);
839             h.executable = handler;
840         } else if (typeof handler == "string") {
841             h = Cc["@mozilla.org/uriloader/web-handler-app;1"]
842                 .createInstance(Ci.nsIWebHandlerApp);
843             var uri = make_uri(handler);
844             h.name = uri.host;
845             h.uriTemplate = handler;
846         }
847         info.alwaysAskBeforeHandling = false;
848         info.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
849         info.possibleApplicationHandlers.clear();
850         info.possibleApplicationHandlers.appendElement(h, false);
851         info.preferredApplicationHandler = h;
852         session_pref(expose_pref, false);
853     } else {
854         // prompt
855         info.alwaysAskBeforeHandling = true;
856         info.preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
857         session_pref(expose_pref, false);
858     }
859     var hs = Cc["@mozilla.org/uriloader/handler-service;1"]
860         .getService(Ci.nsIHandlerService);
861     hs.store(info);
864 provide("utils");