1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-21 13:17:59 +01:00

Complete :sanitize and private mode overhaul.

--HG--
rename : common/content/sanitizer.js => common/modules/sanitizer.jsm
This commit is contained in:
Kris Maglione
2010-09-17 06:15:13 -04:00
parent a5213c3760
commit 152e6d5a1f
27 changed files with 1120 additions and 721 deletions

View File

@@ -280,6 +280,7 @@ const Bookmarks = Module("bookmarks", {
names: ["-tags", "-T"], names: ["-tags", "-T"],
description: "A comma-separated list of tags", description: "A comma-separated list of tags",
completer: function tags(context, args) { completer: function tags(context, args) {
// TODO: Move the bulk of this to parseArgs.
let filter = context.filter; let filter = context.filter;
let have = filter.split(","); let have = filter.split(",");

View File

@@ -52,45 +52,6 @@ const Browser = Module("browser", {
completer: function (context) completion.charset(context) 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"], options.add(["urlseparator"],
"Set the separator regex used to separate multiple URL args", "Set the separator regex used to separate multiple URL args",
"string", ",\\s"); "string", ",\\s");
@@ -99,7 +60,7 @@ const Browser = Module("browser", {
mappings: function () { mappings: function () {
mappings.add([modes.NORMAL], mappings.add([modes.NORMAL],
["y"], "Yank current location to the clipboard", ["y"], "Yank current location to the clipboard",
function () { util.copyToClipboard(buffer.URL, true); }); function () { dactyl.clipboardWrite(buffer.URL, true); });
// opening websites // opening websites
mappings.add([modes.NORMAL], mappings.add([modes.NORMAL],
@@ -218,6 +179,8 @@ const Browser = Module("browser", {
dactyl.open("about:blank"); dactyl.open("about:blank");
}, { }, {
completer: function (context) completion.url(context), completer: function (context) completion.url(context),
domains: function (args) array.compact(dactyl.stringToURLArray(args[0] || "").map(
function (url) util.getHost(url))),
literal: 0, literal: 0,
privateData: true privateData: true
}); });

View File

@@ -1517,14 +1517,14 @@ const Buffer = Module("buffer", {
mappings.add(myModes, ["gP"], mappings.add(myModes, ["gP"],
"Open (put) a URL based on the current clipboard contents in a new buffer", "Open (put) a URL based on the current clipboard contents in a new buffer",
function () { function () {
dactyl.open(util.readFromClipboard(), dactyl.open(dactyl.clipboardRead(),
dactyl[options.get("activate").has("paste") ? "NEW_BACKGROUND_TAB" : "NEW_TAB"]); dactyl[options.get("activate").has("paste") ? "NEW_BACKGROUND_TAB" : "NEW_TAB"]);
}); });
mappings.add(myModes, ["p", "<MiddleMouse>"], mappings.add(myModes, ["p", "<MiddleMouse>"],
"Open (put) a URL based on the current clipboard contents in the current buffer", "Open (put) a URL based on the current clipboard contents in the current buffer",
function () { function () {
let url = util.readFromClipboard(); let url = dactyl.clipboardRead();
dactyl.assert(url); dactyl.assert(url);
dactyl.open(url); dactyl.open(url);
}); });
@@ -1532,7 +1532,7 @@ const Buffer = Module("buffer", {
mappings.add(myModes, ["P"], mappings.add(myModes, ["P"],
"Open (put) a URL based on the current clipboard contents in a new buffer", "Open (put) a URL based on the current clipboard contents in a new buffer",
function () { function () {
let url = util.readFromClipboard(); let url = dactyl.clipboardRead();
dactyl.assert(url); dactyl.assert(url);
dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB }); dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB });
}); });
@@ -1552,7 +1552,7 @@ const Buffer = Module("buffer", {
function () { function () {
let sel = buffer.getCurrentWord(); let sel = buffer.getCurrentWord();
dactyl.assert(sel); dactyl.assert(sel);
util.copyToClipboard(sel, true); dactyl.clipboardWrite(sel, true);
}); });
// zooming // zooming

View File

@@ -23,36 +23,6 @@ const CommandLine = Module("commandline", {
storage.newArray("history-search", { store: true, privateData: true }); storage.newArray("history-search", { store: true, privateData: true });
storage.newArray("history-command", { 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 = { //{{{ this._messageHistory = { //{{{
_messages: [], _messages: [],
get messages() { get messages() {
@@ -71,6 +41,10 @@ const CommandLine = Module("commandline", {
this._messages = []; this._messages = [];
}, },
filter: function filter(fn, self) {
this._messages = this._messages.filter(fn, self);
},
add: function add(message) { add: function add(message) {
if (!message) if (!message)
return; return;
@@ -78,7 +52,9 @@ const CommandLine = Module("commandline", {
if (this._messages.length >= options["messages"]) if (this._messages.length >= options["messages"])
this._messages.shift(); 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) { this._autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
if (!events.feedingKeys && self._completions && options.get("autocomplete").values.length) { dactyl.trapErrors(function () {
self._completions.complete(true, false); if (!events.feedingKeys && self._completions && options.get("autocomplete").values.length) {
if (self._completions) self._completions.complete(true, false);
self._completions.itemList.show(); if (self._completions)
} self._completions.itemList.show();
}
});
}); });
// This timer just prevents <Tab>s from queueing up when the // This timer just prevents <Tab>s from queueing up when the
@@ -115,8 +93,10 @@ const CommandLine = Module("commandline", {
// the completion list scrolling). Multiple <Tab> presses are // the completion list scrolling). Multiple <Tab> presses are
// still processed normally, as the timer is flushed on "keyup". // still processed normally, as the timer is flushed on "keyup".
this._tabTimer = Timer(0, 0, function tabTell(event) { this._tabTimer = Timer(0, 0, function tabTell(event) {
if (self._completions) dactyl.trapErrors(function () {
self._completions.tab(event.shiftKey); if (self._completions)
self._completions.tab(event.shiftKey);
});
}); });
/////////////////////////////////////////////////////////////////////////////}}} /////////////////////////////////////////////////////////////////////////////}}}
@@ -213,7 +193,7 @@ const CommandLine = Module("commandline", {
* *
* @returns {boolean} * @returns {boolean}
*/ */
_commandShown: function () modes.main == modes.COMMAND_LINE && get commandVisible() modes.main == modes.COMMAND_LINE &&
!(modes.extended & (modes.INPUT_MULTILINE | modes.OUTPUT_MULTILINE)), !(modes.extended & (modes.INPUT_MULTILINE | modes.OUTPUT_MULTILINE)),
/** /**
@@ -255,7 +235,7 @@ const CommandLine = Module("commandline", {
dactyl.triggerObserver("echoLine", str, highlightGroup, forceSingle); dactyl.triggerObserver("echoLine", str, highlightGroup, forceSingle);
if (!this._commandShown()) if (!this.commandVisible)
commandline.hide(); commandline.hide();
let field = this.widgets.message.inputField; let field = this.widgets.message.inputField;
@@ -347,9 +327,9 @@ const CommandLine = Module("commandline", {
get quiet() this._quiet, get quiet() this._quiet,
set quiet(val) { set quiet(val) {
this._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" : ""; node.style.opacity = this._quiet || this._silent ? "0" : "";
}); }, this);
}, },
// @param type can be: // @param type can be:
@@ -510,8 +490,11 @@ const CommandLine = Module("commandline", {
highlightGroup = highlightGroup || this.HL_NORMAL; highlightGroup = highlightGroup || this.HL_NORMAL;
if (flags & this.APPEND_TO_MESSAGES) if (flags & this.APPEND_TO_MESSAGES) {
this._messageHistory.add({ str: str, highlight: highlightGroup }); let message = isobject(str) ? str : { message: str };
this._messageHistory.add(update({ highlight: highlightGroup }, str));
str = message.message;
}
if ((flags & this.ACTIVE_WINDOW) && if ((flags & this.ACTIVE_WINDOW) &&
window != services.get("windowWatcher").activeWindow && window != services.get("windowWatcher").activeWindow &&
services.get("windowWatcher").activeWindow.dactyl) services.get("windowWatcher").activeWindow.dactyl)
@@ -625,12 +608,12 @@ const CommandLine = Module("commandline", {
if (event.type == "blur") { if (event.type == "blur") {
// prevent losing focus, there should be a better way, but it just didn't work otherwise // prevent losing focus, there should be a better way, but it just didn't work otherwise
this.setTimeout(function () { 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(); this.widgets.command.inputField.focus();
}, 0); }, 0);
} }
else if (event.type == "focus") { 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(); event.target.blur();
dactyl.beep(); dactyl.beep();
} }
@@ -695,7 +678,7 @@ const CommandLine = Module("commandline", {
} }
} }
catch (e) { catch (e) {
dactyl.reportError(e); dactyl.reportError(e, true);
} }
}, },
@@ -908,7 +891,7 @@ const CommandLine = Module("commandline", {
// copy text to clipboard // copy text to clipboard
case "<C-y>": case "<C-y>":
util.copyToClipboard(win.getSelection()); dactyl.clipboardWrite(win.getSelection());
break; break;
// close the window // close the window
@@ -1032,7 +1015,7 @@ const CommandLine = Module("commandline", {
if (/^\s*$/.test(str)) if (/^\s*$/.test(str))
return; return;
this.store.mutate("filter", function (line) (line.value || line) != str); 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); this.store.truncate(options["history"], true);
}, },
/** /**
@@ -1042,23 +1025,9 @@ const CommandLine = Module("commandline", {
checkPrivate: function (str) { checkPrivate: function (str) {
// Not really the ideal place for this check. // Not really the ideal place for this check.
if (this.mode == "command") if (this.mode == "command")
return (commands.get(commands.parseCommand(str)[1]) || {}).privateData; return commands.hasPrivateData(str);
return false; 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. * Replace the current input field value.
* *
@@ -1443,19 +1412,19 @@ const CommandLine = Module("commandline", {
}); });
commands.add(["mes[sages]"], commands.add(["mes[sages]"],
"Display previously given messages", "Display previously shown messages",
function () { function () {
// TODO: are all messages single line? Some display an aggregation // TODO: are all messages single line? Some display an aggregation
// of single line messages at least. E.g. :source // of single line messages at least. E.g. :source
if (commandline._messageHistory.length == 1) { if (commandline._messageHistory.length == 1) {
let message = commandline._messageHistory.messages[0]; 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) { else if (commandline._messageHistory.length > 1) {
XML.ignoreWhitespace = false; XML.ignoreWhitespace = false;
let list = template.map(commandline._messageHistory.messages, function (message) commandline.commandOutput(
<div highlight={message.highlight + " Message"}>{message.str}</div>); template.map(commandline._messageHistory.messages, function (message)
dactyl.echo(list, commandline.FORCE_MULTILINE); <div highlight={message.highlight + " Message"}>{message.message}</div>));;
} }
}, },
{ argCount: "0" }); { argCount: "0" });
@@ -1471,7 +1440,8 @@ const CommandLine = Module("commandline", {
commandline.runSilently(function () dactyl.execute(args[0], null, true)); commandline.runSilently(function () dactyl.execute(args[0], null, true));
}, { }, {
completer: function (context) completion.ex(context), completer: function (context) completion.ex(context),
literal: 0 literal: 0,
subCommand: 0
}); });
}, },
mappings: function () { mappings: function () {
@@ -1532,6 +1502,34 @@ const CommandLine = Module("commandline", {
"Show the current mode in the command line", "Show the current mode in the command line",
"boolean", true); "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 () { styles: function () {
let fontSize = util.computedStyle(document.getElementById(config.mainWindowId)).fontSize; let fontSize = util.computedStyle(document.getElementById(config.mainWindowId)).fontSize;
styles.registerSheet("chrome://dactyl/skin/dactyl.css"); styles.registerSheet("chrome://dactyl/skin/dactyl.css");

View File

@@ -95,11 +95,13 @@ update(CommandOption, {
* bang - see {@link Command#bang} * bang - see {@link Command#bang}
* completer - see {@link Command#completer} * completer - see {@link Command#completer}
* count - see {@link Command#count} * count - see {@link Command#count}
* domains - see {@link Command#domains}
* heredoc - see {@link Command#heredoc} * heredoc - see {@link Command#heredoc}
* literal - see {@link Command#literal} * literal - see {@link Command#literal}
* options - see {@link Command#options} * options - see {@link Command#options}
* serial - see {@link Command#serial}
* privateData - see {@link Command#privateData} * privateData - see {@link Command#privateData}
* serialize - see {@link Command#serialize}
* subCommand - see {@link Command#subCommand}
* @optional * @optional
* @private * @private
*/ */
@@ -144,24 +146,22 @@ const Command = Class("Command", {
let self = this; let self = this;
function exec(command) { function exec(command) {
// FIXME: Move to parseCommand? // FIXME: Move to parseCommand?
args = self.parseArgs(command); args = this.parseArgs(command);
if (!args)
return;
args.count = count; args.count = count;
args.bang = bang; args.bang = bang;
dactyl.trapErrors(self.action, self, args, modifiers); this.action(args, modifiers);
} }
if (this.hereDoc) { if (this.hereDoc) {
let matches = args.match(/(.*)<<\s*(\S+)$/); let matches = args.match(/(.*)<<\s*(\S+)$/);
if (matches && matches[2]) { if (matches && matches[2]) {
commandline.inputMultiline(RegExp("^" + matches[2] + "$", "m"), commandline.inputMultiline(RegExp("^" + matches[2] + "$", "m"),
function (args) { exec(matches[1] + "\n" + args); }); function (args) { dactyl.trapErrors(exec, self, matches[1] + "\n" + args); });
return; return;
} }
} }
exec(args); dactyl.trapErrors(exec, this, args);
}, },
/** /**
@@ -231,11 +231,6 @@ const Command = Class("Command", {
completer: null, completer: null,
/** @property {boolean} Whether this command accepts a here document. */ /** @property {boolean} Whether this command accepts a here document. */
hereDoc: false, hereDoc: false,
/**
* @property {Array} The options this command takes.
* @see Commands@parseArguments
*/
options: [],
/** /**
* @property {boolean} Whether this command may be called with a bang, * @property {boolean} Whether this command may be called with a bang,
* e.g., :com! * e.g., :com!
@@ -246,6 +241,13 @@ const Command = Class("Command", {
* e.g., :12bdel * e.g., :12bdel
*/ */
count: false, 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 * @property {boolean} At what index this command's literal arguments
* begin. For instance, with a value of 2, all arguments starting with * 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. * key mappings or Ex command lines as arguments.
*/ */
literal: null, 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 <b>Object</b>s suitable * @property {function} Should return an array of <b>Object</b>s suitable
* to be passed to {@link Commands#commandToString}, one for each past * to be passed to {@link Commands#commandToString}, one for each past
@@ -262,12 +277,11 @@ const Command = Class("Command", {
*/ */
serialize: null, serialize: null,
/** /**
* @property {boolean} When true, invocations of this command * @property {number} If this command takes another ex command as an
* may contain private data which should be purged from * argument, the index of that argument. Used in determining whether to
* saved histories when clearing private data. * purge the command from history when clearing private data.
*/ */
privateData: false, subCommand: null,
/** /**
* @property {boolean} Specifies whether this is a user command. User * @property {boolean} Specifies whether this is a user command. User
* commands may be created by plugins, or directly by users, and, * 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); 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? // TODO: should it handle comments?
// : it might be nice to be able to specify that certain quoting // : it might be nice to be able to specify that certain quoting
// should be disabled E.g. backslash without having to resort to // should be disabled E.g. backslash without having to resort to
@@ -541,11 +609,11 @@ const Commands = Module("commands", {
args.completeArg = 0; args.completeArg = 0;
} }
function echoerr(error) { function fail(error) {
if (complete) if (complete)
complete.message = error; complete.message = error;
else else
dactyl.echoerr(error); dactyl.assert(false, error);
} }
outer: 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 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; invalid = true;
if (complete && !/[\s=]/.test(sep))
matchOpts(sub);
let context = null; let context = null;
if (!complete && quote) { if (!complete && quote)
dactyl.echoerr("Invalid argument for option " + optname); fail("Invalid argument for option " + optname);
return null;
}
if (!invalid) { if (!invalid) {
if (complete && count > 0) { if (complete && count > 0) {
@@ -602,33 +671,29 @@ const Commands = Module("commands", {
args.completeFilter = arg; args.completeFilter = arg;
args.quote = Commands.complQuote[quote] || Commands.complQuote[""]; args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
} }
let type = Commands.argTypes[opt.type]; if (!complete || arg != null) {
if (type && (!complete || arg != null)) { let type = Commands.argTypes[opt.type];
let orig = arg; if (type) {
arg = type.parse(arg); let orig = arg;
if (arg == null || (typeof arg == "number" && isNaN(arg))) { arg = type.parse(arg);
if (!complete || orig != "" || args.completeStart != str.length) if (arg == null || (typeof arg == "number" && isNaN(arg))) {
echoerr("Invalid argument for " + type.description + " option: " + optname); if (!complete || orig != "" || args.completeStart != str.length)
if (complete) fail("Invalid argument for " + type.description + " option: " + optname);
complete.highlight(args.completeStart, count - 1, "SPELLCHECK"); if (complete)
else complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
return null; }
}
// 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 // option allowed multiple times
if (opt.multiple) if (opt.multiple)
args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg); 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.quote = Commands.complQuote[quote] || Commands.complQuote[""];
args.completeFilter = arg || ""; args.completeFilter = arg || "";
} }
else if (count == -1) { else if (count == -1)
dactyl.echoerr("Error parsing arguments: " + arg); fail("Error parsing arguments: " + arg);
return null; else if (!onlyArgumentsRemaining && /^-/.test(arg))
} fail("Invalid option: " + arg);
else if (!onlyArgumentsRemaining && /^-/.test(arg)) {
dactyl.echoerr("Invalid option: " + arg);
return null;
}
if (arg != null) if (arg != null)
args.push(arg); args.push(arg);
@@ -700,7 +761,8 @@ const Commands = Module("commands", {
compl = opt.completer || []; compl = opt.completer || [];
context.title = [opt.names[0]]; context.title = [opt.names[0]];
context.quote = args.quote; context.quote = args.quote;
context.completions = compl; if (compl)
context.completions = compl;
} }
complete.advance(args.completeStart); complete.advance(args.completeStart);
complete.keys = { text: "names", description: "description" }; complete.keys = { text: "names", description: "description" };
@@ -712,16 +774,12 @@ const Commands = Module("commands", {
// check for correct number of arguments // check for correct number of arguments
if (args.length == 0 && /^[1+]$/.test(argCount) || if (args.length == 0 && /^[1+]$/.test(argCount) ||
literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || "")) { literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || "")) {
if (!complete) { if (!complete)
dactyl.echoerr("E471: Argument required"); fail("E471: Argument required");
return null;
}
} }
else if (args.length == 1 && (argCount == "0") || else if (args.length == 1 && (argCount == "0") ||
args.length > 1 && /^[01?]$/.test(argCount)) { args.length > 1 && /^[01?]$/.test(argCount))
echoerr("E488: Trailing characters"); fail("E488: Trailing characters");
return null;
}
return args; return args;
}, },
@@ -980,7 +1038,7 @@ const Commands = Module("commands", {
try { try {
var completer = dactyl.eval(completeOpt); 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"); throw new TypeError("User-defined custom completer " + completeOpt.quote() + " is not a function");
} }
catch (e) { catch (e) {

View File

@@ -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 <b>verbose</b> 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 <b>msg</b> is an object it is * Prints a message to the console. If <b>msg</b> is an object it is
* pretty printed. * pretty printed.
@@ -906,7 +964,7 @@ const Dactyl = Module("dactyl", {
return func.apply(self || this, Array.slice(arguments, 2)); return func.apply(self || this, Array.slice(arguments, 2));
} }
catch (e) { catch (e) {
dactyl.reportError(e); dactyl.reportError(e, true);
return undefined; return undefined;
} }
}, },
@@ -917,7 +975,7 @@ const Dactyl = Module("dactyl", {
* *
* @param {Object} error The error object. * @param {Object} error The error object.
*/ */
reportError: function (error) { reportError: function (error, echo) {
if (error instanceof FailedAssertion) { if (error instanceof FailedAssertion) {
if (error.message) if (error.message)
dactyl.echoerr(error.message); dactyl.echoerr(error.message);
@@ -925,6 +983,8 @@ const Dactyl = Module("dactyl", {
dactyl.beep(); dactyl.beep();
return; return;
} }
if (echo)
dactyl.echoerr(error);
if (Cu.reportError) if (Cu.reportError)
Cu.reportError(error); Cu.reportError(error);
@@ -1709,7 +1769,8 @@ const Dactyl = Module("dactyl", {
return completion.javascript(context); return completion.javascript(context);
}, },
count: true, count: true,
literal: 0 literal: 0,
subCommand: 0
}); });
commands.add(["verb[ose]"], commands.add(["verb[ose]"],
@@ -1732,7 +1793,8 @@ const Dactyl = Module("dactyl", {
argCount: "+", argCount: "+",
completer: function (context) completion.ex(context), completer: function (context) completion.ex(context),
count: true, count: true,
literal: 0 literal: 0,
subCommand: 0
}); });
commands.add(["ve[rsion]"], commands.add(["ve[rsion]"],

View File

@@ -112,8 +112,8 @@ const Editor = Module("editor", {
// FIXME: #93 (<s-insert> in the bottom of a long textarea bounces up) // FIXME: #93 (<s-insert> in the bottom of a long textarea bounces up)
let elem = dactyl.focus; let elem = dactyl.focus;
if (elem.setSelectionRange && util.readFromClipboard()) { if (elem.setSelectionRange && dactyl.clipboardRead()) {
// readFromClipboard would return 'undefined' if not checked // clipboardRead would return 'undefined' if not checked
// dunno about .setSelectionRange // dunno about .setSelectionRange
// This is a hacky fix - but it works. // This is a hacky fix - but it works.
let curTop = elem.scrollTop; let curTop = elem.scrollTop;
@@ -122,7 +122,7 @@ const Editor = Module("editor", {
let rangeStart = elem.selectionStart; // caret position let rangeStart = elem.selectionStart; // caret position
let rangeEnd = elem.selectionEnd; let rangeEnd = elem.selectionEnd;
let tempStr1 = elem.value.substring(0, rangeStart); let tempStr1 = elem.value.substring(0, rangeStart);
let tempStr2 = util.readFromClipboard(); let tempStr2 = dactyl.clipboardRead();
let tempStr3 = elem.value.substring(rangeEnd); let tempStr3 = elem.value.substring(rangeEnd);
elem.value = tempStr1 + tempStr2 + tempStr3; elem.value = tempStr1 + tempStr2 + tempStr3;
elem.selectionStart = rangeStart + tempStr2.length; elem.selectionStart = rangeStart + tempStr2.length;
@@ -971,7 +971,7 @@ const Editor = Module("editor", {
else { else {
let sel = window.content.document.getSelection(); let sel = window.content.document.getSelection();
dactyl.assert(sel); dactyl.assert(sel);
util.copyToClipboard(sel, true); dactyl.clipboardWrite(sel, true);
} }
}); });

View File

@@ -22,7 +22,10 @@ const Events = Module("events", {
this.sessionListeners = []; 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"] // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
// matters, so use that string as the first item, that you // 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() && if (file.exists() && !file.isDirectory() && file.isReadable() &&
/^[\w_-]+(\.vimp)?$/i.test(file.leafName)) { /^[\w_-]+(\.vimp)?$/i.test(file.leafName)) {
let name = file.leafName.replace(/\.vimp$/i, ""); 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); method.apply(self, arguments);
} }
catch (e) { catch (e) {
dactyl.reportError(e);
if (e.message == "Interrupted") if (e.message == "Interrupted")
dactyl.echoerr("Interrupted"); dactyl.echoerr("Interrupted");
else else
dactyl.echoerr("Processing " + event.type + " event: " + (e.echoerr || e)); 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) if (/[A-Z]/.test(macro)) { // uppercase (append)
this._currentMacro = macro.toLowerCase(); this._currentMacro = macro.toLowerCase();
if (!this._macros.get(this._currentMacro)) 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 { else {
this._currentMacro = macro; 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 buffer.loaded = 1; // even if not a full page load, assume it did load correctly before starting the macro
modes.isReplaying = true; 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; modes.isReplaying = false;
} }
else { else {
@@ -239,11 +245,8 @@ const Events = Module("events", {
* filter selects all macros. * filter selects all macros.
*/ */
getMacros: function (filter) { getMacros: function (filter) {
if (!filter) let re = RegExp(filter || "");
return this._macros; return ([k, m.keys] for ([k, m] in this._macros) if (re.test(macro)));
let re = RegExp(filter);
return ([macro, keys] for ([macro, keys] in this._macros) if (re.test(macro)));
}, },
/** /**
@@ -253,10 +256,9 @@ const Events = Module("events", {
* filter deletes all macros. * filter deletes all macros.
*/ */
deleteMacros: function (filter) { deleteMacros: function (filter) {
let re = RegExp(filter); let re = RegExp(filter || "");
for (let [item, ] in this._macros) { for (let [item, ] in this._macros) {
if (re.test(item) || !filter) if (!filter || re.test(item))
this._macros.remove(item); this._macros.remove(item);
} }
}, },
@@ -823,13 +825,16 @@ const Events = Module("events", {
if (modes.isRecording) { if (modes.isRecording) {
if (key == "q" && !modes.mainMode.input) { // TODO: should not be hardcoded if (key == "q" && !modes.mainMode.input) { // TODO: should not be hardcoded
modes.isRecording = false; 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 + "'"); dactyl.echomsg("Recorded macro '" + this._currentMacro + "'");
killEvent(); killEvent();
return; return;
} }
else if (!mappings.hasMap(dactyl.mode, this._input.buffer + key)) 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 == "<C-c>") if (key == "<C-c>")
@@ -1188,11 +1193,22 @@ const Events = Module("events", {
mappings.add([modes.NORMAL, modes.PLAYER, modes.MESSAGE], mappings.add([modes.NORMAL, modes.PLAYER, modes.MESSAGE],
["@"], "Play a macro", ["@"], "Play a macro",
function (count, arg) { function (count, arg) {
if (count < 1) count = 1; count = Math.max(count, 1);
while (count-- && events.playMacro(arg)) while (count-- && events.playMacro(arg))
; ;
}, },
{ arg: true, count: true }); { 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);
}
});
} }
}); });

View File

@@ -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)), 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", function (elem, loc) buffer.viewSource(loc, false), extended),
V: Mode("View hint source in external editor", function (elem, loc) buffer.viewSource(loc, true), 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 location", function (elem, loc) dactyl.clipboardWrite(loc, true)),
Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended), Y: Mode("Yank hint description", function (elem) dactyl.clipboardWrite(elem.textContent || "", true), extended),
c: Mode("Open context menu", function (elem) buffer.openContextMenu(elem), 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", 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) I: Mode("Show image in a new tab", function (elem) dactyl.open(elem.src, dactyl.NEW_TAB), images)

View File

@@ -146,7 +146,8 @@ const History = Module("history", {
context.keys = { text: function (item) (sh.index - item.index) + ": " + item.URI.spec, description: "title", icon: "icon" }; context.keys = { text: function (item) (sh.index - item.index) + ": " + item.URI.spec, description: "title", icon: "icon" };
}, },
count: true, count: true,
literal: 0 literal: 0,
privateData: true
}); });
commands.add(["fo[rward]", "fw"], 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" }; context.keys = { text: function (item) (item.index - sh.index) + ": " + item.URI.spec, description: "title", icon: "icon" };
}, },
count: true, count: true,
literal: 0 literal: 0,
privateData: true
}); });
commands.add(["hist[ory]", "hs"], commands.add(["hist[ory]", "hs"],
@@ -194,10 +196,25 @@ const History = Module("history", {
bang: true, bang: true,
completer: function (context) { context.quote = null; completion.history(context); }, completer: function (context) { context.quote = null; completion.history(context); },
// completer: function (filter) completion.history(filter) // 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: 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) { completion.history = function _history(context, maxItems) {
context.format = history.format; context.format = history.format;
context.title = ["History"]; context.title = ["History"];

View File

@@ -770,7 +770,7 @@ lookup:
}; };
completion.addUrlCompleter("f", "Local files", function (context, full) { completion.addUrlCompleter("f", "Local files", function (context, full) {
if (!/^\.?\//.test(context.filter)) if (/^(\.{0,2}|~)\/|^file:/.test(context.filter))
completion.file(context, full); completion.file(context, full);
}); });
}, },

View File

@@ -56,19 +56,6 @@ const JavaScript = Module("javascript", {
return []; return [];
let completions = [k for (k in this.iter(obj, toplevel))]; 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; return completions;
}, },
@@ -322,7 +309,7 @@ const JavaScript = Module("javascript", {
let orig = compl; let orig = compl;
if (!compl) { if (!compl) {
compl = function (context, obj, recurse) { 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: // Sort in a logical fashion for object keys:
// Numbers are sorted as numbers, rather than strings, and appear first. // Numbers are sorted as numbers, rather than strings, and appear first.
// Constants are unsorted, and appear before other non-null strings. // Constants are unsorted, and appear before other non-null strings.
@@ -330,10 +317,21 @@ const JavaScript = Module("javascript", {
let compare = context.compare; let compare = context.compare;
function isnan(item) item != '' && isNaN(item); function isnan(item) item != '' && isNaN(item);
context.compare = function (a, b) { context.compare = function (a, b) {
if (!isnan(a.item.key) && !isnan(b.item.key)) if (!isnan(a.key) && !isnan(b.key))
return a.item.key - b.item.key; return a.key - b.key;
return isnan(b.item.key) - isnan(a.item.key) || compare(a, b); 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. 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)); context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter));
if (obj == self.cache.evalContext) if (obj == self.cache.evalContext)

View File

@@ -178,7 +178,7 @@ const Mappings = Module("mappings", {
modes = modes.slice(); modes = modes.slice();
return (map for ([i, map] in Iterator(stack[modes.shift()].sort(function (m1, m2) String.localeCompare(m1.name, m2.name)))) 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( 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 // NOTE: just normal mode for now

View File

@@ -12,8 +12,9 @@
*/ */
const Marks = Module("marks", { const Marks = Module("marks", {
init: function init() { init: function init() {
this._localMarks = storage.newMap("local-marks", { store: true, privateData: true }); function replacer(key, val) val instanceof Node ? null : val;
this._urlMarks = storage.newMap("url-marks", { store: true, privateData: true }); 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 = []; this._pendingJumps = [];
}, },
@@ -65,7 +66,7 @@ const Marks = Module("marks", {
let position = { x: x, y: y }; let position = { x: x, y: y };
if (Marks.isURLMark(mark)) { 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) if (!silent)
dactyl.log("Adding URL mark: " + Marks.markToString(mark, this._urlMarks.get(mark)), 5); dactyl.log("Adding URL mark: " + Marks.markToString(mark, this._urlMarks.get(mark)), 5);
} }
@@ -74,7 +75,7 @@ const Marks = Module("marks", {
this._removeLocalMark(mark); this._removeLocalMark(mark);
if (!this._localMarks.get(mark)) if (!this._localMarks.get(mark))
this._localMarks.set(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); this._localMarks.get(mark).push(vals);
if (!silent) if (!silent)
dactyl.log("Adding local mark: " + Marks.markToString(mark, vals), 5); 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.keys.description = function ([, m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location;
context.completions = marks.all; 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);
}
});
} }
}); });

View File

@@ -20,11 +20,13 @@
* @param {string} defaultValue The default value for this option. * @param {string} defaultValue The default value for this option.
* @param {Object} extraInfo An optional extra configuration hash. The * @param {Object} extraInfo An optional extra configuration hash. The
* following properties are supported. * following properties are supported.
* scope - see {@link Option#scope} * completer - see {@link Option#completer}
* setter - see {@link Option#setter} * domains - see {@link Option#domains}
* getter - see {@link Option#getter} * getter - see {@link Option#getter}
* completer - see {@link Option#completer} * privateData - see {@link Option#privateData}
* valdator - see {@link Option#validator} * scope - see {@link Option#scope}
* setter - see {@link Option#setter}
* valdator - see {@link Option#validator}
* @optional * @optional
* @private * @private
*/ */
@@ -61,8 +63,8 @@ const Option = Class("Option", {
}, },
/** @property {value} The option's global value. @see #scope */ /** @property {value} The option's global value. @see #scope */
get globalValue() options.store.get(this.name), get globalValue() options.store.get(this.name, {}).value,
set globalValue(val) { options.store.set(this.name, val); }, set globalValue(val) { options.store.set(this.name, { value: val, time: Date.now() }); },
/** /**
* Returns <b>value</b> as an array of parsed values if the option type is * Returns <b>value</b> 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), 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. * Resets the option to its default value.
*/ */
@@ -210,7 +215,7 @@ const Option = Class("Option", {
* {@link #scope}). * {@link #scope}).
* @param {boolean} invert Whether this is an invert boolean operation. * @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); 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; return "Operator " + operator + " not supported for option type " + this.type;
if (!this.isValidValue(newValues)) if (!this.isValidValue(newValues))
return "E474: Invalid argument: " + values; return this.invalidArgument(str || this.joinValues(values), operator);
this.setValues(newValues, scope); this.setValues(newValues, scope);
return null; return null;
@@ -257,6 +262,27 @@ const Option = Class("Option", {
*/ */
description: "", 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 * @property {value} The option's default value. This value will be used
* unless the option is explicitly set either interactively or in an RC * unless the option is explicitly set either interactively or in an RC
@@ -264,19 +290,25 @@ const Option = Class("Option", {
*/ */
defaultValue: null, 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. * @property {function} The function called when the option value is read.
*/ */
getter: null, getter: null,
/** /**
* @property {function(CompletionContext, Args)} This option's completer. * @property {boolean|function(values)} When true, values of this
* @see CompletionContext * 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 * @property {function} The function called to validate the option's value
* when set. * when set.
@@ -294,6 +326,12 @@ const Option = Class("Option", {
*/ */
hasChanged: false, 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 * @property {nsIFile} The script in which this option was last set. null
* implies an interactive command. * implies an interactive command.
@@ -521,10 +559,9 @@ const Options = Module("options", {
}, },
/** @property {Iterator(Option)} @private */ /** @property {Iterator(Option)} @private */
__iterator__: function () { __iterator__: function ()
let sorted = [o for ([i, o] in Iterator(this._optionHash))].sort(function (a, b) String.localeCompare(a.name, b.name)); array(values(this._optionHash)).sort(function (a, b) String.localeCompare(a.name, b.name))
return (v for ([k, v] in Iterator(sorted))); .itervalues(),
},
/** @property {Object} Observes preference value changes. */ /** @property {Object} Observes preference value changes. */
prefObserver: { prefObserver: {
@@ -612,11 +649,9 @@ const Options = Module("options", {
if (name in this._optionHash) if (name in this._optionHash)
return (this._optionHash[name].scope & scope) && this._optionHash[name]; 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)) if (opt.hasName(name))
return (opt.scope & scope) && opt; return (opt.scope & scope) && opt;
}
return null; return null;
}, },
@@ -1067,7 +1102,7 @@ const Options = Module("options", {
opt.values = !opt.unsetBoolean; opt.values = !opt.unsetBoolean;
} }
try { 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) { catch (e) {
res = e; res = e;
@@ -1209,56 +1244,62 @@ const Options = Module("options", {
} }
); );
commands.add(["setl[ocal]"], [
"Set local option", {
function (args, modifiers) { names: ["setl[ocal]"],
modifiers.scope = Option.SCOPE_LOCAL; description: "Set local option",
setAction(args, modifiers); modifiers: { scope: Option.SCOPE_LOCAL }
}, },
{ {
bang: true, names: ["setg[lobal]"],
count: true, description: "Set global option",
completer: function (context, args) { modifiers: { scope: Option.SCOPE_GLOBAL }
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);
}, },
{ {
bang: true, names: ["se[t]"],
count: true, description: "Set an option",
completer: function (context, args) { modifiers: {},
return setCompleter(context, args, { scope: Option.SCOPE_GLOBAL }); extra: {
}, serialize: function () [
literal: 0 {
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))
]
}
} }
); ].forEach(function (params) {
commands.add(params.names, params.description,
commands.add(["se[t]"], function (args, modifiers) {
"Set an option", setAction(args, update(modifiers, params.modifiers));
function (args, modifiers) { setAction(args, modifiers); },
{
bang: true,
completer: function (context, args) {
return setCompleter(context, args);
}, },
serialize: function () [ update({
{ bang: true,
command: this.name, domains: function (args) array.flatten(args.map(function (spec) {
arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name try {
: opt.name + "=" + opt.value] let opt = options.parseOpt(spec);
} if (opt.option && opt.option.domains)
for (opt in options) return opt.option.domains(opt.values);
if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & Option.SCOPE_GLOBAL)) }
] 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]"], commands.add(["unl[et]"],
"Delete a variable", "Delete a variable",
@@ -1332,6 +1373,8 @@ const Options = Module("options", {
context.title = ["Option Value"]; context.title = ["Option Value"];
let completions = completer(context); let completions = completer(context);
if (!isarray(completions))
completions = array(completions).__proto__;
if (!completions) if (!completions)
return; 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.get, [function () ([o.name, o.description] for (o in options))]);
JavaScript.setCompleter([this.getPref, this.safeSetPref, this.setPref, this.resetPref, this.invertPref], JavaScript.setCompleter([this.getPref, this.safeSetPref, this.setPref, this.resetPref, this.invertPref],
[function () options.allPrefs().map(function (pref) [pref, ""])]); [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;
}
}
});
} }
}); });

View File

@@ -1,268 +0,0 @@
// Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
//
// 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:

View File

@@ -572,7 +572,8 @@ const Tabs = Module("tabs", {
bang: true, bang: true,
count: true, count: true,
completer: function (context) completion.buffer(context), completer: function (context) completion.buffer(context),
literal: 0 literal: 0,
privateData: true
}); });
commands.add(["keepa[lt]"], commands.add(["keepa[lt]"],
@@ -589,7 +590,8 @@ const Tabs = Module("tabs", {
}, { }, {
argCount: "+", argCount: "+",
completer: function (context) completion.ex(context), 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 // 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: "+", argCount: "+",
completer: function (context) completion.ex(context), completer: function (context) completion.ex(context),
literal: 0 literal: 0,
subCommand: 0
}); });
commands.add(["tabd[o]", "bufd[o]"], commands.add(["tabd[o]", "bufd[o]"],
@@ -615,7 +618,8 @@ const Tabs = Module("tabs", {
}, { }, {
argCount: "1", argCount: "1",
completer: function (context) completion.ex(context), completer: function (context) completion.ex(context),
literal: 0 literal: 0,
subCommand: 0
}); });
commands.add(["tabl[ast]", "bl[ast]"], commands.add(["tabl[ast]", "bl[ast]"],
@@ -705,7 +709,8 @@ const Tabs = Module("tabs", {
bang: true, bang: true,
count: true, count: true,
completer: function (context) completion.buffer(context), completer: function (context) completion.buffer(context),
literal: 0 literal: 0,
privateData: true
}); });
commands.add(["buffers", "files", "ls", "tabs"], commands.add(["buffers", "files", "ls", "tabs"],
@@ -763,6 +768,7 @@ const Tabs = Module("tabs", {
}, { }, {
bang: true, bang: true,
completer: function (context) completion.url(context), completer: function (context) completion.url(context),
domains: function (args) commands.get("open").domains(args),
literal: 0, literal: 0,
privateData: true privateData: true
}); });
@@ -863,7 +869,8 @@ const Tabs = Module("tabs", {
context.completions = Iterator(tabs.closedTabs); context.completions = Iterator(tabs.closedTabs);
}, },
count: true, count: true,
literal: 0 literal: 0,
privateData: true
}); });
commands.add(["undoa[ll]"], commands.add(["undoa[ll]"],

View File

@@ -1023,29 +1023,12 @@
<tags>'si' 'sanitizeitems'</tags> <tags>'si' 'sanitizeitems'</tags>
<spec>'sanitizeitems' 'si'</spec> <spec>'sanitizeitems' 'si'</spec>
<type>stringlist</type> <type>stringlist</type>
<default>cache,commandline,cookies,formdata,history,marks,sessions</default> <default>all</default>
<description> <description>
<p>The default list of private items to sanitize.</p>
<dl>
<dt>cache </dt> <dd>Cache</dd>
<dt>commandline </dt> <dd>Command-line history</dd>
<dt>cookies </dt> <dd>Cookies</dd>
<dt>downloads </dt> <dd>Download history</dd>
<dt>formdata </dt> <dd>Saved form and search history</dd>
<dt>history </dt> <dd>Browsing history</dd>
<dt>marks </dt> <dd>Local and URL marks</dd>
<dt>macros </dt> <dd>Saved macros</dd>
<dt>offlineapps </dt> <dd>Offline website data</dd>
<dt>passwords </dt> <dd>Saved passwords</dd>
<dt>sessions </dt> <dd>Authenticated sessions</dd>
<dt>sitesettings</dt> <dd>Site preferences</dd>
</dl>
<p> <p>
When history items are sanitized <ex>:open</ex>, The default list of private items to sanitize. See
<ex>:tabopen</ex> and <ex>:winopen</ex> command-line <ex>:sanitize</ex> for a list and explanation of
history entries are also removed. possible values.
</p> </p>
</description> </description>
</item> </item>
@@ -1055,21 +1038,20 @@
<tags>'sts' 'sanitizetimespan'</tags> <tags>'sts' 'sanitizetimespan'</tags>
<spec>'sanitizetimespan' 'sts'</spec> <spec>'sanitizetimespan' 'sts'</spec>
<type>number</type> <type>number</type>
<default>1</default> <default>all</default>
<description> <description>
<p> <p>
The default sanitizer time span. Only items created within this timespan are The default sanitizer time span. Only items created within this timespan are
deleted. deleted. The value must be of the one of the forms,
</p> </p>
<note>This only applies to <em>cookies</em>, <em>history</em>, <em>formdata</em>, and <em>downloads</em>.</note>
<dl> <dl>
<dt>0</dt> <dd>Everything</dd> <dt>all</dt> <dd>Everything</dd>
<dt>1</dt> <dd>Last hour</dd> <dt>session</dt> <dd>The current session</dd>
<dt>2</dt> <dd>Last two hours</dd> <dt><a>n</a>m</dt> <dd>Past <a>n</a> Minutes</dd>
<dt>3</dt> <dd>Last four hours</dd> <dt><a>n</a>h</dt> <dd>Past <a>n</a> Hours</dd>
<dt>4</dt> <dd>Today</dd> <dt><a>n</a>d</dt> <dd>Past <a>n</a> Days</dd>
<dt><a>n</a>w</dt> <dd>Past <a>n</a> Weeks</dd>
</dl> </dl>
</description> </description>
</item> </item>

View File

@@ -94,29 +94,6 @@
</item> </item>
<item>
<tags>:sa :sanitize</tags>
<spec>:sa<oa>nitize</oa> [-timespan=<a>timespan</a>] <a>item</a></spec>
<spec>:sa<oa>nitize</oa>! [-timespan=<a>timespan</a>]</spec>
<description>
<p>
Clear private data items. Where <a>item</a> … is a list of private items to
delete. These may be any of the items valid for <o>sanitizeitems</o>.
</p>
<p>
If <oa>!</oa> is specified then <o>sanitizeitems</o> is used for the list of items to
delete.
</p>
<p>
If <a>timespan</a> is specified then only items within that timespan are deleted,
otherwise the value of <o>sanitizetimespan</o> is used.
</p>
</description>
</item>
<item> <item>
<tags>:sil :silent</tags> <tags>:sil :silent</tags>
<spec>:sil<oa>ent</oa> <a>command</a></spec> <spec>:sil<oa>ent</oa> <a>command</a></spec>
@@ -153,6 +130,109 @@
</description> </description>
</item> </item>
<h2 tag="privacy">Privacy and sensitive information</h2>
<p>
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.
</p>
<p tag="private-mode porn-mode">
<strut/>
&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 <o>private</o>.
</p>
<p tag="sanitizing clearing-data">
<strut/>
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.
</p>
<item>
<tags>:sa :sanitize</tags>
<spec>:sa<oa>nitize</oa> <oa>-host=<a>host</a></oa> <oa>-older</oa> <oa>-timespan=<a>timespan</a></oa> <a>item</a></spec>
<spec>:sa<oa>nitize</oa>! <oa>-host=<a>host</a></oa> <oa>-older</oa> <oa>-timespan=<a>timespan</a></oa></spec>
<description>
<p>
Clear private data items for <a>timespan</a>, where <a>item</a>
is a list of private items to delete. If <oa>!</oa> is specified
then <o>sanitizeitems</o> is used for the list of items to delete.
Items may be any of:
</p>
<dl>
<dt>all </dt> <dd>All items</dd>
<dt>cache </dt> <dd>Cache</dd>
<dt>commandline </dt> <dd>Command-line history</dd>
<dt>cookies </dt> <dd>Cookies</dd>
<dt>downloads </dt> <dd>Download history</dd>
<dt>formdata </dt> <dd>Saved form and search history</dd>
<dt>history </dt> <dd>Browsing history</dd>
<dt>marks </dt> <dd>Local and URL marks</dd>
<dt>macros </dt> <dd>Saved macros</dd>
<dt>messages </dt> <dd>Saved <ex>:messages</ex></dd>
<dt>offlineapps </dt> <dd>Offline website data</dd>
<dt>options </dt> <dd>Options containing hostname data</dd>
<dt>passwords </dt> <dd>Saved passwords</dd>
<dt>sessions </dt> <dd>Authenticated sessions</dd>
<dt>sitesettings</dt> <dd>Site preferences</dd>
</dl>
<p>
When <em>history</em> items are sanitized, all command-line
history items containing URLs or page titles (other than bookmark
commands) are additionally cleared. Invocations of the
<em>:sanitize</em> command are included in this set.
</p>
<p>
If <a>timespan</a> (short name <em>-t</em>) is specified then only
items within that timespan are deleted, otherwise the value of
<o>sanitizetimespan</o> is used. If <oa>-older</oa> (short name
<em>-o</em>) is specified, than only items older than
<a>timespan</a> are deleted.
</p>
<note>
The following items are cleared regardless of <a>timeframe</a>:
<em>cache</em>, <em>offlineapps</em>, <em>passwords</em>,
<em>sessions</em>, <em>sitesettings</em>. Additionally,
<em>options</em> are never cleared.
</note>
<p>
If <a>host</a> (short name <em>-h</em>) is specified, only items
containing a reference to that domain or a subdomain thereof are
cleared. Moreover, if <em>commandline</em> or <em>history</em> is
specified, the invocation of the <em>:sanitize</em> command is
naturally cleared as well.
</p>
<note>
This only applies to <em>commandline</em>, <em>cookies</em>,
<em>history</em>, <em>marks</em>, <em>messages</em>,
<em>options</em>, and <em>sitesettings</em>. All other
domain-specific data is cleared only along with <em>history</em>,
when a request is made to &dactyl.host; to purge all data for
<em>host</em>. Included in this purge are all matchint history
entries, cookies, closed tabs, form data, and location bar
entries.
</note>
</description>
</item>
<h2 tag="online-help">Online help</h2> <h2 tag="online-help">Online help</h2>

View File

@@ -66,11 +66,12 @@ defmodule("base", this, {
// sed -n 's/^(const|function) ([a-zA-Z0-9_]+).*/ "\2",/p' base.jsm | sort | fmt // sed -n 's/^(const|function) ([a-zA-Z0-9_]+).*/ "\2",/p' base.jsm | sort | fmt
exports: [ exports: [
"Cc", "Ci", "Class", "Cr", "Cu", "Module", "Object", "Runnable", "Cc", "Ci", "Class", "Cr", "Cu", "Module", "Object", "Runnable",
"Struct", "StructBase", "Timer", "allkeys", "array", "call", "Struct", "StructBase", "Timer", "XPCOMUtils", "allkeys", "array",
"callable", "curry", "debuggerProperties", "defmodule", "dict", "call", "callable", "curry", "debuggerProperties", "defmodule", "dict",
"endmodule", "extend", "foreach", "isarray", "isgenerator", "endmodule", "extend", "foreach", "isarray", "isgenerator",
"isinstance", "isobject", "isstring", "issubclass", "iter", "memoize", "isinstance", "isobject", "isstring", "issubclass", "iter", "keys",
"properties", "requiresMainThread", "set", "update", "values", "memoize", "properties", "requiresMainThread", "set", "update",
"values",
], ],
use: ["services"] use: ["services"]
}); });
@@ -117,8 +118,9 @@ function debuggerProperties(obj) {
} }
} }
let hasOwnProperty = Object.prototype.hasOwnProperty;
if (!Object.keys) 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) if (!Object.getOwnPropertyNames)
Object.getOwnPropertyNames = function getOwnPropertyNames(obj) { 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) { function values(obj) {
for (var k in obj) for (var k in obj)
if (obj.hasOwnProperty(k)) if (hasOwnProperty.call(obj, k))
yield obj[k]; yield obj[k];
} }
function foreach(iter, fn, self) { function foreach(iter, fn, self) {
@@ -175,7 +182,7 @@ set.add = function (set, key) {
set[key] = true; set[key] = true;
return res; 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]; } set.remove = function (set, key) { delete set[key]; }
function iter(obj) { function iter(obj) {
@@ -204,6 +211,12 @@ function iter(obj) {
for (let i = 0; i < obj.length; i++) for (let i = 0; i < obj.length; i++)
yield [obj.name, obj]; yield [obj.name, obj];
})(); })();
if (obj instanceof Ci.mozIStorageStatement)
return (function (obj) {
while (obj.executeStep())
yield obj.row;
obj.reset();
})(obj);
return Iterator(obj); return Iterator(obj);
} }
@@ -542,15 +555,31 @@ Class.prototype = {
* @param {Object} classProperties Properties to be applied to the class constructor. * @param {Object} classProperties Properties to be applied to the class constructor.
* @return {Class} * @return {Class}
*/ */
function Module(name, prototype, classProperties, init) { function Module(name, prototype) {
const module = Class(name, prototype, classProperties); let init = callable(prototype) ? 4 : 3;
const module = Class.apply(Class, Array.slice(arguments, 0, init));
let instance = module(); let instance = module();
module.name = name.toLowerCase(); module.name = name.toLowerCase();
instance.INIT = init || {}; instance.INIT = arguments[init] || {};
currentModule[module.name] = instance; currentModule[module.name] = instance;
defmodule.modules.push(instance); defmodule.modules.push(instance);
return module; 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 * @class Struct
@@ -677,7 +706,7 @@ const Timer = Class("Timer", {
*/ */
const array = Class("util.Array", Array, { const array = Class("util.Array", Array, {
init: function (ary) { init: function (ary) {
if (isgenerator(ary)) if (isinstance(ary, ["Iterator", "Generator"]))
ary = [k for (k in ary)]; ary = [k for (k in ary)];
else if (ary.length) else if (ary.length)
ary = Array.slice(ary); ary = Array.slice(ary);
@@ -688,12 +717,13 @@ const array = Class("util.Array", Array, {
__noSuchMethod__: function (meth, args) { __noSuchMethod__: function (meth, args) {
var res = array[meth].apply(null, [this.__proto__].concat(args)); var res = array[meth].apply(null, [this.__proto__].concat(args));
if (array.isinstance(res)) if (isarray(res))
return array(res); return array(res);
return res; return res;
}, },
toString: function () this.__proto__.toString(), toString: function () this.__proto__.toString(),
concat: function () this.__proto__.concat.apply(this.__proto__, arguments), 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)) map: function () this.__noSuchMethod__("map", Array.slice(arguments))
}; };
} }

View File

@@ -52,7 +52,6 @@ const Highlights = Module("Highlight", {
return "Unknown highlight keyword: " + class_; return "Unknown highlight keyword: " + class_;
let style = this.highlight[key] || Highlight(key); let style = this.highlight[key] || Highlight(key);
styles.removeSheet(true, style.selector);
if (append) if (append)
newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle); newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle);
@@ -60,22 +59,23 @@ const Highlights = Module("Highlight", {
newStyle = null; newStyle = null;
if (newStyle == null) { if (newStyle == null) {
if (style.default == null) { if (style.default == null) {
delete this.highlight[style.class];
styles.removeSheet(true, style.selector); styles.removeSheet(true, style.selector);
delete this.highlight[style.class];
return null; return null;
} }
newStyle = style.default; newStyle = style.default;
force = true;
} }
let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;") if (!style.loaded || style.value != newStyle) {
.replace(";!important;", ";", "g"); // Seeming Spidermonkey bug styles.removeSheet(true, style.selector);
if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) { let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;")
css = style.selector + " { " + css + " }"; .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); styles.addSheet(true, "highlight:" + style.class, style.filter, css, true);
if (error) style.loaded = true;
return error; }
} }
style.value = newStyle; style.value = newStyle;
this.highlight[style.class] = style; this.highlight[style.class] = style;
@@ -120,9 +120,10 @@ const Highlights = Module("Highlight", {
style.selector = this.selector(style.class) + style.selector; style.selector = this.selector(style.class) + style.selector;
let old = this.highlight[style.class]; let old = this.highlight[style.class];
this.highlight[style.class] = style; if (!old)
if (old && old.value != old.default) this.highlight[style.class] = style;
style.value = old.value; else if (old.value == old.default)
old.value = style.value;
}, this); }, this);
for (let [class_, hl] in Iterator(this.highlight)) for (let [class_, hl] in Iterator(this.highlight))
if (hl.value == hl.default) if (hl.value == hl.default)

View File

@@ -0,0 +1,330 @@
// Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
//
// 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:

View File

@@ -27,12 +27,14 @@ const Services = Module("Services", {
this.add("environment", "@mozilla.org/process/environment;1", Ci.nsIEnvironment); this.add("environment", "@mozilla.org/process/environment;1", Ci.nsIEnvironment);
this.add("extensionManager", "@mozilla.org/extensions/manager;1", Ci.nsIExtensionManager); this.add("extensionManager", "@mozilla.org/extensions/manager;1", Ci.nsIExtensionManager);
this.add("favicon", "@mozilla.org/browser/favicon-service;1", Ci.nsIFaviconService); 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("io", "@mozilla.org/network/io-service;1", Ci.nsIIOService);
this.add("json", "@mozilla.org/dom/json;1", Ci.nsIJSON, "createInstance"); this.add("json", "@mozilla.org/dom/json;1", Ci.nsIJSON, "createInstance");
this.add("livemark", "@mozilla.org/browser/livemark-service;2", Ci.nsILivemarkService); this.add("livemark", "@mozilla.org/browser/livemark-service;2", Ci.nsILivemarkService);
this.add("observer", "@mozilla.org/observer-service;1", Ci.nsIObserverService); 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("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("profile", "@mozilla.org/toolkit/profile-service;1", Ci.nsIToolkitProfileService);
this.add("runtime", "@mozilla.org/xre/runtime;1", [Ci.nsIXULAppInfo, Ci.nsIXULRuntime]); this.add("runtime", "@mozilla.org/xre/runtime;1", [Ci.nsIXULAppInfo, Ci.nsIXULRuntime]);
this.add("rdf", "@mozilla.org/rdf/rdf-service;1", Ci.nsIRDFService); 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("subscriptLoader", "@mozilla.org/moz/jssubscript-loader;1", Ci.mozIJSSubScriptLoader);
this.add("tagging", "@mozilla.org/browser/tagging-service;1", Ci.nsITaggingService); this.add("tagging", "@mozilla.org/browser/tagging-service;1", Ci.nsITaggingService);
this.add("threadManager", "@mozilla.org/thread-manager;1", Ci.nsIThreadManager); 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("windowMediator", "@mozilla.org/appshell/window-mediator;1", Ci.nsIWindowMediator);
this.add("windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", Ci.nsIWindowWatcher); 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/file/local;1", Ci.nsILocalFile);
this.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler); this.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler);
@@ -128,6 +130,6 @@ const Services = Module("Services", {
endmodule(); 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: // vim: set fdm=marker sw=4 sts=4 et ft=javascript:

View File

@@ -21,6 +21,7 @@
}}} ***** END LICENSE BLOCK *****/ }}} ***** END LICENSE BLOCK *****/
"use strict"; "use strict";
const myObject = Object;
Components.utils.import("resource://dactyl/base.jsm"); Components.utils.import("resource://dactyl/base.jsm");
defmodule("storage", this, { defmodule("storage", this, {
exports: ["File", "storage"], exports: ["File", "storage"],
@@ -76,9 +77,9 @@ function savePref(obj) {
} }
const StoreBase = Class("StoreBase", { const StoreBase = Class("StoreBase", {
OPTIONS: ["privateData"], OPTIONS: ["privateData", "replacer"],
fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); }, 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); }, save: function () { savePref(this); },
init: function (name, store, load, options) { init: function (name, store, load, options) {
this._load = load; this._load = load;
@@ -97,7 +98,7 @@ const StoreBase = Class("StoreBase", {
}); });
const ObjectStore = Class("ObjectStore", StoreBase, { const ObjectStore = Class("ObjectStore", StoreBase, {
_constructor: Object, _constructor: myObject,
set: function set(key, val) { set: function set(key, val) {
var defined = key in this._object; var defined = key in this._object;
@@ -120,6 +121,7 @@ const ObjectStore = Class("ObjectStore", StoreBase, {
clear: function () { clear: function () {
this._object = {}; this._object = {};
this.fireEvent("clear", key);
}, },
__iterator__: function () Iterator(this._object), __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 keys) || params.reload || this.alwaysReload[key]) {
if (key in this && !(params.reload || this.alwaysReload[key])) if (key in this && !(params.reload || this.alwaysReload[key]))
throw Error(); 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); keys[key] = new constructor(key, params.store, load, params);
timers[key] = new Timer(1000, 10000, function () storage.save(key)); timers[key] = new Timer(1000, 10000, function () storage.save(key));
this.__defineGetter__(key, function () keys[key]); this.__defineGetter__(key, function () keys[key]);
@@ -234,14 +236,13 @@ const Storage = Module("Storage", {
get observers() observers, get observers() observers,
fireEvent: function fireEvent(key, event, arg) { fireEvent: function fireEvent(key, event, arg) {
if (!(key in this))
return;
this.removeDeadObservers(); this.removeDeadObservers();
// Safe, since we have our own Array object here. // Safe, since we have our own Array object here.
if (key in observers) if (key in observers)
for each (let observer in observers[key]) for each (let observer in observers[key])
observer.callback.get()(key, event, arg); observer.callback.get()(key, event, arg);
timers[key].tell(); if (timers[key])
timers[key].tell();
}, },
load: function load(key) { load: function load(key) {
@@ -261,6 +262,8 @@ const Storage = Module("Storage", {
_privateMode: false, _privateMode: false,
get privateMode() this._privateMode, get privateMode() this._privateMode,
set privateMode(val) { set privateMode(val) {
if (val && !this._privateMode)
this.saveAll();
if (!val && this._privateMode) if (!val && this._privateMode)
for (let key in keys) for (let key in keys)
this.load(key); this.load(key);

View File

@@ -31,6 +31,13 @@ Sheet.prototype.__defineGetter__("fullCSS", function wrapCSS() {
.join(", "); .join(", ");
return "/* Dactyl style #" + this.id + " */ " + namespace + " @-moz-document " + selectors + "{\n" + css + "\n}\n"; 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.__defineGetter__("enabled", function () this._enabled);
Sheet.prototype.__defineSetter__("enabled", function (on) { Sheet.prototype.__defineSetter__("enabled", function (on) {
this._enabled = Boolean(on); this._enabled = Boolean(on);

View File

@@ -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) { callInMainThread: function (callback, self) {
let mainThread = services.get("threadManager").mainThread; let mainThread = services.get("threadManager").mainThread;
if (services.get("threadManager").isMainThread) if (services.get("threadManager").isMainThread)
@@ -123,30 +158,19 @@ const Util = Module("Util", {
}, },
/** /**
* Copies a string to the system clipboard. If <b>verbose</b> is specified * Converts any arbitrary string into an URI object. Returns null on
* the copied string is also echoed to the command line. * failure.
* *
* @param {string} str * @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) { createURI: function createURI(str) {
const fixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); try {
return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP); 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]; 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 <b>url</b> and * Sends a synchronous or asynchronous HTTP request to <b>url</b> and
* returns the XMLHttpRequest object. If <b>callback</b> is specified the * returns the XMLHttpRequest object. If <b>callback</b> is specified the
@@ -390,6 +428,29 @@ const Util = Module("Util", {
bottom: Math.min(r1.bottom, r2.bottom) 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 * Returns an XPath union expression constructed from the specified node
* tests. An expression is built with node tests for both the null and * 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 * Scrolls an element into view if and only if it's not already
* fully visible. * fully visible.

View File

@@ -215,7 +215,8 @@ const Config = Module("config", ConfigBase, {
{ {
argCount: "+", argCount: "+",
completer: function (context) completion.ex(context), completer: function (context) completion.ex(context),
literal: 0 literal: 0,
subCommand: 0
}); });
commands.add(["winc[lose]", "wc[lose]"], commands.add(["winc[lose]", "wc[lose]"],
@@ -235,6 +236,7 @@ const Config = Module("config", ConfigBase, {
}, },
{ {
completer: function (context) completion.url(context), completer: function (context) completion.url(context),
domains: function (args) commands.get("open").domains(args),
literal: 0, literal: 0,
privateData: true privateData: true
}); });