1 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2009-2011 by Kris Maglione <maglione.k at Gmail>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
9 // - fix Sanitize autocommand
10 // - add warning for TIMESPAN_EVERYTHING?
13 // - finish 1.9.0 support if we're going to support sanitizing in Melodactyl
15 Components.utils.import("resource://dactyl/bootstrap.jsm");
16 defineModule("sanitizer", {
17 exports: ["Range", "Sanitizer", "sanitizer"],
18 require: ["config", "prefs", "services", "util"]
21 this.lazyRequire("messages", ["_"]);
22 this.lazyRequire("overlay", ["overlay"]);
23 this.lazyRequire("storage", ["storage"]);
24 this.lazyRequire("template", ["teplate"]);
26 let tmp = Object.create(this);
27 JSMLoader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
28 tmp.Sanitizer.prototype.__proto__ = Class.prototype;
30 var Range = Struct("min", "max");
31 update(Range.prototype, {
32 contains: function (date) date == null ||
33 (this.min == null || date >= this.min) && (this.max == null || date <= this.max),
35 get isEternity() this.max == null && this.min == null,
36 get isSession() this.max == null && this.min == sanitizer.sessionStart,
38 get native() this.isEternity ? null : [this.min || 0, this.max == null ? Number.MAX_VALUE : this.max]
41 var Item = Class("SanitizeItem", {
42 init: function (name, params) {
44 this.description = params.description;
47 // Hack for completion:
48 "0": Class.Property({ get: function () this.name }),
49 "1": Class.Property({ get: function () this.description }),
51 description: Messages.Localized(""),
53 get cpdPref() (this.builtin ? "" : Item.PREFIX) + Item.BRANCH + Sanitizer.argToPref(this.name),
54 get shutdownPref() (this.builtin ? "" : Item.PREFIX) + Item.SHUTDOWN_BRANCH + Sanitizer.argToPref(this.name),
55 get cpd() prefs.get(this.cpdPref),
56 get shutdown() prefs.get(this.shutdownPref),
58 shouldSanitize: function (shutdown) (!shutdown || this.builtin || this.persistent) &&
59 prefs.get(shutdown ? this.shutdownPref : this.pref)
61 PREFIX: config.prefs.branch.root,
62 BRANCH: "privacy.cpd.",
63 SHUTDOWN_BRANCH: "privacy.clearOnShutdown."
66 var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference], tmp.Sanitizer), {
67 sessionStart: Date.now() * 1000,
72 util.addObserver(this);
74 services.add("contentPrefs", "@mozilla.org/content-pref/service;1", Ci.nsIContentPrefService);
75 services.add("cookies", "@mozilla.org/cookiemanager;1", [Ci.nsICookieManager, Ci.nsICookieManager2,
76 Ci.nsICookieService]);
77 services.add("loginManager", "@mozilla.org/login-manager;1", Ci.nsILoginManager);
78 services.add("permissions", "@mozilla.org/permissionmanager;1", Ci.nsIPermissionManager);
82 this.addItem("all", { description: "Sanitize all items", shouldSanitize: function () false });
84 this.addItem("cache", { builtin: true, description: "Cache" });
85 this.addItem("downloads", { builtin: true, description: "Download history" });
86 this.addItem("formdata", { builtin: true, description: "Saved form and search history" });
87 this.addItem("offlineapps", { builtin: true, description: "Offline website data" });
88 this.addItem("passwords", { builtin: true, description: "Saved passwords" });
89 this.addItem("sessions", { builtin: true, description: "Authenticated sessions" });
91 // These builtin methods don't support hosts or otherwise have
92 // insufficient granularity
93 this.addItem("cookies", {
95 description: "Cookies",
97 action: function (range, host) {
98 for (let c in Sanitizer.iterCookies(host))
99 if (range.contains(c.creationTime) || timespan.isSession && c.isSession)
100 services.cookies.remove(c.host, c.name, c.path, false);
104 this.addItem("history", {
106 description: "Browsing history",
108 sessionHistory: true,
109 action: function (range, host) {
111 services.history.removePagesFromHost(host, true);
113 if (range.isEternity)
114 services.history.removeAllPages();
116 services.history.removeVisitsByTimeframe(range.native[0], Math.min(Date.now() * 1000, range.native[1])); // XXX
117 services.observer.notifyObservers(null, "browser:purge-session-history", "");
120 if (!host || util.isDomainURL(prefs.get("general.open_location.last_url"), host))
121 prefs.reset("general.open_location.last_url");
125 if (services.has("privateBrowsing"))
126 this.addItem("host", {
127 description: "All data from the given host",
128 action: function (range, host) {
130 services.privateBrowsing.removeDataFromDomain(host);
133 this.addItem("sitesettings", {
135 description: "Site preferences",
137 action: function (range, host) {
141 for (let p in Sanitizer.iterPermissions(host)) {
142 services.permissions.remove(util.createURI(p.host), p.type);
143 services.permissions.add(util.createURI(p.host), p.type, 0);
145 for (let p in iter(services.contentPrefs.getPrefs(util.createURI(host))))
146 services.contentPrefs.removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name);
149 // "Allow this site to open popups" ...
150 services.permissions.removeAll();
152 services.contentPrefs.removeGroupedPrefs();
155 // "Never remember passwords" ...
156 for each (let domain in services.loginManager.getAllDisabledHosts())
157 if (!host || util.isSubdomain(domain, host))
158 services.loginManager.setLoginSavingEnabled(host, true);
163 function ourItems(persistent) [
164 item for (item in values(self.itemMap))
165 if (!item.builtin && (!persistent || item.persistent) && item.name !== "all")
168 function prefOverlay(branch, persistent, local) update(Object.create(local), {
169 before: array.toObject([
170 [branch.substr(Item.PREFIX.length) + "history",
171 <preferences xmlns={XUL}>{
172 template.map(ourItems(persistent), function (item)
173 <preference type="bool" id={branch + item.name} name={branch + item.name}/>)
174 }</preferences>.*::*]
176 init: function init(win) {
177 let pane = win.document.getElementById("SanitizeDialogPane");
178 for (let [, pref] in iter(pane.preferences))
179 pref.updateElements();
180 init.superapply(this, arguments);
184 util.timeout(function () { // Load order issue...
186 let (branch = Item.PREFIX + Item.SHUTDOWN_BRANCH) {
187 overlay.overlayWindow("chrome://browser/content/preferences/sanitize.xul",
188 function (win) prefOverlay(branch, true, {
191 <groupbox orient="horizontal" xmlns={XUL}>
192 <caption label={config.appName + /*L*/" (see :help privacy)"}/>
194 <columns><column flex="1"/><column flex="1"/></columns>
196 let (items = ourItems(true))
197 template.map(util.range(0, Math.ceil(items.length / 2)), function (i)
199 template.map(items.slice(i * 2, i * 2 + 2), function (item)
200 <checkbox xmlns={XUL} label={item.description} preference={branch + item.name}/>)
208 let (branch = Item.PREFIX + Item.BRANCH) {
209 overlay.overlayWindow("chrome://browser/content/sanitize.xul",
210 function (win) prefOverlay(branch, false, {
213 <listitem xmlns={XUL} label={/*L*/"See :help privacy for the following:"} disabled="true" style="font-style: italic; font-weight: bold;"/>
215 template.map(ourItems(), function ([item, desc])
216 <listitem xmlns={XUL} type="checkbox"
217 label={config.appName + " " + desc}
218 preference={branch + item}
219 onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>)
223 ready: function ready(win) {
224 let elem = win.document.getElementById("itemList");
225 elem.setAttribute("rows", elem.itemCount);
226 win.Sanitizer = Class("Sanitizer", win.Sanitizer, {
227 sanitize: function sanitize() {
228 self.withSavedValues(["sanitizing"], function () {
229 self.sanitizing = true;
230 sanitize.superapply(this, arguments);
231 sanitizer.sanitizeItems([item.name for (item in values(self.itemMap))
232 if (item.shouldSanitize(false))],
233 Range.fromArray(this.range || []));
245 addItem: function addItem(name, params) {
246 let item = this.itemMap[name] || Item(name, params);
247 this.itemMap[name] = item;
249 for (let [k, prop] in iterOwnProperties(params))
250 if (!("value" in prop) || !callable(prop.value) && !(k in item))
251 Object.defineProperty(item, k, prop);
253 let names = Set([name].concat(params.contains || []).map(function (e) "clear-" + e));
255 storage.addObserver("sanitizer",
256 function (key, event, arg) {
258 params.action.apply(params, arg);
260 Class.objectGlobal(params.action));
262 if (params.privateEnter || params.privateLeave)
263 storage.addObserver("private-mode",
264 function (key, event, arg) {
265 let meth = params[arg ? "privateEnter" : "privateLeave"];
269 Class.objectGlobal(params.action));
273 "browser:purge-domain-data": function (subject, host) {
274 storage.fireEvent("sanitize", "domain", host);
275 // If we're sanitizing, our own sanitization functions will already
276 // be called, and with much greater granularity. Only process this
277 // event if it's triggered externally.
278 if (!this.sanitizing)
279 this.sanitizeItems(null, Range(), data);
281 "browser:purge-session-history": function (subject, data) {
283 if (!this.sanitizing)
284 this.sanitizeItems(null, Range(this.sessionStart), null, "sessionHistory");
286 "quit-application-granted": function (subject, data) {
287 if (this.runAtShutdown && !this.sanitizeItems(null, Range(), null, "shutdown"))
288 this.ranAtShutdown = true;
290 "private-browsing": function (subject, data) {
292 storage.privateMode = true;
293 else if (data == "exit")
294 storage.privateMode = false;
295 storage.fireEvent("private-mode", "change", storage.privateMode);
299 get ranAtShutdown() config.prefs.get("didSanitizeOnShutdown"),
300 set ranAtShutdown(val) config.prefs.set("didSanitizeOnShutdown", Boolean(val)),
301 get runAtShutdown() prefs.get("privacy.sanitize.sanitizeOnShutdown"),
302 set runAtShutdown(val) prefs.set("privacy.sanitize.sanitizeOnShutdown", Boolean(val)),
304 sanitize: function (items, range)
305 this.withSavedValues(["sanitizing"], function () {
306 this.sanitizing = true;
307 let errors = this.sanitizeItems(items, range, null);
309 for (let itemName in values(items)) {
311 let item = this.items[Sanitizer.argToPref(itemName)];
312 if (item && !this.itemMap[itemName].override) {
313 item.range = range.native;
314 if ("clear" in item && item.canClear)
319 errors = errors || {};
320 errors[itemName] = e;
321 util.dump("Error sanitizing " + itemName);
328 sanitizeItems: function (items, range, host, key)
329 this.withSavedValues(["sanitizing"], function () {
330 this.sanitizing = true;
332 items = Object.keys(this.itemMap);
335 for (let itemName in values(items))
337 if (!key || this.itemMap[itemName][key])
338 storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]);
341 errors = errors || {};
342 errors[itemName] = e;
343 util.dump("Error sanitizing " + itemName);
356 UNPERMS: Class.Memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
360 allow: /*L*/"Allowed",
362 session: /*L*/"Allowed for the current session",
363 list: /*L*/"List all cookies for domain",
364 clear: /*L*/"Clear all cookies for domain",
365 "clear-persistent": /*L*/"Clear all persistent cookies for domain",
366 "clear-session": /*L*/"Clear all session cookies for domain"
370 offlineapps: "offlineApps",
371 sitesettings: "siteSettings"
373 argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg,
374 prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
376 iterCookies: function iterCookies(host) {
377 for (let c in iter(services.cookies, Ci.nsICookie2))
378 if (!host || util.isSubdomain(c.rawHost, host) ||
379 c.host[0] == "." && c.host.length < host.length
380 && host.indexOf(c.host) == host.length - c.host.length)
384 iterPermissions: function iterPermissions(host) {
385 for (let p in iter(services.permissions, Ci.nsIPermission))
386 if (!host || util.isSubdomain(p.host, host))
390 load: function (dactyl, modules, window) {
391 if (!sanitizer.firstRun++ && sanitizer.runAtShutdown && !sanitizer.ranAtShutdown)
392 sanitizer.sanitizeItems(null, Range(), null, "shutdown");
393 sanitizer.ranAtShutdown = false;
395 autocommands: function (dactyl, modules, window) {
396 const { autocommands } = modules;
398 storage.addObserver("private-mode",
399 function (key, event, value) {
400 autocommands.trigger("PrivateMode", { state: value });
402 storage.addObserver("sanitizer",
403 function (key, event, value) {
404 if (event == "domain")
405 autocommands.trigger("SanitizeDomain", { domain: value });
407 autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
410 commands: function (dactyl, modules, window) {
411 const { commands } = modules;
412 commands.add(["sa[nitize]"],
413 "Clear private data",
415 dactyl.assert(!modules.options['private'], _("command.sanitize.privateMode"));
417 if (args["-host"] && !args.length && !args.bang)
420 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
423 let [match, num, unit] = /^(\d+)([mhdw])$/.exec(timespan) || [];
424 range[args["-older"] ? "max" : "min"] =
425 match ? 1000 * (Date.now() - 1000 * parseInt(num, 10) * { m: 60, h: 3600, d: 3600 * 24, w: 3600 * 24 * 7 }[unit])
426 : (timespan[0] == "s" ? sanitizer.sessionStart : null);
428 let opt = modules.options.get("sanitizeitems");
430 dactyl.assert(args.length == 0, _("error.trailingCharacters"));
432 dactyl.assert(opt.validator(args), _("error.invalidArgument"));
433 opt = { __proto__: opt, value: args.slice() };
436 let items = Object.keys(sanitizer.itemMap).slice(1).filter(opt.has, opt);
438 function sanitize(items) {
439 sanitizer.range = range.native;
440 sanitizer.ignoreTimespan = range.min == null;
441 sanitizer.sanitizing = true;
443 args["-host"].forEach(function (host) {
444 sanitizer.sanitizing = true;
445 sanitizer.sanitizeItems(items, range, host);
449 sanitizer.sanitize(items, range);
452 if (array.nth(opt.value, function (i) i == "all" || /^!/.test(i), 0) == "all" && !args["-host"])
453 modules.commandline.input(_("sanitize.prompt.deleteAll") + " ",
455 if (resp.match(/^y(es)?$/i)) {
457 dactyl.echomsg(_("command.sanitize.allDeleted"));
460 dactyl.echo(_("command.sanitize.noneDeleted"));
467 argCount: "*", // FIXME: should be + and 0
469 completer: function (context) {
470 context.title = ["Privacy Item", "Description"];
471 context.completions = modules.options.get("sanitizeitems").values;
473 domains: function (args) args["-host"] || [],
476 names: ["-host", "-h"],
477 description: "Only sanitize items referring to listed host or hosts",
478 completer: function (context, args) {
479 context.filters.push(function (item)
480 !args["-host"].some(function (host) util.isSubdomain(item.text, host)));
481 modules.completion.domain(context);
483 type: modules.CommandOption.LIST
485 names: ["-older", "-o"],
486 description: "Sanitize items older than timespan",
487 type: modules.CommandOption.NOARG
489 names: ["-timespan", "-t"],
490 description: "Timespan for which to sanitize items",
491 completer: function (context) modules.options.get("sanitizetimespan").completer(context),
492 type: modules.CommandOption.STRING,
493 validator: function (arg) modules.options.get("sanitizetimespan").validator(arg),
499 function getPerms(host) {
500 let uri = util.createURI(host);
502 return Sanitizer.UNPERMS[services.permissions.testPermission(uri, "cookie")];
505 function setPerms(host, perm) {
506 let uri = util.createURI(host);
507 services.permissions.remove(uri, "cookie");
508 services.permissions.add(uri, "cookie", Sanitizer.PERMS[perm]);
510 commands.add(["cookies", "ck"],
511 "Change cookie permissions for sites",
513 let host = args.shift();
516 args = modules.options["cookies"];
518 for (let [, cmd] in Iterator(args))
521 for (let c in Sanitizer.iterCookies(host))
522 services.cookies.remove(c.host, c.name, c.path, false);
524 case "clear-persistent":
526 case "clear-session":
527 for (let c in Sanitizer.iterCookies(host))
528 if (c.isSession == session)
529 services.cookies.remove(c.host, c.name, c.path, false);
533 modules.commandline.commandOutput(template.tabular(
534 ["Host", "Expiry (UTC)", "Path", "Name", "Value"],
535 ["padding-right: 1em", "padding-right: 1em", "padding-right: 1em", "max-width: 12em; overflow: hidden;", "padding-left: 1ex;"],
537 c.isSession ? <span highlight="Enabled">session</span>
538 : (new Date(c.expiry * 1000).toJSON() || "Never").replace(/:\d\d\.000Z/, "").replace("T", " ").replace(/-/g, "/"),
542 for (c in Sanitizer.iterCookies(host)))));
545 util.assert(cmd in Sanitizer.PERMS, _("error.invalidArgument"));
550 completer: function (context, args) {
551 switch (args.completeArg) {
553 modules.completion.visibleHosts(context);
554 context.title[1] = "Current Permissions";
555 context.keys.description = function desc(host) {
557 for (let c in Sanitizer.iterCookies(host))
558 count[c.isSession + 0]++;
559 return <>{Sanitizer.COMMANDS[getPerms(host)]} (session: {count[1]} persistent: {count[0]})</>;
563 context.completions = Sanitizer.COMMANDS;
569 completion: function (dactyl, modules, window) {
570 modules.completion.visibleHosts = function completeHosts(context) {
571 let res = util.visibleHosts(window.content);
572 if (context.filter && !res.some(function (host) host.indexOf(context.filter) >= 0))
573 res.push(context.filter);
575 context.title = ["Domain"];
576 context.anchored = false;
577 context.compare = modules.CompletionContext.Sort.unsorted;
578 context.keys = { text: util.identity, description: util.identity };
579 context.completions = res;
582 options: function (dactyl, modules) {
583 const options = modules.options;
584 if (services.has("privateBrowsing"))
585 options.add(["private", "pornmode"],
586 "Set the 'private browsing' option",
590 getter: function () services.privateBrowsing.privateBrowsingEnabled,
591 setter: function (value) {
592 if (services.privateBrowsing.privateBrowsingEnabled != value)
593 services.privateBrowsing.privateBrowsingEnabled = value;
598 options.add(["sanitizeitems", "si"],
599 "The default list of private items to sanitize",
602 get values() values(sanitizer.itemMap).toArray(),
604 completer: function completer(context, extra) {
605 if (context.filter[0] == "!")
607 return completer.superapply(this, arguments);
610 has: function has(val)
611 let (res = array.nth(this.value, function (v) v == "all" || v.replace(/^!/, "") == val, 0))
612 res && !/^!/.test(res),
614 validator: function (values) values.length &&
615 values.every(function (val) val === "all" || Set.has(sanitizer.itemMap, val.replace(/^!/, "")))
618 options.add(["sanitizeshutdown", "ss"],
619 "The items to sanitize automatically at shutdown",
623 get values() [i for (i in values(sanitizer.itemMap)) if (i.persistent || i.builtin)],
624 getter: function () !sanitizer.runAtShutdown ? [] : [
625 item.name for (item in values(sanitizer.itemMap))
626 if (item.shouldSanitize(true))
628 setter: function (value) {
629 if (value.length === 0)
630 sanitizer.runAtShutdown = false;
632 sanitizer.runAtShutdown = true;
633 let have = Set(value);
634 for (let item in values(sanitizer.itemMap))
635 prefs.set(item.shutdownPref,
636 Boolean(Set.has(have, item.name) ^ Set.has(have, "all")));
642 options.add(["sanitizetimespan", "sts"],
643 "The default sanitizer time span",
646 completer: function (context) {
647 context.compare = context.constructor.Sort.Unsorted;
648 context.completions = this.values;
652 "session": "The current session",
653 "10m": "Last ten minutes",
658 validator: bind("test", /^(a(ll)?|s(ession)|\d+[mhdw])$/)
661 options.add(["cookies", "ck"],
662 "The default mode for newly added cookie permissions",
663 "stringlist", "session",
664 { get values() Sanitizer.COMMANDS });
666 options.add(["cookieaccept", "ca"],
667 "When to accept cookies",
670 PREF: "network.cookie.cookieBehavior",
672 ["all", "Accept all cookies"],
673 ["samesite", "Accept all non-third-party cookies"],
674 ["none", "Accept no cookies"]
676 getter: function () (this.values[prefs.get(this.PREF)] || ["all"])[0],
677 setter: function (val) {
678 prefs.set(this.PREF, this.values.map(function (i) i[0]).indexOf(val));
685 options.add(["cookielifetime", "cl"],
686 "The lifetime for which to accept cookies",
687 "string", "default", {
688 PREF: "network.cookie.lifetimePolicy",
689 PREF_DAYS: "network.cookie.lifetime.days",
691 ["default", "The lifetime requested by the setter"],
692 ["prompt", "Always prompt for a lifetime"],
693 ["session", "The current session"]
695 getter: function () (this.values[prefs.get(this.PREF)] || [prefs.get(this.PREF_DAYS)])[0],
696 setter: function (value) {
697 let val = this.values.map(function (i) i[0]).indexOf(value);
699 prefs.set(this.PREF, val);
701 prefs.set(this.PREF, 3);
702 prefs.set(this.PREF_DAYS, parseInt(value));
707 validator: function validator(val) parseInt(val) == val || validator.superapply(this, arguments)
714 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
716 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: