new module to enable editing and deleting of bookmarks
[conkeror/arlinius.git] / modules / utils.js
blobceea81094cd0dd43507d58af1bd0a14522bed8b4
1 /**
2 * (C) Copyright 2004-2007 Shawn Betts
3 * (C) Copyright 2007-2011 John J. Foerch
4 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
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) == "~\\"))
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);
46 return f;
50 function make_file_from_chrome (url) {
51 var crs = Cc['@mozilla.org/chrome/chrome-registry;1']
52 .getService(Ci.nsIChromeRegistry);
53 var file = crs.convertChromeURL(make_uri(url));
54 return make_file(file.path);
57 function get_document_content_disposition (document_o) {
58 var content_disposition = null;
59 try {
60 content_disposition = document_o.defaultView
61 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
62 .getInterface(Components.interfaces.nsIDOMWindowUtils)
63 .getDocumentMetadata("content-disposition");
64 } catch (e) { }
65 return content_disposition;
69 function set_focus_no_scroll (window, element) {
70 window.document.commandDispatcher.suppressFocusScroll = true;
71 element.focus();
72 window.document.commandDispatcher.suppressFocusScroll = false;
75 function do_repeatedly_positive (func, n) {
76 var args = Array.prototype.slice.call(arguments, 2);
77 while (n-- > 0)
78 func.apply(null, args);
81 function do_repeatedly (func, n, positive_args, negative_args) {
82 if (n < 0)
83 do func.apply(null, negative_args); while (++n < 0);
84 else
85 while (n-- > 0) func.apply(null, positive_args);
89 /**
90 * Given a node, returns its position relative to the document.
92 * @param node The node to get the position of.
93 * @return An object with properties "x" and "y" representing its offset from
94 * the left and top of the document, respectively.
96 function abs_point (node) {
97 var orig = node;
98 var pt = {};
99 try {
100 pt.x = node.offsetLeft;
101 pt.y = node.offsetTop;
102 // find imagemap's coordinates
103 if (node.tagName == "AREA") {
104 var coords = node.getAttribute("coords").split(",");
105 pt.x += Number(coords[0]);
106 pt.y += Number(coords[1]);
109 node = node.offsetParent;
110 // Sometimes this fails, so just return what we got.
112 while (node.tagName != "BODY") {
113 pt.x += node.offsetLeft;
114 pt.y += node.offsetTop;
115 node = node.offsetParent;
117 } catch(e) {
118 // node = orig;
119 // while (node.tagName != "BODY") {
120 // alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
121 // node = node.offsetParent;
122 // }
124 return pt;
128 function method_caller (obj, func) {
129 return function () {
130 func.apply(obj, arguments);
135 function get_window_from_frame (frame) {
136 try {
137 var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
138 .getInterface(Ci.nsIWebNavigation)
139 .QueryInterface(Ci.nsIDocShellTreeItem)
140 .rootTreeItem
141 .QueryInterface(Ci.nsIInterfaceRequestor)
142 .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
143 /* window is now an XPCSafeJSObjectWrapper */
144 window.escape_wrapper(function (w) { window = w; });
145 /* window is now completely unwrapped */
146 return window;
147 } catch (e) {
148 return null;
152 function get_buffer_from_frame (window, frame) {
153 var count = window.buffers.count;
154 for (var i = 0; i < count; ++i) {
155 var b = window.buffers.get_buffer(i);
156 if (b.top_frame == frame.top)
157 return b;
159 return null;
164 * Generates a QueryInterface function suitable for an implemenation
165 * of an XPCOM interface. Unlike XPCOMUtils, this uses the Function
166 * constructor to generate a slightly more efficient version. The
167 * arguments can be either Strings or elements of
168 * Components.interfaces.
170 function generate_QI () {
171 var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
172 var fstr = "if(" +
173 Array.prototype.map.call(args, function (x) {
174 return "iid.equals(Components.interfaces." + x + ")";
176 .join("||") +
177 ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
178 return new Function("iid", fstr);
182 function abort (str) {
183 var e = new Error(str);
184 e.__proto__ = abort.prototype;
185 return e;
187 abort.prototype.__proto__ = Error.prototype;
190 function get_temporary_file (name) {
191 if (name == null)
192 name = "temp.txt";
193 var file = file_locator_service.get("TmpD", Ci.nsIFile);
194 file.append(name);
195 // Create the file now to ensure that no exploits are possible
196 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
197 return file;
201 /* FIXME: This should be moved somewhere else, perhaps. */
202 function create_info_panel (window, panel_class, row_arr) {
203 /* Show information panel above minibuffer */
205 var g = new dom_generator(window.document, XUL_NS);
207 var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
208 var grid = g.element("grid", p);
209 var cols = g.element("columns", grid);
210 g.element("column", cols, "flex", "0");
211 g.element("column", cols, "flex", "1");
213 var rows = g.element("rows", grid);
214 var row;
216 for each (let [row_class, row_label, row_value] in row_arr) {
217 row = g.element("row", rows, "class", row_class);
218 g.element("label", row,
219 "value", row_label,
220 "class", "panel-row-label");
221 g.element("label", row,
222 "value", row_value,
223 "class", "panel-row-value",
224 "crop", "end");
226 window.minibuffer.insert_before(p);
228 p.destroy = function () {
229 this.parentNode.removeChild(this);
232 return p;
237 * Return clipboard contents as string. When which_clipboard is given, it
238 * may be an nsIClipboard constant specifying which clipboard to use.
240 function read_from_clipboard (which_clipboard) {
241 var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
242 .getService(Ci.nsIClipboard);
243 if (which_clipboard == null)
244 which_clipboard = clipboard.kGlobalClipboard;
246 var flavors = ["text/unicode"];
248 // Don't barf if there's nothing on the clipboard
249 if (!clipboard.hasDataMatchingFlavors(flavors, flavors.length, which_clipboard))
250 return "";
252 // Create transferable that will transfer the text.
253 var trans = Cc["@mozilla.org/widget/transferable;1"]
254 .createInstance(Ci.nsITransferable);
256 for each (let flavor in flavors) {
257 trans.addDataFlavor(flavor);
259 clipboard.getData(trans, which_clipboard);
261 var data_flavor = {};
262 var data = {};
263 var dataLen = {};
264 trans.getAnyTransferData(data_flavor, data, dataLen);
266 if (data) {
267 data = data.value.QueryInterface(Ci.nsISupportsString);
268 var data_length = dataLen.value;
269 if (data_flavor.value == "text/unicode")
270 data_length = dataLen.value / 2;
271 return data.data.substring(0, data_length);
272 } else
273 return ""; //XXX: is this even reachable?
278 * Return selection clipboard contents as a string, or regular clipboard
279 * contents if the system does not support a selection clipboard.
281 function read_from_x_primary_selection () {
282 var clipboard = Cc["@mozilla.org/widget/clipboard;1"]
283 .getService(Ci.nsIClipboard);
284 // fall back to global clipboard if the
285 // system doesn't support a selection
286 var which_clipboard = clipboard.supportsSelectionClipboard() ?
287 clipboard.kSelectionClipboard : clipboard.kGlobalClipboard;
288 return read_from_clipboard(which_clipboard);
292 function predicate_alist_match (alist, key) {
293 for each (let i in alist) {
294 if (i[0] instanceof RegExp) {
295 if (i[0].exec(key))
296 return i[1];
297 } else if (i[0](key))
298 return i[1];
300 return undefined;
304 function get_meta_title (doc) {
305 var title = doc.evaluate("//meta[@name='title']/@content", doc, xpath_lookup_namespace,
306 Ci.nsIDOMXPathResult.STRING_TYPE , null);
307 if (title && title.stringValue)
308 return title.stringValue;
309 return null;
313 function queue () {
314 this.input = [];
315 this.output = [];
317 queue.prototype = {
318 constructor: queue,
319 get length () {
320 return this.input.length + this.output.length;
322 push: function (x) {
323 this.input[this.input.length] = x;
325 pop: function (x) {
326 let l = this.output.length;
327 if (!l) {
328 l = this.input.length;
329 if (!l)
330 return undefined;
331 this.output = this.input.reverse();
332 this.input = [];
333 let x = this.output[l];
334 this.output.length--;
335 return x;
340 function frame_iterator (root_frame, start_with) {
341 var q = new queue, x;
342 if (start_with) {
343 x = start_with;
344 do {
345 yield x;
346 for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
347 q.push(x.frames[i]);
348 } while ((x = q.pop()));
350 x = root_frame;
351 do {
352 if (x == start_with)
353 continue;
354 yield x;
355 for (let i = 0, nframes = x.frames.length; i < nframes; ++i)
356 q.push(x.frames[i]);
357 } while ((x = q.pop()));
360 function xml_http_request () {
361 return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
362 .createInstance(Ci.nsIXMLHttpRequest)
363 .QueryInterface(Ci.nsIJSXMLHttpRequest)
364 .QueryInterface(Ci.nsIDOMEventTarget);
367 var xml_http_request_load_listener = {
368 // nsIBadCertListener2
369 notifyCertProblem: function SSLL_certProblem (socketInfo, status, targetSite) {
370 return true;
373 // nsISSLErrorListener
374 notifySSLError: function SSLL_SSLError (socketInfo, error, targetSite) {
375 return true;
378 // nsIInterfaceRequestor
379 getInterface: function SSLL_getInterface (iid) {
380 return this.QueryInterface(iid);
383 // nsISupports
385 // FIXME: array comprehension used here to hack around the lack of
386 // Ci.nsISSLErrorListener in 2007 versions of xulrunner 1.9pre.
387 // make it a simple generateQI when xulrunner is more stable.
388 QueryInterface: XPCOMUtils.generateQI(
389 [i for each (i in [Ci.nsIBadCertListener2,
390 Ci.nsISSLErrorListener,
391 Ci.nsIInterfaceRequestor])
392 if (i)])
397 * Coroutine interface for sending an HTTP request and waiting for the
398 * response. (This includes so-called "AJAX" requests.)
400 * @param lspec (required) a load_spec object or URI string (see load-spec.js)
402 * The request URI is obtained from this argument. In addition, if the
403 * load spec specifies post data, a POST request is made instead of a
404 * GET request, and the post data included in the load spec is
405 * sent. Specifically, the request_mime_type and raw_post_data
406 * properties of the load spec are used.
408 * @param $user (optional) HTTP user name to include in the request headers
409 * @param $password (optional) HTTP password to include in the request headers
411 * @param $override_mime_type (optional) Force the response to be interpreted
412 * as having the specified MIME type. This is only
413 * really useful for forcing the MIME type to be
414 * text/xml or something similar, such that it is
415 * automatically parsed into a DOM document.
416 * @param $headers (optional) an array of [name,value] pairs (each specified as
417 * a two-element array) specifying additional headers to add to
418 * the request.
420 * @returns After the request completes (either successfully or with an error),
421 * the nsIXMLHttpRequest object is returned. Its responseText (for any
422 * arbitrary document) or responseXML (if the response type is an XML
423 * content type) properties can be accessed to examine the response
424 * document.
426 * If an exception is thrown to the continutation (which can be obtained by the
427 * caller by calling yield CONTINUATION prior to calling this function) while the
428 * request is in progress (i.e. before this coroutine returns), the request will
429 * be aborted, and the exception will be propagated to the caller.
432 define_keywords("$user", "$password", "$override_mime_type", "$headers");
433 function send_http_request (lspec) {
434 // why do we get warnings in jsconsole unless we initialize the
435 // following keywords?
436 keywords(arguments, $user = undefined, $password = undefined,
437 $override_mime_type = undefined, $headers = undefined);
438 if (! (lspec instanceof load_spec))
439 lspec = load_spec(lspec);
440 var req = xml_http_request();
441 var cc = yield CONTINUATION;
442 var aborting = false;
443 req.onreadystatechange = function send_http_request__onreadystatechange () {
444 if (req.readyState != 4)
445 return;
446 if (aborting)
447 return;
448 cc();
451 if (arguments.$override_mime_type)
452 req.overrideMimeType(arguments.$override_mime_type);
454 var post_data = load_spec_raw_post_data(lspec);
456 var method = post_data ? "POST" : "GET";
458 req.open(method, load_spec_uri_string(lspec), true, arguments.$user, arguments.$password);
459 req.channel.notificationCallbacks = xml_http_request_load_listener;
461 for each (let [name,value] in arguments.$headers) {
462 req.setRequestHeader(name, value);
465 if (post_data) {
466 req.setRequestHeader("Content-Type", load_spec_request_mime_type(lspec));
467 req.send(post_data);
468 } else
469 req.send(null);
471 try {
472 yield SUSPEND;
473 } catch (e) {
474 aborting = true;
475 req.abort();
476 throw e;
479 // Let the caller access the status and reponse data
480 yield co_return(req);
485 * scroll_selection_into_view takes an editable element, and scrolls it so
486 * that the selection (or insertion point) are visible.
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
505 function compute_up_url (uri) {
506 try {
507 uri = uri.clone().QueryInterface(Ci.nsIURL);
508 } catch (e) {
509 return uri.spec;
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;
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.
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.
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;
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.
575 function make_post_data (data) {
576 data = [(encodeURIComponent(pair[0])+'='+encodeURIComponent(pair[1]))
577 for each (pair in data)].join('&');
578 data = string_input_stream(data);
579 return mime_input_stream(
580 data, [["Content-Type", "application/x-www-form-urlencoded"]]);
585 * Centers the viewport around a given element.
587 * @param win The window to scroll.
588 * @param elem The element arund which we put the viewport.
590 function center_in_viewport (win, elem) {
591 let point = abs_point(elem);
593 point.x -= win.innerWidth / 2;
594 point.y -= win.innerHeight / 2;
596 win.scrollTo(point.x, point.y);
601 * Simple predicate returns true if elem is an nsIDOMNode or
602 * nsIDOMWindow.
604 function dom_node_or_window_p (elem) {
605 if (elem instanceof Ci.nsIDOMNode)
606 return true;
607 if (elem instanceof Ci.nsIDOMWindow)
608 return true;
609 return false;
613 * Given a hook name, a buffer and a function, waits until the buffer document
614 * has fully loaded, then calls the function with the buffer as its only
615 * argument.
617 * @param {String} The hook name.
618 * @param {buffer} The buffer.
619 * @param {function} The function to call with the buffer as its argument once
620 * the buffer has loaded.
622 function do_when (hook, buffer, fun) {
623 if (buffer.browser.webProgress.isLoadingDocument)
624 add_hook.call(buffer, hook, fun);
625 else
626 fun(buffer);
631 * evaluate string s as javascript in the 'this' scope in which evaluate
632 * is called.
634 function evaluate (s) {
635 try {
636 var obs = Cc["@mozilla.org/observer-service;1"]
637 .getService(Ci.nsIObserverService);
638 obs.notifyObservers(null, "startupcache-invalidate", null);
639 var temp = get_temporary_file("conkeror-evaluate.tmp.js");
640 write_text_file(temp, s);
641 var url = make_uri(temp).spec;
642 return load_url(url, this);
643 } finally {
644 if (temp && temp.exists())
645 temp.remove(false);
651 * set_protocol_handler takes a protocol and a handler spec. If the
652 * handler is true, Mozilla will (try to) handle this protocol internally.
653 * If the handler null, the user will be prompted for a handler when a
654 * resource of this protocol is requested. If the handler is an nsIFile,
655 * the program it gives will be launched with the url as an argument. If
656 * the handler is a string, it will be interpreted as an URL template for
657 * a web service and the sequence '%s' within it will be replaced by the
658 * url-encoded url.
660 function set_protocol_handler (protocol, handler) {
661 var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
662 .getService(Ci.nsIExternalProtocolService);
663 var info = eps.getProtocolHandlerInfo(protocol);
664 var expose_pref = "network.protocol-handler.expose."+protocol;
665 if (handler == true) {
666 // internal handling
667 clear_default_pref(expose_pref);
668 } else if (handler) {
669 // external handling
670 if (handler instanceof Ci.nsIFile) {
671 var h = Cc["@mozilla.org/uriloader/local-handler-app;1"]
672 .createInstance(Ci.nsILocalHandlerApp);
673 h.executable = handler;
674 } else if (typeof handler == "string") {
675 h = Cc["@mozilla.org/uriloader/web-handler-app;1"]
676 .createInstance(Ci.nsIWebHandlerApp);
677 var uri = make_uri(handler);
678 h.name = uri.host;
679 h.uriTemplate = handler;
681 info.alwaysAskBeforeHandling = false;
682 info.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
683 info.possibleApplicationHandlers.clear();
684 info.possibleApplicationHandlers.appendElement(h, false);
685 info.preferredApplicationHandler = h;
686 session_pref(expose_pref, false);
687 } else {
688 // prompt
689 info.alwaysAskBeforeHandling = true;
690 info.preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
691 session_pref(expose_pref, false);
693 var hs = Cc["@mozilla.org/uriloader/handler-service;1"]
694 .getService(Ci.nsIHandlerService);
695 hs.store(info);
698 provide("utils");