1 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2009-2014 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 defineModule("sanitizer", {
16 exports: ["Range", "Sanitizer", "sanitizer"],
17 require: ["config", "prefs", "services", "util"]
20 lazyRequire("messages", ["_"]);
21 lazyRequire("overlay", ["overlay"]);
22 lazyRequire("storage", ["storage"]);
23 lazyRequire("template", ["template"]);
25 let tmp = Object.create(this);
26 JSMLoader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
27 tmp.Sanitizer.prototype.__proto__ = Class.prototype;
29 var Range = Struct("min", "max");
30 update(Range.prototype, {
31 contains: function (date) date == null ||
32 (this.min == null || date >= this.min) && (this.max == null || date <= this.max),
34 get isEternity() this.max == null && this.min == null,
35 get isSession() this.max == null && this.min == sanitizer.sessionStart,
37 get native() this.isEternity ? null : [this.min || 0, this.max == null ? Number.MAX_VALUE : this.max]
40 var Item = Class("SanitizeItem", {
41 init: function (name, params) {
43 this.description = params.description;
46 // Hack for completion:
47 "0": Class.Property({ get: function () this.name }),
48 "1": Class.Property({ get: function () this.description }),
50 description: Messages.Localized(""),
52 get cpdPref() (this.builtin ? "" : Item.PREFIX) + Item.BRANCH + Sanitizer.argToPref(this.name),
53 get shutdownPref() (this.builtin ? "" : Item.PREFIX) + Item.SHUTDOWN_BRANCH + Sanitizer.argToPref(this.name),
54 get cpd() prefs.get(this.cpdPref),
55 get shutdown() prefs.get(this.shutdownPref),
57 shouldSanitize: function (shutdown) (!shutdown || this.builtin || this.persistent) &&
58 prefs.get(shutdown ? this.shutdownPref : this.pref)
60 PREFIX: config.prefs.branch.root,
61 BRANCH: "privacy.cpd.",
62 SHUTDOWN_BRANCH: "privacy.clearOnShutdown."
65 var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference], tmp.Sanitizer), {
66 sessionStart: Date.now() * 1000,
71 util.addObserver(this);
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");
124 var { ForgetAboutSite } = Cu.import("resource://gre/modules/ForgetAboutSite.jsm", {});
128 this.addItem("host", {
129 description: "All data from the given host",
130 action: function (range, host) {
132 ForgetAboutSite.removeDataFromDomain(host);
135 this.addItem("sitesettings", {
137 description: "Site preferences",
139 action: function (range, host) {
143 for (let p in Sanitizer.iterPermissions(host)) {
144 services.permissions.remove(util.createURI(p.host), p.type);
145 services.permissions.add(util.createURI(p.host), p.type, 0);
147 for (let p in iter(services.contentPrefs.getPrefs(util.createURI(host))))
148 services.contentPrefs.removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name);
151 // "Allow this site to open popups" ...
152 services.permissions.removeAll();
154 services.contentPrefs.removeAllDomains(null);
157 // "Never remember passwords" ...
158 for (let domain of services.loginManager.getAllDisabledHosts())
159 if (!host || util.isSubdomain(domain, host))
160 services.loginManager.setLoginSavingEnabled(host, true);
165 function ourItems(persistent) [
166 item for (item in values(self.itemMap))
167 if (!item.builtin && (!persistent || item.persistent) && item.name !== "all")
170 function prefOverlay(branch, persistent, local) update(Object.create(local), {
172 ["preferences", { id: branch.substr(Item.PREFIX.length) + "history",
174 template.map(ourItems(persistent), item =>
175 ["preference", { type: "bool", id: branch + item.name, name: branch + item.name }])]
177 init: function init(win) {
178 let pane = win.document.getElementById("SanitizeDialogPane");
179 for (let [, pref] in iter(pane.preferences))
180 pref.updateElements();
181 init.superapply(this, arguments);
185 util.timeout(function () { // Load order issue...
187 let (branch = Item.PREFIX + Item.SHUTDOWN_BRANCH) {
188 overlay.overlayWindow("chrome://browser/content/preferences/sanitize.xul",
189 function (win) prefOverlay(branch, true, {
192 ["groupbox", { orient: "horizontal", xmlns: "xul" },
193 ["caption", { label: config.appName + /*L*/" (see :help privacy)" }],
194 ["grid", { flex: "1" },
196 ["column", { flex: "1" }],
197 ["column", { flex: "1" }]],
199 let (items = ourItems(true))
200 template.map(util.range(0, Math.ceil(items.length / 2)), i =>
202 template.map(items.slice(i * 2, i * 2 + 2), item =>
203 ["checkbox", { xmlns: XUL, label: item.description, preference: branch + item.name }])])]]]
207 let (branch = Item.PREFIX + Item.BRANCH) {
208 overlay.overlayWindow("chrome://browser/content/sanitize.xul",
209 function (win) prefOverlay(branch, false, {
212 ["listitem", { xmlns: "xul", label: /*L*/"See :help privacy for the following:",
213 disabled: "true", style: "font-style: italic; font-weight: bold;" }],
214 template.map(ourItems(), ([item, desc]) =>
215 ["listitem", { xmlns: "xul", preference: branch + item,
216 type: "checkbox", label: config.appName + ", " + desc,
217 onsyncfrompreference: "return gSanitizePromptDialog.onReadGeneric();" }])
220 ready: function ready(win) {
221 let elem = win.document.getElementById("itemList");
222 elem.setAttribute("rows", elem.itemCount);
223 win.Sanitizer = Class("Sanitizer", win.Sanitizer, {
224 sanitize: function sanitize() {
225 self.withSavedValues(["sanitizing"], function () {
226 self.sanitizing = true;
227 sanitize.superapply(this, arguments);
228 sanitizer.sanitizeItems([item.name for (item in values(self.itemMap))
229 if (item.shouldSanitize(false))],
230 Range.fromArray(this.range || []));
242 addItem: function addItem(name, params) {
243 let item = this.itemMap[name] || Item(name, params);
244 this.itemMap[name] = item;
246 for (let [k, prop] in iterOwnProperties(params))
247 if (!("value" in prop) || !callable(prop.value) && !(k in item))
248 Object.defineProperty(item, k, prop);
250 function getWindow(obj) {
251 obj = Class.objectGlobal(obj);
252 return obj.window || obj;
255 let names = RealSet([name].concat(params.contains || []).map(e => "clear-" + e));
257 storage.addObserver("sanitizer",
258 function (key, event, arg) {
259 if (names.has(event))
260 params.action.apply(params, arg);
262 getWindow(params.action));
264 if (params.privateEnter || params.privateLeave)
265 storage.addObserver("private-mode",
266 function (key, event, arg) {
267 let meth = params[arg ? "privateEnter" : "privateLeave"];
271 getWindow(params.privateEnter || params.privateLeave));
275 "browser:purge-domain-data": function (subject, host) {
276 storage.fireEvent("sanitize", "domain", host);
277 // If we're sanitizing, our own sanitization functions will already
278 // be called, and with much greater granularity. Only process this
279 // event if it's triggered externally.
280 if (!this.sanitizing)
281 this.sanitizeItems(null, Range(), data);
283 "browser:purge-session-history": function (subject, data) {
285 if (!this.sanitizing)
286 this.sanitizeItems(null, Range(this.sessionStart), null, "sessionHistory");
288 "quit-application-granted": function (subject, data) {
289 if (this.runAtShutdown && !this.sanitizeItems(null, Range(), null, "shutdown"))
290 this.ranAtShutdown = true;
292 "private-browsing": function (subject, data) {
294 storage.privateMode = true;
295 else if (data == "exit")
296 storage.privateMode = false;
297 storage.fireEvent("private-mode", "change", storage.privateMode);
302 * Returns a load context for the given thing, to be used with
303 * interfaces needing one for per-window private browsing support.
305 * @param {Window|Document|Node} thing The thing for which to return
308 getContext: function getContext(thing) {
309 if (!Ci.nsILoadContext)
312 if (thing instanceof Ci.nsIDOMNode && thing.ownerDocument)
313 thing = thing.ownerDocument;
314 if (thing instanceof Ci.nsIDOMDocument)
315 thing = thing.defaultView;
316 if (thing instanceof Ci.nsIInterfaceRequestor)
317 thing = thing.getInterface(Ci.nsIWebNavigation);
318 return thing.QueryInterface(Ci.nsILoadContext);
321 get ranAtShutdown() config.prefs.get("didSanitizeOnShutdown"),
322 set ranAtShutdown(val) config.prefs.set("didSanitizeOnShutdown", Boolean(val)),
323 get runAtShutdown() prefs.get("privacy.sanitize.sanitizeOnShutdown"),
324 set runAtShutdown(val) prefs.set("privacy.sanitize.sanitizeOnShutdown", Boolean(val)),
326 sanitize: function sanitize(items, range)
327 this.withSavedValues(["sanitizing"], function () {
328 this.sanitizing = true;
329 let errors = this.sanitizeItems(items, range, null);
331 for (let itemName in values(items)) {
333 let item = this.items[Sanitizer.argToPref(itemName)];
334 if (item && !this.itemMap[itemName].override) {
335 item.range = range.native;
336 if ("clear" in item && item.canClear)
341 errors = errors || {};
342 errors[itemName] = e;
343 util.dump("Error sanitizing " + itemName);
350 sanitizeItems: function sanitizeItems(items, range, host, key)
351 this.withSavedValues(["sanitizing"], function () {
352 this.sanitizing = true;
354 items = Object.keys(this.itemMap);
357 for (let itemName in values(items))
359 if (!key || this.itemMap[itemName][key])
360 storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]);
363 errors = errors || {};
364 errors[itemName] = e;
365 util.dump("Error sanitizing " + itemName);
378 UNPERMS: Class.Memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
381 "unset": /*L*/"Unset",
382 "allow": /*L*/"Allowed",
383 "deny": /*L*/"Denied",
384 "session": /*L*/"Allowed for the current session",
385 "list": /*L*/"List all cookies for domain",
386 "clear": /*L*/"Clear all cookies for domain",
387 "clear-persistent": /*L*/"Clear all persistent cookies for domain",
388 "clear-session": /*L*/"Clear all session cookies for domain"
392 offlineapps: "offlineApps",
393 sitesettings: "siteSettings"
395 argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg,
396 prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
398 iterCookies: function iterCookies(host) {
399 for (let c in iter(services.cookies, Ci.nsICookie2))
400 if (!host || util.isSubdomain(c.rawHost, host) ||
401 c.host[0] == "." && c.host.length < host.length
402 && host.endsWith(c.host))
406 iterPermissions: function iterPermissions(host) {
407 for (let p in iter(services.permissions, Ci.nsIPermission))
408 if (!host || util.isSubdomain(p.host, host))
412 load: function initLoad(dactyl, modules, window) {
413 if (!sanitizer.firstRun++ && sanitizer.runAtShutdown && !sanitizer.ranAtShutdown)
414 sanitizer.sanitizeItems(null, Range(), null, "shutdown");
415 sanitizer.ranAtShutdown = false;
417 autocommands: function initAutocommands(dactyl, modules, window) {
418 const { autocommands } = modules;
420 storage.addObserver("private-mode",
421 function (key, event, value) {
422 autocommands.trigger("PrivateMode", { state: value });
424 storage.addObserver("sanitizer",
425 function (key, event, value) {
426 if (event == "domain")
427 autocommands.trigger("SanitizeDomain", { domain: value });
429 autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
432 commands: function initCommands(dactyl, modules, window) {
433 const { commands } = modules;
434 commands.add(["sa[nitize]"],
435 "Clear private data",
437 dactyl.assert(!modules.options['private'], _("command.sanitize.privateMode"));
439 if (args["-host"] && !args.length && !args.bang)
442 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
445 let [match, num, unit] = /^(\d+)([mhdw])$/.exec(timespan) || [];
446 range[args["-older"] ? "max" : "min"] =
447 match ? 1000 * (Date.now() - 1000 * parseInt(num, 10) * { m: 60, h: 3600, d: 3600 * 24, w: 3600 * 24 * 7 }[unit])
448 : (timespan[0] == "s" ? sanitizer.sessionStart : null);
450 let opt = modules.options.get("sanitizeitems");
452 dactyl.assert(args.length == 0, _("error.trailingCharacters"));
454 dactyl.assert(args.length, _("error.argumentRequired"));
455 dactyl.assert(opt.validator(args), _("error.invalidArgument"));
456 opt = { __proto__: opt, value: args.slice() };
459 let items = Object.keys(sanitizer.itemMap)
461 .filter(opt.has, opt);
463 function sanitize(items) {
464 sanitizer.range = range.native;
465 sanitizer.ignoreTimespan = range.min == null;
466 sanitizer.sanitizing = true;
468 args["-host"].forEach(function (host) {
469 sanitizer.sanitizing = true;
470 sanitizer.sanitizeItems(items, range, host);
474 sanitizer.sanitize(items, range);
477 if ("all" == opt.value.find(i => (i == "all" ||
481 modules.commandline.input(_("sanitize.prompt.deleteAll") + " ",
483 if (resp.match(/^y(es)?$/i)) {
485 dactyl.echomsg(_("command.sanitize.allDeleted"));
488 dactyl.echo(_("command.sanitize.noneDeleted"));
495 argCount: "*", // FIXME: should be + and 0
497 completer: function (context) {
498 context.title = ["Privacy Item", "Description"];
499 context.completions = modules.options.get("sanitizeitems").values;
501 domains: function (args) args["-host"] || [],
504 names: ["-host", "-h"],
505 description: "Only sanitize items referring to listed host or hosts",
506 completer: function (context, args) {
507 context.filters.push(item =>
508 !args["-host"].some(host => util.isSubdomain(item.text, host)));
509 modules.completion.domain(context);
511 type: modules.CommandOption.LIST
513 names: ["-older", "-o"],
514 description: "Sanitize items older than timespan",
515 type: modules.CommandOption.NOARG
517 names: ["-timespan", "-t"],
518 description: "Timespan for which to sanitize items",
519 completer: function (context) modules.options.get("sanitizetimespan").completer(context),
520 type: modules.CommandOption.STRING,
521 validator: function (arg) modules.options.get("sanitizetimespan").validator(arg)
527 function getPerms(host) {
528 let uri = util.createURI(host);
530 return Sanitizer.UNPERMS[services.permissions.testPermission(uri, "cookie")];
533 function setPerms(host, perm) {
534 let uri = util.createURI(host);
535 services.permissions.remove(uri.host, "cookie");
536 services.permissions.add(uri, "cookie", Sanitizer.PERMS[perm]);
538 commands.add(["cookies", "ck"],
539 "Change cookie permissions for sites",
541 let host = args.shift();
544 args = modules.options["cookies"];
546 for (let [, cmd] in Iterator(args))
549 for (let c in Sanitizer.iterCookies(host))
550 services.cookies.remove(c.host, c.name, c.path, false);
552 case "clear-persistent":
554 case "clear-session":
555 for (let c in Sanitizer.iterCookies(host))
556 if (c.isSession == session)
557 services.cookies.remove(c.host, c.name, c.path, false);
561 modules.commandline.commandOutput(template.tabular(
562 ["Host", "Expiry (UTC)", "Path", "Name", "Value"],
563 ["padding-right: 1em", "padding-right: 1em", "padding-right: 1em", "max-width: 12em; overflow: hidden;", "padding-left: 1ex;"],
565 c.isSession ? ["span", { highlight: "Enabled" }, "session"]
566 : (new Date(c.expiry * 1000).toJSON() || "Never").replace(/:\d\d\.000Z/, "").replace("T", " ").replace(/-/g, "/"),
570 for (c in Sanitizer.iterCookies(host)))));
573 util.assert(cmd in Sanitizer.PERMS, _("error.invalidArgument"));
578 completer: function (context, args) {
579 switch (args.completeArg) {
581 modules.completion.visibleHosts(context);
582 context.title[1] = "Current Permissions";
583 context.keys.description = function desc(host) {
585 for (let c in Sanitizer.iterCookies(host))
586 count[c.isSession + 0]++;
587 return [Sanitizer.COMMANDS[getPerms(host)], " (session: ", count[1], " persistent: ", count[0], ")"].join("");
591 context.completions = Sanitizer.COMMANDS;
597 completion: function initCompletion(dactyl, modules, window) {
598 modules.completion.visibleHosts = function completeHosts(context) {
599 let res = util.visibleHosts(window.content);
600 if (context.filter && !res.some(host => host.contains(context.filter)))
601 res.push(context.filter);
603 context.title = ["Domain"];
604 context.anchored = false;
605 context.compare = modules.CompletionContext.Sort.unsorted;
606 context.keys = { text: util.identity, description: util.identity };
607 context.completions = res;
610 options: function initOptions(dactyl, modules) {
611 const options = modules.options;
613 options.add(["sanitizeitems", "si"],
614 "The default list of private items to sanitize",
617 get values() values(sanitizer.itemMap).toArray(),
619 completer: function completer(context, extra) {
620 if (context.filter[0] == "!")
622 return completer.superapply(this, arguments);
625 has: function has(val)
626 let (res = this.value.find(v => (v == "all" || v.replace(/^!/, "") == val)))
627 res && !/^!/.test(res),
629 validator: function (values) values.length &&
630 values.every(val => (val === "all" || hasOwnProperty(sanitizer.itemMap, val.replace(/^!/, ""))))
633 options.add(["sanitizeshutdown", "ss"],
634 "The items to sanitize automatically at shutdown",
638 get values() [i for (i in values(sanitizer.itemMap)) if (i.persistent || i.builtin)],
639 getter: function () !sanitizer.runAtShutdown ? [] : [
640 item.name for (item in values(sanitizer.itemMap))
641 if (item.shouldSanitize(true))
643 setter: function (value) {
644 if (value.length === 0)
645 sanitizer.runAtShutdown = false;
647 sanitizer.runAtShutdown = true;
648 let have = RealSet(value);
649 for (let item in values(sanitizer.itemMap))
650 prefs.set(item.shutdownPref,
651 Boolean(have.has(item.name) ^ have.has("all")));
657 options.add(["sanitizetimespan", "sts"],
658 "The default sanitizer time span",
661 completer: function (context) {
662 context.compare = context.constructor.Sort.Unsorted;
663 context.completions = this.values;
667 "session": "The current session",
668 "10m": "Last ten minutes",
673 validator: bind("test", /^(a(ll)?|s(ession)|\d+[mhdw])$/)
676 options.add(["cookies", "ck"],
677 "The default mode for newly added cookie permissions",
678 "stringlist", "session",
679 { get values() Sanitizer.COMMANDS });
681 options.add(["cookieaccept", "ca"],
682 "When to accept cookies",
685 PREF: "network.cookie.cookieBehavior",
687 ["all", "Accept all cookies"],
688 ["samesite", "Accept all non-third-party cookies"],
689 ["none", "Accept no cookies"]
691 getter: function () (this.values[prefs.get(this.PREF)] || ["all"])[0],
692 setter: function (val) {
693 prefs.set(this.PREF, this.values.map(i => i[0]).indexOf(val));
700 options.add(["cookielifetime", "cl"],
701 "The lifetime for which to accept cookies",
702 "string", "default", {
703 PREF: "network.cookie.lifetimePolicy",
704 PREF_DAYS: "network.cookie.lifetime.days",
706 ["default", "The lifetime requested by the setter"],
707 ["prompt", "Always prompt for a lifetime"],
708 ["session", "The current session"]
710 getter: function () (this.values[prefs.get(this.PREF)] || [prefs.get(this.PREF_DAYS)])[0],
711 setter: function (value) {
712 let val = this.values.map(i => i[0]).indexOf(value);
714 prefs.set(this.PREF, val);
716 prefs.set(this.PREF, 3);
717 prefs.set(this.PREF_DAYS, parseInt(value));
722 validator: function validator(val) parseInt(val) == val || validator.superapply(this, arguments)
729 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
731 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: