for_each_frame: new util, iterates frames with a callback
[conkeror.git] / modules / utils.js
blobcef6090bd281f1182bb557efd72c42fe5d583090
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);
200 function abort (str) {
201     var e = new Error(str);
202     e.__proto__ = abort.prototype;
203     return e;
205 abort.prototype.__proto__ = Error.prototype;
208 function get_temporary_file (name) {
209     if (name == null)
210         name = "temp.txt";
211     var file = file_locator_service.get("TmpD", Ci.nsIFile);
212     file.append(name);
213     // Create the file now to ensure that no exploits are possible
214     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
215     return file;
219 /* FIXME: This should be moved somewhere else, perhaps. */
220 function create_info_panel (window, panel_class, row_arr) {
221     /* Show information panel above minibuffer */
223     var g = new dom_generator(window.document, XUL_NS);
225     var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
226     var grid = g.element("grid", p);
227     var cols = g.element("columns", grid);
228     g.element("column", cols, "flex", "0");
229     g.element("column", cols, "flex", "1");
231     var rows = g.element("rows", grid);
232     var row;
234     for each (let [row_class, row_label, row_value] in row_arr) {
235         row = g.element("row", rows, "class", row_class);
236         g.element("label", row,
237                   "value", row_label,
238                   "class", "panel-row-label");
239         g.element("label", row,
240                   "value", row_value,
241                   "class", "panel-row-value",
242                   "crop", "end");
243     }
244     window.minibuffer.insert_before(p);
246     p.destroy = function () {
247         this.parentNode.removeChild(this);
248     };
250     return p;
255  * Return clipboard contents as string.  When which_clipboard is given, it
256  * may be an nsIClipboard constant specifying which clipboard to use.
257  */
258 function read_from_clipboard (which_clipboard) {
259     var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
260         .getService(Ci.nsIClipboard);
261     if (which_clipboard == null)
262         which_clipboard = clipboard.kGlobalClipboard;
264     var flavors = ["text/unicode"];
266     // Don't barf if there's nothing on the clipboard
267     if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
268         return "";
270     // Create transferable that will transfer the text.
271     var trans = Cc["@mozilla.org/widget/transferable;1"]
272         .createInstance(Ci.nsITransferable);
274     for each (let flavor in flavors) {
275         trans.addDataFlavor(flavor);
276     }
277     clipboard.getData(trans, which_clipboard);
279     var data_flavor = {};
280     var data = {};
281     var dataLen = {};
282     trans.getAnyTransferData(data_flavor, data, dataLen);
284     if (data) {
285         data = data.value.QueryInterface(Ci.nsISupportsString);
286         var data_length = dataLen.value;
287         if (data_flavor.value == "text/unicode")
288             data_length = dataLen.value / 2;
289         return data.data.substring(0, data_length);
290     } else
291         return ""; //XXX: is this even reachable?
296  * Return selection clipboard contents as a string, or regular clipboard
297  * contents if the system does not support a selection clipboard.
298  */
299 function read_from_x_primary_selection () {
300     var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
301         .getService(Ci.nsIClipboard);
302     // fall back to global clipboard if the
303     // system doesn't support a selection
304     var which_clipboard = clipboard.supportsSelectionClipboard() ?
305         clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
306     return read_from_clipboard(which_clipboard);
310 function predicate_alist_match (alist, key) {
311     for each (let i in alist) {
312         if (i[0] instanceof RegExp) {
313             if (i[0].exec(key))
314                 return i[1];
315         } else if (i[0](key))
316             return i[1];
317     }
318     return undefined;
322 function get_meta_title (doc) {
323     var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
324                              Ci.nsIDOMXPathResult.STRING_TYPE , null);
325     if (title && title.stringValue)
326         return title.stringValue;
327     return null;
331 function queue () {
332     this.input = [];
333     this.output = [];
335 queue.prototype = {
336     constructor: queue,
337     get length () {
338         return this.input.length + this.output.length;
339     },
340     push: function (x) {
341         this.input[this.input.length] = x;
342     },
343     pop: function (x) {
344         let l = this.output.length;
345         if (!l) {
346             l = this.input.length;
347             if (!l)
348                 return undefined;
349             this.output = this.input.reverse();
350             this.input = [];
351             let x = this.output[l];
352             this.output.length--;
353             return x;
354         }
355     }
358 function for_each_frame (win, callback) {
359     callback(win);
360     if (win.frames && win.frames.length) {
361         for (var i = 0, n = win.frames.length; i < n; ++i)
362             for_each_frame(win.frames[i], callback);
363     }
366 function frame_iterator (root_frame, start_with) {
367     var q = new queue, x;
368     if (start_with) {
369         x = start_with;
370         do {
371             yield x;
372             for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
373                 q.push(x.frames[i]);
374         } while ((x = q.pop()));
375     }
376     x = root_frame;
377     do {
378         if (x == start_with)
379             continue;
380         yield x;
381         for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
382             q.push(x.frames[i]);
383     } while ((x = q.pop()));
386 function xml_http_request () {
387     return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
388         .createInstance(Ci.nsIXMLHttpRequest)
389         .QueryInterface(Ci.nsIJSXMLHttpRequest)
390         .QueryInterface(Ci.nsIDOMEventTarget);
393 var xml_http_request_load_listener = {
394   // nsIBadCertListener2
395   notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
396     return true;
397   },
399   // nsISSLErrorListener
400   notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
401     return true;
402   },
404   // nsIInterfaceRequestor
405   getInterface: function SSLL_getInterface (iid) {
406     return this.QueryInterface(iid);
407   },
409   // nsISupports
410   //
411   // FIXME: array comprehension used here to hack around the lack of
412   // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
413   // make it a simple generateQI when xulrunner is more stable.
414   QueryInterface: XPCOMUtils.generateQI(
415       [i for each (i in [Ci.nsIBadCertListener2,
416                          Ci.nsISSLErrorListener,
417                          Ci.nsIInterfaceRequestor])
418        if (i)])
423  * Coroutine interface for sending an HTTP request and waiting for the
424  * response. (This includes so-called "AJAX" requests.)
426  * @param lspec (required) a load_spec object or URI string (see load-spec.js)
428  * The request URI is obtained from this argument. In addition, if the
429  * load spec specifies post data, a POST request is made instead of a
430  * GET request, and the post data included in the load spec is
431  * sent. Specifically, the request_mime_type and raw_post_data
432  * properties of the load spec are used.
434  * @param $user (optional) HTTP user name to include in the request headers
435  * @param $password (optional) HTTP password to include in the request headers
437  * @param $override_mime_type (optional) Force the response to be interpreted
438  *                            as having the specified MIME type.  This is only
439  *                            really useful for forcing the MIME type to be
440  *                            text/xml or something similar, such that it is
441  *                            automatically parsed into a DOM document.
442  * @param $headers (optional) an array of [name,value] pairs (each specified as
443  *                 a two-element array) specifying additional headers to add to
444  *                 the request.
446  * @returns After the request completes (either successfully or with an error),
447  *          the nsIXMLHttpRequest object is returned.  Its responseText (for any
448  *          arbitrary document) or responseXML (if the response type is an XML
449  *          content type) properties can be accessed to examine the response
450  *          document.
452  * If an exception is thrown to the continutation (which can be obtained by the
453  * caller by calling yield CONTINUATION prior to calling this function) while the
454  * request is in progress (i.e. before this coroutine returns), the request will
455  * be aborted, and the exception will be propagated to the caller.
457  **/
458 define_keywords("$user", "$password", "$override_mime_type", "$headers");
459 function send_http_request (lspec) {
460     // why do we get warnings in jsconsole unless we initialize the
461     // following keywords?
462     keywords(arguments, $user = undefined, $password = undefined,
463              $override_mime_type = undefined, $headers = undefined);
464     if (! (lspec instanceof load_spec))
465         lspec = load_spec(lspec);
466     var req = xml_http_request();
467     var cc = yield CONTINUATION;
468     var aborting = false;
469     req.onreadystatechange = function send_http_request__onreadystatechange () {
470         if (req.readyState != 4)
471             return;
472         if (aborting)
473             return;
474         cc();
475     };
477     if (arguments.$override_mime_type)
478         req.overrideMimeType(arguments.$override_mime_type);
480     var post_data = load_spec_raw_post_data(lspec);
482     var method = post_data ? "POST" : "GET";
484     req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
485     req.channel.notificationCallbacks = xml_http_request_load_listener;
487     for each (let [name,value] in arguments.$headers) {
488         req.setRequestHeader(name, value);
489     }
491     if (post_data) {
492         req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
493         req.send(post_data);
494     } else
495         req.send(null);
497     try {
498         yield SUSPEND;
499     } catch (e) {
500         aborting = true;
501         req.abort();
502         throw e;
503     }
505     // Let the caller access the status and reponse data
506     yield co_return(req);
511  * scroll_selection_into_view takes an editable element, and scrolls it so
512  * that the selection (or insertion point) are visible.
513  */
514 function scroll_selection_into_view (field) {
515     if (field.namespaceURI == XUL_NS)
516         field = field.inputField;
517     try {
518         field.QueryInterface(Ci.nsIDOMNSEditableElement)
519             .editor
520             .selectionController
521             .scrollSelectionIntoView(
522                 Ci.nsISelectionController.SELECTION_NORMAL,
523                 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
524                 true);
525     } catch (e) {
526         // we'll get here for richedit fields
527     }
531 function compute_up_url (uri) {
532     try {
533         uri = uri.clone().QueryInterface(Ci.nsIURL);
534     } catch (e) {
535         return uri.spec;
536     }
537     for (let [k, p] in Iterator(["ref", "query", "param", "fileName"])) {
538         if (p in uri && uri[p] != "") {
539             uri[p] = "";
540             return uri.spec;
541         }
542     }
543     return uri.resolve("..");
547 function url_path_trim (url) {
548     var uri = make_uri(url);
549     uri.spec = url;
550     uri.path = "";
551     return uri.spec;
555  * possibly_valid_url returns true if its argument is an url-like string,
556  * meaning likely a valid thing to pass to nsIWebNavigation.loadURI.
557  */
558 function possibly_valid_url (str) {
559     // no inner space before first /
560     return /^\s*[^\/\s]*(\/|\s*$)/.test(str)
561         && !(/^\s*$/.test(str));
565 /* get_contents_synchronously returns the contents of the given
566  * url (string or nsIURI) as a string on success, or null on failure.
567  */
568 function get_contents_synchronously (url) {
569     var ioService=Cc["@mozilla.org/network/io-service;1"]
570         .getService(Ci.nsIIOService);
571     var scriptableStream=Cc["@mozilla.org/scriptableinputstream;1"]
572         .getService(Ci.nsIScriptableInputStream);
573     var channel;
574     var input;
575     try {
576         if (url instanceof Ci.nsIURI)
577             channel = ioService.newChannelFromURI(url);
578         else
579             channel = ioService.newChannel(url, null, null);
580         input=channel.open();
581     } catch (e) {
582         return null;
583     }
584     scriptableStream.init(input);
585     var str=scriptableStream.read(input.available());
586     scriptableStream.close();
587     input.close();
588     return str;
593  * data is an an alist (array of 2 element arrays) where each pair is a key
594  * and a value.
596  * The return type is a mime input stream that can be passed as postData to
597  * nsIWebNavigation.loadURI.  In terms of Conkeror's API, the return value
598  * of this function is of the correct type for the `post_data' field of a
599  * load_spec.
600  */
601 function make_post_data (data) {
602     data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
603             for each (pair in data)].join('&');
604     data = string_input_stream(data);
605     return mime_input_stream(
606         data, [["Content-Type", "application/x-www-form-urlencoded"]]);
611  * Centers the viewport around a given element.
613  * @param win  The window to scroll.
614  * @param elem The element arund which we put the viewport.
615  */
616 function center_in_viewport (win, elem) {
617     let point = abs_point(elem);
619     point.x -= win.innerWidth / 2;
620     point.y -= win.innerHeight / 2;
622     win.scrollTo(point.x, point.y);
627  * Simple predicate returns true if elem is an nsIDOMNode or
628  * nsIDOMWindow.
629  */
630 function dom_node_or_window_p (elem) {
631     if (elem instanceof Ci.nsIDOMNode)
632         return true;
633     if (elem instanceof Ci.nsIDOMWindow)
634         return true;
635     return false;
639  * Given a hook name, a buffer and a function, waits until the buffer document
640  * has fully loaded, then calls the function with the buffer as its only
641  * argument.
643  * @param {String} The hook name.
644  * @param {buffer} The buffer.
645  * @param {function} The function to call with the buffer as its argument once
646  *                   the buffer has loaded.
647  */
648 function do_when (hook, buffer, fun) {
649     if (buffer.browser.webProgress.isLoadingDocument)
650         add_hook.call(buffer, hook, fun);
651     else
652         fun(buffer);
657  * evaluate string s as javascript in the 'this' scope in which evaluate
658  * is called.
659  */
660 function evaluate (s) {
661     try {
662         var obs = Cc["@mozilla.org/observer-service;1"]
663             .getService(Ci.nsIObserverService);
664         obs.notifyObservers(null, "startupcache-invalidate", null);
665         var temp = get_temporary_file("conkeror-evaluate.tmp.js");
666         write_text_file(temp, s);
667         var url = make_uri(temp).spec;
668         return load_url(url, this);
669     } finally {
670         if (temp && temp.exists())
671             temp.remove(false);
672     }
677  * set_protocol_handler takes a protocol and a handler spec.  If the
678  * handler is true, Mozilla will (try to) handle this protocol internally.
679  * If the handler null, the user will be prompted for a handler when a
680  * resource of this protocol is requested.  If the handler is an nsIFile,
681  * the program it gives will be launched with the url as an argument.  If
682  * the handler is a string, it will be interpreted as an URL template for
683  * a web service and the sequence '%s' within it will be replaced by the
684  * url-encoded url.
685  */
686 function set_protocol_handler (protocol, handler) {
687     var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
688         .getService(Ci.nsIExternalProtocolService);
689     var info = eps.getProtocolHandlerInfo(protocol);
690     var expose_pref = "network.protocol-handler.expose."+protocol;
691     if (handler == true) {
692         // internal handling
693         clear_default_pref(expose_pref);
694     } else if (handler) {
695         // external handling
696         if (handler instanceof Ci.nsIFile) {
697             var h = Cc["@mozilla.org/uriloader/local-handler-app;1"]
698                 .createInstance(Ci.nsILocalHandlerApp);
699             h.executable = handler;
700         } else if (typeof handler == "string") {
701             h = Cc["@mozilla.org/uriloader/web-handler-app;1"]
702                 .createInstance(Ci.nsIWebHandlerApp);
703             var uri = make_uri(handler);
704             h.name = uri.host;
705             h.uriTemplate = handler;
706         }
707         info.alwaysAskBeforeHandling = false;
708         info.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
709         info.possibleApplicationHandlers.clear();
710         info.possibleApplicationHandlers.appendElement(h, false);
711         info.preferredApplicationHandler = h;
712         session_pref(expose_pref, false);
713     } else {
714         // prompt
715         info.alwaysAskBeforeHandling = true;
716         info.preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
717         session_pref(expose_pref, false);
718     }
719     var hs = Cc["@mozilla.org/uriloader/handler-service;1"]
720         .getService(Ci.nsIHandlerService);
721     hs.store(info);
724 provide("utils");