diff options
author | Kris Maglione <maglione.k@gmail.com> | 2010-10-14 03:29:56 -0400 |
---|---|---|
committer | Kris Maglione <maglione.k@gmail.com> | 2010-10-14 03:29:56 -0400 |
commit | a703d0a3bf1d31e39444d643dfe993e630235e7c (patch) | |
tree | 2b0d0a33ab43a04244203aa0fc9a60b7d723db6b /common/modules/util.jsm | |
parent | 9e42f55fa19936a18eb846d4801f9a880da3e152 (diff) | |
download | pentadactyl-a703d0a3bf1d31e39444d643dfe993e630235e7c.tar.gz |
Integrate sanitizer with host UI, sanitize at shutdown support, and control which items are sanitized when more thoroughly. Closes issue #70.
Diffstat (limited to 'common/modules/util.jsm')
-rw-r--r-- | common/modules/util.jsm | 406 |
1 files changed, 401 insertions, 5 deletions
diff --git a/common/modules/util.jsm b/common/modules/util.jsm index 830f3508..7bc801bc 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -6,9 +6,11 @@ // given in the LICENSE.txt file included with this file. "use strict"; +try { + Components.utils.import("resource://dactyl/base.jsm"); defineModule("util", { - exports: ["Math", "NS", "Util", "XHTML", "XUL", "util"], + exports: ["FailedAssertion", "Math", "NS", "Prefs", "Util", "XHTML", "XUL", "prefs", "util"], require: ["services"], use: ["highlight", "template"] }); @@ -18,12 +20,33 @@ const XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there. const NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator"); default xml namespace = XHTML; -const Util = Module("Util", { +const FailedAssertion = Class("FailedAssertion", Error, { + init: function (message) { + this.message = message; + } +}); + +function wrapCallback(fn) + fn.wrapper = function wrappedCallback () { + try { + return fn.apply(this, arguments); + } + catch (e) { + util.reportError(e); + return undefined; + } + } + +const Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), { init: function () { this.Array = array; + + this.addObserver(this); + this.overlays = {}; }, - get activeWindow() services.get("windowWatcher").activeWindow, + // FIXME: Only works for Pentadactyl + get activeWindow() services.get("windowMediator").getMostRecentWindow("navigator:browser"), dactyl: { __noSuchMethod__: function (meth, args) { let win = util.activeWindow; @@ -61,6 +84,19 @@ const Util = Module("Util", { register("addObserver"); }, + /* + * Tests a condition and throws a FailedAssertion error on + * failure. + * + * @param {boolean} condition The condition to test. + * @param {string} message The message to present to the + * user on failure. + */ + assert: function (condition, message) { + if (!condition) + throw new FailedAssertion(message); + }, + /** * Calls a function synchronously in the main thread. Return values are not * preserved. @@ -644,6 +680,41 @@ const Util = Module("Util", { return color ? string : [s for each (s in string)].join(""); }, + observe: { + "toplevel-window-ready": function (window, data) { + window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) { + window.removeEventListener("DOMContentLoaded", listener.wrapper, true); + + if (event.originalTarget !== window.document) + return; + + let obj = util.overlays[window.document.documentURI]; + if (obj) { + obj = obj(window); + + function overlay(key, fn) { + for (let [elem, xml] in Iterator(obj[key] || {})) + if (elem = window.document.getElementById(elem)) + fn(elem, util.xmlToDom(xml, window.document)); + } + + overlay("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem)); + overlay("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling)); + overlay("append", function (elem, dom) elem.appendChild(dom)); + overlay("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild)); + if (obj.init) + obj.init(window, event); + + if (obj.load) + window.document.addEventListener("load", function (event) { + if (event.originalTarget === event.target) + obj.load(window, event); + }, true); + } + }), true) + } + }, + /** * Parses the fields of a form and returns a URL/POST-data pair * that is the equivalent of submitting the form. @@ -756,6 +827,12 @@ const Util = Module("Util", { } }, + overlayWindow: function (url, fn) { + Array.concat(url).forEach(function (url) { + this.overlays[url] = fn; + }, this); + }, + /** * Scrolls an element into view if and only if it's not already * fully visible. @@ -882,6 +959,22 @@ const Util = Module("Util", { }, /** + * Traps errors in the called function, possibly reporting them. + * + * @param {function} func The function to call + * @param {object} self The 'this' object for the function. + */ + trapErrors: function trapErrors(func, self) { + try { + return func.apply(self || this, Array.slice(arguments, 2)); + } + catch (e) { + util.reportError(e); + return undefined; + } + }, + + /** * Converts an E4X XML literal to a DOM node. Any attribute named * highlight is present, it is transformed into dactyl:highlight, * and the named highlight groups are guaranteed to be loaded. @@ -927,6 +1020,309 @@ const Util = Module("Util", { Array: array }); +const Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), { + SAVED: "extensions.dactyl.saved.", + + init: function () { + this._prefContexts = []; + + util.addObserver(this); + this._branch = services.get("pref").getBranch("").QueryInterface(Ci.nsIPrefBranch2); + this._branch.addObserver("", this, false); + this._observers = {}; + }, + + observe: { + "nsPref:changed": function (subject, data) { + let observers = this._observers[data]; + if (observers) { + let value = this.get(data, false); + this._observers[data] = observers.filter(function (callback) { + if (!callback.get()) + return false; + util.trapErrors(callback.get(), null, value); + return true; + }); + } + } + }, + + /** + * Adds a new preference observer for the given preference. + * + * @param {string} pref The preference to observe. + * @param {function(object)} callback The callback, called with the + * new value of the preference whenever it changes. + */ + watch: function (pref, callback, strong) { + if (!this._observers[pref]) + this._observers[pref] = []; + this._observers[pref].push(!strong ? Cu.getWeakReference(callback) : { get: function () callback }); + }, + + /** + * Lists all preferences matching <b>filter</b> or only those with + * changed values if <b>onlyNonDefault</b> is specified. + * + * @param {boolean} onlyNonDefault Limit the list to prefs with a + * non-default value. + * @param {string} filter The list filter. A null filter lists all + * prefs. + * @optional + */ + list: function list(onlyNonDefault, filter) { + if (!filter) + filter = ""; + + let prefArray = this.getNames(); + prefArray.sort(); + function prefs() { + for (let [, pref] in Iterator(prefArray)) { + let userValue = services.get("pref").prefHasUserValue(pref); + if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1) + continue; + + let value = this.get(pref); + + let option = { + isDefault: !userValue, + default: this._load(pref, null, true), + value: <>={template.highlight(value, true, 100)}</>, + name: pref, + pre: "\u00a0\u00a0" // Unicode nonbreaking space. + }; + + yield option; + } + }; + + return template.options(services.get("dactyl:").host + " Preferences", prefs.call(this)); + }, + + /** + * Returns the value of a preference. + * + * @param {string} name The preference name. + * @param {value} defaultValue The value to return if the preference + * is unset. + */ + get: function (name, defaultValue) { + return this._load(name, defaultValue); + }, + + /** + * Returns the default value of a preference + * + * @param {string} name The preference name. + * @param {value} defaultValue The value to return if the preference + * has no default value. + */ + getDefault: function (name, defaultValue) { + return this._load(name, defaultValue, true); + }, + + /** + * Returns the names of all preferences. + * + * @param {string} branch The branch in which to search preferences. + * @default "" + */ + getNames: function (branch) services.get("pref").getChildList(branch || "", { value: 0 }), + + _checkSafe: function (name, message, value) { + let curval = this._load(name, null, false); + if (arguments.length > 2 && curval === value) + return; + let defval = this._load(name, null, true); + let saved = this._load(this.SAVED + name); + + if (saved == null && curval != defval || curval != saved) { + let msg = "Warning: setting preference " + name + ", but it's changed from its default value."; + if (message) + msg += " " + message; + util.dactyl.echomsg(msg); + } + }, + + /** + * Resets the preference <b>name</b> to </b>value</b> but warns the user + * if the value is changed from its default. + * + * @param {string} name The preference name. + * @param {value} value The new preference value. + */ + safeReset: function (name, message) { + this._checkSafe(name, message); + this.reset(name); + this.reset(this.SAVED + name); + }, + + /** + * Sets the preference <b>name</b> to </b>value</b> but warns the user + * if the value is changed from its default. + * + * @param {string} name The preference name. + * @param {value} value The new preference value. + */ + safeSet: function (name, value, message) { + this._checkSafe(name, message, value); + this._store(name, value); + this._store(this.SAVED + name, value); + }, + + /** + * Sets the preference <b>name</b> to </b>value</b>. + * + * @param {string} name The preference name. + * @param {value} value The new preference value. + */ + set: function (name, value) { + this._store(name, value); + }, + + /** + * Resets the preference <b>name</b> to its default value. + * + * @param {string} name The preference name. + */ + reset: function (name) { + try { + services.get("pref").clearUserPref(name); + } + catch (e) {} // ignore - thrown if not a user set value + }, + + /** + * Toggles the value of the boolean preference <b>name</b>. + * + * @param {string} name The preference name. + */ + toggle: function (name) { + util.assert(services.get("pref").getPrefType(name) === Ci.nsIPrefBranch.PREF_BOOL, + "E488: Trailing characters: " + name + "!"); + this.set(name, !this.get(name)); + }, + + /** + * Pushes a new preference context onto the context stack. + * + * @see #withContext + */ + pushContext: function () { + this._prefContexts.push({}); + }, + + /** + * Pops the top preference context from the stack. + * + * @see #withContext + */ + popContext: function () { + for (let [k, v] in Iterator(this._prefContexts.pop())) + this._store(k, v); + }, + + /** + * Executes <b>func</b> with a new preference context. When <b>func</b> + * returns, the context is popped and any preferences set via + * {@link #setPref} or {@link #invertPref} are restored to their + * previous values. + * + * @param {function} func The function to call. + * @param {Object} func The 'this' object with which to call <b>func</b> + * @see #pushContext + * @see #popContext + */ + withContext: function (func, self) { + try { + this.pushContext(); + return func.call(self); + } + finally { + this.popContext(); + } + }, + + _store: function (name, value) { + if (this._prefContexts.length) { + let val = this._load(name, null); + if (val != null) + this._prefContexts[this._prefContexts.length - 1][name] = val; + } + + function assertType(needType) + util.assert(type === Ci.nsIPrefBranch.PREF_INVALID || type === needType, + type === Ci.nsIPrefBranch.PREF_INT + ? "E521: Number required after =: " + name + "=" + value + : "E474: Invalid argument: " + name + "=" + value); + + let type = services.get("pref").getPrefType(name); + switch (typeof value) { + case "string": + assertType(Ci.nsIPrefBranch.PREF_STRING); + + let supportString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + supportString.data = value; + services.get("pref").setComplexValue(name, Ci.nsISupportsString, supportString); + break; + case "number": + assertType(Ci.nsIPrefBranch.PREF_INT); + + services.get("pref").setIntPref(name, value); + break; + case "boolean": + assertType(Ci.nsIPrefBranch.PREF_BOOL); + + services.get("pref").setBoolPref(name, value); + break; + default: + throw FailedAssertion("Unknown preference type: " + typeof value + " (" + name + "=" + value + ")"); + } + }, + + _load: function (name, defaultValue, defaultBranch) { + if (defaultValue == null) + defaultValue = null; + + let branch = defaultBranch ? services.get("pref").getDefaultBranch("") : services.get("pref"); + let type = services.get("pref").getPrefType(name); + try { + switch (type) { + case Ci.nsIPrefBranch.PREF_STRING: + let value = branch.getComplexValue(name, Ci.nsISupportsString).data; + // try in case it's a localized string (will throw an exception if not) + if (!services.get("pref").prefIsLocked(name) && !services.get("pref").prefHasUserValue(name) && + RegExp("chrome://.+/locale/.+\\.properties").test(value)) + value = branch.getComplexValue(name, Ci.nsIPrefLocalizedString).data; + return value; + case Ci.nsIPrefBranch.PREF_INT: + return branch.getIntPref(name); + case Ci.nsIPrefBranch.PREF_BOOL: + return branch.getBoolPref(name); + default: + return defaultValue; + } + } + catch (e) { + return defaultValue; + } + } +}, { +}, { + completion: function (dactyl, modules) { + modules.completion.preference = function preference(context) { + context.anchored = false; + context.title = [services.get("dactyl:").host + " Preference", "Value"]; + context.keys = { text: function (item) item, description: function (item) prefs.get(item) }; + context.completions = prefs.getNames(); + }; + }, + javascript: function (dactyl, modules) { + modules.JavaScript.setCompleter([this.get, this.safeSet, this.set, this.reset, this.toggle], + [function (context) (context.anchored=false, prefs.getNames().map(function (pref) [pref, ""]))]); + } +}); + /** * Math utility methods. * @singleton @@ -945,8 +1341,8 @@ var Math = update(Object.create(GlobalMath), { constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max) }); -// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);} - endModule(); +} catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);} + // vim: set fdm=marker sw=4 ts=4 et ft=javascript: |