]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/sanitizer.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / sanitizer.jsm
1 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2009-2012 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("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);
78
79         this.itemMap = {};
80
81         this.addItem("all", { description: "Sanitize all items", shouldSanitize: function () false });
82         // Builtin items
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" });
89
90         // These builtin methods don't support hosts or otherwise have
91         // insufficient granularity
92         this.addItem("cookies", {
93             builtin: true,
94             description: "Cookies",
95             persistent: true,
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);
100             },
101             override: true
102         });
103         this.addItem("history", {
104             builtin: true,
105             description: "Browsing history",
106             persistent: true,
107             sessionHistory: true,
108             action: function (range, host) {
109                 if (host)
110                     services.history.removePagesFromHost(host, true);
111                 else {
112                     if (range.isEternity)
113                         services.history.removeAllPages();
114                     else
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", "");
117                 }
118
119                 if (!host || util.isDomainURL(prefs.get("general.open_location.last_url"), host))
120                     prefs.reset("general.open_location.last_url");
121             },
122             override: true
123         });
124         try {
125             var { ForgetAboutSite } = Cu.import("resource://gre/modules/ForgetAboutSite.jsm", {});
126         }
127         catch (e) {}
128         if (ForgetAboutSite)
129             this.addItem("host", {
130                 description: "All data from the given host",
131                 action: function (range, host) {
132                     if (host)
133                         ForgetAboutSite.removeDataFromDomain(host);
134                 }
135             });
136         this.addItem("sitesettings", {
137             builtin: true,
138             description: "Site preferences",
139             persistent: true,
140             action: function (range, host) {
141                 if (range.isSession)
142                     return;
143                 if (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);
147                     }
148                     for (let p in iter(services.contentPrefs.getPrefs(util.createURI(host))))
149                         services.contentPrefs.removePref(util.createURI(host), p.QueryInterface(Ci.nsIProperty).name);
150                 }
151                 else {
152                     // "Allow this site to open popups" ...
153                     services.permissions.removeAll();
154                     // Zoom level, ...
155                     services.contentPrefs.removeGroupedPrefs();
156                 }
157
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);
162             },
163             override: true
164         });
165
166         function ourItems(persistent) [
167             item for (item in values(self.itemMap))
168             if (!item.builtin && (!persistent || item.persistent) && item.name !== "all")
169         ];
170
171         function prefOverlay(branch, persistent, local) update(Object.create(local), {
172             before: [
173                 ["preferences", { id: branch.substr(Item.PREFIX.length) + "history",
174                                   xmlns: "xul" },
175                   template.map(ourItems(persistent), function (item)
176                       ["preference", { type: "bool", id: branch + item.name, name: branch + item.name }])]
177             ],
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);
183             }
184         });
185
186         util.timeout(function () { // Load order issue...
187
188             let (branch = Item.PREFIX + Item.SHUTDOWN_BRANCH) {
189                 overlay.overlayWindow("chrome://browser/content/preferences/sanitize.xul",
190                                    function (win) prefOverlay(branch, true, {
191                     append: {
192                         SanitizeDialogPane:
193                             ["groupbox", { orient: "horizontal", xmlns: "xul" },
194                               ["caption", { label: config.appName + /*L*/" (see :help privacy)" }],
195                               ["grid", { flex: "1" },
196                                 ["columns", {},
197                                     ["column", { flex: "1" }],
198                                     ["column", { flex: "1" }]],
199                                 ["rows", {},
200                                   let (items = ourItems(true))
201                                      template.map(util.range(0, Math.ceil(items.length / 2)), function (i)
202                                          ["row", {},
203                                              template.map(items.slice(i * 2, i * 2 + 2), function (item)
204                                                 ["checkbox", { xmlns: XUL, label: item.description, preference: branch + item.name }])])]]],
205                     }
206                 }));
207             }
208             let (branch = Item.PREFIX + Item.BRANCH) {
209                 overlay.overlayWindow("chrome://browser/content/sanitize.xul",
210                                    function (win) prefOverlay(branch, false, {
211                     append: {
212                         itemList: [
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();" }]),
219                         ]
220                     },
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 || []));
232                                 }, this);
233                             }
234                         });
235                     }
236                 }));
237             }
238         });
239     },
240
241     firstRun: 0,
242
243     addItem: function addItem(name, params) {
244         let item = this.itemMap[name] || Item(name, params);
245         this.itemMap[name] = item;
246
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);
250
251         let names = Set([name].concat(params.contains || []).map(function (e) "clear-" + e));
252         if (params.action)
253             storage.addObserver("sanitizer",
254                 function (key, event, arg) {
255                     if (event in names)
256                         params.action.apply(params, arg);
257                 },
258                 Class.objectGlobal(params.action));
259
260         if (params.privateEnter || params.privateLeave)
261             storage.addObserver("private-mode",
262                 function (key, event, arg) {
263                     let meth = params[arg ? "privateEnter" : "privateLeave"];
264                     if (meth)
265                         meth.call(params);
266                 },
267                 Class.objectGlobal(params.action));
268     },
269
270     observers: {
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);
278         },
279         "browser:purge-session-history": function (subject, data) {
280             // See above.
281             if (!this.sanitizing)
282                 this.sanitizeItems(null, Range(this.sessionStart), null, "sessionHistory");
283         },
284         "quit-application-granted": function (subject, data) {
285             if (this.runAtShutdown && !this.sanitizeItems(null, Range(), null, "shutdown"))
286                 this.ranAtShutdown = true;
287         },
288         "private-browsing": function (subject, data) {
289             if (data == "enter")
290                 storage.privateMode = true;
291             else if (data == "exit")
292                 storage.privateMode = false;
293             storage.fireEvent("private-mode", "change", storage.privateMode);
294         }
295     },
296
297     /**
298      * Returns a load context for the given thing, to be used with
299      * interfaces needing one for per-window private browsing support.
300      *
301      * @param {Window|Document|Node} thing The thing for which to return
302      *      a load context.
303      */
304     getContext: function getContext(thing) {
305         if (!Ci.nsILoadContext)
306             return null;
307
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);
315     },
316
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)),
321
322     sanitize: function sanitize(items, range)
323         this.withSavedValues(["sanitizing"], function () {
324             this.sanitizing = true;
325             let errors = this.sanitizeItems(items, range, null);
326
327             for (let itemName in values(items)) {
328                 try {
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)
333                             item.clear();
334                     }
335                 }
336                 catch (e) {
337                     errors = errors || {};
338                     errors[itemName] = e;
339                     util.dump("Error sanitizing " + itemName);
340                     util.reportError(e);
341                 }
342             }
343             return errors;
344         }),
345
346     sanitizeItems: function sanitizeItems(items, range, host, key)
347         this.withSavedValues(["sanitizing"], function () {
348             this.sanitizing = true;
349             if (items == null)
350                 items = Object.keys(this.itemMap);
351
352             let errors;
353             for (let itemName in values(items))
354                 try {
355                     if (!key || this.itemMap[itemName][key])
356                         storage.fireEvent("sanitizer", "clear-" + itemName, [range, host]);
357                 }
358                 catch (e) {
359                     errors = errors || {};
360                     errors[itemName] = e;
361                     util.dump("Error sanitizing " + itemName);
362                     util.reportError(e);
363                 }
364             return errors;
365         })
366 }, {
367     PERMS: {
368         unset:   0,
369         allow:   1,
370         deny:    2,
371         session: 8
372     },
373
374     UNPERMS: Class.Memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
375
376     COMMANDS: {
377         unset:   /*L*/"Unset",
378         allow:   /*L*/"Allowed",
379         deny:    /*L*/"Denied",
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"
385     },
386
387     argPrefMap: {
388         offlineapps:  "offlineApps",
389         sitesettings: "siteSettings"
390     },
391     argToPref: function (arg) Sanitizer.argPrefMap[arg] || arg,
392     prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
393
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)
399                 yield c;
400
401     },
402     iterPermissions: function iterPermissions(host) {
403         for (let p in iter(services.permissions, Ci.nsIPermission))
404             if (!host || util.isSubdomain(p.host, host))
405                 yield p;
406     }
407 }, {
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;
412     },
413     autocommands: function initAutocommands(dactyl, modules, window) {
414         const { autocommands } = modules;
415
416         storage.addObserver("private-mode",
417             function (key, event, value) {
418                 autocommands.trigger("PrivateMode", { state: value });
419             }, window);
420         storage.addObserver("sanitizer",
421             function (key, event, value) {
422                 if (event == "domain")
423                     autocommands.trigger("SanitizeDomain", { domain: value });
424                 else if (!value[1])
425                     autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
426             }, window);
427     },
428     commands: function initCommands(dactyl, modules, window) {
429         const { commands } = modules;
430         commands.add(["sa[nitize]"],
431             "Clear private data",
432             function (args) {
433                 dactyl.assert(!modules.options['private'], _("command.sanitize.privateMode"));
434
435                 if (args["-host"] && !args.length && !args.bang)
436                     args[0] = "all";
437
438                 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
439
440                 let range = Range();
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);
445
446                 let opt = modules.options.get("sanitizeitems");
447                 if (args.bang)
448                     dactyl.assert(args.length == 0, _("error.trailingCharacters"));
449                 else {
450                     dactyl.assert(opt.validator(args), _("error.invalidArgument"));
451                     opt = { __proto__: opt, value: args.slice() };
452                 }
453
454                 let items = Object.keys(sanitizer.itemMap).slice(1).filter(opt.has, opt);
455
456                 function sanitize(items) {
457                     sanitizer.range = range.native;
458                     sanitizer.ignoreTimespan = range.min == null;
459                     sanitizer.sanitizing = true;
460                     if (args["-host"]) {
461                         args["-host"].forEach(function (host) {
462                             sanitizer.sanitizing = true;
463                             sanitizer.sanitizeItems(items, range, host);
464                         });
465                     }
466                     else
467                         sanitizer.sanitize(items, range);
468                 }
469
470                 if (array.nth(opt.value, function (i) i == "all" || /^!/.test(i), 0) == "all" && !args["-host"])
471                     modules.commandline.input(_("sanitize.prompt.deleteAll") + " ",
472                         function (resp) {
473                             if (resp.match(/^y(es)?$/i)) {
474                                 sanitize(items);
475                                 dactyl.echomsg(_("command.sanitize.allDeleted"));
476                             }
477                             else
478                                 dactyl.echo(_("command.sanitize.noneDeleted"));
479                         });
480                 else
481                     sanitize(items);
482
483             },
484             {
485                 argCount: "*", // FIXME: should be + and 0
486                 bang: true,
487                 completer: function (context) {
488                     context.title = ["Privacy Item", "Description"];
489                     context.completions = modules.options.get("sanitizeitems").values;
490                 },
491                 domains: function (args) args["-host"] || [],
492                 options: [
493                     {
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);
500                         },
501                         type: modules.CommandOption.LIST
502                     }, {
503                         names: ["-older", "-o"],
504                         description: "Sanitize items older than timespan",
505                         type: modules.CommandOption.NOARG
506                     }, {
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),
512                     }
513                 ],
514                 privateData: true
515             });
516
517             function getPerms(host) {
518                 let uri = util.createURI(host);
519                 if (uri)
520                     return Sanitizer.UNPERMS[services.permissions.testPermission(uri, "cookie")];
521                 return "unset";
522             }
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]);
527             }
528             commands.add(["cookies", "ck"],
529                 "Change cookie permissions for sites",
530                 function (args) {
531                     let host = args.shift();
532                     let session = true;
533                     if (!args.length)
534                         args = modules.options["cookies"];
535
536                     for (let [, cmd] in Iterator(args))
537                         switch (cmd) {
538                         case "clear":
539                             for (let c in Sanitizer.iterCookies(host))
540                                 services.cookies.remove(c.host, c.name, c.path, false);
541                             break;
542                         case "clear-persistent":
543                             session = false;
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);
548                             return;
549
550                         case "list":
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;"],
554                                 ([c.host,
555                                   c.isSession ? ["span", { highlight: "Enabled" }, "session"]
556                                               : (new Date(c.expiry * 1000).toJSON() || "Never").replace(/:\d\d\.000Z/, "").replace("T", " ").replace(/-/g, "/"),
557                                   c.path,
558                                   c.name,
559                                   c.value]
560                                   for (c in Sanitizer.iterCookies(host)))));
561                             return;
562                         default:
563                             util.assert(cmd in Sanitizer.PERMS, _("error.invalidArgument"));
564                             setPerms(host, cmd);
565                         }
566                 }, {
567                     argCount: "+",
568                     completer: function (context, args) {
569                         switch (args.completeArg) {
570                         case 0:
571                             modules.completion.visibleHosts(context);
572                             context.title[1] = "Current Permissions";
573                             context.keys.description = function desc(host) {
574                                 let count = [0, 0];
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("");
578                             };
579                             break;
580                         case 1:
581                             context.completions = Sanitizer.COMMANDS;
582                             break;
583                         }
584                     },
585                 });
586     },
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);
592
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;
598         };
599     },
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",
605                 "boolean", false,
606                 {
607                     initialValue: true,
608                     getter: function () services.privateBrowsing.privateBrowsingEnabled,
609                     setter: function (value) {
610                         if (services.privateBrowsing.privateBrowsingEnabled != value)
611                             services.privateBrowsing.privateBrowsingEnabled = value;
612                     },
613                     persist: false
614                 });
615
616         options.add(["sanitizeitems", "si"],
617             "The default list of private items to sanitize",
618             "stringlist", "all",
619             {
620                 get values() values(sanitizer.itemMap).toArray(),
621
622                 completer: function completer(context, extra) {
623                     if (context.filter[0] == "!")
624                         context.advance(1);
625                     return completer.superapply(this, arguments);
626                 },
627
628                 has: function has(val)
629                     let (res = array.nth(this.value, function (v) v == "all" || v.replace(/^!/, "") == val, 0))
630                         res && !/^!/.test(res),
631
632                 validator: function (values) values.length &&
633                     values.every(function (val) val === "all" || Set.has(sanitizer.itemMap, val.replace(/^!/, "")))
634             });
635
636         options.add(["sanitizeshutdown", "ss"],
637             "The items to sanitize automatically at shutdown",
638             "stringlist", "",
639             {
640                 initialValue: true,
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))
645                 ],
646                 setter: function (value) {
647                     if (value.length === 0)
648                         sanitizer.runAtShutdown = false;
649                     else {
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")));
655                     }
656                     return value;
657                 }
658             });
659
660         options.add(["sanitizetimespan", "sts"],
661             "The default sanitizer time span",
662             "string", "all",
663             {
664                 completer: function (context) {
665                     context.compare = context.constructor.Sort.Unsorted;
666                     context.completions = this.values;
667                 },
668                 values: {
669                     "all":     "Everything",
670                     "session": "The current session",
671                     "10m":     "Last ten minutes",
672                     "1h":      "Past hour",
673                     "1d":      "Past day",
674                     "1w":      "Past week"
675                 },
676                 validator: bind("test", /^(a(ll)?|s(ession)|\d+[mhdw])$/)
677             });
678
679         options.add(["cookies", "ck"],
680             "The default mode for newly added cookie permissions",
681             "stringlist", "session",
682             { get values() Sanitizer.COMMANDS });
683
684         options.add(["cookieaccept", "ca"],
685             "When to accept cookies",
686             "string", "all",
687             {
688                 PREF: "network.cookie.cookieBehavior",
689                 values: [
690                     ["all", "Accept all cookies"],
691                     ["samesite", "Accept all non-third-party cookies"],
692                     ["none", "Accept no cookies"]
693                 ],
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));
697                     return val;
698                 },
699                 initialValue: true,
700                 persist: false
701             });
702
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",
708                 values: [
709                     ["default", "The lifetime requested by the setter"],
710                     ["prompt",  "Always prompt for a lifetime"],
711                     ["session", "The current session"]
712                 ],
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);
716                     if (val > -1)
717                         prefs.set(this.PREF, val);
718                     else {
719                         prefs.set(this.PREF, 3);
720                         prefs.set(this.PREF_DAYS, parseInt(value));
721                     }
722                 },
723                 initialValue: true,
724                 persist: false,
725                 validator: function validator(val) parseInt(val) == val || validator.superapply(this, arguments)
726             });
727     }
728 });
729
730 endModule();
731
732 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
733
734 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: