]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/sanitizer.jsm
finalize changelog for 7904
[dactyl.git] / common / modules / sanitizer.jsm
1 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2009-2014 Kris Maglione <maglione.k at Gmail>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 "use strict";
7
8 // TODO:
9 //   - fix Sanitize autocommand
10 //   - add warning for TIMESPAN_EVERYTHING?
11
12 // FIXME:
13 //   - finish 1.9.0 support if we're going to support sanitizing in Melodactyl
14
15 defineModule("sanitizer", {
16     exports: ["Range", "Sanitizer", "sanitizer"],
17     require: ["config", "prefs", "services", "util"]
18 });
19
20 lazyRequire("messages", ["_"]);
21 lazyRequire("overlay", ["overlay"]);
22 lazyRequire("storage", ["storage"]);
23 lazyRequire("template", ["template"]);
24
25 let tmp = Object.create(this);
26 JSMLoader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
27 tmp.Sanitizer.prototype.__proto__ = Class.prototype;
28
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),
33
34     get isEternity() this.max == null && this.min == null,
35     get isSession() this.max == null && this.min == sanitizer.sessionStart,
36
37     get native() this.isEternity ? null : [this.min || 0, this.max == null ? Number.MAX_VALUE : this.max]
38 });
39
40 var Item = Class("SanitizeItem", {
41     init: function (name, params) {
42         this.name = name;
43         this.description = params.description;
44     },
45
46     // Hack for completion:
47     "0": Class.Property({ get: function () this.name }),
48     "1": Class.Property({ get: function () this.description }),
49
50     description: Messages.Localized(""),
51
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),
56
57     shouldSanitize: function (shutdown) (!shutdown || this.builtin || this.persistent) &&
58         prefs.get(shutdown ? this.shutdownPref : this.pref)
59 }, {
60     PREFIX: config.prefs.branch.root,
61     BRANCH: "privacy.cpd.",
62     SHUTDOWN_BRANCH: "privacy.clearOnShutdown."
63 });
64
65 var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference], tmp.Sanitizer), {
66     sessionStart: Date.now() * 1000,
67
68     init: function () {
69         const self = this;
70
71         util.addObserver(this);
72
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);
77
78         this.itemMap = {};
79
80         this.addItem("all", { description: "Sanitize all items", shouldSanitize: function () false });
81         // Builtin items
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" });
88
89         // These builtin methods don't support hosts or otherwise have
90         // insufficient granularity
91         this.addItem("cookies", {
92             builtin: true,
93             description: "Cookies",
94             persistent: true,
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);
99             },
100             override: true
101         });
102         this.addItem("history", {
103             builtin: true,
104             description: "Browsing history",
105             persistent: true,
106             sessionHistory: true,
107             action: function (range, host) {
108                 if (host)
109                     services.history.removePagesFromHost(host, true);
110                 else {
111                     if (range.isEternity)
112                         services.history.removeAllPages();
113                     else
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", "");
116                 }
117
118                 if (!host || util.isDomainURL(prefs.get("general.open_location.last_url"), host))
119                     prefs.reset("general.open_location.last_url");
120             },
121             override: true
122         });
123         try {
124             var { ForgetAboutSite } = Cu.import("resource://gre/modules/ForgetAboutSite.jsm", {});
125         }
126         catch (e) {}
127         if (ForgetAboutSite)
128             this.addItem("host", {
129                 description: "All data from the given host",
130                 action: function (range, host) {
131                     if (host)
132                         ForgetAboutSite.removeDataFromDomain(host);
133                 }
134             });
135         this.addItem("sitesettings", {
136             builtin: true,
137             description: "Site preferences",
138             persistent: true,
139             action: function (range, host) {
140                 if (range.isSession)
141                     return;
142                 if (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);
146                     }
147                     for (let p in iter(services.contentPrefs.getPrefs(util.createURI(host))))
148                         services.contentPrefs.removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name);
149                 }
150                 else {
151                     // "Allow this site to open popups" ...
152                     services.permissions.removeAll();
153                     // Zoom level, ...
154                     services.contentPrefs.removeAllDomains(null);
155                 }
156
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);
161             },
162             override: true
163         });
164
165         function ourItems(persistent) [
166             item for (item in values(self.itemMap))
167             if (!item.builtin && (!persistent || item.persistent) && item.name !== "all")
168         ];
169
170         function prefOverlay(branch, persistent, local) update(Object.create(local), {
171             before: [
172                 ["preferences", { id: branch.substr(Item.PREFIX.length) + "history",
173                                   xmlns: "xul" },
174                   template.map(ourItems(persistent), item =>
175                       ["preference", { type: "bool", id: branch + item.name, name: branch + item.name }])]
176             ],
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);
182             }
183         });
184
185         util.timeout(function () { // Load order issue...
186
187             let (branch = Item.PREFIX + Item.SHUTDOWN_BRANCH) {
188                 overlay.overlayWindow("chrome://browser/content/preferences/sanitize.xul",
189                                       function (win) prefOverlay(branch, true, {
190                     append: {
191                         SanitizeDialogPane:
192                             ["groupbox", { orient: "horizontal", xmlns: "xul" },
193                               ["caption", { label: config.appName + /*L*/" (see :help privacy)" }],
194                               ["grid", { flex: "1" },
195                                 ["columns", {},
196                                     ["column", { flex: "1" }],
197                                     ["column", { flex: "1" }]],
198                                 ["rows", {},
199                                   let (items = ourItems(true))
200                                      template.map(util.range(0, Math.ceil(items.length / 2)), i =>
201                                          ["row", {},
202                                              template.map(items.slice(i * 2, i * 2 + 2), item =>
203                                                 ["checkbox", { xmlns: XUL, label: item.description, preference: branch + item.name }])])]]]
204                     }
205                 }));
206             }
207             let (branch = Item.PREFIX + Item.BRANCH) {
208                 overlay.overlayWindow("chrome://browser/content/sanitize.xul",
209                                    function (win) prefOverlay(branch, false, {
210                     append: {
211                         itemList: [
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();" }])
218                         ]
219                     },
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 || []));
231                                 }, this);
232                             }
233                         });
234                     }
235                 }));
236             }
237         });
238     },
239
240     firstRun: 0,
241
242     addItem: function addItem(name, params) {
243         let item = this.itemMap[name] || Item(name, params);
244         this.itemMap[name] = item;
245
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);
249
250         function getWindow(obj) {
251             obj = Class.objectGlobal(obj);
252             return obj.window || obj;
253         }
254
255         let names = RealSet([name].concat(params.contains || []).map(e => "clear-" + e));
256         if (params.action)
257             storage.addObserver("sanitizer",
258                 function (key, event, arg) {
259                     if (names.has(event))
260                         params.action.apply(params, arg);
261                 },
262                 getWindow(params.action));
263
264         if (params.privateEnter || params.privateLeave)
265             storage.addObserver("private-mode",
266                 function (key, event, arg) {
267                     let meth = params[arg ? "privateEnter" : "privateLeave"];
268                     if (meth)
269                         meth.call(params);
270                 },
271                 getWindow(params.privateEnter || params.privateLeave));
272     },
273
274     observers: {
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);
282         },
283         "browser:purge-session-history": function (subject, data) {
284             // See above.
285             if (!this.sanitizing)
286                 this.sanitizeItems(null, Range(this.sessionStart), null, "sessionHistory");
287         },
288         "quit-application-granted": function (subject, data) {
289             if (this.runAtShutdown && !this.sanitizeItems(null, Range(), null, "shutdown"))
290                 this.ranAtShutdown = true;
291         },
292         "private-browsing": function (subject, data) {
293             if (data == "enter")
294                 storage.privateMode = true;
295             else if (data == "exit")
296                 storage.privateMode = false;
297             storage.fireEvent("private-mode", "change", storage.privateMode);
298         }
299     },
300
301     /**
302      * Returns a load context for the given thing, to be used with
303      * interfaces needing one for per-window private browsing support.
304      *
305      * @param {Window|Document|Node} thing The thing for which to return
306      *      a load context.
307      */
308     getContext: function getContext(thing) {
309         if (!Ci.nsILoadContext)
310             return null;
311
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);
319     },
320
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)),
325
326     sanitize: function sanitize(items, range)
327         this.withSavedValues(["sanitizing"], function () {
328             this.sanitizing = true;
329             let errors = this.sanitizeItems(items, range, null);
330
331             for (let itemName in values(items)) {
332                 try {
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)
337                             item.clear();
338                     }
339                 }
340                 catch (e) {
341                     errors = errors || {};
342                     errors[itemName] = e;
343                     util.dump("Error sanitizing " + itemName);
344                     util.reportError(e);
345                 }
346             }
347             return errors;
348         }),
349
350     sanitizeItems: function sanitizeItems(items, range, host, key)
351         this.withSavedValues(["sanitizing"], function () {
352             this.sanitizing = true;
353             if (items == null)
354                 items = Object.keys(this.itemMap);
355
356             let errors;
357             for (let itemName in values(items))
358                 try {
359                     if (!key || this.itemMap[itemName][key])
360                         storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]);
361                 }
362                 catch (e) {
363                     errors = errors || {};
364                     errors[itemName] = e;
365                     util.dump("Error sanitizing " + itemName);
366                     util.reportError(e);
367                 }
368             return errors;
369         })
370 }, {
371     PERMS: {
372         unset:   0,
373         allow:   1,
374         deny:    2,
375         session: 8
376     },
377
378     UNPERMS: Class.Memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
379
380     COMMANDS: {
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"
389     },
390
391     argPrefMap: {
392         offlineapps:  "offlineApps",
393         sitesettings: "siteSettings"
394     },
395     argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg,
396     prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
397
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))
403                 yield c;
404
405     },
406     iterPermissions: function iterPermissions(host) {
407         for (let p in iter(services.permissions, Ci.nsIPermission))
408             if (!host || util.isSubdomain(p.host, host))
409                 yield p;
410     }
411 }, {
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;
416     },
417     autocommands: function initAutocommands(dactyl, modules, window) {
418         const { autocommands } = modules;
419
420         storage.addObserver("private-mode",
421             function (key, event, value) {
422                 autocommands.trigger("PrivateMode", { state: value });
423             }, window);
424         storage.addObserver("sanitizer",
425             function (key, event, value) {
426                 if (event == "domain")
427                     autocommands.trigger("SanitizeDomain", { domain: value });
428                 else if (!value[1])
429                     autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
430             }, window);
431     },
432     commands: function initCommands(dactyl, modules, window) {
433         const { commands } = modules;
434         commands.add(["sa[nitize]"],
435             "Clear private data",
436             function (args) {
437                 dactyl.assert(!modules.options['private'], _("command.sanitize.privateMode"));
438
439                 if (args["-host"] && !args.length && !args.bang)
440                     args[0] = "all";
441
442                 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
443
444                 let range = Range();
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);
449
450                 let opt = modules.options.get("sanitizeitems");
451                 if (args.bang)
452                     dactyl.assert(args.length == 0, _("error.trailingCharacters"));
453                 else {
454                     dactyl.assert(args.length, _("error.argumentRequired"));
455                     dactyl.assert(opt.validator(args), _("error.invalidArgument"));
456                     opt = { __proto__: opt, value: args.slice() };
457                 }
458
459                 let items = Object.keys(sanitizer.itemMap)
460                                   .slice(1)
461                                   .filter(opt.has, opt);
462
463                 function sanitize(items) {
464                     sanitizer.range = range.native;
465                     sanitizer.ignoreTimespan = range.min == null;
466                     sanitizer.sanitizing = true;
467                     if (args["-host"]) {
468                         args["-host"].forEach(function (host) {
469                             sanitizer.sanitizing = true;
470                             sanitizer.sanitizeItems(items, range, host);
471                         });
472                     }
473                     else
474                         sanitizer.sanitize(items, range);
475                 }
476
477                 if ("all" == opt.value.find(i => (i == "all" ||
478                                                   /^!/.test(i)))
479                     && !args["-host"])
480
481                     modules.commandline.input(_("sanitize.prompt.deleteAll") + " ",
482                         function (resp) {
483                             if (resp.match(/^y(es)?$/i)) {
484                                 sanitize(items);
485                                 dactyl.echomsg(_("command.sanitize.allDeleted"));
486                             }
487                             else
488                                 dactyl.echo(_("command.sanitize.noneDeleted"));
489                         });
490                 else
491                     sanitize(items);
492
493             },
494             {
495                 argCount: "*", // FIXME: should be + and 0
496                 bang: true,
497                 completer: function (context) {
498                     context.title = ["Privacy Item", "Description"];
499                     context.completions = modules.options.get("sanitizeitems").values;
500                 },
501                 domains: function (args) args["-host"] || [],
502                 options: [
503                     {
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);
510                         },
511                         type: modules.CommandOption.LIST
512                     }, {
513                         names: ["-older", "-o"],
514                         description: "Sanitize items older than timespan",
515                         type: modules.CommandOption.NOARG
516                     }, {
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)
522                     }
523                 ],
524                 privateData: true
525             });
526
527             function getPerms(host) {
528                 let uri = util.createURI(host);
529                 if (uri)
530                     return Sanitizer.UNPERMS[services.permissions.testPermission(uri, "cookie")];
531                 return "unset";
532             }
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]);
537             }
538             commands.add(["cookies", "ck"],
539                 "Change cookie permissions for sites",
540                 function (args) {
541                     let host = args.shift();
542                     let session = true;
543                     if (!args.length)
544                         args = modules.options["cookies"];
545
546                     for (let [, cmd] in Iterator(args))
547                         switch (cmd) {
548                         case "clear":
549                             for (let c in Sanitizer.iterCookies(host))
550                                 services.cookies.remove(c.host, c.name, c.path, false);
551                             break;
552                         case "clear-persistent":
553                             session = false;
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);
558                             return;
559
560                         case "list":
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;"],
564                                 ([c.host,
565                                   c.isSession ? ["span", { highlight: "Enabled" }, "session"]
566                                               : (new Date(c.expiry * 1000).toJSON() || "Never").replace(/:\d\d\.000Z/, "").replace("T", " ").replace(/-/g, "/"),
567                                   c.path,
568                                   c.name,
569                                   c.value]
570                                   for (c in Sanitizer.iterCookies(host)))));
571                             return;
572                         default:
573                             util.assert(cmd in Sanitizer.PERMS, _("error.invalidArgument"));
574                             setPerms(host, cmd);
575                         }
576                 }, {
577                     argCount: "+",
578                     completer: function (context, args) {
579                         switch (args.completeArg) {
580                         case 0:
581                             modules.completion.visibleHosts(context);
582                             context.title[1] = "Current Permissions";
583                             context.keys.description = function desc(host) {
584                                 let count = [0, 0];
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("");
588                             };
589                             break;
590                         case 1:
591                             context.completions = Sanitizer.COMMANDS;
592                             break;
593                         }
594                     },
595                 });
596     },
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);
602
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;
608         };
609     },
610     options: function initOptions(dactyl, modules) {
611         const options = modules.options;
612
613         options.add(["sanitizeitems", "si"],
614             "The default list of private items to sanitize",
615             "stringlist", "all",
616             {
617                 get values() values(sanitizer.itemMap).toArray(),
618
619                 completer: function completer(context, extra) {
620                     if (context.filter[0] == "!")
621                         context.advance(1);
622                     return completer.superapply(this, arguments);
623                 },
624
625                 has: function has(val)
626                     let (res = this.value.find(v => (v == "all" || v.replace(/^!/, "") == val)))
627                         res && !/^!/.test(res),
628
629                 validator: function (values) values.length &&
630                     values.every(val => (val === "all" || hasOwnProperty(sanitizer.itemMap, val.replace(/^!/, ""))))
631             });
632
633         options.add(["sanitizeshutdown", "ss"],
634             "The items to sanitize automatically at shutdown",
635             "stringlist", "",
636             {
637                 initialValue: true,
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))
642                 ],
643                 setter: function (value) {
644                     if (value.length === 0)
645                         sanitizer.runAtShutdown = false;
646                     else {
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")));
652                     }
653                     return value;
654                 }
655             });
656
657         options.add(["sanitizetimespan", "sts"],
658             "The default sanitizer time span",
659             "string", "all",
660             {
661                 completer: function (context) {
662                     context.compare = context.constructor.Sort.Unsorted;
663                     context.completions = this.values;
664                 },
665                 values: {
666                     "all":     "Everything",
667                     "session": "The current session",
668                     "10m":     "Last ten minutes",
669                     "1h":      "Past hour",
670                     "1d":      "Past day",
671                     "1w":      "Past week"
672                 },
673                 validator: bind("test", /^(a(ll)?|s(ession)|\d+[mhdw])$/)
674             });
675
676         options.add(["cookies", "ck"],
677             "The default mode for newly added cookie permissions",
678             "stringlist", "session",
679             { get values() Sanitizer.COMMANDS });
680
681         options.add(["cookieaccept", "ca"],
682             "When to accept cookies",
683             "string", "all",
684             {
685                 PREF: "network.cookie.cookieBehavior",
686                 values: [
687                     ["all", "Accept all cookies"],
688                     ["samesite", "Accept all non-third-party cookies"],
689                     ["none", "Accept no cookies"]
690                 ],
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));
694                     return val;
695                 },
696                 initialValue: true,
697                 persist: false
698             });
699
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",
705                 values: [
706                     ["default", "The lifetime requested by the setter"],
707                     ["prompt",  "Always prompt for a lifetime"],
708                     ["session", "The current session"]
709                 ],
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);
713                     if (val > -1)
714                         prefs.set(this.PREF, val);
715                     else {
716                         prefs.set(this.PREF, 3);
717                         prefs.set(this.PREF_DAYS, parseInt(value));
718                     }
719                 },
720                 initialValue: true,
721                 persist: false,
722                 validator: function validator(val) parseInt(val) == val || validator.superapply(this, arguments)
723             });
724     }
725 });
726
727 endModule();
728
729 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
730
731 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: