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
17 Components.utils.import("resource://dactyl/bootstrap.jsm");
18 defineModule("sanitizer", {
19 exports: ["Range", "Sanitizer", "sanitizer"],
21 require: ["messages", "prefs", "services", "storage", "template", "util"]
25 JSMLoader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
26 tmp.Sanitizer.prototype.__proto__ = Class.prototype;
28 var Range = Struct("min", "max");
29 update(Range.prototype, {
30 contains: function (date) date == null ||
31 (this.min == null || date >= this.min) && (this.max == null || date <= this.max),
33 get isEternity() this.max == null && this.min == null,
34 get isSession() this.max == null && this.min == sanitizer.sessionStart,
36 get native() this.isEternity ? null : [this.min || 0, this.max == null ? Number.MAX_VALUE : this.max]
39 var Item = Class("SanitizeItem", {
40 init: function (name, params) {
42 this.description = params.description;
45 // Hack for completion:
46 "0": Class.Property({ get: function () this.name }),
47 "1": Class.Property({ get: function () this.description }),
49 description: Messages.Localized(""),
51 get cpdPref() (this.builtin ? "" : Item.PREFIX) + Item.BRANCH + Sanitizer.argToPref(this.name),
52 get shutdownPref() (this.builtin ? "" : Item.PREFIX) + Item.SHUTDOWN_BRANCH + Sanitizer.argToPref(this.name),
53 get cpd() prefs.get(this.cpdPref),
54 get shutdown() prefs.get(this.shutdownPref),
56 shouldSanitize: function (shutdown) (!shutdown || this.builtin || this.persistent) &&
57 prefs.get(shutdown ? this.shutdownPref : this.pref)
59 PREFIX: localPrefs.branch.root,
60 BRANCH: "privacy.cpd.",
61 SHUTDOWN_BRANCH: "privacy.clearOnShutdown."
64 var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference], tmp.Sanitizer), {
65 sessionStart: Date.now() * 1000,
70 util.addObserver(this);
72 services.add("contentPrefs", "@mozilla.org/content-pref/service;1", Ci.nsIContentPrefService);
73 services.add("cookies", "@mozilla.org/cookiemanager;1", [Ci.nsICookieManager, Ci.nsICookieManager2,
74 Ci.nsICookieService]);
75 services.add("loginManager", "@mozilla.org/login-manager;1", Ci.nsILoginManager);
76 services.add("permissions", "@mozilla.org/permissionmanager;1", Ci.nsIPermissionManager);
80 this.addItem("all", { description: "Sanitize all items", shouldSanitize: function () false });
82 this.addItem("cache", { builtin: true, description: "Cache" });
83 this.addItem("downloads", { builtin: true, description: "Download history" });
84 this.addItem("formdata", { builtin: true, description: "Saved form and search history" });
85 this.addItem("offlineapps", { builtin: true, description: "Offline website data" });
86 this.addItem("passwords", { builtin: true, description: "Saved passwords" });
87 this.addItem("sessions", { builtin: true, description: "Authenticated sessions" });
89 // These builtin methods don't support hosts or otherwise have
90 // insufficient granularity
91 this.addItem("cookies", {
93 description: "Cookies",
95 action: function (range, host) {
96 for (let c in Sanitizer.iterCookies(host))
97 if (range.contains(c.creationTime) || timespan.isSession && c.isSession)
98 services.cookies.remove(c.host, c.name, c.path, false);
102 this.addItem("history", {
104 description: "Browsing history",
106 sessionHistory: true,
107 action: function (range, host) {
109 services.history.removePagesFromHost(host, true);
111 if (range.isEternity)
112 services.history.removeAllPages();
114 services.history.removeVisitsByTimeframe(range.native[0], Math.min(Date.now() * 1000, range.native[1])); // XXX
115 services.observer.notifyObservers(null, "browser:purge-session-history", "");
118 if (!host || util.isDomainURL(prefs.get("general.open_location.last_url"), host))
119 prefs.reset("general.open_location.last_url");
123 if (services.has("privateBrowsing"))
124 this.addItem("host", {
125 description: "All data from the given host",
126 action: function (range, host) {
128 services.privateBrowsing.removeDataFromDomain(host);
131 this.addItem("sitesettings", {
133 description: "Site preferences",
135 action: function (range, host) {
139 for (let p in Sanitizer.iterPermissions(host)) {
140 services.permissions.remove(util.createURI(p.host), p.type);
141 services.permissions.add(util.createURI(p.host), p.type, 0);
143 for (let p in iter(services.contentPrefs.getPrefs(util.createURI(host))))
144 services.contentPrefs.removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name);
147 // "Allow this site to open popups" ...
148 services.permissions.removeAll();
150 services.contentPrefs.removeGroupedPrefs();
153 // "Never remember passwords" ...
154 for each (let domain in services.loginManager.getAllDisabledHosts())
155 if (!host || util.isSubdomain(domain, host))
156 services.loginManager.setLoginSavingEnabled(host, true);
161 function ourItems(persistent) [
162 item for (item in values(self.itemMap))
163 if (!item.builtin && (!persistent || item.persistent) && item.name !== "all")
166 function prefOverlay(branch, persistent, local) update(Object.create(local), {
167 before: array.toObject([
168 [branch.substr(Item.PREFIX.length) + "history",
169 <preferences xmlns={XUL}>{
170 template.map(ourItems(persistent), function (item)
171 <preference type="bool" id={branch + item.name} name={branch + item.name}/>)
172 }</preferences>.*::*]
174 init: function init(win) {
175 let pane = win.document.getElementById("SanitizeDialogPane");
176 for (let [, pref] in iter(pane.preferences))
177 pref.updateElements();
178 init.superapply(this, arguments);
182 let (branch = Item.PREFIX + Item.SHUTDOWN_BRANCH) {
183 util.overlayWindow("chrome://browser/content/preferences/sanitize.xul",
184 function (win) prefOverlay(branch, true, {
187 <groupbox orient="horizontal" xmlns={XUL}>
188 <caption label={config.appName + /*L*/" (see :help privacy)"}/>
190 <columns><column flex="1"/><column flex="1"/></columns>
192 let (items = ourItems(true))
193 template.map(util.range(0, Math.ceil(items.length / 2)), function (i)
195 template.map(items.slice(i * 2, i * 2 + 2), function (item)
196 <checkbox xmlns={XUL} label={item.description} preference={branch + item.name}/>)
204 let (branch = Item.PREFIX + Item.BRANCH) {
205 util.overlayWindow("chrome://browser/content/sanitize.xul",
206 function (win) prefOverlay(branch, false, {
209 <listitem xmlns={XUL} label={/*L*/"See :help privacy for the following:"} disabled="true" style="font-style: italic; font-weight: bold;"/>
211 template.map(ourItems(), function ([item, desc])
212 <listitem xmlns={XUL} type="checkbox"
213 label={config.appName + " " + desc}
214 preference={branch + item}
215 onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>)
219 init: function (win) {
220 let elem = win.document.getElementById("itemList");
221 elem.setAttribute("rows", elem.itemCount);
222 win.Sanitizer = Class("Sanitizer", win.Sanitizer, {
223 sanitize: function sanitize() {
224 self.withSavedValues(["sanitizing"], function () {
225 self.sanitizing = true;
226 sanitize.superapply(this, arguments);
227 sanitizer.sanitizeItems([item.name for (item in values(self.itemMap))
228 if (item.shouldSanitize(false))],
229 Range.fromArray(this.range || []));
240 addItem: function addItem(name, params) {
241 let item = this.itemMap[name] || Item(name, params);
242 this.itemMap[name] = item;
244 for (let [k, prop] in iterOwnProperties(params))
245 if (!("value" in prop) || !callable(prop.value) && !(k in item))
246 Object.defineProperty(item, k, prop);
248 let names = Set([name].concat(params.contains || []).map(function (e) "clear-" + e));
250 storage.addObserver("sanitizer",
251 function (key, event, arg) {
253 params.action.apply(params, arg);
255 Class.objectGlobal(params.action));
257 if (params.privateEnter || params.privateLeave)
258 storage.addObserver("private-mode",
259 function (key, event, arg) {
260 let meth = params[arg ? "privateEnter" : "privateLeave"];
264 Class.objectGlobal(params.action));
268 "browser:purge-domain-data": function (subject, host) {
269 storage.fireEvent("sanitize", "domain", host);
270 // If we're sanitizing, our own sanitization functions will already
271 // be called, and with much greater granularity. Only process this
272 // event if it's triggered externally.
273 if (!this.sanitizing)
274 this.sanitizeItems(null, Range(), data);
276 "browser:purge-session-history": function (subject, data) {
278 if (!this.sanitizing)
279 this.sanitizeItems(null, Range(this.sessionStart), null, "sessionHistory");
281 "quit-application-granted": function (subject, data) {
282 if (this.runAtShutdown && !this.sanitizeItems(null, Range(), null, "shutdown"))
283 this.ranAtShutdown = true;
285 "private-browsing": function (subject, data) {
287 storage.privateMode = true;
288 else if (data == "exit")
289 storage.privateMode = false;
290 storage.fireEvent("private-mode", "change", storage.privateMode);
294 get ranAtShutdown() localPrefs.get("didSanitizeOnShutdown"),
295 set ranAtShutdown(val) localPrefs.set("didSanitizeOnShutdown", Boolean(val)),
296 get runAtShutdown() prefs.get("privacy.sanitize.sanitizeOnShutdown"),
297 set runAtShutdown(val) prefs.set("privacy.sanitize.sanitizeOnShutdown", Boolean(val)),
299 sanitize: function (items, range)
300 this.withSavedValues(["sanitizing"], function () {
301 this.sanitizing = true;
302 let errors = this.sanitizeItems(items, range, null);
304 for (let itemName in values(items)) {
306 let item = this.items[Sanitizer.argToPref(itemName)];
307 if (item && !this.itemMap[itemName].override) {
308 item.range = range.native;
309 if ("clear" in item && item.canClear)
314 errors = errors || {};
315 errors[itemName] = e;
316 util.dump("Error sanitizing " + itemName);
323 sanitizeItems: function (items, range, host, key)
324 this.withSavedValues(["sanitizing"], function () {
325 this.sanitizing = true;
327 items = Object.keys(this.itemMap);
330 for (let itemName in values(items))
332 if (!key || this.itemMap[itemName][key])
333 storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]);
336 errors = errors || {};
337 errors[itemName] = e;
338 util.dump("Error sanitizing " + itemName);
351 UNPERMS: Class.memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
355 allow: /*L*/"Allowed",
357 session: /*L*/"Allowed for the current session",
358 list: /*L*/"List all cookies for domain",
359 clear: /*L*/"Clear all cookies for domain",
360 "clear-persistent": /*L*/"Clear all persistent cookies for domain",
361 "clear-session": /*L*/"Clear all session cookies for domain"
365 offlineapps: "offlineApps",
366 sitesettings: "siteSettings"
368 argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg,
369 prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
371 iterCookies: function iterCookies(host) {
372 let iterator = host ? services.cookies.getCookiesFromHost(host)
374 for (let c in iter(iterator))
375 yield c.QueryInterface(Ci.nsICookie2);
377 iterPermissions: function iterPermissions(host) {
378 for (let p in iter(services.permissions)) {
379 p.QueryInterface(Ci.nsIPermission);
380 if (!host || util.isSubdomain(p.host, host))
385 load: function (dactyl, modules, window) {
386 if (!sanitizer.firstRun++ && sanitizer.runAtShutdown && !sanitizer.ranAtShutdown)
387 sanitizer.sanitizeItems(null, Range(), null, "shutdown");
388 sanitizer.ranAtShutdown = false;
390 autocommands: function (dactyl, modules, window) {
391 storage.addObserver("private-mode",
392 function (key, event, value) {
393 modules.autocommands.trigger("PrivateMode", { state: value });
395 storage.addObserver("sanitizer",
396 function (key, event, value) {
397 if (event == "domain")
398 modules.autocommands.trigger("SanitizeDomain", { domain: value });
400 modules.autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
403 commands: function (dactyl, modules, window) {
404 const { commands } = modules;
405 commands.add(["sa[nitize]"],
406 "Clear private data",
408 dactyl.assert(!modules.options['private'], _("command.sanitize.privateMode"));
410 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
413 let [match, num, unit] = /^(\d+)([mhdw])$/.exec(timespan) || [];
414 range[args["-older"] ? "max" : "min"] =
415 match ? 1000 * (Date.now() - 1000 * parseInt(num, 10) * { m: 60, h: 3600, d: 3600 * 24, w: 3600 * 24 * 7 }[unit])
416 : (timespan[0] == "s" ? sanitizer.sessionStart : null);
418 let items = args.slice();
419 if (args["-host"] && !args.length)
423 dactyl.assert(args.length == 0, _("error.trailingCharacters"));
424 items = Object.keys(sanitizer.itemMap).filter(
425 function (k) modules.options.get("sanitizeitems").has(k));
428 dactyl.assert(modules.options.get("sanitizeitems").validator(items), _("error.invalidArgument"));
430 function sanitize(items) {
431 sanitizer.range = range.native;
432 sanitizer.ignoreTimespan = range.min == null;
433 sanitizer.sanitizing = true;
435 args["-host"].forEach(function (host) {
436 sanitizer.sanitizing = true;
437 sanitizer.sanitizeItems(items, range, host);
441 sanitizer.sanitize(items, range);
444 if (items.indexOf("all") >= 0)
445 modules.commandline.input(_("sanitize.prompt.deleteAll") + " ",
447 if (resp.match(/^y(es)?$/i)) {
448 items = Object.keys(sanitizer.itemMap).filter(function (k) items.indexOf(k) === -1);
450 dactyl.echo(_("command.sanitize.allDeleted"));
453 dactyl.echo(_("command.sanitize.noneDeleted"));
460 argCount: "*", // FIXME: should be + and 0
462 completer: function (context) {
463 context.title = ["Privacy Item", "Description"];
464 context.completions = modules.options.get("sanitizeitems").values;
466 domains: function (args) args["-host"] || [],
469 names: ["-host", "-h"],
470 description: "Only sanitize items referring to listed host or hosts",
471 completer: function (context, args) {
472 context.filters.push(function (item)
473 !args["-host"].some(function (host) util.isSubdomain(item.text, host)));
474 modules.completion.domain(context);
476 type: modules.CommandOption.LIST
478 names: ["-older", "-o"],
479 description: "Sanitize items older than timespan",
480 type: modules.CommandOption.NOARG
482 names: ["-timespan", "-t"],
483 description: "Timespan for which to sanitize items",
484 completer: function (context) modules.options.get("sanitizetimespan").completer(context),
485 type: modules.CommandOption.STRING,
486 validator: function (arg) modules.options.get("sanitizetimespan").validator(arg),
492 function getPerms(host) {
493 let uri = util.createURI(host);
495 return Sanitizer.UNPERMS[services.permissions.testPermission(uri, "cookie")];
498 function setPerms(host, perm) {
499 let uri = util.createURI(host);
500 services.permissions.remove(uri, "cookie");
501 services.permissions.add(uri, "cookie", Sanitizer.PERMS[perm]);
503 commands.add(["cookies", "ck"],
504 "Change cookie permissions for sites",
506 let host = args.shift();
509 args = modules.options["cookies"];
511 for (let [, cmd] in Iterator(args))
514 for (let c in Sanitizer.iterCookies(host))
515 services.cookies.remove(c.host, c.name, c.path, false);
517 case "clear-persistent":
519 case "clear-session":
520 for (let c in Sanitizer.iterCookies(host))
521 if (c.isSession == session)
522 services.cookies.remove(c.host, c.name, c.path, false);
526 modules.commandline.commandOutput(template.tabular(
527 ["Host", "Expiry (UTC)", "Path", "Name", "Value"],
528 ["padding-right: 1em", "padding-right: 1em", "padding-right: 1em", "max-width: 12em; overflow: hidden;", "padding-left: 1ex;"],
530 c.isSession ? <span highlight="Enabled">session</span>
531 : (new Date(c.expiry * 1000).toJSON() || "Never").replace(/:\d\d\.000Z/, "").replace("T", " ").replace(/-/g, "/"),
535 for (c in Sanitizer.iterCookies(host)))));
538 util.assert(cmd in Sanitizer.PERMS, _("error.invalidArgument"));
543 completer: function (context, args) {
544 switch (args.completeArg) {
546 modules.completion.visibleHosts(context);
547 context.title[1] = "Current Permissions";
548 context.keys.description = function desc(host) {
550 for (let c in Sanitizer.iterCookies(host))
551 count[c.isSession + 0]++;
552 return <>{Sanitizer.COMMANDS[getPerms(host)]} (session: {count[1]} persistent: {count[0]})</>;
556 context.completions = Sanitizer.COMMANDS;
562 completion: function (dactyl, modules, window) {
563 modules.completion.visibleHosts = function completeHosts(context) {
564 let res = util.visibleHosts(window.content);
565 if (context.filter && !res.some(function (host) host.indexOf(context.filter) >= 0))
566 res.push(context.filter);
568 context.title = ["Domain"];
569 context.anchored = false;
570 context.compare = modules.CompletionContext.Sort.unsorted;
571 context.keys = { text: util.identity, description: util.identity };
572 context.completions = res;
575 options: function (dactyl, modules) {
576 const options = modules.options;
577 if (services.has("privateBrowsing"))
578 options.add(["private", "pornmode"],
579 "Set the 'private browsing' option",
583 getter: function () services.privateBrowsing.privateBrowsingEnabled,
584 setter: function (value) {
585 if (services.privateBrowsing.privateBrowsingEnabled != value)
586 services.privateBrowsing.privateBrowsingEnabled = value;
591 options.add(["sanitizeitems", "si"],
592 "The default list of private items to sanitize",
595 get values() values(sanitizer.itemMap).toArray(),
596 has: modules.Option.has.toggleAll,
597 validator: function (values) values.length &&
598 values.every(function (val) val === "all" || Set.has(sanitizer.itemMap, val))
601 options.add(["sanitizeshutdown", "ss"],
602 "The items to sanitize automatically at shutdown",
606 get values() [i for (i in values(sanitizer.itemMap)) if (i.persistent || i.builtin)],
607 getter: function () !sanitizer.runAtShutdown ? [] : [
608 item.name for (item in values(sanitizer.itemMap))
609 if (item.shouldSanitize(true))
611 setter: function (value) {
612 if (value.length === 0)
613 sanitizer.runAtShutdown = false;
615 sanitizer.runAtShutdown = true;
616 let have = Set(value);
617 for (let item in values(sanitizer.itemMap))
618 prefs.set(item.shutdownPref,
619 Boolean(Set.has(have, item.name) ^ Set.has(have, "all")));
625 options.add(["sanitizetimespan", "sts"],
626 "The default sanitizer time span",
629 completer: function (context) {
630 context.compare = context.constructor.Sort.Unsorted;
631 context.completions = this.values;
635 "session": "The current session",
636 "10m": "Last ten minutes",
641 validator: function (value) /^(a(ll)?|s(ession)|\d+[mhdw])$/.test(value)
644 options.add(["cookies", "ck"],
645 "The default mode for newly added cookie permissions",
646 "stringlist", "session",
647 { get values() iter(Sanitizer.COMMANDS) });
649 options.add(["cookieaccept", "ca"],
650 "When to accept cookies",
653 PREF: "network.cookie.cookieBehavior",
655 ["all", "Accept all cookies"],
656 ["samesite", "Accept all non-third-party cookies"],
657 ["none", "Accept no cookies"]
659 getter: function () (this.values[prefs.get(this.PREF)] || ["all"])[0],
660 setter: function (val) {
661 prefs.set(this.PREF, this.values.map(function (i) i[0]).indexOf(val));
668 options.add(["cookielifetime", "cl"],
669 "The lifetime for which to accept cookies",
670 "string", "default", {
671 PREF: "network.cookie.lifetimePolicy",
672 PREF_DAYS: "network.cookie.lifetime.days",
674 ["default", "The lifetime requested by the setter"],
675 ["prompt", "Always prompt for a lifetime"],
676 ["session", "The current session"]
678 getter: function () (this.values[prefs.get(this.PREF)] || [prefs.get(this.PREF_DAYS)])[0],
679 setter: function (value) {
680 let val = this.values.map(function (i) i[0]).indexOf(value);
682 prefs.set(this.PREF, val);
684 prefs.set(this.PREF, 3);
685 prefs.set(this.PREF_DAYS, parseInt(value));
690 validator: function validator(val) parseInt(val) == val || validator.superapply(this, arguments)
697 } catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
699 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: