1 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2009-2012 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("contentPrefs", "@mozilla.org/content-pref/service;1", Ci.nsIContentPrefService);
74 services.add("cookies", "@mozilla.org/cookiemanager;1", [Ci.nsICookieManager, Ci.nsICookieManager2,
75 Ci.nsICookieService]);
76 services.add("loginManager", "@mozilla.org/login-manager;1", Ci.nsILoginManager);
77 services.add("permissions", "@mozilla.org/permissionmanager;1", Ci.nsIPermissionManager);
81 this.addItem("all", { description: "Sanitize all items", shouldSanitize: function () false });
83 this.addItem("cache", { builtin: true, description: "Cache" });
84 this.addItem("downloads", { builtin: true, description: "Download history" });
85 this.addItem("formdata", { builtin: true, description: "Saved form and search history" });
86 this.addItem("offlineapps", { builtin: true, description: "Offline website data" });
87 this.addItem("passwords", { builtin: true, description: "Saved passwords" });
88 this.addItem("sessions", { builtin: true, description: "Authenticated sessions" });
90 // These builtin methods don't support hosts or otherwise have
91 // insufficient granularity
92 this.addItem("cookies", {
94 description: "Cookies",
96 action: function (range, host) {
97 for (let c in Sanitizer.iterCookies(host))
98 if (range.contains(c.creationTime) || timespan.isSession && c.isSession)
99 services.cookies.remove(c.host, c.name, c.path, false);
103 this.addItem("history", {
105 description: "Browsing history",
107 sessionHistory: true,
108 action: function (range, host) {
110 services.history.removePagesFromHost(host, true);
112 if (range.isEternity)
113 services.history.removeAllPages();
115 services.history.removeVisitsByTimeframe(range.native[0], Math.min(Date.now() * 1000, range.native[1])); // XXX
116 services.observer.notifyObservers(null, "browser:purge-session-history", "");
119 if (!host || util.isDomainURL(prefs.get("general.open_location.last_url"), host))
120 prefs.reset("general.open_location.last_url");
125 var { ForgetAboutSite } = Cu.import("resource://gre/modules/ForgetAboutSite.jsm", {});
129 this.addItem("host", {
130 description: "All data from the given host",
131 action: function (range, host) {
133 ForgetAboutSite.removeDataFromDomain(host);
136 this.addItem("sitesettings", {
138 description: "Site preferences",
140 action: function (range, host) {
144 for (let p in Sanitizer.iterPermissions(host)) {
145 services.permissions.remove(util.createURI(p.host), p.type);
146 services.permissions.add(util.createURI(p.host), p.type, 0);
148 for (let p in iter(services.contentPrefs.getPrefs(util.createURI(host))))
149 services.contentPrefs.removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name);
152 // "Allow this site to open popups" ...
153 services.permissions.removeAll();
155 services.contentPrefs.removeGroupedPrefs();
158 // "Never remember passwords" ...
159 for each (let domain in services.loginManager.getAllDisabledHosts())
160 if (!host || util.isSubdomain(domain, host))
161 services.loginManager.setLoginSavingEnabled(host, true);
166 function ourItems(persistent) [
167 item for (item in values(self.itemMap))
168 if (!item.builtin && (!persistent || item.persistent) && item.name !== "all")
171 function prefOverlay(branch, persistent, local) update(Object.create(local), {
173 ["preferences", { id: branch.substr(Item.PREFIX.length) + "history",
175 template.map(ourItems(persistent), function (item)
176 ["preference", { type: "bool", id: branch + item.name, name: branch + item.name }])]
178 init: function init(win) {
179 let pane = win.document.getElementById("SanitizeDialogPane");
180 for (let [, pref] in iter(pane.preferences))
181 pref.updateElements();
182 init.superapply(this, arguments);
186 util.timeout(function () { // Load order issue...
188 let (branch = Item.PREFIX + Item.SHUTDOWN_BRANCH) {
189 overlay.overlayWindow("chrome://browser/content/preferences/sanitize.xul",
190 function (win) prefOverlay(branch, true, {
193 ["groupbox", { orient: "horizontal", xmlns: "xul" },
194 ["caption", { label: config.appName + /*L*/" (see :help privacy)" }],
195 ["grid", { flex: "1" },
197 ["column", { flex: "1" }],
198 ["column", { flex: "1" }]],
200 let (items = ourItems(true))
201 template.map(util.range(0, Math.ceil(items.length / 2)), function (i)
203 template.map(items.slice(i * 2, i * 2 + 2), function (item)
204 ["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:",
214 disabled: "true", style: "font-style: italic; font-weight: bold;" }],
215 template.map(ourItems(), function ([item, desc])
216 ["listitem", { xmlns: "xul", preference: branch + item,
217 type: "checkbox", label: config.appName + ", " + desc,
218 onsyncfrompreference: "return gSanitizePromptDialog.onReadGeneric();" }]),
221 ready: function ready(win) {
222 let elem = win.document.getElementById("itemList");
223 elem.setAttribute("rows", elem.itemCount);
224 win.Sanitizer = Class("Sanitizer", win.Sanitizer, {
225 sanitize: function sanitize() {
226 self.withSavedValues(["sanitizing"], function () {
227 self.sanitizing = true;
228 sanitize.superapply(this, arguments);
229 sanitizer.sanitizeItems([item.name for (item in values(self.itemMap))
230 if (item.shouldSanitize(false))],
231 Range.fromArray(this.range || []));
243 addItem: function addItem(name, params) {
244 let item = this.itemMap[name] || Item(name, params);
245 this.itemMap[name] = item;
247 for (let [k, prop] in iterOwnProperties(params))
248 if (!("value" in prop) || !callable(prop.value) && !(k in item))
249 Object.defineProperty(item, k, prop);
251 let names = Set([name].concat(params.contains || []).map(function (e) "clear-" + e));
253 storage.addObserver("sanitizer",
254 function (key, event, arg) {
256 params.action.apply(params, arg);
258 Class.objectGlobal(params.action));
260 if (params.privateEnter || params.privateLeave)
261 storage.addObserver("private-mode",
262 function (key, event, arg) {
263 let meth = params[arg ? "privateEnter" : "privateLeave"];
267 Class.objectGlobal(params.action));
271 "browser:purge-domain-data": function (subject, host) {
272 storage.fireEvent("sanitize", "domain", host);
273 // If we're sanitizing, our own sanitization functions will already
274 // be called, and with much greater granularity. Only process this
275 // event if it's triggered externally.
276 if (!this.sanitizing)
277 this.sanitizeItems(null, Range(), data);
279 "browser:purge-session-history": function (subject, data) {
281 if (!this.sanitizing)
282 this.sanitizeItems(null, Range(this.sessionStart), null, "sessionHistory");
284 "quit-application-granted": function (subject, data) {
285 if (this.runAtShutdown && !this.sanitizeItems(null, Range(), null, "shutdown"))
286 this.ranAtShutdown = true;
288 "private-browsing": function (subject, data) {
290 storage.privateMode = true;
291 else if (data == "exit")
292 storage.privateMode = false;
293 storage.fireEvent("private-mode", "change", storage.privateMode);
298 * Returns a load context for the given thing, to be used with
299 * interfaces needing one for per-window private browsing support.
301 * @param {Window|Document|Node} thing The thing for which to return
304 getContext: function getContext(thing) {
305 if (!Ci.nsILoadContext)
308 if (thing instanceof Ci.nsIDOMNode && thing.ownerDocument)
309 thing = thing.ownerDocument;
310 if (thing instanceof Ci.nsIDOMDocument)
311 thing = thing.defaultView;
312 if (thing instanceof Ci.nsIInterfaceRequestor)
313 thing = thing.getInterface(Ci.nsIWebNavigation);
314 return thing.QueryInterface(Ci.nsILoadContext);
317 get ranAtShutdown() config.prefs.get("didSanitizeOnShutdown"),
318 set ranAtShutdown(val) config.prefs.set("didSanitizeOnShutdown", Boolean(val)),
319 get runAtShutdown() prefs.get("privacy.sanitize.sanitizeOnShutdown"),
320 set runAtShutdown(val) prefs.set("privacy.sanitize.sanitizeOnShutdown", Boolean(val)),
322 sanitize: function sanitize(items, range)
323 this.withSavedValues(["sanitizing"], function () {
324 this.sanitizing = true;
325 let errors = this.sanitizeItems(items, range, null);
327 for (let itemName in values(items)) {
329 let item = this.items[Sanitizer.argToPref(itemName)];
330 if (item && !this.itemMap[itemName].override) {
331 item.range = range.native;
332 if ("clear" in item && item.canClear)
337 errors = errors || {};
338 errors[itemName] = e;
339 util.dump("Error sanitizing " + itemName);
346 sanitizeItems: function sanitizeItems(items, range, host, key)
347 this.withSavedValues(["sanitizing"], function () {
348 this.sanitizing = true;
350 items = Object.keys(this.itemMap);
353 for (let itemName in values(items))
355 if (!key || this.itemMap[itemName][key])
356 storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]);
359 errors = errors || {};
360 errors[itemName] = e;
361 util.dump("Error sanitizing " + itemName);
374 UNPERMS: Class.Memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
378 allow: /*L*/"Allowed",
380 session: /*L*/"Allowed for the current session",
381 list: /*L*/"List all cookies for domain",
382 clear: /*L*/"Clear all cookies for domain",
383 "clear-persistent": /*L*/"Clear all persistent cookies for domain",
384 "clear-session": /*L*/"Clear all session cookies for domain"
388 offlineapps: "offlineApps",
389 sitesettings: "siteSettings"
391 argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg,
392 prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
394 iterCookies: function iterCookies(host) {
395 for (let c in iter(services.cookies, Ci.nsICookie2))
396 if (!host || util.isSubdomain(c.rawHost, host) ||
397 c.host[0] == "." && c.host.length < host.length
398 && host.indexOf(c.host) == host.length - c.host.length)
402 iterPermissions: function iterPermissions(host) {
403 for (let p in iter(services.permissions, Ci.nsIPermission))
404 if (!host || util.isSubdomain(p.host, host))
408 load: function initLoad(dactyl, modules, window) {
409 if (!sanitizer.firstRun++ && sanitizer.runAtShutdown && !sanitizer.ranAtShutdown)
410 sanitizer.sanitizeItems(null, Range(), null, "shutdown");
411 sanitizer.ranAtShutdown = false;
413 autocommands: function initAutocommands(dactyl, modules, window) {
414 const { autocommands } = modules;
416 storage.addObserver("private-mode",
417 function (key, event, value) {
418 autocommands.trigger("PrivateMode", { state: value });
420 storage.addObserver("sanitizer",
421 function (key, event, value) {
422 if (event == "domain")
423 autocommands.trigger("SanitizeDomain", { domain: value });
425 autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
428 commands: function initCommands(dactyl, modules, window) {
429 const { commands } = modules;
430 commands.add(["sa[nitize]"],
431 "Clear private data",
433 dactyl.assert(!modules.options['private'], _("command.sanitize.privateMode"));
435 if (args["-host"] && !args.length && !args.bang)
438 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
441 let [match, num, unit] = /^(\d+)([mhdw])$/.exec(timespan) || [];
442 range[args["-older"] ? "max" : "min"] =
443 match ? 1000 * (Date.now() - 1000 * parseInt(num, 10) * { m: 60, h: 3600, d: 3600 * 24, w: 3600 * 24 * 7 }[unit])
444 : (timespan[0] == "s" ? sanitizer.sessionStart : null);
446 let opt = modules.options.get("sanitizeitems");
448 dactyl.assert(args.length == 0, _("error.trailingCharacters"));
450 dactyl.assert(opt.validator(args), _("error.invalidArgument"));
451 opt = { __proto__: opt, value: args.slice() };
454 let items = Object.keys(sanitizer.itemMap).slice(1).filter(opt.has, opt);
456 function sanitize(items) {
457 sanitizer.range = range.native;
458 sanitizer.ignoreTimespan = range.min == null;
459 sanitizer.sanitizing = true;
461 args["-host"].forEach(function (host) {
462 sanitizer.sanitizing = true;
463 sanitizer.sanitizeItems(items, range, host);
467 sanitizer.sanitize(items, range);
470 if (array.nth(opt.value, function (i) i == "all" || /^!/.test(i), 0) == "all" && !args["-host"])
471 modules.commandline.input(_("sanitize.prompt.deleteAll") + " ",
473 if (resp.match(/^y(es)?$/i)) {
475 dactyl.echomsg(_("command.sanitize.allDeleted"));
478 dactyl.echo(_("command.sanitize.noneDeleted"));
485 argCount: "*", // FIXME: should be + and 0
487 completer: function (context) {
488 context.title = ["Privacy Item", "Description"];
489 context.completions = modules.options.get("sanitizeitems").values;
491 domains: function (args) args["-host"] || [],
494 names: ["-host", "-h"],
495 description: "Only sanitize items referring to listed host or hosts",
496 completer: function (context, args) {
497 context.filters.push(function (item)
498 !args["-host"].some(function (host) util.isSubdomain(item.text, host)));
499 modules.completion.domain(context);
501 type: modules.CommandOption.LIST
503 names: ["-older", "-o"],
504 description: "Sanitize items older than timespan",
505 type: modules.CommandOption.NOARG
507 names: ["-timespan", "-t"],
508 description: "Timespan for which to sanitize items",
509 completer: function (context) modules.options.get("sanitizetimespan").completer(context),
510 type: modules.CommandOption.STRING,
511 validator: function (arg) modules.options.get("sanitizetimespan").validator(arg),
517 function getPerms(host) {
518 let uri = util.createURI(host);
520 return Sanitizer.UNPERMS[services.permissions.testPermission(uri, "cookie")];
523 function setPerms(host, perm) {
524 let uri = util.createURI(host);
525 services.permissions.remove(uri.host, "cookie");
526 services.permissions.add(uri, "cookie", Sanitizer.PERMS[perm]);
528 commands.add(["cookies", "ck"],
529 "Change cookie permissions for sites",
531 let host = args.shift();
534 args = modules.options["cookies"];
536 for (let [, cmd] in Iterator(args))
539 for (let c in Sanitizer.iterCookies(host))
540 services.cookies.remove(c.host, c.name, c.path, false);
542 case "clear-persistent":
544 case "clear-session":
545 for (let c in Sanitizer.iterCookies(host))
546 if (c.isSession == session)
547 services.cookies.remove(c.host, c.name, c.path, false);
551 modules.commandline.commandOutput(template.tabular(
552 ["Host", "Expiry (UTC)", "Path", "Name", "Value"],
553 ["padding-right: 1em", "padding-right: 1em", "padding-right: 1em", "max-width: 12em; overflow: hidden;", "padding-left: 1ex;"],
555 c.isSession ? ["span", { highlight: "Enabled" }, "session"]
556 : (new Date(c.expiry * 1000).toJSON() || "Never").replace(/:\d\d\.000Z/, "").replace("T", " ").replace(/-/g, "/"),
560 for (c in Sanitizer.iterCookies(host)))));
563 util.assert(cmd in Sanitizer.PERMS, _("error.invalidArgument"));
568 completer: function (context, args) {
569 switch (args.completeArg) {
571 modules.completion.visibleHosts(context);
572 context.title[1] = "Current Permissions";
573 context.keys.description = function desc(host) {
575 for (let c in Sanitizer.iterCookies(host))
576 count[c.isSession + 0]++;
577 return [Sanitizer.COMMANDS[getPerms(host)], " (session: ", count[1], " persistent: ", count[0], ")"].join("");
581 context.completions = Sanitizer.COMMANDS;
587 completion: function initCompletion(dactyl, modules, window) {
588 modules.completion.visibleHosts = function completeHosts(context) {
589 let res = util.visibleHosts(window.content);
590 if (context.filter && !res.some(function (host) host.indexOf(context.filter) >= 0))
591 res.push(context.filter);
593 context.title = ["Domain"];
594 context.anchored = false;
595 context.compare = modules.CompletionContext.Sort.unsorted;
596 context.keys = { text: util.identity, description: util.identity };
597 context.completions = res;
600 options: function initOptions(dactyl, modules) {
601 const options = modules.options;
602 if (services.has("privateBrowsing") && "privateBrowsingEnabled" in services.privateBrowsing)
603 options.add(["private", "pornmode"],
604 "Set the 'private browsing' option",
608 getter: function () services.privateBrowsing.privateBrowsingEnabled,
609 setter: function (value) {
610 if (services.privateBrowsing.privateBrowsingEnabled != value)
611 services.privateBrowsing.privateBrowsingEnabled = value;
616 options.add(["sanitizeitems", "si"],
617 "The default list of private items to sanitize",
620 get values() values(sanitizer.itemMap).toArray(),
622 completer: function completer(context, extra) {
623 if (context.filter[0] == "!")
625 return completer.superapply(this, arguments);
628 has: function has(val)
629 let (res = array.nth(this.value, function (v) v == "all" || v.replace(/^!/, "") == val, 0))
630 res && !/^!/.test(res),
632 validator: function (values) values.length &&
633 values.every(function (val) val === "all" || Set.has(sanitizer.itemMap, val.replace(/^!/, "")))
636 options.add(["sanitizeshutdown", "ss"],
637 "The items to sanitize automatically at shutdown",
641 get values() [i for (i in values(sanitizer.itemMap)) if (i.persistent || i.builtin)],
642 getter: function () !sanitizer.runAtShutdown ? [] : [
643 item.name for (item in values(sanitizer.itemMap))
644 if (item.shouldSanitize(true))
646 setter: function (value) {
647 if (value.length === 0)
648 sanitizer.runAtShutdown = false;
650 sanitizer.runAtShutdown = true;
651 let have = Set(value);
652 for (let item in values(sanitizer.itemMap))
653 prefs.set(item.shutdownPref,
654 Boolean(Set.has(have, item.name) ^ Set.has(have, "all")));
660 options.add(["sanitizetimespan", "sts"],
661 "The default sanitizer time span",
664 completer: function (context) {
665 context.compare = context.constructor.Sort.Unsorted;
666 context.completions = this.values;
670 "session": "The current session",
671 "10m": "Last ten minutes",
676 validator: bind("test", /^(a(ll)?|s(ession)|\d+[mhdw])$/)
679 options.add(["cookies", "ck"],
680 "The default mode for newly added cookie permissions",
681 "stringlist", "session",
682 { get values() Sanitizer.COMMANDS });
684 options.add(["cookieaccept", "ca"],
685 "When to accept cookies",
688 PREF: "network.cookie.cookieBehavior",
690 ["all", "Accept all cookies"],
691 ["samesite", "Accept all non-third-party cookies"],
692 ["none", "Accept no cookies"]
694 getter: function () (this.values[prefs.get(this.PREF)] || ["all"])[0],
695 setter: function (val) {
696 prefs.set(this.PREF, this.values.map(function (i) i[0]).indexOf(val));
703 options.add(["cookielifetime", "cl"],
704 "The lifetime for which to accept cookies",
705 "string", "default", {
706 PREF: "network.cookie.lifetimePolicy",
707 PREF_DAYS: "network.cookie.lifetime.days",
709 ["default", "The lifetime requested by the setter"],
710 ["prompt", "Always prompt for a lifetime"],
711 ["session", "The current session"]
713 getter: function () (this.values[prefs.get(this.PREF)] || [prefs.get(this.PREF_DAYS)])[0],
714 setter: function (value) {
715 let val = this.values.map(function (i) i[0]).indexOf(value);
717 prefs.set(this.PREF, val);
719 prefs.set(this.PREF, 3);
720 prefs.set(this.PREF_DAYS, parseInt(value));
725 validator: function validator(val) parseInt(val) == val || validator.superapply(this, arguments)
732 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
734 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: