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 : [range.min || 0, range.max == null ? Number.MAX_VALUE : range.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 services.history.removeVisitsByTimeframe(this.range.min, this.range.max);
114 services.observer.notifyObservers(null, "browser:purge-session-history", "");
116 if (!host || util.isDomainURL(prefs.get("general.open_location.last_url"), host))
117 prefs.reset("general.open_location.last_url");
121 if (services.has("privateBrowsing"))
122 this.addItem("host", {
123 description: "All data from the given host",
124 action: function (range, host) {
126 services.privateBrowsing.removeDataFromDomain(host);
129 this.addItem("sitesettings", {
131 description: "Site preferences",
133 action: function (range, host) {
137 for (let p in Sanitizer.iterPermissions(host)) {
138 services.permissions.remove(util.createURI(p.host), p.type);
139 services.permissions.add(util.createURI(p.host), p.type, 0);
141 for (let p in iter(services.contentprefs.getPrefs(util.createURI(host))))
142 services.contentprefs.removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name);
145 // "Allow this site to open popups" ...
146 services.permissions.removeAll();
148 services.contentprefs.removeGroupedPrefs();
151 // "Never remember passwords" ...
152 for each (let domain in services.loginmanager.getAllDisabledHosts())
153 if (!host || util.isSubdomain(domain, host))
154 services.loginmanager.setLoginSavingEnabled(host, true);
159 function ourItems(persistent) [
160 item for (item in values(self.itemMap))
161 if (!item.builtin && (!persistent || item.persistent) && item.name !== "all")
164 function prefOverlay(branch, persistent, local) update(Object.create(local), {
165 before: array.toObject([
166 [branch.substr(Item.PREFIX.length) + "history",
167 <preferences xmlns={XUL}>{
168 template.map(ourItems(persistent), function (item)
169 <preference type="bool" id={branch + item.name} name={branch + item.name}/>)
170 }</preferences>.*::*]
172 init: function init(win) {
173 let pane = win.document.getElementById("SanitizeDialogPane");
174 for (let [, pref] in iter(pane.preferences))
175 pref.updateElements();
176 init.superapply(this, arguments);
180 let (branch = Item.PREFIX + Item.SHUTDOWN_BRANCH) {
181 util.overlayWindow("chrome://browser/content/preferences/sanitize.xul",
182 function (win) prefOverlay(branch, true, {
185 <groupbox orient="horizontal" xmlns={XUL}>
186 <caption label={config.appName + " (see :help privacy)"}/>
188 <columns><column flex="1"/><column flex="1"/></columns>
190 let (items = ourItems(true))
191 template.map(util.range(0, Math.ceil(items.length / 2)), function (i)
193 template.map(items.slice(i * 2, i * 2 + 2), function (item)
194 <checkbox xmlns={XUL} label={item.description} preference={branch + item.name}/>)
202 let (branch = Item.PREFIX + Item.BRANCH) {
203 util.overlayWindow("chrome://browser/content/sanitize.xul",
204 function (win) prefOverlay(branch, false, {
207 <listitem xmlns={XUL} label="See :help privacy for the following:" disabled="true" style="font-style: italic; font-weight: bold;"/>
209 template.map(ourItems(), function ([item, desc])
210 <listitem xmlns={XUL} type="checkbox"
211 label={config.appName + " " + desc}
212 preference={branch + item}
213 onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>)
217 init: function (win) {
218 let elem = win.document.getElementById("itemList");
219 elem.setAttribute("rows", elem.itemCount);
220 win.Sanitizer = Class("Sanitizer", win.Sanitizer, {
221 sanitize: function sanitize() {
222 self.withSavedValues(["sanitizing"], function () {
223 self.sanitizing = true;
224 sanitize.superapply(this, arguments);
225 sanitizer.sanitizeItems([item.name for (item in values(self.itemMap))
226 if (item.shouldSanitize(false))],
227 Range.fromArray(this.range || []));
236 addItem: function addItem(name, params) {
237 let item = this.itemMap[name] || Item(name, params);
238 this.itemMap[name] = item;
240 for (let [k, prop] in iterOwnProperties(params))
241 if (!("value" in prop) || !callable(prop.value) && !(k in item))
242 Object.defineProperty(item, k, prop);
244 let names = set([name].concat(params.contains || []).map(function (e) "clear-" + e));
246 storage.addObserver("sanitizer",
247 function (key, event, arg) {
249 params.action.apply(params, arg);
251 Class.objectGlobal(params.action));
253 if (params.privateEnter || params.privateLeave)
254 storage.addObserver("private-mode",
255 function (key, event, arg) {
256 let meth = params[arg ? "privateEnter" : "privateLeave"];
260 Class.objectGlobal(params.action));
264 "browser:purge-domain-data": function (subject, host) {
265 storage.fireEvent("sanitize", "domain", host);
266 // If we're sanitizing, our own sanitization functions will already
267 // be called, and with much greater granularity. Only process this
268 // event if it's triggered externally.
269 if (!this.sanitizing)
270 this.sanitizeItems(null, Range(), data);
272 "browser:purge-session-history": function (subject, data) {
274 if (!this.sanitizing)
275 this.sanitizeItems(null, Range(this.sessionStart), null, "sessionHistory");
277 "quit-application-granted": function (subject, data) {
278 if (this.runAtShutdown && !this.sanitizeItems(null, Range(), null, "shutdown"))
279 this.ranAtShutdown = true;
281 "private-browsing": function (subject, data) {
283 storage.privateMode = true;
284 else if (data == "exit")
285 storage.privateMode = false;
286 storage.fireEvent("private-mode", "change", storage.privateMode);
290 get ranAtShutdown() localPrefs.get("didSanitizeOnShutdown"),
291 set ranAtShutdown(val) localPrefs.set("didSanitizeOnShutdown", Boolean(val)),
292 get runAtShutdown() prefs.get("privacy.sanitize.sanitizeOnShutdown"),
293 set runAtShutdown(val) prefs.set("privacy.sanitize.sanitizeOnShutdown", Boolean(val)),
295 sanitize: function (items, range)
296 this.withSavedValues(["sanitizing"], function () {
297 this.sanitizing = true;
298 let errors = this.sanitizeItems(items, range, null);
300 for (let itemName in values(items)) {
302 let item = this.items[Sanitizer.argToPref(itemName)];
303 if (item && !this.itemMap[itemName].override) {
304 item.range = range.native;
305 if ("clear" in item && item.canClear)
310 errors = errors || {};
311 errors[itemName] = e;
312 util.dump("Error sanitizing " + itemName);
319 sanitizeItems: function (items, range, host, key)
320 this.withSavedValues(["sanitizing"], function () {
321 this.sanitizing = true;
323 items = Object.keys(this.itemMap);
326 for (let itemName in values(items))
328 if (!key || this.itemMap[itemName][key])
329 storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]);
332 errors = errors || {};
333 errors[itemName] = e;
334 util.dump("Error sanitizing " + itemName);
346 UNPERMS: Class.memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
351 session: "Allowed for the current session",
352 list: "List all cookies for domain",
353 clear: "Clear all cookies for domain",
354 "clear-persistent": "Clear all persistent cookies for domain",
355 "clear-session": "Clear all session cookies for domain"
359 offlineapps: "offlineApps",
360 sitesettings: "siteSettings"
362 argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg,
363 prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
365 iterCookies: function iterCookies(host) {
366 let iterator = host ? services.cookies.getCookiesFromHost(host)
368 for (let c in iter(iterator))
369 yield c.QueryInterface(Ci.nsICookie2);
371 iterPermissions: function iterPermissions(host) {
372 for (let p in iter(services.permissions)) {
373 p.QueryInterface(Ci.nsIPermission);
374 if (!host || util.isSubdomain(p.host, host))
379 load: function (dactyl, modules, window) {
380 if (sanitizer.runAtShutdown && !sanitizer.ranAtShutdown)
381 sanitizer.sanitizeItems(null, Range(), null, "shutdown");
382 sanitizer.ranAtShutdown = false;
384 autocommands: function (dactyl, modules, window) {
385 storage.addObserver("private-mode",
386 function (key, event, value) {
387 modules.autocommands.trigger("PrivateMode", { state: value });
389 storage.addObserver("sanitizer",
390 function (key, event, value) {
391 if (event == "domain")
392 modules.autocommands.trigger("SanitizeDomain", { domain: value });
394 modules.autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
397 commands: function (dactyl, modules, window) {
398 const { commands } = modules;
399 commands.add(["sa[nitize]"],
400 "Clear private data",
402 dactyl.assert(!modules.options['private'], "Cannot sanitize items in private mode");
404 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
407 let [match, num, unit] = /^(\d+)([mhdw])$/.exec(timespan) || [];
408 range[args["-older"] ? "max" : "min"] =
409 match ? 1000 * (Date.now() - 1000 * parseInt(num, 10) * { m: 60, h: 3600, d: 3600 * 24, w: 3600 * 24 * 7 }[unit])
410 : (timespan[0] == "s" ? sanitizer.sessionStart : null);
412 let items = args.slice();
413 if (args["-host"] && !args.length)
417 dactyl.assert(args.length == 0, _("error.trailing"));
418 items = Object.keys(sanitizer.itemMap).filter(
419 function (k) modules.options.get("sanitizeitems").has(k));
422 dactyl.assert(modules.options.get("sanitizeitems").validator(items), "Valid items required");
424 if (items.indexOf("all") >= 0)
425 items = Object.keys(sanitizer.itemMap).filter(function (k) items.indexOf(k) === -1);
427 sanitizer.range = range.native;
428 sanitizer.ignoreTimespan = range.min == null;
429 sanitizer.sanitizing = true;
431 args["-host"].forEach(function (host) {
432 sanitizer.sanitizing = true;
433 sanitizer.sanitizeItems(items, range, host)
437 sanitizer.sanitize(items, range);
440 argCount: "*", // FIXME: should be + and 0
442 completer: function (context) {
443 context.title = ["Privacy Item", "Description"];
444 context.completions = modules.options.get("sanitizeitems").values;
446 domains: function (args) args["-host"] || [],
449 names: ["-host", "-h"],
450 description: "Only sanitize items referring to listed host or hosts",
451 completer: function (context, args) {
452 context.filters.push(function (item)
453 !args["-host"].some(function (host) util.isSubdomain(item.text, host)));
454 modules.completion.domain(context);
456 type: modules.CommandOption.LIST,
458 names: ["-older", "-o"],
459 description: "Sanitize items older than timespan",
460 type: modules.CommandOption.NOARG
462 names: ["-timespan", "-t"],
463 description: "Timespan for which to sanitize items",
464 completer: function (context) modules.options.get("sanitizetimespan").completer(context),
465 type: modules.CommandOption.STRING,
466 validator: function (arg) modules.options.get("sanitizetimespan").validator(arg),
472 function getPerms(host) {
473 let uri = util.createURI(host);
475 return Sanitizer.UNPERMS[services.permissions.testPermission(uri, "cookie")];
478 function setPerms(host, perm) {
479 let uri = util.createURI(host);
480 services.permissions.remove(uri, "cookie");
481 services.permissions.add(uri, "cookie", Sanitizer.PERMS[perm]);
483 commands.add(["cookies", "ck"],
484 "Change cookie permissions for sites",
486 let host = args.shift();
489 args = modules.options["cookies"];
491 for (let [, cmd] in Iterator(args))
494 for (let c in Sanitizer.iterCookies(host))
495 services.cookies.remove(c.host, c.name, c.path, false);
497 case "clear-persistent":
499 case "clear-session":
500 for (let c in Sanitizer.iterCookies(host))
501 if (c.isSession == session)
502 services.cookies.remove(c.host, c.name, c.path, false);
506 modules.commandline.commandOutput(template.tabular(
507 ["Host", "Expiry (UTC)", "Path", "Name", "Value"],
508 ["padding-right: 1em", "padding-right: 1em", "padding-right: 1em", "max-width: 12em; overflow: hidden;", "padding-left: 1ex;"],
510 c.isSession ? <span highlight="Enabled">session</span>
511 : (new Date(c.expiry * 1000).toJSON() || "Never").replace(/:\d\d\.000Z/, "").replace("T", " ").replace(/-/g, "/"),
515 for (c in Sanitizer.iterCookies(host)))));
518 util.assert(cmd in Sanitizer.PERMS, _("error.invalidArgument"));
523 completer: function (context, args) {
524 switch (args.completeArg) {
526 modules.completion.visibleHosts(context);
527 context.title[1] = "Current Permissions";
528 context.keys.description = function desc(host) {
530 for (let c in Sanitizer.iterCookies(host))
531 count[c.isSession + 0]++;
532 return <>{Sanitizer.COMMANDS[getPerms(host)]} (session: {count[1]} persistent: {count[0]})</>;
536 context.completions = Sanitizer.COMMANDS;
542 completion: function (dactyl, modules, window) {
543 modules.completion.visibleHosts = function completeHosts(context) {
544 let res = util.visibleHosts(window.content);
545 if (context.filter && !res.some(function (host) host.indexOf(context.filter) >= 0))
546 res.push(context.filter);
548 context.title = ["Domain"];
549 context.anchored = false;
550 context.compare = modules.CompletionContext.Sort.unsorted;
551 context.keys = { text: util.identity, description: util.identity };
552 context.completions = res;
555 options: function (dactyl, modules) {
556 const options = modules.options;
557 if (services.has("privateBrowsing"))
558 options.add(["private", "pornmode"],
559 "Set the 'private browsing' option",
563 getter: function () services.privateBrowsing.privateBrowsingEnabled,
564 setter: function (value) {
565 if (services.privateBrowsing.privateBrowsingEnabled != value)
566 services.privateBrowsing.privateBrowsingEnabled = value;
571 options.add(["sanitizeitems", "si"],
572 "The default list of private items to sanitize",
575 get values() values(sanitizer.itemMap).toArray(),
576 has: modules.Option.has.toggleAll,
577 validator: function (values) values.length &&
578 values.every(function (val) val === "all" || set.has(sanitizer.itemMap, val))
581 options.add(["sanitizeshutdown", "ss"],
582 "The items to sanitize automatically at shutdown",
586 get values() [i for (i in values(sanitizer.itemMap)) if (i.persistent || i.builtin)],
587 getter: function () !sanitizer.runAtShutdown ? [] : [
588 item.name for (item in values(sanitizer.itemMap))
589 if (item.shouldSanitize(true))
591 setter: function (value) {
592 if (value.length === 0)
593 sanitizer.runAtShutdown = false;
595 sanitizer.runAtShutdown = true;
596 let have = set(value);
597 for (let item in values(sanitizer.itemMap))
598 prefs.set(item.shutdownPref,
599 Boolean(set.has(have, item.name) ^ set.has(have, "all")));
605 options.add(["sanitizetimespan", "sts"],
606 "The default sanitizer time span",
609 completer: function (context) {
610 context.compare = context.constructor.Sort.Unsorted;
611 context.completions = this.values;
615 "session": "The current session",
616 "10m": "Last ten minutes",
621 validator: function (value) /^(a(ll)?|s(ession)|\d+[mhdw])$/.test(value)
624 options.add(["cookies", "ck"],
625 "The default mode for newly added cookie permissions",
626 "stringlist", "session",
627 { get values() iter(Sanitizer.COMMANDS) });
629 options.add(["cookieaccept", "ca"],
630 "When to accept cookies",
633 PREF: "network.cookie.cookieBehavior",
635 ["all", "Accept all cookies"],
636 ["samesite", "Accept all non-third-party cookies"],
637 ["none", "Accept no cookies"]
639 getter: function () (this.values[prefs.get(this.PREF)] || ["all"])[0],
640 setter: function (val) {
641 prefs.set(this.PREF, this.values.map(function (i) i[0]).indexOf(val));
648 options.add(["cookielifetime", "cl"],
649 "The lifetime for which to accept cookies",
650 "string", "default", {
651 PREF: "network.cookie.lifetimePolicy",
652 PREF_DAYS: "network.cookie.lifetime.days",
654 ["default", "The lifetime requested by the setter"],
655 ["prompt", "Always prompt for a lifetime"],
656 ["session", "The current session"]
658 getter: function () (this.values[prefs.get(this.PREF)] || [prefs.get(this.PREF_DAYS)])[0],
659 setter: function (value) {
660 let val = this.values.map(function (i) i[0]).indexOf(value);
662 prefs.set(this.PREF, val);
664 prefs.set(this.PREF, 3);
665 prefs.set(this.PREF_DAYS, parseInt(value));
670 validator: function (val) parseInt(val) == val || modules.Option.validateCompleter.call(this, val)
677 } catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
679 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: