Replace uses of co_call/continuation API with uses of spawn/Promise API
[conkeror.git] / modules / utils.js
blobc5e6940c04cc932d0b9a99913c15559535c0b97b
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);
58 /**
59  * file_symlink_p takes an nsIFile and returns the value of
60  * file.isSymlink(), but also catches the error and returns false if the
61  * file does not exist.  Note that this cannot be tested with
62  * file.exists(), because that method automatically resolves symlinks.
63  */
64 function file_symlink_p (file) {
65     try {
66         return file.isSymlink();
67     } catch (e if (e instanceof Ci.nsIException) &&
68              e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
69     {
70         return false;
71     }
75 function get_document_content_disposition (document_o) {
76     var content_disposition = null;
77     try {
78         content_disposition = document_o.defaultView
79             .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
80             .getInterface(Components.interfaces.nsIDOMWindowUtils)
81             .getDocumentMetadata("content-disposition");
82     } catch (e) { }
83     return content_disposition;
87 function set_focus_no_scroll (window, element) {
88     window.document.commandDispatcher.suppressFocusScroll = true;
89     element.focus();
90     window.document.commandDispatcher.suppressFocusScroll = false;
93 function do_repeatedly_positive (func, n) {
94     var args = Array.prototype.slice.call(arguments, 2);
95     while (n-- > 0)
96         func.apply(null, args);
99 function do_repeatedly (func, n, positive_args, negative_args) {
100     if (n < 0)
101         do func.apply(null, negative_args); while (++n < 0);
102     else
103         while (n-- > 0) func.apply(null, positive_args);
108  * Given a node, returns its position relative to the document.
110  * @param node The node to get the position of.
111  * @return An object with properties "x" and "y" representing its offset from
112  *         the left and top of the document, respectively.
113  */
114 function abs_point (node) {
115     var orig = node;
116     var pt = {};
117     try {
118         pt.x = node.offsetLeft;
119         pt.y = node.offsetTop;
120         // find imagemap's coordinates
121         if (node.tagName == "AREA") {
122             var coords = node.getAttribute("coords").split(",");
123             pt.x += Number(coords[0]);
124             pt.y += Number(coords[1]);
125         }
127         node = node.offsetParent;
128         // Sometimes this fails, so just return what we got.
130         while (node.tagName != "BODY") {
131             pt.x += node.offsetLeft;
132             pt.y += node.offsetTop;
133             node = node.offsetParent;
134         }
135     } catch(e) {
136         // node = orig;
137         // while (node.tagName != "BODY") {
138         //     alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
139         //     node = node.offsetParent;
140         // }
141     }
142     return pt;
146 function method_caller (obj, func) {
147     return function () {
148         func.apply(obj, arguments);
149     };
153 function get_window_from_frame (frame) {
154     try {
155         var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
156             .getInterface(Ci.nsIWebNavigation)
157             .QueryInterface(Ci.nsIDocShellTreeItem)
158             .rootTreeItem
159             .QueryInterface(Ci.nsIInterfaceRequestor)
160             .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
161         /* window is now an XPCSafeJSObjectWrapper */
162         window.escape_wrapper(function (w) { window = w; });
163         /* window is now completely unwrapped */
164         return window;
165     } catch (e) {
166         return null;
167     }
170 function get_buffer_from_frame (window, frame) {
171     var count = window.buffers.count;
172     for (var i = 0; i < count; ++i) {
173         var b = window.buffers.get_buffer(i);
174         if (b.top_frame == frame.top)
175             return b;
176     }
177     return null;
182  * Generates a QueryInterface function suitable for an implemenation
183  * of an XPCOM interface.  Unlike XPCOMUtils, this uses the Function
184  * constructor to generate a slightly more efficient version.  The
185  * arguments can be either Strings or elements of
186  * Components.interfaces.
187  */
188 function generate_QI () {
189     var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
190     var fstr = "if(" +
191         Array.prototype.map.call(args, function (x) {
192             return "iid.equals(Components.interfaces." + x + ")";
193         })
194         .join("||") +
195         ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
196     return new Function("iid", fstr);
199 var abort = task_canceled;
201 function get_temporary_file (name) {
202     if (name == null)
203         name = "temp.txt";
204     var file = file_locator_service.get("TmpD", Ci.nsIFile);
205     file.append(name);
206     // Create the file now to ensure that no exploits are possible
207     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
208     return file;
212 /* FIXME: This should be moved somewhere else, perhaps. */
213 function create_info_panel (window, panel_class, row_arr) {
214     /* Show information panel above minibuffer */
216     var g = new dom_generator(window.document, XUL_NS);
218     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
219     var grid = g.element("grid", p);
220     var cols = g.element("columns", grid);
221     g.element("column", cols, "flex", "0");
222     g.element("column", cols, "flex", "1");
224     var rows = g.element("rows", grid);
225     var row;
227     for each (let [row_class, row_label, row_value] in row_arr) {
228         row = g.element("row", rows, "class", row_class);
229         g.element("label", row,
230                   "value", row_label,
231                   "class", "panel-row-label");
232         g.element("label", row,
233                   "value", row_value,
234                   "class", "panel-row-value",
235                   "crop", "end");
236     }
237     window.minibuffer.insert_before(p);
239     p.destroy = function () {
240         this.parentNode.removeChild(this);
241     };
243     return p;
248  * Return clipboard contents as string.  When which_clipboard is given, it
249  * may be an nsIClipboard constant specifying which clipboard to use.
250  */
251 function read_from_clipboard (which_clipboard) {
252     var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
253         .getService(Ci.nsIClipboard);
254     if (which_clipboard == null)
255         which_clipboard = clipboard.kGlobalClipboard;
257     var flavors = ["text/unicode"];
259     // Don't barf if there's nothing on the clipboard
260     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
261         return "";
263     // Create transferable that will transfer the text.
264     var trans = Cc["@mozilla.org/widget/transferable;1"]
265         .createInstance(Ci.nsITransferable);
267     for each (let flavor in flavors) {
268         trans.addDataFlavor(flavor);
269     }
270     clipboard.getData(trans, which_clipboard);
272     var data_flavor = {};
273     var data = {};
274     var dataLen = {};
275     trans.getAnyTransferData(data_flavor, data, dataLen);
277     if (data) {
278         data = data.value.QueryInterface(Ci.nsISupportsString);
279         var data_length = dataLen.value;
280         if (data_flavor.value == "text/unicode")
281             data_length = dataLen.value / 2;
282         return data.data.substring(0, data_length);
283     } else
284         return ""; //XXX: is this even reachable?
289  * Return selection clipboard contents as a string, or regular clipboard
290  * contents if the system does not support a selection clipboard.
291  */
292 function read_from_x_primary_selection () {
293     var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
294         .getService(Ci.nsIClipboard);
295     // fall back to global clipboard if the
296     // system doesn't support a selection
297     var which_clipboard = clipboard.supportsSelectionClipboard() ?
298         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
299     return read_from_clipboard(which_clipboard);
303 function predicate_alist_match (alist, key) {
304     for each (let i in alist) {
305         if (i[0] instanceof RegExp) {
306             if (i[0].exec(key))
307                 return i[1];
308         } else if (i[0](key))
309             return i[1];
310     }
311     return undefined;
315 function get_meta_title (doc) {
316     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
317                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
318     if (title && title.stringValue)
319         return title.stringValue;
320     return null;
324 function queue () {
325     this.input = [];
326     this.output = [];
328 queue.prototype = {
329     constructor: queue,
330     get length () {
331         return this.input.length + this.output.length;
332     },
333     push: function (x) {
334         this.input[this.input.length] = x;
335     },
336     pop: function (x) {
337         let l = this.output.length;
338         if (!l) {
339             l = this.input.length;
340             if (!l)
341                 return undefined;
342             this.output = this.input.reverse();
343             this.input = [];
344             let x = this.output[l];
345             this.output.length--;
346             return x;
347         }
348     }
351 function for_each_frame (win, callback) {
352     callback(win);
353     if (win.frames && win.frames.length) {
354         for (var i = 0, n = win.frames.length; i < n; ++i)
355             for_each_frame(win.frames[i], callback);
356     }
359 function frame_iterator (root_frame, start_with) {
360     var q = new queue, x;
361     if (start_with) {
362         x = start_with;
363         do {
364             yield x;
365             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
366                 q.push(x.frames[i]);
367         } while ((x = q.pop()));
368     }
369     x = root_frame;
370     do {
371         if (x == start_with)
372             continue;
373         yield x;
374         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
375             q.push(x.frames[i]);
376     } while ((x = q.pop()));
379 function xml_http_request () {
380     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
381         .createInstance(Ci.nsIXMLHttpRequest)
382         .QueryInterface(Ci.nsIJSXMLHttpRequest)
383         .QueryInterface(Ci.nsIDOMEventTarget);
386 var xml_http_request_load_listener = {
387   // nsIBadCertListener2
388   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
389     return true;
390   },
392   // nsISSLErrorListener
393   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
394     return true;
395   },
397   // nsIInterfaceRequestor
398   getInterface: function SSLL_getInterface (iid) {
399     return this.QueryInterface(iid);
400   },
402   // nsISupports
403   //
404   // FIXME: array comprehension used here to hack around the lack of
405   // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
406   // make it a simple generateQI when xulrunner is more stable.
407   QueryInterface: XPCOMUtils.generateQI(
408       [i for each (i in [Ci.nsIBadCertListener2,
409                          Ci.nsISSLErrorListener,
410                          Ci.nsIInterfaceRequestor])
411        if (i)])
416  * Promise interface for sending an HTTP request and waiting for the
417  * response. (This includes so-called "AJAX" requests.)
419  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
421  * The request URI is obtained from this argument. In addition, if the
422  * load spec specifies post data, a POST request is made instead of a
423  * GET request, and the post data included in the load spec is
424  * sent. Specifically, the request_mime_type and raw_post_data
425  * properties of the load spec are used.
427  * @param $user (optional) HTTP user name to include in the request headers
428  * @param $password (optional) HTTP password to include in the request headers
430  * @param $override_mime_type (optional) Force the response to be interpreted
431  *                            as having the specified MIME type.  This is only
432  *                            really useful for forcing the MIME type to be
433  *                            text/xml or something similar, such that it is
434  *                            automatically parsed into a DOM document.
435  * @param $headers (optional) an array of [name,value] pairs (each specified as
436  *                 a two-element array) specifying additional headers to add to
437  *                 the request.
439  * @returns Promise that resolves to nsIXMLHttpRequest after the request
440  *          completes (either successfully or with an error).  Its responseText
441  *          (for any arbitrary document) or responseXML (if the response type is
442  *          an XML content type) properties can be accessed to examine the
443  *          response document.
445  **/
446 define_keywords("$user", "$password", "$override_mime_type", "$headers");
447 function send_http_request (lspec) {
448     // why do we get warnings in jsconsole unless we initialize the
449     // following keywords?
450     keywords(arguments, $user = undefined, $password = undefined,
451              $override_mime_type = undefined, $headers = undefined);
452     if (! (lspec instanceof load_spec))
453         lspec = load_spec(lspec);
454     var req = xml_http_request();
456     let deferred = Promise.defer();
457     req.onreadystatechange = function send_http_request__onreadystatechange () {
458         if (req.readyState != 4)
459             return;
460         deferred.resolve(req);
461     };
463     if (arguments.$override_mime_type)
464         req.overrideMimeType(arguments.$override_mime_type);
466     var post_data = load_spec_raw_post_data(lspec);
468     var method = post_data ? "POST" : "GET";
470     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
471     req.channel.notificationCallbacks = xml_http_request_load_listener;
473     for each (let [name,value] in arguments.$headers) {
474         req.setRequestHeader(name, value);
475     }
477     if (post_data) {
478         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
479         req.send(post_data);
480     } else
481         req.send(null);
483     return make_simple_cancelable(deferred);
488  * scroll_selection_into_view takes an editable element, and scrolls it so
489  * that the selection (or insertion point) are visible.
490  */
491 function scroll_selection_into_view (field) {
492     if (field.namespaceURI == XUL_NS)
493         field = field.inputField;
494     try {
495         field.QueryInterface(Ci.nsIDOMNSEditableElement)
496             .editor
497             .selectionController
498             .scrollSelectionIntoView(
499                 Ci.nsISelectionController.SELECTION_NORMAL,
500                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
501                 true);
502     } catch (e) {
503         // we'll get here for richedit fields
504     }
508 function compute_up_url (uri) {
509     try {
510         uri = uri.clone().QueryInterface(Ci.nsIURL);
511     } catch (e) {
512         return uri.spec;
513     }
514     for (let [k, p] in Iterator(["ref", "query", "param", "fileName"])) {
515         if (p in uri && uri[p] != "") {
516             uri[p] = "";
517             return uri.spec;
518         }
519     }
520     return uri.resolve("..");
524 function url_path_trim (url) {
525     var uri = make_uri(url);
526     uri.spec = url;
527     uri.path = "";
528     return uri.spec;
532  * possibly_valid_url returns true if its argument is an url-like string,
533  * meaning likely a valid thing to pass to nsIWebNavigation.loadURI.
534  */
535 function possibly_valid_url (str) {
536     // no inner space before first /
537     return /^\s*[^\/\s]*(\/|\s*$)/.test(str)
538         && !(/^\s*$/.test(str));
542 /* get_contents_synchronously returns the contents of the given
543  * url (string or nsIURI) as a string on success, or null on failure.
544  */
545 function get_contents_synchronously (url) {
546     var ioService=Cc["@mozilla.org/network/io-service;1"]
547         .getService(Ci.nsIIOService);
548     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
549         .getService(Ci.nsIScriptableInputStream);
550     var channel;
551     var input;
552     try {
553         if (url instanceof Ci.nsIURI)
554             channel = ioService.newChannelFromURI(url);
555         else
556             channel = ioService.newChannel(url, null, null);
557         input=channel.open();
558     } catch (e) {
559         return null;
560     }
561     scriptableStream.init(input);
562     var str=scriptableStream.read(input.available());
563     scriptableStream.close();
564     input.close();
565     return str;
570  * data is an an alist (array of 2 element arrays) where each pair is a key
571  * and a value.
573  * The return type is a mime input stream that can be passed as postData to
574  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
575  * of this function is of the correct type for the `post_data' field of a
576  * load_spec.
577  */
578 function make_post_data (data) {
579     data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
580             for each (pair in data)].join('&');
581     data = string_input_stream(data);
582     return mime_input_stream(
583         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
588  * Centers the viewport around a given element.
590  * @param win  The window to scroll.
591  * @param elem The element arund which we put the viewport.
592  */
593 function center_in_viewport (win, elem) {
594     let point = abs_point(elem);
596     point.x -= win.innerWidth / 2;
597     point.y -= win.innerHeight / 2;
599     win.scrollTo(point.x, point.y);
604  * Simple predicate returns true if elem is an nsIDOMNode or
605  * nsIDOMWindow.
606  */
607 function dom_node_or_window_p (elem) {
608     if (elem instanceof Ci.nsIDOMNode)
609         return true;
610     if (elem instanceof Ci.nsIDOMWindow)
611         return true;
612     return false;
616  * Given a hook name, a buffer and a function, waits until the buffer document
617  * has fully loaded, then calls the function with the buffer as its only
618  * argument.
620  * @param {String} The hook name.
621  * @param {buffer} The buffer.
622  * @param {function} The function to call with the buffer as its argument once
623  *                   the buffer has loaded.
624  */
625 function do_when (hook, buffer, fun) {
626     if (buffer.browser.webProgress.isLoadingDocument)
627         add_hook.call(buffer, hook, fun);
628     else
629         fun(buffer);
634  * evaluate string s as javascript in the 'this' scope in which evaluate
635  * is called.
636  */
637 function evaluate (s) {
638     try {
639         var obs = Cc["@mozilla.org/observer-service;1"]
640             .getService(Ci.nsIObserverService);
641         obs.notifyObservers(null, "startupcache-invalidate", null);
642         var temp = get_temporary_file("conkeror-evaluate.tmp.js");
643         write_text_file(temp, s);
644         var url = make_uri(temp).spec;
645         return load_url(url, this);
646     } finally {
647         if (temp && temp.exists())
648             temp.remove(false);
649     }
654  * set_protocol_handler takes a protocol and a handler spec.  If the
655  * handler is true, Mozilla will (try to) handle this protocol internally.
656  * If the handler null, the user will be prompted for a handler when a
657  * resource of this protocol is requested.  If the handler is an nsIFile,
658  * the program it gives will be launched with the url as an argument.  If
659  * the handler is a string, it will be interpreted as an URL template for
660  * a web service and the sequence '%s' within it will be replaced by the
661  * url-encoded url.
662  */
663 function set_protocol_handler (protocol, handler) {
664     var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
665         .getService(Ci.nsIExternalProtocolService);
666     var info = eps.getProtocolHandlerInfo(protocol);
667     var expose_pref = "network.protocol-handler.expose."+protocol;
668     if (handler == true) {
669         // internal handling
670         clear_default_pref(expose_pref);
671     } else if (handler) {
672         // external handling
673         if (handler instanceof Ci.nsIFile) {
674             var h = Cc["@mozilla.org/uriloader/local-handler-app;1"]
675                 .createInstance(Ci.nsILocalHandlerApp);
676             h.executable = handler;
677         } else if (typeof handler == "string") {
678             h = Cc["@mozilla.org/uriloader/web-handler-app;1"]
679                 .createInstance(Ci.nsIWebHandlerApp);
680             var uri = make_uri(handler);
681             h.name = uri.host;
682             h.uriTemplate = handler;
683         }
684         info.alwaysAskBeforeHandling = false;
685         info.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
686         info.possibleApplicationHandlers.clear();
687         info.possibleApplicationHandlers.appendElement(h, false);
688         info.preferredApplicationHandler = h;
689         session_pref(expose_pref, false);
690     } else {
691         // prompt
692         info.alwaysAskBeforeHandling = true;
693         info.preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
694         session_pref(expose_pref, false);
695     }
696     var hs = Cc["@mozilla.org/uriloader/handler-service;1"]
697         .getService(Ci.nsIHandlerService);
698     hs.store(info);
701 provide("utils");