diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js index 39d983cf..ca83d81e 100644 --- a/common/content/bookmarks.js +++ b/common/content/bookmarks.js @@ -280,6 +280,7 @@ const Bookmarks = Module("bookmarks", { names: ["-tags", "-T"], description: "A comma-separated list of tags", completer: function tags(context, args) { + // TODO: Move the bulk of this to parseArgs. let filter = context.filter; let have = filter.split(","); diff --git a/common/content/browser.js b/common/content/browser.js index cb4ac460..4334ffc6 100644 --- a/common/content/browser.js +++ b/common/content/browser.js @@ -52,45 +52,6 @@ const Browser = Module("browser", { completer: function (context) completion.charset(context) }); - // only available in FF 3.5 - services.add("privateBrowsing", "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService); - if (services.get("privateBrowsing")) { - options.add(["private", "pornmode"], - "Set the 'private browsing' option", - "boolean", false, - { - setter: function (value) services.get("privateBrowsing").privateBrowsingEnabled = value, - getter: function () services.get("privateBrowsing").privateBrowsingEnabled - }); - let services = modules.services; // Storage objects are global to all windows, 'modules' isn't. - storage.newObject("private-mode", function () { - ({ - init: function () { - services.get("observer").addObserver(this, "private-browsing", false); - services.get("observer").addObserver(this, "quit-application", false); - this.private = services.get("privateBrowsing").privateBrowsingEnabled; - }, - observe: function (subject, topic, data) { - if (topic == "private-browsing") { - if (data == "enter") - storage.privateMode = true; - else if (data == "exit") - storage.privateMode = false; - storage.fireEvent("private-mode", "change", storage.privateMode); - } - else if (topic == "quit-application") { - services.get("observer").removeObserver(this, "quit-application"); - services.get("observer").removeObserver(this, "private-browsing"); - } - } - }).init(); - }, { store: false }); - storage.addObserver("private-mode", - function (key, event, value) { - autocommands.trigger("PrivateMode", { state: value }); - }, window); - } - options.add(["urlseparator"], "Set the separator regex used to separate multiple URL args", "string", ",\\s"); @@ -99,7 +60,7 @@ const Browser = Module("browser", { mappings: function () { mappings.add([modes.NORMAL], ["y"], "Yank current location to the clipboard", - function () { util.copyToClipboard(buffer.URL, true); }); + function () { dactyl.clipboardWrite(buffer.URL, true); }); // opening websites mappings.add([modes.NORMAL], @@ -218,6 +179,8 @@ const Browser = Module("browser", { dactyl.open("about:blank"); }, { completer: function (context) completion.url(context), + domains: function (args) array.compact(dactyl.stringToURLArray(args[0] || "").map( + function (url) util.getHost(url))), literal: 0, privateData: true }); diff --git a/common/content/buffer.js b/common/content/buffer.js index d0d495a5..0e7e242c 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -1517,14 +1517,14 @@ const Buffer = Module("buffer", { mappings.add(myModes, ["gP"], "Open (put) a URL based on the current clipboard contents in a new buffer", function () { - dactyl.open(util.readFromClipboard(), + dactyl.open(dactyl.clipboardRead(), dactyl[options.get("activate").has("paste") ? "NEW_BACKGROUND_TAB" : "NEW_TAB"]); }); mappings.add(myModes, ["p", ""], "Open (put) a URL based on the current clipboard contents in the current buffer", function () { - let url = util.readFromClipboard(); + let url = dactyl.clipboardRead(); dactyl.assert(url); dactyl.open(url); }); @@ -1532,7 +1532,7 @@ const Buffer = Module("buffer", { mappings.add(myModes, ["P"], "Open (put) a URL based on the current clipboard contents in a new buffer", function () { - let url = util.readFromClipboard(); + let url = dactyl.clipboardRead(); dactyl.assert(url); dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB }); }); @@ -1552,7 +1552,7 @@ const Buffer = Module("buffer", { function () { let sel = buffer.getCurrentWord(); dactyl.assert(sel); - util.copyToClipboard(sel, true); + dactyl.clipboardWrite(sel, true); }); // zooming diff --git a/common/content/commandline.js b/common/content/commandline.js index 951da1dd..38f22733 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -23,36 +23,6 @@ const CommandLine = Module("commandline", { storage.newArray("history-search", { store: true, privateData: true }); storage.newArray("history-command", { store: true, privateData: true }); - // Really inideal. - let services = modules.services; // Storage objects are global to all windows, 'modules' isn't. - storage.newObject("sanitize", function () { - ({ - CLEAR: "browser:purge-session-history", - QUIT: "quit-application", - init: function () { - services.get("observer").addObserver(this, this.CLEAR, false); - services.get("observer").addObserver(this, this.QUIT, false); - }, - observe: function (subject, topic, data) { - switch (topic) { - case this.CLEAR: - ["search", "command"].forEach(function (mode) { - CommandLine.History(null, mode).sanitize(); - }); - break; - case this.QUIT: - services.get("observer").removeObserver(this, this.CLEAR); - services.get("observer").removeObserver(this, this.QUIT); - break; - } - } - }).init(); - }, { store: false }); - storage.addObserver("sanitize", - function (key, event, value) { - autocommands.trigger("Sanitize", {}); - }, window); - this._messageHistory = { //{{{ _messages: [], get messages() { @@ -71,6 +41,10 @@ const CommandLine = Module("commandline", { this._messages = []; }, + filter: function filter(fn, self) { + this._messages = this._messages.filter(fn, self); + }, + add: function add(message) { if (!message) return; @@ -78,7 +52,9 @@ const CommandLine = Module("commandline", { if (this._messages.length >= options["messages"]) this._messages.shift(); - this._messages.push(message); + this._messages.push(update({ + timestamp: Date.now() + }, message)); } }; //}}} @@ -103,11 +79,13 @@ const CommandLine = Module("commandline", { }); this._autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) { - if (!events.feedingKeys && self._completions && options.get("autocomplete").values.length) { - self._completions.complete(true, false); - if (self._completions) - self._completions.itemList.show(); - } + dactyl.trapErrors(function () { + if (!events.feedingKeys && self._completions && options.get("autocomplete").values.length) { + self._completions.complete(true, false); + if (self._completions) + self._completions.itemList.show(); + } + }); }); // This timer just prevents s from queueing up when the @@ -115,8 +93,10 @@ const CommandLine = Module("commandline", { // the completion list scrolling). Multiple presses are // still processed normally, as the timer is flushed on "keyup". this._tabTimer = Timer(0, 0, function tabTell(event) { - if (self._completions) - self._completions.tab(event.shiftKey); + dactyl.trapErrors(function () { + if (self._completions) + self._completions.tab(event.shiftKey); + }); }); /////////////////////////////////////////////////////////////////////////////}}} @@ -213,7 +193,7 @@ const CommandLine = Module("commandline", { * * @returns {boolean} */ - _commandShown: function () modes.main == modes.COMMAND_LINE && + get commandVisible() modes.main == modes.COMMAND_LINE && !(modes.extended & (modes.INPUT_MULTILINE | modes.OUTPUT_MULTILINE)), /** @@ -255,7 +235,7 @@ const CommandLine = Module("commandline", { dactyl.triggerObserver("echoLine", str, highlightGroup, forceSingle); - if (!this._commandShown()) + if (!this.commandVisible) commandline.hide(); let field = this.widgets.message.inputField; @@ -347,9 +327,9 @@ const CommandLine = Module("commandline", { get quiet() this._quiet, set quiet(val) { this._quiet = val; - Array.forEach(document.getElementById("dactyl-commandline").childNodes, function (node) { + Array.forEach(this.widgets.commandline.childNodes, function (node) { node.style.opacity = this._quiet || this._silent ? "0" : ""; - }); + }, this); }, // @param type can be: @@ -510,8 +490,11 @@ const CommandLine = Module("commandline", { highlightGroup = highlightGroup || this.HL_NORMAL; - if (flags & this.APPEND_TO_MESSAGES) - this._messageHistory.add({ str: str, highlight: highlightGroup }); + if (flags & this.APPEND_TO_MESSAGES) { + let message = isobject(str) ? str : { message: str }; + this._messageHistory.add(update({ highlight: highlightGroup }, str)); + str = message.message; + } if ((flags & this.ACTIVE_WINDOW) && window != services.get("windowWatcher").activeWindow && services.get("windowWatcher").activeWindow.dactyl) @@ -625,12 +608,12 @@ const CommandLine = Module("commandline", { if (event.type == "blur") { // prevent losing focus, there should be a better way, but it just didn't work otherwise this.setTimeout(function () { - if (this._commandShown() && event.originalTarget == this.widgets.command.inputField) + if (this.commandVisible && event.originalTarget == this.widgets.command.inputField) this.widgets.command.inputField.focus(); }, 0); } else if (event.type == "focus") { - if (!this._commandShown() && event.target == this.widgets.command.inputField) { + if (!this.commandVisible && event.target == this.widgets.command.inputField) { event.target.blur(); dactyl.beep(); } @@ -695,7 +678,7 @@ const CommandLine = Module("commandline", { } } catch (e) { - dactyl.reportError(e); + dactyl.reportError(e, true); } }, @@ -908,7 +891,7 @@ const CommandLine = Module("commandline", { // copy text to clipboard case "": - util.copyToClipboard(win.getSelection()); + dactyl.clipboardWrite(win.getSelection()); break; // close the window @@ -1032,7 +1015,7 @@ const CommandLine = Module("commandline", { if (/^\s*$/.test(str)) return; this.store.mutate("filter", function (line) (line.value || line) != str); - this.store.push({ value: str, timestamp: Date.now(), privateData: this.checkPrivate(str) }); + this.store.push({ value: str, timestamp: Date.now()*1000, privateData: this.checkPrivate(str) }); this.store.truncate(options["history"], true); }, /** @@ -1042,23 +1025,9 @@ const CommandLine = Module("commandline", { checkPrivate: function (str) { // Not really the ideal place for this check. if (this.mode == "command") - return (commands.get(commands.parseCommand(str)[1]) || {}).privateData; + return commands.hasPrivateData(str); return false; }, - /** - * Removes any private data from this history. - */ - sanitize: function (timespan) { - let range = [0, Number.MAX_VALUE]; - if (dactyl.has("sanitizer") && (timespan || options["sanitizetimespan"])) - range = Sanitizer.getClearRange(timespan || options["sanitizetimespan"]); - - const self = this; - this.store.mutate("filter", function (item) { - let timestamp = (item.timestamp || Date.now()/1000) * 1000; - return !line.privateData || timestamp < self.range[0] || timestamp > self.range[1]; - }); - }, /** * Replace the current input field value. * @@ -1443,19 +1412,19 @@ const CommandLine = Module("commandline", { }); commands.add(["mes[sages]"], - "Display previously given messages", + "Display previously shown messages", function () { // TODO: are all messages single line? Some display an aggregation // of single line messages at least. E.g. :source if (commandline._messageHistory.length == 1) { let message = commandline._messageHistory.messages[0]; - commandline.echo(message.str, message.highlight, commandline.FORCE_SINGLELINE); + commandline.echo(message.message, message.highlight, commandline.FORCE_SINGLELINE); } else if (commandline._messageHistory.length > 1) { XML.ignoreWhitespace = false; - let list = template.map(commandline._messageHistory.messages, function (message) -
{message.str}
); - dactyl.echo(list, commandline.FORCE_MULTILINE); + commandline.commandOutput( + template.map(commandline._messageHistory.messages, function (message) +
{message.message}
));; } }, { argCount: "0" }); @@ -1471,7 +1440,8 @@ const CommandLine = Module("commandline", { commandline.runSilently(function () dactyl.execute(args[0], null, true)); }, { completer: function (context) completion.ex(context), - literal: 0 + literal: 0, + subCommand: 0 }); }, mappings: function () { @@ -1532,6 +1502,34 @@ const CommandLine = Module("commandline", { "Show the current mode in the command line", "boolean", true); }, + sanitizer: function () { + sanitizer.addItem("commandline", { + description: "Command-line and search history", + action: function (timespan, host) { + if (!host) + storage["history-search"].mutate("filter", function (item) !timespan.contains(item.timestamp)); + storage["history-command"].mutate("filter", function (item) + !(timespan.contains(item.timestamp) && (!host || commands.hasDomain(item.value, host)))); + } + }); + // Delete history-like items from the commandline and messages on history purge + sanitizer.addItem("history", { + action: function (timespan, host) { + storage["history-command"].mutate("filter", function (item) + !(timespan.contains(item.timestamp) && (host ? commands.hasDomain(item.value, host) : item.privateData))); + commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) || + !item.domains && !item.privateData || + host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host)))); + } + }); + sanitizer.addItem("messages", { + description: "Saved :messages", + action: function (timespan, host) { + commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) || + host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host)))); + } + }); + }, styles: function () { let fontSize = util.computedStyle(document.getElementById(config.mainWindowId)).fontSize; styles.registerSheet("chrome://dactyl/skin/dactyl.css"); diff --git a/common/content/commands.js b/common/content/commands.js index cb7cc3ae..8a25bebb 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -95,11 +95,13 @@ update(CommandOption, { * bang - see {@link Command#bang} * completer - see {@link Command#completer} * count - see {@link Command#count} + * domains - see {@link Command#domains} * heredoc - see {@link Command#heredoc} * literal - see {@link Command#literal} * options - see {@link Command#options} - * serial - see {@link Command#serial} * privateData - see {@link Command#privateData} + * serialize - see {@link Command#serialize} + * subCommand - see {@link Command#subCommand} * @optional * @private */ @@ -144,24 +146,22 @@ const Command = Class("Command", { let self = this; function exec(command) { // FIXME: Move to parseCommand? - args = self.parseArgs(command); - if (!args) - return; + args = this.parseArgs(command); args.count = count; args.bang = bang; - dactyl.trapErrors(self.action, self, args, modifiers); + this.action(args, modifiers); } if (this.hereDoc) { let matches = args.match(/(.*)<<\s*(\S+)$/); if (matches && matches[2]) { commandline.inputMultiline(RegExp("^" + matches[2] + "$", "m"), - function (args) { exec(matches[1] + "\n" + args); }); + function (args) { dactyl.trapErrors(exec, self, matches[1] + "\n" + args); }); return; } } - exec(args); + dactyl.trapErrors(exec, this, args); }, /** @@ -231,11 +231,6 @@ const Command = Class("Command", { completer: null, /** @property {boolean} Whether this command accepts a here document. */ hereDoc: false, - /** - * @property {Array} The options this command takes. - * @see Commands@parseArguments - */ - options: [], /** * @property {boolean} Whether this command may be called with a bang, * e.g., :com! @@ -246,6 +241,13 @@ const Command = Class("Command", { * e.g., :12bdel */ count: false, + /** + * @property {function(args)} A function which should return a list + * of domains referenced in the given args. Used in determing + * whether to purge the command from history when clearing + * private data. + */ + domains: function (args) [], /** * @property {boolean} At what index this command's literal arguments * begin. For instance, with a value of 2, all arguments starting with @@ -254,6 +256,19 @@ const Command = Class("Command", { * key mappings or Ex command lines as arguments. */ literal: null, + /** + * @property {Array} The options this command takes. + * @see Commands@parseArguments + */ + options: [], + /** + * @property {boolean|function(args)} When true, invocations of this + * command may contain private data which should be purged from + * saved histories when clearing private data. If a function, it + * should return true if an invocation with the given args + * contains private data + */ + privateData: true, /** * @property {function} Should return an array of Objects suitable * to be passed to {@link Commands#commandToString}, one for each past @@ -262,12 +277,11 @@ const Command = Class("Command", { */ serialize: null, /** - * @property {boolean} When true, invocations of this command - * may contain private data which should be purged from - * saved histories when clearing private data. + * @property {number} If this command takes another ex command as an + * argument, the index of that argument. Used in determining whether to + * purge the command from history when clearing private data. */ - privateData: false, - + subCommand: null, /** * @property {boolean} Specifies whether this is a user command. User * commands may be created by plugins, or directly by users, and, @@ -441,6 +455,60 @@ const Commands = Module("commands", { return this._exCommands.filter(function (cmd) cmd.user); }, + _subCommands: function (command) { + while (command) { + // FIXME: This parseCommand/commands.get/parseArgs is duplicated too often. + let [count, cmd, bang, args] = commands.parseCommand(command); + command = commands.get(cmd); + if (command) { + try { + args = command.parseArgs(args, null, { count: count, bang: bang }); + yield [command, args]; + if (commands.subCommand == null) + break; + command = args[command.subCommand]; + } + catch (e) { + break; + } + } + } + }, + + /** + * Returns true if a command invocation contains a URL referring to the + * domain 'host'. + * + * @param {string} command + * @param {string} host + * @returns {boolean} + */ + hasDomain: function (command, host) { + try { + for (let [cmd, args] in this._subCommands(command)) + if (Array.concat(cmd.domains(args)).some(function (domain) util.isSubdomain(domain, host))) + return true; + } + catch (e) { + dactyl.reportError(e); + } + return false; + }, + + /** + * Returns true if a command invocation contains private data which should + * be cleared when purging private data. + * + * @param {string} command + * @returns {boolean} + */ + hasPrivateData: function (command) { + for (let [cmd, args] in this._subCommands(command)) + if (cmd.privateData) + return !callable(cmd.privateData) || cmd.privateData(args); + return false; + }, + // TODO: should it handle comments? // : it might be nice to be able to specify that certain quoting // should be disabled E.g. backslash without having to resort to @@ -541,11 +609,11 @@ const Commands = Module("commands", { args.completeArg = 0; } - function echoerr(error) { + function fail(error) { if (complete) complete.message = error; else - dactyl.echoerr(error); + dactyl.assert(false, error); } outer: @@ -589,11 +657,12 @@ const Commands = Module("commands", { else if (!/\s/.test(sep) && sep != undefined) // this isn't really an option as it has trailing characters, parse it as an argument invalid = true; + if (complete && !/[\s=]/.test(sep)) + matchOpts(sub); + let context = null; - if (!complete && quote) { - dactyl.echoerr("Invalid argument for option " + optname); - return null; - } + if (!complete && quote) + fail("Invalid argument for option " + optname); if (!invalid) { if (complete && count > 0) { @@ -602,33 +671,29 @@ const Commands = Module("commands", { args.completeFilter = arg; args.quote = Commands.complQuote[quote] || Commands.complQuote[""]; } - let type = Commands.argTypes[opt.type]; - if (type && (!complete || arg != null)) { - let orig = arg; - arg = type.parse(arg); - if (arg == null || (typeof arg == "number" && isNaN(arg))) { - if (!complete || orig != "" || args.completeStart != str.length) - echoerr("Invalid argument for " + type.description + " option: " + optname); - if (complete) - complete.highlight(args.completeStart, count - 1, "SPELLCHECK"); - else - return null; + if (!complete || arg != null) { + let type = Commands.argTypes[opt.type]; + if (type) { + let orig = arg; + arg = type.parse(arg); + if (arg == null || (typeof arg == "number" && isNaN(arg))) { + if (!complete || orig != "" || args.completeStart != str.length) + fail("Invalid argument for " + type.description + " option: " + optname); + if (complete) + complete.highlight(args.completeStart, count - 1, "SPELLCHECK"); + } + } + + // we have a validator function + if (typeof opt.validator == "function") { + if (opt.validator.call(this, arg) == false) { + fail("Invalid argument for option: " + optname); + if (complete) // Always true. + complete.highlight(args.completeStart, count - 1, "SPELLCHECK"); + } } } - // we have a validator function - if (typeof opt.validator == "function") { - if (opt.validator.call(this, arg) == false) { - echoerr("Invalid argument for option: " + optname); - if (complete) - complete.highlight(args.completeStart, count - 1, "SPELLCHECK"); - else - return null; - } - } - - matchOpts(sub); - // option allowed multiple times if (opt.multiple) args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg); @@ -670,14 +735,10 @@ const Commands = Module("commands", { args.quote = Commands.complQuote[quote] || Commands.complQuote[""]; args.completeFilter = arg || ""; } - else if (count == -1) { - dactyl.echoerr("Error parsing arguments: " + arg); - return null; - } - else if (!onlyArgumentsRemaining && /^-/.test(arg)) { - dactyl.echoerr("Invalid option: " + arg); - return null; - } + else if (count == -1) + fail("Error parsing arguments: " + arg); + else if (!onlyArgumentsRemaining && /^-/.test(arg)) + fail("Invalid option: " + arg); if (arg != null) args.push(arg); @@ -700,7 +761,8 @@ const Commands = Module("commands", { compl = opt.completer || []; context.title = [opt.names[0]]; context.quote = args.quote; - context.completions = compl; + if (compl) + context.completions = compl; } complete.advance(args.completeStart); complete.keys = { text: "names", description: "description" }; @@ -712,16 +774,12 @@ const Commands = Module("commands", { // check for correct number of arguments if (args.length == 0 && /^[1+]$/.test(argCount) || literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || "")) { - if (!complete) { - dactyl.echoerr("E471: Argument required"); - return null; - } + if (!complete) + fail("E471: Argument required"); } else if (args.length == 1 && (argCount == "0") || - args.length > 1 && /^[01?]$/.test(argCount)) { - echoerr("E488: Trailing characters"); - return null; - } + args.length > 1 && /^[01?]$/.test(argCount)) + fail("E488: Trailing characters"); return args; }, @@ -980,7 +1038,7 @@ const Commands = Module("commands", { try { var completer = dactyl.eval(completeOpt); - if (!(completer instanceof Function)) + if (!callable(completer)) throw new TypeError("User-defined custom completer " + completeOpt.quote() + " is not a function"); } catch (e) { diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 00db62bf..e491b9c2 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -137,6 +137,64 @@ const Dactyl = Module("dactyl", { } }), + /** + * Reads a string from the system clipboard. + * + * This is same as Firefox's readFromClipboard function, but is needed for + * apps like Thunderbird which do not provide it. + * + * @returns {string} + */ + clipboardRead: function clipboardRead() { + let str; + + try { + const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); + const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + + transferable.addDataFlavor("text/unicode"); + + if (clipboard.supportsSelectionClipboard()) + clipboard.getData(transferable, clipboard.kSelectionClipboard); + else + clipboard.getData(transferable, clipboard.kGlobalClipboard); + + let data = {}; + let dataLen = {}; + + transferable.getTransferData("text/unicode", data, dataLen); + + if (data) { + data = data.value.QueryInterface(Ci.nsISupportsString); + str = data.data.substring(0, dataLen.value / 2); + } + } + catch (e) {} + + return str; + }, + + /** + * Copies a string to the system clipboard. If verbose is specified + * the copied string is also echoed to the command line. + * + * @param {string} str + * @param {boolean} verbose + */ + clipboardWrite: function clipboardWrite(str, verbose) { + const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); + clipboardHelper.copyString(str); + + if (verbose) { + let message = { message: "Yanked " + str }; + try { + message.domains = [util.newURI(str).host]; + } + catch (e) {}; + dactyl.echomsg(message); + } + }, + /** * Prints a message to the console. If msg is an object it is * pretty printed. @@ -906,7 +964,7 @@ const Dactyl = Module("dactyl", { return func.apply(self || this, Array.slice(arguments, 2)); } catch (e) { - dactyl.reportError(e); + dactyl.reportError(e, true); return undefined; } }, @@ -917,7 +975,7 @@ const Dactyl = Module("dactyl", { * * @param {Object} error The error object. */ - reportError: function (error) { + reportError: function (error, echo) { if (error instanceof FailedAssertion) { if (error.message) dactyl.echoerr(error.message); @@ -925,6 +983,8 @@ const Dactyl = Module("dactyl", { dactyl.beep(); return; } + if (echo) + dactyl.echoerr(error); if (Cu.reportError) Cu.reportError(error); @@ -1709,7 +1769,8 @@ const Dactyl = Module("dactyl", { return completion.javascript(context); }, count: true, - literal: 0 + literal: 0, + subCommand: 0 }); commands.add(["verb[ose]"], @@ -1732,7 +1793,8 @@ const Dactyl = Module("dactyl", { argCount: "+", completer: function (context) completion.ex(context), count: true, - literal: 0 + literal: 0, + subCommand: 0 }); commands.add(["ve[rsion]"], diff --git a/common/content/editor.js b/common/content/editor.js index 6a2c7197..9a517073 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -112,8 +112,8 @@ const Editor = Module("editor", { // FIXME: #93 ( in the bottom of a long textarea bounces up) let elem = dactyl.focus; - if (elem.setSelectionRange && util.readFromClipboard()) { - // readFromClipboard would return 'undefined' if not checked + if (elem.setSelectionRange && dactyl.clipboardRead()) { + // clipboardRead would return 'undefined' if not checked // dunno about .setSelectionRange // This is a hacky fix - but it works. let curTop = elem.scrollTop; @@ -122,7 +122,7 @@ const Editor = Module("editor", { let rangeStart = elem.selectionStart; // caret position let rangeEnd = elem.selectionEnd; let tempStr1 = elem.value.substring(0, rangeStart); - let tempStr2 = util.readFromClipboard(); + let tempStr2 = dactyl.clipboardRead(); let tempStr3 = elem.value.substring(rangeEnd); elem.value = tempStr1 + tempStr2 + tempStr3; elem.selectionStart = rangeStart + tempStr2.length; @@ -971,7 +971,7 @@ const Editor = Module("editor", { else { let sel = window.content.document.getSelection(); dactyl.assert(sel); - util.copyToClipboard(sel, true); + dactyl.clipboardWrite(sel, true); } }); diff --git a/common/content/events.js b/common/content/events.js index 5dd187d7..03e5296f 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -22,7 +22,10 @@ const Events = Module("events", { this.sessionListeners = []; - this._macros = storage.newMap("macros", true, { privateData: true }); + this._macros = storage.newMap("macros", { privateData: true, store: true }); + for (let [k, m] in this._macros) + if (isstring(m)) + m = { keys: m, timeRecorded: Date.now() }; // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"] // matters, so use that string as the first item, that you @@ -85,9 +88,12 @@ const Events = Module("events", { if (file.exists() && !file.isDirectory() && file.isReadable() && /^[\w_-]+(\.vimp)?$/i.test(file.leafName)) { let name = file.leafName.replace(/\.vimp$/i, ""); - this._macros.set(name, file.read().split("\n")[0]); + this._macros.set(name, { + keys: file.read().split("\n")[0], + timeRecorded: file.lastModifiedTime + }); - dactyl.log("Macro " + name + " added: " + this._macros.get(name), 5); + dactyl.log("Macro " + name + " added: " + this._macros.get(name).keys, 5); } } } @@ -146,11 +152,11 @@ const Events = Module("events", { method.apply(self, arguments); } catch (e) { + dactyl.reportError(e); if (e.message == "Interrupted") dactyl.echoerr("Interrupted"); else dactyl.echoerr("Processing " + event.type + " event: " + (e.echoerr || e)); - dactyl.reportError(e); } }; }, @@ -176,11 +182,11 @@ const Events = Module("events", { if (/[A-Z]/.test(macro)) { // uppercase (append) this._currentMacro = macro.toLowerCase(); if (!this._macros.get(this._currentMacro)) - this._macros.set(this._currentMacro, ""); // initialize if it does not yet exist + this._macros.set(this._currentMacro, { keys: "", timeRecorded: Date.now() }); // initialize if it does not yet exist } else { this._currentMacro = macro; - this._macros.set(this._currentMacro, ""); + this._macros.set(this._currentMacro, { keys: "", timeRecorded: Date.now() }); } }, @@ -219,7 +225,7 @@ const Events = Module("events", { buffer.loaded = 1; // even if not a full page load, assume it did load correctly before starting the macro modes.isReplaying = true; - res = events.feedkeys(this._macros.get(this._lastMacro), { noremap: true }); + res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true }); modes.isReplaying = false; } else { @@ -239,11 +245,8 @@ const Events = Module("events", { * filter selects all macros. */ getMacros: function (filter) { - if (!filter) - return this._macros; - - let re = RegExp(filter); - return ([macro, keys] for ([macro, keys] in this._macros) if (re.test(macro))); + let re = RegExp(filter || ""); + return ([k, m.keys] for ([k, m] in this._macros) if (re.test(macro))); }, /** @@ -253,10 +256,9 @@ const Events = Module("events", { * filter deletes all macros. */ deleteMacros: function (filter) { - let re = RegExp(filter); - + let re = RegExp(filter || ""); for (let [item, ] in this._macros) { - if (re.test(item) || !filter) + if (!filter || re.test(item)) this._macros.remove(item); } }, @@ -823,13 +825,16 @@ const Events = Module("events", { if (modes.isRecording) { if (key == "q" && !modes.mainMode.input) { // TODO: should not be hardcoded modes.isRecording = false; - dactyl.log("Recorded " + this._currentMacro + ": " + this._macros.get(this._currentMacro), 9); + dactyl.log("Recorded " + this._currentMacro + ": " + this._macros.get(this._currentMacro, {}).keys, 9); dactyl.echomsg("Recorded macro '" + this._currentMacro + "'"); killEvent(); return; } else if (!mappings.hasMap(dactyl.mode, this._input.buffer + key)) - this._macros.set(this._currentMacro, this._macros.get(this._currentMacro) + key); + this._macros.set(this._currentMacro, { + keys: this._macros.get(this._currentMacro, {}).keys + key, + timeRecorded: Date.now() + }); } if (key == "") @@ -1188,11 +1193,22 @@ const Events = Module("events", { mappings.add([modes.NORMAL, modes.PLAYER, modes.MESSAGE], ["@"], "Play a macro", function (count, arg) { - if (count < 1) count = 1; + count = Math.max(count, 1); while (count-- && events.playMacro(arg)) ; }, { arg: true, count: true }); + }, + sanitizer: function () { + sanitizer.addItem("macros", { + description: "Saved macros", + action: function (timespan, host) { + if (!host) + for (let [k, m] in events._macros) + if (timespan.contains(m.timeRecorded * 1000)) + events._macros.remove(k); + } + }); } }); diff --git a/common/content/hints.js b/common/content/hints.js index 2c6b8efc..1ddf33b5 100644 --- a/common/content/hints.js +++ b/common/content/hints.js @@ -51,8 +51,8 @@ const Hints = Module("hints", { W: Mode("Generate a ':winopen URL' using hint", function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX)), v: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, false), extended), V: Mode("View hint source in external editor", function (elem, loc) buffer.viewSource(loc, true), extended), - y: Mode("Yank hint location", function (elem, loc) util.copyToClipboard(loc, true)), - Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended), + y: Mode("Yank hint location", function (elem, loc) dactyl.clipboardWrite(loc, true)), + Y: Mode("Yank hint description", function (elem) dactyl.clipboardWrite(elem.textContent || "", true), extended), c: Mode("Open context menu", function (elem) buffer.openContextMenu(elem), extended), i: Mode("Show image", function (elem) dactyl.open(elem.src), images), I: Mode("Show image in a new tab", function (elem) dactyl.open(elem.src, dactyl.NEW_TAB), images) diff --git a/common/content/history.js b/common/content/history.js index d8962567..691810ed 100644 --- a/common/content/history.js +++ b/common/content/history.js @@ -146,7 +146,8 @@ const History = Module("history", { context.keys = { text: function (item) (sh.index - item.index) + ": " + item.URI.spec, description: "title", icon: "icon" }; }, count: true, - literal: 0 + literal: 0, + privateData: true }); commands.add(["fo[rward]", "fw"], @@ -185,7 +186,8 @@ const History = Module("history", { context.keys = { text: function (item) (item.index - sh.index) + ": " + item.URI.spec, description: "title", icon: "icon" }; }, count: true, - literal: 0 + literal: 0, + privateData: true }); commands.add(["hist[ory]", "hs"], @@ -194,10 +196,25 @@ const History = Module("history", { bang: true, completer: function (context) { context.quote = null; completion.history(context); }, // completer: function (filter) completion.history(filter) - options: [{ names: ["-max", "-m"], description: "The maximum number of items to list", type: CommandOption.INT }] + options: [{ names: ["-max", "-m"], description: "The maximum number of items to list", type: CommandOption.INT }], + privateData: true }); }, completion: function () { + completion.domain = function (context) { + context.anchored = false; + context.compare = function (a, b) String.localeCompare(a.key, b.key); + context.keys = { text: util.identity, description: util.identity, + key: function (host) host.split(".").reverse().join(".") }; + + // FIXME: Schema-specific + context.generate = function () [ + Array.slice(row.rev_host).reverse().join("").slice(1) + for (row in iter(services.get("history").DBConnection + .createStatement("SELECT DISTINCT rev_host FROM moz_places;"))) + ].slice(2); + }; + completion.history = function _history(context, maxItems) { context.format = history.format; context.title = ["History"]; diff --git a/common/content/io.js b/common/content/io.js index 1f516b11..ce092f87 100644 --- a/common/content/io.js +++ b/common/content/io.js @@ -770,7 +770,7 @@ lookup: }; completion.addUrlCompleter("f", "Local files", function (context, full) { - if (!/^\.?\//.test(context.filter)) + if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)) completion.file(context, full); }); }, diff --git a/common/content/javascript.js b/common/content/javascript.js index ea730d2a..40295627 100644 --- a/common/content/javascript.js +++ b/common/content/javascript.js @@ -56,19 +56,6 @@ const JavaScript = Module("javascript", { return []; let completions = [k for (k in this.iter(obj, toplevel))]; - - // Add keys for sorting later. - // Numbers are parsed to ints. - // Constants, which should be unsorted, are found and marked null. - completions.forEach(function (item) { - let key = item[0]; - if (!isNaN(key)) - key = parseInt(key); - else if (/^[A-Z_][A-Z0-9_]*$/.test(key)) - key = ""; - item.key = key; - }); - return completions; }, @@ -322,7 +309,7 @@ const JavaScript = Module("javascript", { let orig = compl; if (!compl) { compl = function (context, obj, recurse) { - context.process = [null, function highlight(item, v) template.highlight(v, true)]; + context.process = [null, function highlight(item, v) template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : v, true)]; // Sort in a logical fashion for object keys: // Numbers are sorted as numbers, rather than strings, and appear first. // Constants are unsorted, and appear before other non-null strings. @@ -330,10 +317,21 @@ const JavaScript = Module("javascript", { let compare = context.compare; function isnan(item) item != '' && isNaN(item); context.compare = function (a, b) { - if (!isnan(a.item.key) && !isnan(b.item.key)) - return a.item.key - b.item.key; - return isnan(b.item.key) - isnan(a.item.key) || compare(a, b); + if (!isnan(a.key) && !isnan(b.key)) + return a.key - b.key; + return isnan(b.key) - isnan(a.key) || compare(a, b); }; + context.keys = { text: 0, description: 1, + key: function (item) { + let key = item[0]; + if (!isNaN(key)) + return parseInt(key); + else if (/^[A-Z_][A-Z0-9_]*$/.test(key)) + return "" + return key; + } + }; + if (!context.anchored) // We've already listed anchored matches, so don't list them again here. context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter)); if (obj == self.cache.evalContext) diff --git a/common/content/mappings.js b/common/content/mappings.js index da646b9b..a8cfebe1 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -178,7 +178,7 @@ const Mappings = Module("mappings", { modes = modes.slice(); return (map for ([i, map] in Iterator(stack[modes.shift()].sort(function (m1, m2) String.localeCompare(m1.name, m2.name)))) if (modes.every(function (mode) stack[mode].some( - function (mapping) m.rhs == map.rhs && m.name == map.name)))) + function (m) m.rhs == map.rhs && m.name == map.name)))) }, // NOTE: just normal mode for now diff --git a/common/content/marks.js b/common/content/marks.js index f6ee4755..194807fe 100644 --- a/common/content/marks.js +++ b/common/content/marks.js @@ -12,8 +12,9 @@ */ const Marks = Module("marks", { init: function init() { - this._localMarks = storage.newMap("local-marks", { store: true, privateData: true }); - this._urlMarks = storage.newMap("url-marks", { store: true, privateData: true }); + function replacer(key, val) val instanceof Node ? null : val; + this._localMarks = storage.newMap("local-marks", { privateData: true, replacer: replacer, store: true }); + this._urlMarks = storage.newMap("url-marks", { privateData: true, replacer: replacer, store: true }); this._pendingJumps = []; }, @@ -65,7 +66,7 @@ const Marks = Module("marks", { let position = { x: x, y: y }; if (Marks.isURLMark(mark)) { - this._urlMarks.set(mark, { location: win.location.href, position: position, tab: tabs.getTab() }); + this._urlMarks.set(mark, { location: win.location.href, position: position, tab: tabs.getTab(), timestamp: Date.now()*1000 }); if (!silent) dactyl.log("Adding URL mark: " + Marks.markToString(mark, this._urlMarks.get(mark)), 5); } @@ -74,7 +75,7 @@ const Marks = Module("marks", { this._removeLocalMark(mark); if (!this._localMarks.get(mark)) this._localMarks.set(mark, []); - let vals = { location: win.location.href, position: position }; + let vals = { location: win.location.href, position: position, timestamp: Date.now()*1000 }; this._localMarks.get(mark).push(vals); if (!silent) dactyl.log("Adding local mark: " + Marks.markToString(mark, vals), 5); @@ -327,6 +328,20 @@ const Marks = Module("marks", { context.keys.description = function ([, m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location; context.completions = marks.all; }; + }, + sanitizer: function () { + sanitizer.addItem("marks", { + description: "Local and URL marks", + action: function (timespan, host) { + function filter(mark) !(timespan.contains(mark.timestamp) && (!host || util.isDomainURL(mark.location, host))); + + for (let [k, v] in storage["local-marks"]) + storage["local-marks"].set(k, v.filter(filter)); + + for (let key in (k for ([k, v] in storage["url-marks"]) if (!filter(v)))) + storage["url-marks"].remove(key); + } + }); } }); diff --git a/common/content/options.js b/common/content/options.js index c2ae2867..b925787a 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -20,11 +20,13 @@ * @param {string} defaultValue The default value for this option. * @param {Object} extraInfo An optional extra configuration hash. The * following properties are supported. - * scope - see {@link Option#scope} - * setter - see {@link Option#setter} - * getter - see {@link Option#getter} - * completer - see {@link Option#completer} - * valdator - see {@link Option#validator} + * completer - see {@link Option#completer} + * domains - see {@link Option#domains} + * getter - see {@link Option#getter} + * privateData - see {@link Option#privateData} + * scope - see {@link Option#scope} + * setter - see {@link Option#setter} + * valdator - see {@link Option#validator} * @optional * @private */ @@ -61,8 +63,8 @@ const Option = Class("Option", { }, /** @property {value} The option's global value. @see #scope */ - get globalValue() options.store.get(this.name), - set globalValue(val) { options.store.set(this.name, val); }, + get globalValue() options.store.get(this.name, {}).value, + set globalValue(val) { options.store.set(this.name, { value: val, time: Date.now() }); }, /** * Returns value as an array of parsed values if the option type is @@ -194,6 +196,9 @@ const Option = Class("Option", { */ isValidValue: function (values) this.validator(values), + invalidArgument: function (arg, op) "E474: Invalid argument: " + + this.name + (op || "").replace(/=?$/, "=") + arg, + /** * Resets the option to its default value. */ @@ -210,7 +215,7 @@ const Option = Class("Option", { * {@link #scope}). * @param {boolean} invert Whether this is an invert boolean operation. */ - op: function (operator, values, scope, invert) { + op: function (operator, values, scope, invert, str) { let newValues = this._op(operator, values, scope, invert); @@ -218,7 +223,7 @@ const Option = Class("Option", { return "Operator " + operator + " not supported for option type " + this.type; if (!this.isValidValue(newValues)) - return "E474: Invalid argument: " + values; + return this.invalidArgument(str || this.joinValues(values), operator); this.setValues(newValues, scope); return null; @@ -257,6 +262,27 @@ const Option = Class("Option", { */ description: "", + /** + * @property {function(CompletionContext, Args)} This option's completer. + * @see CompletionContext + */ + completer: null, + + /** + * @property {function(host, values)} A function which should return a list + * of domains referenced in the given values. Used in determing whether + * to purge the command from history when clearing private data. + * @see Command#domains + */ + domains: null, + + /** + * @property {function(host, values)} A function which should strip + * references to a given domain from the given values. + */ + filterDomain: function filterDomain(host, values) + Array.filter(values, function (val) !util.isSubdomain(val, host)), + /** * @property {value} The option's default value. This value will be used * unless the option is explicitly set either interactively or in an RC @@ -264,19 +290,25 @@ const Option = Class("Option", { */ defaultValue: null, - /** - * @property {function} The function called when the option value is set. - */ - setter: null, /** * @property {function} The function called when the option value is read. */ getter: null, + /** - * @property {function(CompletionContext, Args)} This option's completer. - * @see CompletionContext + * @property {boolean|function(values)} When true, values of this + * option may contain private data which should be purged from + * saved histories when clearing private data. If a function, it + * should return true if an invocation with the given values + * contains private data */ - completer: null, + privateData: false, + + /** + * @property {function} The function called when the option value is set. + */ + setter: null, + /** * @property {function} The function called to validate the option's value * when set. @@ -294,6 +326,12 @@ const Option = Class("Option", { */ hasChanged: false, + /** + * Returns the timestamp when the option's value was last changed. + */ + get lastSet() options.store.get(this.name).time, + set lastSet(val) { options.store.set(this.name, { value: this.globalValue, time: Date.now() }); }, + /** * @property {nsIFile} The script in which this option was last set. null * implies an interactive command. @@ -521,10 +559,9 @@ const Options = Module("options", { }, /** @property {Iterator(Option)} @private */ - __iterator__: function () { - let sorted = [o for ([i, o] in Iterator(this._optionHash))].sort(function (a, b) String.localeCompare(a.name, b.name)); - return (v for ([k, v] in Iterator(sorted))); - }, + __iterator__: function () + array(values(this._optionHash)).sort(function (a, b) String.localeCompare(a.name, b.name)) + .itervalues(), /** @property {Object} Observes preference value changes. */ prefObserver: { @@ -612,11 +649,9 @@ const Options = Module("options", { if (name in this._optionHash) return (this._optionHash[name].scope & scope) && this._optionHash[name]; - for (let opt in Iterator(options)) { + for (let opt in values(this._optionHash)) if (opt.hasName(name)) return (opt.scope & scope) && opt; - } - return null; }, @@ -1067,7 +1102,7 @@ const Options = Module("options", { opt.values = !opt.unsetBoolean; } try { - var res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert); + var res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert, opt.value); } catch (e) { res = e; @@ -1209,56 +1244,62 @@ const Options = Module("options", { } ); - commands.add(["setl[ocal]"], - "Set local option", - function (args, modifiers) { - modifiers.scope = Option.SCOPE_LOCAL; - setAction(args, modifiers); + [ + { + names: ["setl[ocal]"], + description: "Set local option", + modifiers: { scope: Option.SCOPE_LOCAL } }, { - bang: true, - count: true, - completer: function (context, args) { - return setCompleter(context, args, { scope: Option.SCOPE_LOCAL }); - }, - literal: 0 - } - ); - - commands.add(["setg[lobal]"], - "Set global option", - function (args, modifiers) { - modifiers.scope = Option.SCOPE_GLOBAL; - setAction(args, modifiers); + names: ["setg[lobal]"], + description: "Set global option", + modifiers: { scope: Option.SCOPE_GLOBAL } }, { - bang: true, - count: true, - completer: function (context, args) { - return setCompleter(context, args, { scope: Option.SCOPE_GLOBAL }); - }, - literal: 0 + names: ["se[t]"], + description: "Set an option", + modifiers: {}, + extra: { + serialize: function () [ + { + command: this.name, + arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name + : opt.name + "=" + opt.value] + } + for (opt in options) + if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & Option.SCOPE_GLOBAL)) + ] + } } - ); - - commands.add(["se[t]"], - "Set an option", - function (args, modifiers) { setAction(args, modifiers); }, - { - bang: true, - completer: function (context, args) { - return setCompleter(context, args); + ].forEach(function (params) { + commands.add(params.names, params.description, + function (args, modifiers) { + setAction(args, update(modifiers, params.modifiers)); }, - serialize: function () [ - { - command: this.name, - arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name - : opt.name + "=" + opt.value] - } - for (opt in options) - if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & Option.SCOPE_GLOBAL)) - ] - }); + update({ + bang: true, + domains: function (args) array.flatten(args.map(function (spec) { + try { + let opt = options.parseOpt(spec); + if (opt.option && opt.option.domains) + return opt.option.domains(opt.values); + } + catch (e) { + dactyl.reportError(e); + } + return []; + })), + completer: function (context, args) { + return setCompleter(context, args); + }, + privateData: function (args) args.some(function (spec) { + let opt = options.parseOpt(spec); + return opt.option && opt.option.privateData && + (!callable(opt.option.privateData) || + opt.option.privateData(opt.values)) + }) + }, params.extra || {})); + }); commands.add(["unl[et]"], "Delete a variable", @@ -1332,6 +1373,8 @@ const Options = Module("options", { context.title = ["Option Value"]; let completions = completer(context); + if (!isarray(completions)) + completions = array(completions).__proto__; if (!completions) return; @@ -1362,6 +1405,34 @@ const Options = Module("options", { JavaScript.setCompleter(this.get, [function () ([o.name, o.description] for (o in options))]); JavaScript.setCompleter([this.getPref, this.safeSetPref, this.setPref, this.resetPref, this.invertPref], [function () options.allPrefs().map(function (pref) [pref, ""])]); + }, + sanitizer: function () { + sanitizer.addItem("options", { + description: "Options containing hostname data", + action: function (timespan, host) { + if (host) + for (let opt in values(options._optionHash)) + if (timespan.contains(opt.lastSet * 1000) && opt.domains) + try { + opt.values = opt.filterDomain(host, opt.values); + } + catch (e) { + dactyl.reportError(e); + } + }, + privateEnter: function () { + for (let opt in values(options._optionHash)) + if (opt.privateData && (!callable(opt.privateData) || opt.privateData(opt.values))) + opt.oldValue = opt.value; + }, + privateLeave: function () { + for (let opt in values(options._optionHash)) + if (opt.oldValue != null) { + opt.value = opt.oldValue; + opt.oldValue = null; + } + } + }); } }); diff --git a/common/content/sanitizer.js b/common/content/sanitizer.js deleted file mode 100644 index 83d93aaa..00000000 --- a/common/content/sanitizer.js +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) 2009 by Doug Kearns -// -// This work is licensed for reuse under an MIT license. Details are -// given in the LICENSE.txt file included with this file. -"use strict"; - -// TODO: -// - fix Sanitize autocommand -// - add warning for TIMESPAN_EVERYTHING? -// - respect privacy.clearOnShutdown et al or recommend Leave autocommand? -// - add support for :set sanitizeitems=all like 'eventignore'? -// - integrate with the Clear Private Data dialog? - -// FIXME: -// - finish 1.9.0 support if we're going to support sanitizing in Xulmus - -const Sanitizer = Module("sanitizer", { - init: function () { - const self = this; - dactyl.loadScript("chrome://browser/content/sanitize.js", Sanitizer); - Sanitizer.getClearRange = Sanitizer.Sanitizer.getClearRange; - this.__proto__.__proto__ = new Sanitizer.Sanitizer; // Good enough. - - // TODO: remove this version test - if (/^1.9.1/.test(services.get("xulAppInfo").platformVersion)) - self.prefDomain = "privacy.cpd."; - else - self.prefDomain = "privacy.item."; - - self.prefDomain2 = "extensions.dactyl.privacy.cpd."; - }, - - // Largely ripped from from browser/base/content/sanitize.js so we can override - // the pref strategy without stepping on the global prefs namespace. - sanitize: function () { - const prefService = services.get("pref"); - let branch = prefService.getBranch(this.prefDomain); - let branch2 = prefService.getBranch(this.prefDomain2); - let errors = null; - - function prefSet(name) { - try { - return branch.getBoolPref(name); - } - catch (e) { - return branch2.getBoolPref(name); - } - } - - // Cache the range of times to clear - if (this.ignoreTimespan) - var range = null; // If we ignore timespan, clear everything - else - range = this.range || Sanitizer.getClearRange(); - - for (let itemName in this.items) { - let item = this.items[itemName]; - item.range = range; - - if ("clear" in item && item.canClear && prefSet(itemName)) { - dactyl.log("Sanitizing " + itemName + " items..."); - // Some of these clear() may raise exceptions (see bug #265028) - // to sanitize as much as possible, we catch and store them, - // rather than fail fast. - // Callers should check returned errors and give user feedback - // about items that could not be sanitized - try { - item.clear(); - } - catch (e) { - if (!errors) - errors = {}; - errors[itemName] = e; - dump("Error sanitizing " + itemName + ": " + e + "\n"); - } - } - } - - return errors; - }, - - get prefNames() util.Array.flatten([this.prefDomain, this.prefDomain2].map(options.allPrefs)) -}, { - prefArgList: [["commandLine", "commandline"], - ["offlineApps", "offlineapps"], - ["siteSettings", "sitesettings"]], - prefToArg: function (pref) { - pref = pref.replace(/.*\./, ""); - return util.Array.toObject(Sanitizer.prefArgList)[pref] || pref; - }, - - argToPref: function (arg) [k for ([k, v] in values(Sanitizer.prefArgList)) if (v == arg)][0] || arg -}, { - commands: function () { - commands.add(["sa[nitize]"], - "Clear private data", - function (args) { - dactyl.assert(!options['private'], "Cannot sanitize items in private mode"); - - let timespan = args["-timespan"] || options["sanitizetimespan"]; - - sanitizer.range = Sanitizer.getClearRange(timespan); - sanitizer.ignoreTimespan = !sanitizer.range; - - if (args.bang) { - dactyl.assert(args.length == 0, "E488: Trailing characters"); - - dactyl.log("Sanitizing all items in 'sanitizeitems'..."); - - let errors = sanitizer.sanitize(); - - if (errors) { - for (let item in errors) - dactyl.echoerr("Error sanitizing " + item + ": " + errors[item]); - } - } - else { - dactyl.assert(args.length > 0, "E471: Argument required"); - - for (let [, item] in Iterator(args.map(Sanitizer.argToPref))) { - dactyl.log("Sanitizing " + item + " items..."); - - if (sanitizer.canClearItem(item)) { - try { - sanitizer.items[item].range = sanitizer.range; - sanitizer.clearItem(item); - } - catch (e) { - dactyl.echoerr("Error sanitizing " + item + ": " + e); - } - } - else - dactyl.echomsg("Cannot sanitize " + item); - } - } - }, - { - argCount: "*", // FIXME: should be + and 0 - bang: true, - completer: function (context) { - context.title = ["Privacy Item", "Description"]; - context.completions = options.get("sanitizeitems").completer(); - }, - options: [ - { - names: ["-timespan", "-t"], - description: "Timespan for which to sanitize items", - completer: function () options.get("sanitizetimespan").completer(), - type: CommandOption.INT, - validator: function (arg) /^[0-4]$/.test(arg) - } - ] - }); - }, - options: function () { - const self = this; - - // add dactyl-specific private items - [ - { - name: "commandLine", - action: function () { - let stores = ["command", "search"]; - - if (self.range) { - stores.forEach(function (store) { - storage["history-" + store].mutate("filter", function (item) { - let timestamp = item.timestamp * 1000; - return timestamp < self.range[0] || timestamp > self.range[1]; - }); - }); - } - else - stores.forEach(function (store) { storage["history-" + store].truncate(0); }); - } - }, - { - name: "macros", - action: function () { storage["macros"].clear(); } - }, - { - name: "marks", - action: function () { - storage["local-marks"].clear(); - storage["url-marks"].clear(); - } - } - ].forEach(function (item) { - let pref = self.prefDomain2 + item.name; - - if (options.getPref(pref) == null) - options.setPref(pref, false); - - self.items[item.name] = { - canClear: true, - clear: item.action - }; - }); - - // call Sanitize autocommand - for (let [name, item] in Iterator(self.items)) { - let arg = Sanitizer.prefToArg(name); - - if (item.clear) { - let func = item.clear; - item.clear = function () { - autocommands.trigger("Sanitize", { name: arg }); - func.call(item); - }; - } - } - - options.add(["sanitizeitems", "si"], - "The default list of private items to sanitize", - "stringlist", "cache,commandline,cookies,formdata,history,marks,sessions", - { - setter: function (values) { - for (let [, pref] in Iterator(sanitizer.prefNames)) { - options.setPref(pref, false); - - for (let [, value] in Iterator(values)) { - if (Sanitizer.prefToArg(pref) == value) { - options.setPref(pref, true); - break; - } - } - } - - return values; - }, - getter: function () sanitizer.prefNames.filter(function (pref) options.getPref(pref)).map(Sanitizer.prefToArg), - completer: function (value) [ - ["cache", "Cache"], - ["commandline", "Command-line history"], - ["cookies", "Cookies"], - ["downloads", "Download history"], - ["formdata", "Saved form and search history"], - ["history", "Browsing history"], - ["macros", "Saved macros"], - ["marks", "Local and URL marks"], - ["offlineapps", "Offline website data"], - ["passwords", "Saved passwords"], - ["sessions", "Authenticated sessions"], - ["sitesettings", "Site preferences"] - ] - }); - - options.add(["sanitizetimespan", "sts"], - "The default sanitizer time span", - "number", 1, - { - setter: function (value) { - options.setPref("privacy.sanitize.timeSpan", value); - return value; - }, - getter: function () options.getPref("privacy.sanitize.timeSpan", this.defaultValue), - completer: function (value) [ - ["0", "Everything"], - ["1", "Last hour"], - ["2", "Last two hours"], - ["3", "Last four hours"], - ["4", "Today"] - ] - }); - } -}); - -// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/tabs.js b/common/content/tabs.js index efba231c..f258f835 100644 --- a/common/content/tabs.js +++ b/common/content/tabs.js @@ -572,7 +572,8 @@ const Tabs = Module("tabs", { bang: true, count: true, completer: function (context) completion.buffer(context), - literal: 0 + literal: 0, + privateData: true }); commands.add(["keepa[lt]"], @@ -589,7 +590,8 @@ const Tabs = Module("tabs", { }, { argCount: "+", completer: function (context) completion.ex(context), - literal: 0 + literal: 0, + subCommand: 0 }); // TODO: this should open in a new tab positioned directly after the current one, not at the end @@ -602,7 +604,8 @@ const Tabs = Module("tabs", { }, { argCount: "+", completer: function (context) completion.ex(context), - literal: 0 + literal: 0, + subCommand: 0 }); commands.add(["tabd[o]", "bufd[o]"], @@ -615,7 +618,8 @@ const Tabs = Module("tabs", { }, { argCount: "1", completer: function (context) completion.ex(context), - literal: 0 + literal: 0, + subCommand: 0 }); commands.add(["tabl[ast]", "bl[ast]"], @@ -705,7 +709,8 @@ const Tabs = Module("tabs", { bang: true, count: true, completer: function (context) completion.buffer(context), - literal: 0 + literal: 0, + privateData: true }); commands.add(["buffers", "files", "ls", "tabs"], @@ -763,6 +768,7 @@ const Tabs = Module("tabs", { }, { bang: true, completer: function (context) completion.url(context), + domains: function (args) commands.get("open").domains(args), literal: 0, privateData: true }); @@ -863,7 +869,8 @@ const Tabs = Module("tabs", { context.completions = Iterator(tabs.closedTabs); }, count: true, - literal: 0 + literal: 0, + privateData: true }); commands.add(["undoa[ll]"], diff --git a/common/locale/en-US/options.xml b/common/locale/en-US/options.xml index 921394eb..9f2eb111 100644 --- a/common/locale/en-US/options.xml +++ b/common/locale/en-US/options.xml @@ -1023,29 +1023,12 @@ 'si' 'sanitizeitems' 'sanitizeitems' 'si' stringlist - cache,commandline,cookies,formdata,history,marks,sessions + all -

The default list of private items to sanitize.

- -
-
cache
Cache
-
commandline
Command-line history
-
cookies
Cookies
-
downloads
Download history
-
formdata
Saved form and search history
-
history
Browsing history
-
marks
Local and URL marks
-
macros
Saved macros
-
offlineapps
Offline website data
-
passwords
Saved passwords
-
sessions
Authenticated sessions
-
sitesettings
Site preferences
-
-

- When history items are sanitized :open, - :tabopen and :winopen command-line - history entries are also removed. + The default list of private items to sanitize. See + :sanitize for a list and explanation of + possible values.

@@ -1055,21 +1038,20 @@ 'sts' 'sanitizetimespan' 'sanitizetimespan' 'sts' number - 1 + all

The default sanitizer time span. Only items created within this timespan are - deleted. + deleted. The value must be of the one of the forms,

- This only applies to cookies, history, formdata, and downloads. -
-
0
Everything
-
1
Last hour
-
2
Last two hours
-
3
Last four hours
-
4
Today
+
all
Everything
+
session
The current session
+
nm
Past n Minutes
+
nh
Past n Hours
+
nd
Past n Days
+
nw
Past n Weeks
diff --git a/common/locale/en-US/various.xml b/common/locale/en-US/various.xml index caf78003..27ca2f92 100644 --- a/common/locale/en-US/various.xml +++ b/common/locale/en-US/various.xml @@ -94,29 +94,6 @@ - - :sa :sanitize - :sanitize [-timespan=timespan] item - :sanitize! [-timespan=timespan] - -

- Clear private data items. Where item … is a list of private items to - delete. These may be any of the items valid for sanitizeitems. -

- -

- If ! is specified then sanitizeitems is used for the list of items to - delete. -

- -

- If timespan is specified then only items within that timespan are deleted, - otherwise the value of sanitizetimespan is used. -

-
-
- - :sil :silent :silent command @@ -153,6 +130,109 @@ +

Privacy and sensitive information

+ +

+ Part of &dactyl.appname;'s user efficiency comes at the cost of storing a + lot of potentially private data, including command-line history, page + marks, and the like. Because we know that keeping a detailed trail of all + of your activities isn't always welcome, &dactyl.appname; provides + comprehensive facilities for erasing potentially sensitive data. +

+ +

+ + &dactyl.appname; fully supports &dactyl.host;'s private browsing mode. + When in private browsing mode, no other than Bookmarks and QuickMarks are + written to disk. Further, upon exiting private mode, all new data, + including command-line history, local and URL marks, and macros, are + purged. For more information, see private. +

+ +

+ + In addition to private mode, &dactyl.appname; provides a comprehensive + facility for clearing any potentially sensitive data generated by either + &dactyl.appname; or &dactyl.host;. It directly integrates with + &dactyl.host;'s own sanitization facility, and so automatically clears any + domain data and session history when requested. Further, &dactyl.appname; + provides its own more granular sanitization facility, which allows, e.g., + clearing the command-line and macro history for the past ten minutes. +

+ + + :sa :sanitize + :sanitize -host=host -older -timespan=timespan item + :sanitize! -host=host -older -timespan=timespan + +

+ Clear private data items for timespan, where item … + is a list of private items to delete. If ! is specified + then sanitizeitems is used for the list of items to delete. + Items may be any of: +

+ +
+
all
All items
+
cache
Cache
+
commandline
Command-line history
+
cookies
Cookies
+
downloads
Download history
+
formdata
Saved form and search history
+
history
Browsing history
+
marks
Local and URL marks
+
macros
Saved macros
+
messages
Saved :messages
+
offlineapps
Offline website data
+
options
Options containing hostname data
+
passwords
Saved passwords
+
sessions
Authenticated sessions
+
sitesettings
Site preferences
+
+ +

+ When history items are sanitized, all command-line + history items containing URLs or page titles (other than bookmark + commands) are additionally cleared. Invocations of the + :sanitize command are included in this set. +

+ +

+ If timespan (short name -t) is specified then only + items within that timespan are deleted, otherwise the value of + sanitizetimespan is used. If -older (short name + -o) is specified, than only items older than + timespan are deleted. +

+ + + The following items are cleared regardless of timeframe: + cache, offlineapps, passwords, + sessions, sitesettings. Additionally, + options are never cleared. + + +

+ If host (short name -h) is specified, only items + containing a reference to that domain or a subdomain thereof are + cleared. Moreover, if commandline or history is + specified, the invocation of the :sanitize command is + naturally cleared as well. +

+ + + This only applies to commandline, cookies, + history, marks, messages, + options, and sitesettings. All other + domain-specific data is cleared only along with history, + when a request is made to &dactyl.host; to purge all data for + host. Included in this purge are all matchint history + entries, cookies, closed tabs, form data, and location bar + entries. + +
+
+

Online help

diff --git a/common/modules/base.jsm b/common/modules/base.jsm index 4156c113..d3abaa96 100644 --- a/common/modules/base.jsm +++ b/common/modules/base.jsm @@ -66,11 +66,12 @@ defmodule("base", this, { // sed -n 's/^(const|function) ([a-zA-Z0-9_]+).*/ "\2",/p' base.jsm | sort | fmt exports: [ "Cc", "Ci", "Class", "Cr", "Cu", "Module", "Object", "Runnable", - "Struct", "StructBase", "Timer", "allkeys", "array", "call", - "callable", "curry", "debuggerProperties", "defmodule", "dict", + "Struct", "StructBase", "Timer", "XPCOMUtils", "allkeys", "array", + "call", "callable", "curry", "debuggerProperties", "defmodule", "dict", "endmodule", "extend", "foreach", "isarray", "isgenerator", - "isinstance", "isobject", "isstring", "issubclass", "iter", "memoize", - "properties", "requiresMainThread", "set", "update", "values", + "isinstance", "isobject", "isstring", "issubclass", "iter", "keys", + "memoize", "properties", "requiresMainThread", "set", "update", + "values", ], use: ["services"] }); @@ -117,8 +118,9 @@ function debuggerProperties(obj) { } } +let hasOwnProperty = Object.prototype.hasOwnProperty; if (!Object.keys) - Object.keys = function keys(obj) [k for (k in obj) if (obj.hasOwnProperty(k))]; + Object.keys = function keys(obj) [k for (k in obj) if (hasOwnProperty.call(obj, k))]; if (!Object.getOwnPropertyNames) Object.getOwnPropertyNames = function getOwnPropertyNames(obj) { @@ -144,9 +146,14 @@ function properties(obj, prototypes) { } } +function keys(obj) { + for (var k in obj) + if (hasOwnProperty.call(obj, k)) + yield k; +} function values(obj) { for (var k in obj) - if (obj.hasOwnProperty(k)) + if (hasOwnProperty.call(obj, k)) yield obj[k]; } function foreach(iter, fn, self) { @@ -175,7 +182,7 @@ set.add = function (set, key) { set[key] = true; return res; } -set.has = function (set, key) Object.prototype.hasOwnProperty.call(set, key); +set.has = function (set, key) hasOwnProperty.call(set, key); set.remove = function (set, key) { delete set[key]; } function iter(obj) { @@ -204,6 +211,12 @@ function iter(obj) { for (let i = 0; i < obj.length; i++) yield [obj.name, obj]; })(); + if (obj instanceof Ci.mozIStorageStatement) + return (function (obj) { + while (obj.executeStep()) + yield obj.row; + obj.reset(); + })(obj); return Iterator(obj); } @@ -542,15 +555,31 @@ Class.prototype = { * @param {Object} classProperties Properties to be applied to the class constructor. * @return {Class} */ -function Module(name, prototype, classProperties, init) { - const module = Class(name, prototype, classProperties); +function Module(name, prototype) { + let init = callable(prototype) ? 4 : 3; + const module = Class.apply(Class, Array.slice(arguments, 0, init)); let instance = module(); module.name = name.toLowerCase(); - instance.INIT = init || {}; + instance.INIT = arguments[init] || {}; currentModule[module.name] = instance; defmodule.modules.push(instance); return module; } +if (Cu.getGlobalForObject) + Module.callerGlobal = function (caller) { + try { + return Cu.getGlobalForObject(caller); + } + catch (e) { + return null; + } + }; +else + Module.callerGlobal = function (caller) { + while (caller.__parent__) + caller = caller.__parent__; + return caller; + }; /** * @class Struct @@ -677,7 +706,7 @@ const Timer = Class("Timer", { */ const array = Class("util.Array", Array, { init: function (ary) { - if (isgenerator(ary)) + if (isinstance(ary, ["Iterator", "Generator"])) ary = [k for (k in ary)]; else if (ary.length) ary = Array.slice(ary); @@ -688,12 +717,13 @@ const array = Class("util.Array", Array, { __noSuchMethod__: function (meth, args) { var res = array[meth].apply(null, [this.__proto__].concat(args)); - if (array.isinstance(res)) + if (isarray(res)) return array(res); return res; }, toString: function () this.__proto__.toString(), concat: function () this.__proto__.concat.apply(this.__proto__, arguments), + filter: function () this.__noSuchMethod__("filter", Array.slice(arguments)), map: function () this.__noSuchMethod__("map", Array.slice(arguments)) }; } diff --git a/common/modules/highlight.jsm b/common/modules/highlight.jsm index fa184d0e..84efa919 100644 --- a/common/modules/highlight.jsm +++ b/common/modules/highlight.jsm @@ -52,7 +52,6 @@ const Highlights = Module("Highlight", { return "Unknown highlight keyword: " + class_; let style = this.highlight[key] || Highlight(key); - styles.removeSheet(true, style.selector); if (append) newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle); @@ -60,22 +59,23 @@ const Highlights = Module("Highlight", { newStyle = null; if (newStyle == null) { if (style.default == null) { - delete this.highlight[style.class]; styles.removeSheet(true, style.selector); + delete this.highlight[style.class]; return null; } newStyle = style.default; - force = true; } - let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;") - .replace(";!important;", ";", "g"); // Seeming Spidermonkey bug - if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) { - css = style.selector + " { " + css + " }"; + if (!style.loaded || style.value != newStyle) { + styles.removeSheet(true, style.selector); + let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;") + .replace(";!important;", ";", "g"); // Seeming Spidermonkey bug + if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) { + css = style.selector + " { " + css + " }"; - let error = styles.addSheet(true, "highlight:" + style.class, style.filter, css, true); - if (error) - return error; + styles.addSheet(true, "highlight:" + style.class, style.filter, css, true); + style.loaded = true; + } } style.value = newStyle; this.highlight[style.class] = style; @@ -120,9 +120,10 @@ const Highlights = Module("Highlight", { style.selector = this.selector(style.class) + style.selector; let old = this.highlight[style.class]; - this.highlight[style.class] = style; - if (old && old.value != old.default) - style.value = old.value; + if (!old) + this.highlight[style.class] = style; + else if (old.value == old.default) + old.value = style.value; }, this); for (let [class_, hl] in Iterator(this.highlight)) if (hl.value == hl.default) diff --git a/common/modules/sanitizer.jsm b/common/modules/sanitizer.jsm new file mode 100644 index 00000000..af1753ba --- /dev/null +++ b/common/modules/sanitizer.jsm @@ -0,0 +1,330 @@ +// Copyright (c) 2009 by Doug Kearns +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +// TODO: +// - fix Sanitize autocommand +// - add warning for TIMESPAN_EVERYTHING? +// - respect privacy.clearOnShutdown et al or recommend Leave autocommand? +// - integrate with the Clear Private Data dialog? + +// FIXME: +// - finish 1.9.0 support if we're going to support sanitizing in Xulmus + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("sanitizer", this, { + exports: ["Range", "Sanitizer", "sanitizer"], + require: ["services", "storage", "util"] +}); + +let tmp = {}; +services.get("subscriptLoader").loadSubScript("chrome://browser/content/sanitize.js", tmp); + +const Range = Struct("min", "max"); +Range.prototype.contains = function (date) + date == null || (this.min == null || date >= this.min) && (this.max == null || date <= this.max); +Range.prototype.__defineGetter__("isEternity", function () this.max == null && this.min == null); +Range.prototype.__defineGetter__("isSession", function () this.max == null && this.min == sanitizer.sessionStart); + +const Sanitizer = Module("sanitizer", tmp.Sanitizer, { + sessionStart: Date.now() * 1000, + + init: function () { + services.add("contentprefs", "@mozilla.org/content-pref/service;1", Ci.nsIContentPrefService); + services.add("cookies", "@mozilla.org/cookiemanager;1", [Ci.nsICookieManager, Ci.nsICookieManager2, + Ci.nsICookieService]); + services.add("loginmanager", "@mozilla.org/login-manager;1", Ci.nsILoginManager); + services.add("permissions", "@mozilla.org/permissionmanager;1", Ci.nsIPermissionManager); + + this.itemOverrides = {}; + this.itemDescriptions = { + all: "Sanitize all items", + // Builtin items + cache: "Cache", + downloads: "Download history", + formdata: "Saved form and search history", + history: "Browsing history", + offlineapps: "Offline website data", + passwords: "Saved passwords", + sessions: "Authenticated sessions", + }; + // These builtin methods don't support hosts or have + // insufficient granularity + this.addItem("cookies", { + description: "Cookies", + action: function (range, host) { + for (let c in Sanitizer.iterCookies(host)) + if (range.contains(c.creationTime) || timespan.isSession && c.isSession) + services.get("cookies").remove(c.host, c.name, c.path, false); + }, + override: true + }); + this.addItem("sitesettings", { + description: "Site preferences", + action: function (range, host) { + if (host) { + for (let p in Sanitizer.iterPermissions(host)) { + services.get("permissions").remove(util.createURI(p.host), p.type); + services.get("permissions").add(util.createURI(p.host), p.type, 0); + } + for (let p in iter(services.get("contentprefs").getPrefs(util.createURI(host)).enumerator)) + services.get("contentprefs").removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name); + } + else { + // "Allow this site to open popups" ... + services.get("permissions").removeAll(); + // Zoom level, ... + services.get("contentprefs").removeGroupedPrefs(); + } + + // "Never remember passwords" ... + for each (let domain in services.get("loginmanager").getAllDisabledHosts()) + if (!host || util.isSubdomain(domain, host)) + services.get("loginmanager").setLoginSavingEnabled(host, true); + }, + override: true + }); + util.addObserver(this); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + addItem: function addItem(name, params) { + if (params.description) + this.itemDescriptions[name] = params.description; + if (params.override) + set.add(this.itemOverrides, name); + + name = "clear-" + name; + storage.addObserver("sanitizer", + function (key, event, arg) { + if (event == name) + params.action.apply(params, arg); + }, Module.callerGlobal(params.action)); + + if (params.privateEnter || params.privateLeave) + storage.addObserver("private-mode", + function (key, event, arg) { + let meth = params[arg ? "privateEnter" : "privateLeave"]; + if (meth) + meth.call(params); + }, Module.callerGlobal(params.action)); + }, + + observe: { + "browser:purge-domain-data": function (subject, data) { + storage.fireEvent("sanitize", "domain", data); + // If we're sanitizing, our own sanitization functions will already + // be called, and with much greater granularity. Only process this + // event if it's triggered externally. + if (!this.sanitizing) + this.sanitizeItems(null, Range(), data); + }, + "browser:purge-session-history": function (subject, data) { + // See above. + if (!this.sanitizing) + this.sanitizeItems(null, Range(this.sessionStart/1000), null); + }, + "private-browsing": function (subject, data) { + if (data == "enter") + storage.privateMode = true; + else if (data == "exit") + storage.privateMode = false; + storage.fireEvent("private-mode", "change", storage.privateMode); + } + }, + + sanitize: function (items, range) { + this.sanitizing = true; + let errors = this.sanitizeItems(items, range, null); + + for (let itemName in values(items)) { + try { + let item = this.items[Sanitizer.argToPref(itemName)]; + if (item && !this.itemOverrides[itemName]) { + item.range = range; + if ("clear" in item && item.canClear) + item.clear(); + } + } + catch (e) { + errors = errors || {}; + errors[itemName] = e; + dump("Error sanitizing " + itemName + ": " + e + "\n" + e.stack + "\n"); + } + } + + this.sanitizing = false; + return errors; + }, + + sanitizeItems: function (items, range, host) { + if (items == null) + items = Object.keys(this.itemDescriptions); + let errors; + for (let itemName in values(items)) + try { + storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]); + } + catch (e) { + errors = errors || {}; + errors[itemName] = e; + dump("Error sanitizing " + itemName + ": " + e + "\n" + e.stack + "\n"); + } + return errors; + } +}, { + argPrefMap: { + commandline: "commandLine", + offlineapps: "offlineApps", + sitesettings: "siteSettings", + }, + argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg, + prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(), + + iterCookies: function iterCookies(host) { + for (let c in iter(services.get("cookies").enumerator)) + if (!host || util.isSubdomain(c.QueryInterface(Ci.nsICookie2).rawHost, host)) + yield c; + }, + iterPermissions: function iterPermissions(host) { + for (let p in iter(services.get("permissions").enumerator)) + if (p.QueryInterface(Ci.nsIPermission) && (!host || util.isSubdomain(p.host, host))) + yield p; + } +}, { + autocommands: function (dactyl, modules, window) { + storage.addObserver("private-mode", + function (key, event, value) { + modules.autocommands.trigger("PrivateMode", { state: value }); + }, window); + storage.addObserver("sanitizer", + function (key, event, value) { + if (event == "domain") + modules.autocommands.trigger("SanitizeDomain", { domain: value }); + else if (!value[1]) + modules.autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] }); + }, window); + }, + commands: function (dactyl, modules, window) { + const commands = modules.commands; + commands.add(["sa[nitize]"], + "Clear private data", + function (args) { + dactyl.assert(!modules.options['private'], "Cannot sanitize items in private mode"); + + let timespan = args["-timespan"] || modules.options["sanitizetimespan"]; + + let range = Range(), match = /^(\d+)([mhdw])$/.exec(timespan); + range[args["-older"] ? "max" : "min"] = + match ? 1000 * (Date.now() - 1000 * parseInt(match[1], 10) * { m: 60, h: 3600, d: 3600 * 24, w: 3600 * 24 * 7 }[match[2]]) + : (timespan[0] == "s" ? sanitizer.sessionStart : null); + + let items = args.slice(); + if (args.bang) { + dactyl.assert(args.length == 0, "E488: Trailing characters"); + items = modules.options.get("sanitizeitems").values; + } + else + dactyl.assert(modules.options.get("sanitizeitems").validator(items), "Valid items required"); + + if (items[0] == "all") + items = Object.keys(sanitizer.itemDescriptions); + + sanitizer.range = range; + sanitizer.ignoreTimespan = range.min == null; + sanitizer.sanitizing = true; + if (args["-host"]) { + args["-host"].forEach(function (host) { + sanitizer.sanitizing = true; + if (items.indexOf("history") > -1) + services.get("privateBrowsing").removeDataFromDomain(host); + sanitizer.sanitizeItems(items, range, host) + }); + } + else + sanitizer.sanitize(items, range); + }, + { + argCount: "*", // FIXME: should be + and 0 + bang: true, + completer: function (context) { + context.title = ["Privacy Item", "Description"]; + context.completions = modules.options.get("sanitizeitems").completer(); + }, + domains: function (args) args["-host"] || [], + options: [ + { + names: ["-host", "-h"], + description: "Only sanitize items referring to listed host or hosts", + completer: function (context, args) { + let hosts = context.filter.split(","); + context.advance(context.filter.length - hosts.pop().length); + context.filters.push(function (item) + !hosts.some(function (host) util.isSubdomain(item.text, host))); + modules.completion.domain(context); + }, + type: modules.CommandOption.LIST, + }, { + names: ["-older", "-o"], + description: "Sanitize items older than timespan", + type: modules.CommandOption.NOARG + }, { + names: ["-timespan", "-t"], + description: "Timespan for which to sanitize items", + completer: function (context) modules.options.get("sanitizetimespan").completer(context), + type: modules.CommandOption.STRING, + validator: function (arg) modules.options.get("sanitizetimespan").validator(arg), + } + ], + privateData: true + }); + }, + options: function (dactyl, modules) { + const options = modules.options; + if (services.get("privateBrowsing")) + options.add(["private", "pornmode"], + "Set the 'private browsing' option", + "boolean", false, + { + setter: function (value) services.get("privateBrowsing").privateBrowsingEnabled = value, + getter: function () services.get("privateBrowsing").privateBrowsingEnabled + }); + + options.add(["sanitizeitems", "si"], + "The default list of private items to sanitize", + "stringlist", "all", + { + completer: function (value) Iterator(sanitizer.itemDescriptions), + validator: function (values) values.length && + values.every(function (val) set.has(sanitizer.itemDescriptions, val)) && + (values.length == 1 || !values.some(function (val) val == "all")) + }); + + options.add(["sanitizetimespan", "sts"], + "The default sanitizer time span", + "string", "all", + { + completer: function (context) { + context.compare = context.constructor.Sort.Unsorted; + return [ + ["all", "Everything"], + ["session", "The current session"], + ["10m", "Last ten minutes"], + ["1h", "Past hour"], + ["1d", "Past day"], + ["1w", "Past week"], + ] + }, + validator: function (value) /^(a(ll)?|s(ession)|\d+[mhdw])$/.test(value) + }); + } +}); + +endmodule(); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);} + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/services.jsm b/common/modules/services.jsm index afef4cd8..c4442fc0 100644 --- a/common/modules/services.jsm +++ b/common/modules/services.jsm @@ -27,12 +27,14 @@ const Services = Module("Services", { this.add("environment", "@mozilla.org/process/environment;1", Ci.nsIEnvironment); this.add("extensionManager", "@mozilla.org/extensions/manager;1", Ci.nsIExtensionManager); this.add("favicon", "@mozilla.org/browser/favicon-service;1", Ci.nsIFaviconService); - this.add("history", "@mozilla.org/browser/global-history;2", [Ci.nsIBrowserHistory, Ci.nsIGlobalHistory3, Ci.nsINavHistoryService]); + this.add("history", "@mozilla.org/browser/global-history;2", [Ci.nsIBrowserHistory, Ci.nsIGlobalHistory3, + Ci.nsINavHistoryService, Ci.nsPIPlacesDatabase]); this.add("io", "@mozilla.org/network/io-service;1", Ci.nsIIOService); this.add("json", "@mozilla.org/dom/json;1", Ci.nsIJSON, "createInstance"); this.add("livemark", "@mozilla.org/browser/livemark-service;2", Ci.nsILivemarkService); this.add("observer", "@mozilla.org/observer-service;1", Ci.nsIObserverService); this.add("pref", "@mozilla.org/preferences-service;1", [Ci.nsIPrefBranch, Ci.nsIPrefBranch2, Ci.nsIPrefService]); + this.add("privateBrowsing", "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService); this.add("profile", "@mozilla.org/toolkit/profile-service;1", Ci.nsIToolkitProfileService); this.add("runtime", "@mozilla.org/xre/runtime;1", [Ci.nsIXULAppInfo, Ci.nsIXULRuntime]); this.add("rdf", "@mozilla.org/rdf/rdf-service;1", Ci.nsIRDFService); @@ -41,9 +43,9 @@ const Services = Module("Services", { this.add("subscriptLoader", "@mozilla.org/moz/jssubscript-loader;1", Ci.mozIJSSubScriptLoader); this.add("tagging", "@mozilla.org/browser/tagging-service;1", Ci.nsITaggingService); this.add("threadManager", "@mozilla.org/thread-manager;1", Ci.nsIThreadManager); + this.add("urifixup", "@mozilla.org/docshell/urifixup;1", Ci.nsIURIFixup); this.add("windowMediator", "@mozilla.org/appshell/window-mediator;1", Ci.nsIWindowMediator); this.add("windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", Ci.nsIWindowWatcher); - this.add("xulAppInfo", "@mozilla.org/xre/app-info;1", Ci.nsIXULAppInfo); this.addClass("file", "@mozilla.org/file/local;1", Ci.nsILocalFile); this.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler); @@ -128,6 +130,6 @@ const Services = Module("Services", { endmodule(); -// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);} // vim: set fdm=marker sw=4 sts=4 et ft=javascript: diff --git a/common/modules/storage.jsm b/common/modules/storage.jsm index 4d09d380..b79f7a57 100644 --- a/common/modules/storage.jsm +++ b/common/modules/storage.jsm @@ -21,6 +21,7 @@ }}} ***** END LICENSE BLOCK *****/ "use strict"; +const myObject = Object; Components.utils.import("resource://dactyl/base.jsm"); defmodule("storage", this, { exports: ["File", "storage"], @@ -76,9 +77,9 @@ function savePref(obj) { } const StoreBase = Class("StoreBase", { - OPTIONS: ["privateData"], + OPTIONS: ["privateData", "replacer"], fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); }, - get serial() services.get("json").encode(this._object), + get serial() JSON.stringify(this._object, this.replacer), save: function () { savePref(this); }, init: function (name, store, load, options) { this._load = load; @@ -97,7 +98,7 @@ const StoreBase = Class("StoreBase", { }); const ObjectStore = Class("ObjectStore", StoreBase, { - _constructor: Object, + _constructor: myObject, set: function set(key, val) { var defined = key in this._object; @@ -120,6 +121,7 @@ const ObjectStore = Class("ObjectStore", StoreBase, { clear: function () { this._object = {}; + this.fireEvent("clear", key); }, __iterator__: function () Iterator(this._object), @@ -181,7 +183,7 @@ const Storage = Module("Storage", { if (!(key in keys) || params.reload || this.alwaysReload[key]) { if (key in this && !(params.reload || this.alwaysReload[key])) throw Error(); - let load = function () loadPref(key, params.store, params.type || Object); + let load = function () loadPref(key, params.store, params.type || myObject); keys[key] = new constructor(key, params.store, load, params); timers[key] = new Timer(1000, 10000, function () storage.save(key)); this.__defineGetter__(key, function () keys[key]); @@ -234,14 +236,13 @@ const Storage = Module("Storage", { get observers() observers, fireEvent: function fireEvent(key, event, arg) { - if (!(key in this)) - return; this.removeDeadObservers(); // Safe, since we have our own Array object here. if (key in observers) for each (let observer in observers[key]) observer.callback.get()(key, event, arg); - timers[key].tell(); + if (timers[key]) + timers[key].tell(); }, load: function load(key) { @@ -261,6 +262,8 @@ const Storage = Module("Storage", { _privateMode: false, get privateMode() this._privateMode, set privateMode(val) { + if (val && !this._privateMode) + this.saveAll(); if (!val && this._privateMode) for (let key in keys) this.load(key); diff --git a/common/modules/styles.jsm b/common/modules/styles.jsm index 8b8b1c1c..4105c2b2 100644 --- a/common/modules/styles.jsm +++ b/common/modules/styles.jsm @@ -31,6 +31,13 @@ Sheet.prototype.__defineGetter__("fullCSS", function wrapCSS() { .join(", "); return "/* Dactyl style #" + this.id + " */ " + namespace + " @-moz-document " + selectors + "{\n" + css + "\n}\n"; }); +Sheet.prototype.__defineGetter__("css", function () this[3]); +Sheet.prototype.__defineSetter__("css", function (val) { + this.enabled = false; + this[3] = val; + this.enabled = true; + return val; +}); Sheet.prototype.__defineGetter__("enabled", function () this._enabled); Sheet.prototype.__defineSetter__("enabled", function (on) { this._enabled = Boolean(on); diff --git a/common/modules/util.jsm b/common/modules/util.jsm index 9b124a29..bc61078e 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -33,6 +33,41 @@ const Util = Module("Util", { } }, + /** + * Registers a obj as a new observer with the observer service. obj.observe + * must be an object where each key is the name of a target to observe and + * each value is a function(subject, data) to be called when the given + * target is broadcast. obj.observe will be replaced with a new opaque + * function. The observer is automatically unregistered on application + * shutdown. + * + * @param {object} obj + */ + addObserver: function (obj) { + let observers = obj.observe; + function register(meth) { + services.get("observer")[meth](obj, "quit-application", true); + for (let target in keys(observers)) + services.get("observer")[meth](obj, target, true); + } + Class.replaceProperty(obj, "observe", + function (subject, target, data) { + if (target == "quit-application") + register("removeObserver"); + if (observers[target]) + observers[target].call(obj, subject, data); + }); + register("addObserver"); + }, + + /** + * Calls a function synchronously in the main thread. Return values are not + * preserved. + * + * @param {function} callback + * @param {object} self The this object for the call. + * @returns {function} + */ callInMainThread: function (callback, self) { let mainThread = services.get("threadManager").mainThread; if (services.get("threadManager").isMainThread) @@ -123,30 +158,19 @@ const Util = Module("Util", { }, /** - * Copies a string to the system clipboard. If verbose is specified - * the copied string is also echoed to the command line. + * Converts any arbitrary string into an URI object. Returns null on + * failure. * * @param {string} str - * @param {boolean} verbose + * @returns {nsIURI|null} */ - copyToClipboard: function copyToClipboard(str, verbose) { - const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); - clipboardHelper.copyString(str); - - if (verbose) - util.dactyl.echomsg("Yanked " + str); - }, - - /** - * Converts any arbitrary string into an URI object. - * - * @param {string} str - * @returns {Object} - */ - // FIXME: newURI needed too? createURI: function createURI(str) { - const fixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); - return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP); + try { + return services.get("urifixup").createFixupURI(str, services.get("urifixup").FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP); + } + catch (e) { + return null; + } }, /** @@ -336,6 +360,20 @@ const Util = Module("Util", { return strNum[0] + " " + unitVal[unitIndex]; }, + /** + * Returns the host for the given URL, or null if invalid. + * + * @param {string} url + * @returns {string|null} + */ + getHost: function (url) { + try { + return util.createURI(url).host; + } + catch (e) {} + return null; + }, + /** * Sends a synchronous or asynchronous HTTP request to url and * returns the XMLHttpRequest object. If callback is specified the @@ -390,6 +428,29 @@ const Util = Module("Util", { bottom: Math.min(r1.bottom, r2.bottom) }), + /** + * Returns true if 'url' is in the domain 'domain'. + * + * @param {string} url + * @param {string} domain + * @returns {boolean} + */ + isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain), + + /** + * Returns true if 'host' is a subdomain of 'domain'. + * + * @param {string} host The host to check. + * @param {string} domain The base domain to check the host agains. + * @returns {boolean} + */ + isSubdomain: function isSubdomain(host, domain) { + if (host == null) + return false; + let idx = host.lastIndexOf(domain); + return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx-1] == "."); + }, + /** * Returns an XPath union expression constructed from the specified node * tests. An expression is built with node tests for both the null and @@ -589,43 +650,6 @@ const Util = Module("Util", { } }, - /** - * Reads a string from the system clipboard. - * - * This is same as Firefox's readFromClipboard function, but is needed for - * apps like Thunderbird which do not provide it. - * - * @returns {string} - */ - readFromClipboard: function readFromClipboard() { - let str; - - try { - const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); - const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); - - transferable.addDataFlavor("text/unicode"); - - if (clipboard.supportsSelectionClipboard()) - clipboard.getData(transferable, clipboard.kSelectionClipboard); - else - clipboard.getData(transferable, clipboard.kGlobalClipboard); - - let data = {}; - let dataLen = {}; - - transferable.getTransferData("text/unicode", data, dataLen); - - if (data) { - data = data.value.QueryInterface(Ci.nsISupportsString); - str = data.data.substring(0, dataLen.value / 2); - } - } - catch (e) {} - - return str; - }, - /** * Scrolls an element into view if and only if it's not already * fully visible. diff --git a/pentadactyl/content/config.js b/pentadactyl/content/config.js index b727bbf3..336a0ea0 100644 --- a/pentadactyl/content/config.js +++ b/pentadactyl/content/config.js @@ -215,7 +215,8 @@ const Config = Module("config", ConfigBase, { { argCount: "+", completer: function (context) completion.ex(context), - literal: 0 + literal: 0, + subCommand: 0 }); commands.add(["winc[lose]", "wc[lose]"], @@ -235,6 +236,7 @@ const Config = Module("config", ConfigBase, { }, { completer: function (context) completion.url(context), + domains: function (args) commands.get("open").domains(args), literal: 0, privateData: true });