components, modules: change deprecated replace flag
[conkeror.git] / modules / utils.js
blobd6665430ec446ee688d82f425725c492c80ef7f0
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2012 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     }
33     if (path == "~") {
34         return get_home_directory();
35     }
36     if (WINDOWS) {
37         path = path.replace(/\//g, "\\");
38     }
39     if ((POSIX && path.substring(0,2) == "~/") ||
40         (WINDOWS && path.substring(0,2) == "~\\"))
41     {
42         var f = get_home_directory();
43         f.appendRelativePath(path.substring(2));
44     } else {
45         f = Cc["@mozilla.org/file/local;1"]
46             .createInstance(Ci.nsILocalFile);
47         f.initWithPath(path);
48     }
49     return f;
53 function make_file_from_chrome (url) {
54     var crs = Cc['@mozilla.org/chrome/chrome-registry;1']
55         .getService(Ci.nsIChromeRegistry);
56     var file = crs.convertChromeURL(make_uri(url));
57     return make_file(file.path);
61 /**
62  * file_symlink_p takes an nsIFile and returns the value of
63  * file.isSymlink(), but also catches the error and returns false if the
64  * file does not exist.  Note that this cannot be tested with
65  * file.exists(), because that method automatically resolves symlinks.
66  */
67 function file_symlink_p (file) {
68     try {
69         return file.isSymlink();
70     } catch (e if (e instanceof Ci.nsIException) &&
71              e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
72     {
73         return false;
74     }
78 function get_document_content_disposition (document_o) {
79     var content_disposition = null;
80     try {
81         content_disposition = document_o.defaultView
82             .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
83             .getInterface(Components.interfaces.nsIDOMWindowUtils)
84             .getDocumentMetadata("content-disposition");
85     } catch (e) { }
86     return content_disposition;
90 function set_focus_no_scroll (window, element) {
91     window.document.commandDispatcher.suppressFocusScroll = true;
92     element.focus();
93     window.document.commandDispatcher.suppressFocusScroll = false;
96 function do_repeatedly_positive (func, n) {
97     var args = Array.prototype.slice.call(arguments, 2);
98     while (n-- > 0)
99         func.apply(null, args);
102 function do_repeatedly (func, n, positive_args, negative_args) {
103     if (n < 0)
104         do func.apply(null, negative_args); while (++n < 0);
105     else
106         while (n-- > 0) func.apply(null, positive_args);
111  * Given a node, returns its position relative to the document.
113  * @param node The node to get the position of.
114  * @return An object with properties "x" and "y" representing its offset from
115  *         the left and top of the document, respectively.
116  */
117 function abs_point (node) {
118     var orig = node;
119     var pt = {};
120     try {
121         pt.x = node.offsetLeft;
122         pt.y = node.offsetTop;
123         // find imagemap's coordinates
124         if (node.tagName == "AREA") {
125             var coords = node.getAttribute("coords").split(",");
126             pt.x += Number(coords[0]);
127             pt.y += Number(coords[1]);
128         }
130         node = node.offsetParent;
131         // Sometimes this fails, so just return what we got.
133         while (node.tagName != "BODY") {
134             pt.x += node.offsetLeft;
135             pt.y += node.offsetTop;
136             node = node.offsetParent;
137         }
138     } catch(e) {
139         // node = orig;
140         // while (node.tagName != "BODY") {
141         //     alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
142         //     node = node.offsetParent;
143         // }
144     }
145     return pt;
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;
185  * Generates a QueryInterface function suitable for an implemenation
186  * of an XPCOM interface.  Unlike XPCOMUtils, this uses the Function
187  * constructor to generate a slightly more efficient version.  The
188  * arguments can be either Strings or elements of
189  * Components.interfaces.
190  */
191 function generate_QI () {
192     var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
193     var fstr = "if(" +
194         Array.prototype.map.call(args, function (x) {
195             return "iid.equals(Components.interfaces." + x + ")";
196         })
197         .join("||") +
198         ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
199     return new Function("iid", fstr);
202 var abort = task_canceled;
204 function get_temporary_file (name) {
205     if (name == null)
206         name = "temp.txt";
207     var file = file_locator_service.get("TmpD", Ci.nsIFile);
208     file.append(name);
209     // Create the file now to ensure that no exploits are possible
210     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0600", 8));
211     return file;
215 /* FIXME: This should be moved somewhere else, perhaps. */
216 function create_info_panel (window, panel_class, row_arr) {
217     /* Show information panel above minibuffer */
219     var g = new dom_generator(window.document, XUL_NS);
221     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
222     var grid = g.element("grid", p);
223     var cols = g.element("columns", grid);
224     g.element("column", cols, "flex", "0");
225     g.element("column", cols, "flex", "1");
227     var rows = g.element("rows", grid);
228     var row;
230     for each (let [row_class, row_label, row_value] in row_arr) {
231         row = g.element("row", rows, "class", row_class);
232         g.element("label", row,
233                   "value", row_label,
234                   "class", "panel-row-label");
235         g.element("label", row,
236                   "value", row_value,
237                   "class", "panel-row-value",
238                   "crop", "end");
239     }
240     window.minibuffer.insert_before(p);
242     p.destroy = function () {
243         this.parentNode.removeChild(this);
244     };
246     return p;
251  * Return clipboard contents as string.  When which_clipboard is given, it
252  * may be an nsIClipboard constant specifying which clipboard to use.
253  */
254 function read_from_clipboard (which_clipboard) {
255     var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
256         .getService(Ci.nsIClipboard);
257     if (which_clipboard == null)
258         which_clipboard = clipboard.kGlobalClipboard;
260     var flavors = ["text/unicode"];
262     // Don't barf if there's nothing on the clipboard
263     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
264         return "";
266     // Create transferable that will transfer the text.
267     var trans = Cc["@mozilla.org/widget/transferable;1"]
268         .createInstance(Ci.nsITransferable);
270     for each (let flavor in flavors) {
271         trans.addDataFlavor(flavor);
272     }
273     clipboard.getData(trans, which_clipboard);
275     var data_flavor = {};
276     var data = {};
277     var dataLen = {};
278     trans.getAnyTransferData(data_flavor, data, dataLen);
280     if (data) {
281         data = data.value.QueryInterface(Ci.nsISupportsString);
282         var data_length = dataLen.value;
283         if (data_flavor.value == "text/unicode")
284             data_length = dataLen.value / 2;
285         return data.data.substring(0, data_length);
286     } else
287         return ""; //XXX: is this even reachable?
292  * Return selection clipboard contents as a string, or regular clipboard
293  * contents if the system does not support a selection clipboard.
294  */
295 function read_from_x_primary_selection () {
296     var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
297         .getService(Ci.nsIClipboard);
298     // fall back to global clipboard if the
299     // system doesn't support a selection
300     var which_clipboard = clipboard.supportsSelectionClipboard() ?
301         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
302     return read_from_clipboard(which_clipboard);
306 function predicate_alist_match (alist, key) {
307     for each (let i in alist) {
308         if (i[0] instanceof RegExp) {
309             if (i[0].exec(key))
310                 return i[1];
311         } else if (i[0](key))
312             return i[1];
313     }
314     return undefined;
318 function get_meta_title (doc) {
319     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
320                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
321     if (title && title.stringValue)
322         return title.stringValue;
323     return null;
327 function queue () {
328     this.input = [];
329     this.output = [];
331 queue.prototype = {
332     constructor: queue,
333     get length () {
334         return this.input.length + this.output.length;
335     },
336     push: function (x) {
337         this.input[this.input.length] = x;
338     },
339     pop: function (x) {
340         let l = this.output.length;
341         if (!l) {
342             l = this.input.length;
343             if (!l)
344                 return undefined;
345             this.output = this.input.reverse();
346             this.input = [];
347             let x = this.output[l];
348             this.output.length--;
349             return x;
350         }
351     }
354 function for_each_frame (win, callback) {
355     callback(win);
356     if (win.frames && win.frames.length) {
357         for (var i = 0, n = win.frames.length; i < n; ++i)
358             for_each_frame(win.frames[i], callback);
359     }
362 function frame_iterator (root_frame, start_with) {
363     var q = new queue, x;
364     if (start_with) {
365         x = start_with;
366         do {
367             yield x;
368             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
369                 q.push(x.frames[i]);
370         } while ((x = q.pop()));
371     }
372     x = root_frame;
373     do {
374         if (x == start_with)
375             continue;
376         yield x;
377         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
378             q.push(x.frames[i]);
379     } while ((x = q.pop()));
382 function xml_http_request () {
383     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
384         .createInstance(Ci.nsIXMLHttpRequest)
385         .QueryInterface(Ci.nsIJSXMLHttpRequest)
386         .QueryInterface(Ci.nsIDOMEventTarget);
389 var xml_http_request_load_listener = {
390   // nsIBadCertListener2
391   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
392     return true;
393   },
395   // nsISSLErrorListener
396   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
397     return true;
398   },
400   // nsIInterfaceRequestor
401   getInterface: function SSLL_getInterface (iid) {
402     return this.QueryInterface(iid);
403   },
405   // nsISupports
406   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBadCertListener2,
407                                          Ci.nsISSLErrorListener,
408                                          Ci.nsIInterfaceRequestor])
413  * Promise interface for sending an HTTP request and waiting for the
414  * response. (This includes so-called "AJAX" requests.)
416  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
418  * The request URI is obtained from this argument. In addition, if the
419  * load spec specifies post data, a POST request is made instead of a
420  * GET request, and the post data included in the load spec is
421  * sent. Specifically, the request_mime_type and raw_post_data
422  * properties of the load spec are used.
424  * @param $user (optional) HTTP user name to include in the request headers
425  * @param $password (optional) HTTP password to include in the request headers
427  * @param $override_mime_type (optional) Force the response to be interpreted
428  *                            as having the specified MIME type.  This is only
429  *                            really useful for forcing the MIME type to be
430  *                            text/xml or something similar, such that it is
431  *                            automatically parsed into a DOM document.
432  * @param $headers (optional) an array of [name,value] pairs (each specified as
433  *                 a two-element array) specifying additional headers to add to
434  *                 the request.
436  * @returns Promise that resolves to nsIXMLHttpRequest after the request
437  *          completes (either successfully or with an error).  Its responseText
438  *          (for any arbitrary document) or responseXML (if the response type is
439  *          an XML content type) properties can be accessed to examine the
440  *          response document.
442  **/
443 define_keywords("$user", "$password", "$override_mime_type", "$headers");
444 function send_http_request (lspec) {
445     // why do we get warnings in jsconsole unless we initialize the
446     // following keywords?
447     keywords(arguments, $user = undefined, $password = undefined,
448              $override_mime_type = undefined, $headers = undefined);
449     if (! (lspec instanceof load_spec))
450         lspec = load_spec(lspec);
451     var req = xml_http_request();
453     let deferred = Promise.defer();
454     req.onreadystatechange = function send_http_request__onreadystatechange () {
455         if (req.readyState != 4)
456             return;
457         deferred.resolve(req);
458     };
460     if (arguments.$override_mime_type)
461         req.overrideMimeType(arguments.$override_mime_type);
463     var post_data = load_spec_raw_post_data(lspec);
465     var method = post_data ? "POST" : "GET";
467     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
468     req.channel.notificationCallbacks = xml_http_request_load_listener;
470     for each (let [name,value] in arguments.$headers) {
471         req.setRequestHeader(name, value);
472     }
474     if (post_data) {
475         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
476         req.send(post_data);
477     } else
478         req.send(null);
480     return make_simple_cancelable(deferred);
485  * scroll_selection_into_view takes an editable element, and scrolls it so
486  * that the selection (or insertion point) are visible.
487  */
488 function scroll_selection_into_view (field) {
489     if (field.namespaceURI == XUL_NS)
490         field = field.inputField;
491     try {
492         field.QueryInterface(Ci.nsIDOMNSEditableElement)
493             .editor
494             .selectionController
495             .scrollSelectionIntoView(
496                 Ci.nsISelectionController.SELECTION_NORMAL,
497                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
498                 true);
499     } catch (e) {
500         // we'll get here for richedit fields
501     }
505 function compute_up_url (uri) {
506     try {
507         uri = uri.clone().QueryInterface(Ci.nsIURL);
508     } catch (e) {
509         return uri.spec;
510     }
511     for (let [k, p] in Iterator(["ref", "query", "param", "fileName"])) {
512         if (p in uri && uri[p] != "") {
513             uri[p] = "";
514             return uri.spec;
515         }
516     }
517     return uri.resolve("..");
521 function url_path_trim (url) {
522     var uri = make_uri(url);
523     uri.spec = url;
524     uri.path = "";
525     return uri.spec;
529  * possibly_valid_url returns true if its argument is an url-like string,
530  * meaning likely a valid thing to pass to nsIWebNavigation.loadURI.
531  */
532 function possibly_valid_url (str) {
533     // no inner space before first /
534     return /^\s*[^\/\s]*(\/|\s*$)/.test(str)
535         && !(/^\s*$/.test(str));
539 /* get_contents_synchronously returns the contents of the given
540  * url (string or nsIURI) as a string on success, or null on failure.
541  */
542 function get_contents_synchronously (url) {
543     var ioService=Cc["@mozilla.org/network/io-service;1"]
544         .getService(Ci.nsIIOService);
545     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
546         .getService(Ci.nsIScriptableInputStream);
547     var channel;
548     var input;
549     try {
550         if (url instanceof Ci.nsIURI)
551             channel = ioService.newChannelFromURI(url);
552         else
553             channel = ioService.newChannel(url, null, null);
554         input=channel.open();
555     } catch (e) {
556         return null;
557     }
558     scriptableStream.init(input);
559     var str=scriptableStream.read(input.available());
560     scriptableStream.close();
561     input.close();
562     return str;
567  * data is an an alist (array of 2 element arrays) where each pair is a key
568  * and a value.
570  * The return type is a mime input stream that can be passed as postData to
571  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
572  * of this function is of the correct type for the `post_data' field of a
573  * load_spec.
574  */
575 function make_post_data (data) {
576     data = data.map(function(pair) {
577         return (encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
578     }).join('&');
579     data = string_input_stream(data);
580     return mime_input_stream(
581         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
586  * Centers the viewport around a given element.
588  * @param win  The window to scroll.
589  * @param elem The element arund which we put the viewport.
590  */
591 function center_in_viewport (win, elem) {
592     let point = abs_point(elem);
594     point.x -= win.innerWidth / 2;
595     point.y -= win.innerHeight / 2;
597     win.scrollTo(point.x, point.y);
602  * Simple predicate returns true if elem is an nsIDOMNode or
603  * nsIDOMWindow.
604  */
605 function dom_node_or_window_p (elem) {
606     if (elem instanceof Ci.nsIDOMNode)
607         return true;
608     if (elem instanceof Ci.nsIDOMWindow)
609         return true;
610     return false;
614  * Given a hook name, a buffer and a function, waits until the buffer document
615  * has fully loaded, then calls the function with the buffer as its only
616  * argument.
618  * @param {String} The hook name.
619  * @param {buffer} The buffer.
620  * @param {function} The function to call with the buffer as its argument once
621  *                   the buffer has loaded.
622  */
623 function do_when (hook, buffer, fun) {
624     if (buffer.browser.webProgress.isLoadingDocument)
625         add_hook.call(buffer, hook, fun);
626     else
627         fun(buffer);
632  * evaluate string s as javascript in the 'this' scope in which evaluate
633  * is called.
634  */
635 function evaluate (s) {
636     try {
637         var obs = Cc["@mozilla.org/observer-service;1"]
638             .getService(Ci.nsIObserverService);
639         obs.notifyObservers(null, "startupcache-invalidate", null);
640         var temp = get_temporary_file("conkeror-evaluate.tmp.js");
641         write_text_file(temp, s);
642         var url = make_uri(temp).spec;
643         return load_url(url, this);
644     } finally {
645         if (temp && temp.exists())
646             temp.remove(false);
647     }
652  * set_protocol_handler takes a protocol and a handler spec.  If the
653  * handler is true, Mozilla will (try to) handle this protocol internally.
654  * If the handler null, the user will be prompted for a handler when a
655  * resource of this protocol is requested.  If the handler is an nsIFile,
656  * the program it gives will be launched with the url as an argument.  If
657  * the handler is a string, it will be interpreted as an URL template for
658  * a web service and the sequence '%s' within it will be replaced by the
659  * url-encoded url.
660  */
661 function set_protocol_handler (protocol, handler) {
662     var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
663         .getService(Ci.nsIExternalProtocolService);
664     var info = eps.getProtocolHandlerInfo(protocol);
665     var expose_pref = "network.protocol-handler.expose."+protocol;
666     if (handler == true) {
667         // internal handling
668         clear_default_pref(expose_pref);
669     } else if (handler) {
670         // external handling
671         if (handler instanceof Ci.nsIFile) {
672             var h = Cc["@mozilla.org/uriloader/local-handler-app;1"]
673                 .createInstance(Ci.nsILocalHandlerApp);
674             h.executable = handler;
675         } else if (typeof handler == "string") {
676             h = Cc["@mozilla.org/uriloader/web-handler-app;1"]
677                 .createInstance(Ci.nsIWebHandlerApp);
678             var uri = make_uri(handler);
679             h.name = uri.host;
680             h.uriTemplate = handler;
681         }
682         info.alwaysAskBeforeHandling = false;
683         info.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
684         info.possibleApplicationHandlers.clear();
685         info.possibleApplicationHandlers.appendElement(h, false);
686         info.preferredApplicationHandler = h;
687         session_pref(expose_pref, false);
688     } else {
689         // prompt
690         info.alwaysAskBeforeHandling = true;
691         info.preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
692         session_pref(expose_pref, false);
693     }
694     var hs = Cc["@mozilla.org/uriloader/handler-service;1"]
695         .getService(Ci.nsIHandlerService);
696     hs.store(info);
701  * The identity function; returns its argument unchanged.
702  */
703 function identity (x) {
704     return x;
709  * Return a function that always returns x.
710  */
711 function constantly (x) {
712     return function () {
713         return x;
714     };
718 provide("utils");