Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / browser / modules / Sanitizer.sys.mjs
blob2e7d0323b0d6344447a9f27e1136acdaa2204dc9
1 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
8 const lazy = {};
10 ChromeUtils.defineESModuleGetters(lazy, {
11   ContextualIdentityService:
12     "resource://gre/modules/ContextualIdentityService.sys.mjs",
13   FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
14   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
15   PrincipalsCollector: "resource://gre/modules/PrincipalsCollector.sys.mjs",
16 });
18 var logConsole;
19 function log(msg) {
20   if (!logConsole) {
21     logConsole = console.createInstance({
22       prefix: "** Sanitizer.jsm",
23       maxLogLevelPref: "browser.sanitizer.loglevel",
24     });
25   }
27   logConsole.log(msg);
30 // Used as unique id for pending sanitizations.
31 var gPendingSanitizationSerial = 0;
33 var gPrincipalsCollector = null;
35 export var Sanitizer = {
36   /**
37    * Whether we should sanitize on shutdown.
38    */
39   PREF_SANITIZE_ON_SHUTDOWN: "privacy.sanitize.sanitizeOnShutdown",
41   /**
42    * During a sanitization this is set to a JSON containing an array of the
43    * pending sanitizations. This allows to retry sanitizations on startup in
44    * case they dind't run or were interrupted by a crash.
45    * Use addPendingSanitization and removePendingSanitization to manage it.
46    */
47   PREF_PENDING_SANITIZATIONS: "privacy.sanitize.pending",
49   /**
50    * Pref branches to fetch sanitization options from.
51    */
52   PREF_CPD_BRANCH: "privacy.cpd.",
53   PREF_SHUTDOWN_BRANCH: "privacy.clearOnShutdown.",
55   /**
56    * The fallback timestamp used when no argument is given to
57    * Sanitizer.getClearRange.
58    */
59   PREF_TIMESPAN: "privacy.sanitize.timeSpan",
61   /**
62    * Pref to newTab segregation. If true, on shutdown, the private container
63    * used in about:newtab is cleaned up.  Exposed because used in tests.
64    */
65   PREF_NEWTAB_SEGREGATION:
66     "privacy.usercontext.about_newtab_segregation.enabled",
68   /**
69    * Time span constants corresponding to values of the privacy.sanitize.timeSpan
70    * pref.  Used to determine how much history to clear, for various items
71    */
72   TIMESPAN_EVERYTHING: 0,
73   TIMESPAN_HOUR: 1,
74   TIMESPAN_2HOURS: 2,
75   TIMESPAN_4HOURS: 3,
76   TIMESPAN_TODAY: 4,
77   TIMESPAN_5MIN: 5,
78   TIMESPAN_24HOURS: 6,
80   /**
81    * Whether we should sanitize on shutdown.
82    * When this is set, a pending sanitization should also be added and removed
83    * when shutdown sanitization is complete. This allows to retry incomplete
84    * sanitizations on startup.
85    */
86   shouldSanitizeOnShutdown: false,
88   /**
89    * Whether we should sanitize the private container for about:newtab.
90    */
91   shouldSanitizeNewTabContainer: false,
93   /**
94    * Shows a sanitization dialog to the user. Returns after the dialog box has
95    * closed.
96    *
97    * @param parentWindow the browser window to use as parent for the created
98    *        dialog.
99    * @throws if parentWindow is undefined or doesn't have a gDialogBox.
100    */
101   showUI(parentWindow) {
102     // Treat the hidden window as not being a parent window:
103     if (
104       parentWindow?.document.documentURI ==
105       "chrome://browser/content/hiddenWindowMac.xhtml"
106     ) {
107       parentWindow = null;
108     }
109     if (parentWindow?.gDialogBox) {
110       parentWindow.gDialogBox.open("chrome://browser/content/sanitize.xhtml", {
111         inBrowserWindow: true,
112       });
113     } else {
114       Services.ww.openWindow(
115         parentWindow,
116         "chrome://browser/content/sanitize.xhtml",
117         "Sanitize",
118         "chrome,titlebar,dialog,centerscreen,modal",
119         { needNativeUI: true }
120       );
121     }
122   },
124   /**
125    * Performs startup tasks:
126    *  - Checks if sanitizations were not completed during the last session.
127    *  - Registers sanitize-on-shutdown.
128    */
129   async onStartup() {
130     // First, collect pending sanitizations from the last session, before we
131     // add pending sanitizations for this session.
132     let pendingSanitizations = getAndClearPendingSanitizations();
134     // Check if we should sanitize on shutdown.
135     this.shouldSanitizeOnShutdown = Services.prefs.getBoolPref(
136       Sanitizer.PREF_SANITIZE_ON_SHUTDOWN,
137       false
138     );
139     Services.prefs.addObserver(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, this, true);
140     // Add a pending shutdown sanitization, if necessary.
141     if (this.shouldSanitizeOnShutdown) {
142       let itemsToClear = getItemsToClearFromPrefBranch(
143         Sanitizer.PREF_SHUTDOWN_BRANCH
144       );
145       addPendingSanitization("shutdown", itemsToClear, {});
146     }
147     // Shutdown sanitization is always pending, but the user may change the
148     // sanitize on shutdown prefs during the session. Then the pending
149     // sanitization would become stale and must be updated.
150     Services.prefs.addObserver(Sanitizer.PREF_SHUTDOWN_BRANCH, this, true);
152     // Make sure that we are triggered during shutdown.
153     let shutdownClient = lazy.PlacesUtils.history.shutdownClient.jsclient;
154     // We need to pass to sanitize() (through sanitizeOnShutdown) a state object
155     // that tracks the status of the shutdown blocker. This `progress` object
156     // will be updated during sanitization and reported with the crash in case of
157     // a shutdown timeout.
158     // We use the `options` argument to pass the `progress` object to sanitize().
159     let progress = { isShutdown: true, clearHonoringExceptions: true };
160     shutdownClient.addBlocker(
161       "sanitize.js: Sanitize on shutdown",
162       () => sanitizeOnShutdown(progress),
163       { fetchState: () => ({ progress }) }
164     );
166     this.shouldSanitizeNewTabContainer = Services.prefs.getBoolPref(
167       this.PREF_NEWTAB_SEGREGATION,
168       false
169     );
170     if (this.shouldSanitizeNewTabContainer) {
171       addPendingSanitization("newtab-container", [], {});
172     }
174     let i = pendingSanitizations.findIndex(s => s.id == "newtab-container");
175     if (i != -1) {
176       pendingSanitizations.splice(i, 1);
177       sanitizeNewTabSegregation();
178     }
180     // Finally, run the sanitizations that were left pending, because we crashed
181     // before completing them.
182     for (let { itemsToClear, options } of pendingSanitizations) {
183       try {
184         // We need to set this flag to watch out for the users exceptions like we do on shutdown
185         options.progress = { clearHonoringExceptions: true };
186         await this.sanitize(itemsToClear, options);
187       } catch (ex) {
188         console.error(
189           "A previously pending sanitization failed: ",
190           itemsToClear,
191           ex
192         );
193       }
194     }
195   },
197   /**
198    * Returns a 2 element array representing the start and end times,
199    * in the uSec-since-epoch format that PRTime likes. If we should
200    * clear everything, this function returns null.
201    *
202    * @param ts [optional] a timespan to convert to start and end time.
203    *                      Falls back to the privacy.sanitize.timeSpan preference
204    *                      if this argument is omitted.
205    *                      If this argument is provided, it has to be one of the
206    *                      Sanitizer.TIMESPAN_* constants. This function will
207    *                      throw an error otherwise.
208    *
209    * @return {Array} a 2-element Array containing the start and end times.
210    */
211   getClearRange(ts) {
212     if (ts === undefined) {
213       ts = Services.prefs.getIntPref(Sanitizer.PREF_TIMESPAN);
214     }
215     if (ts === Sanitizer.TIMESPAN_EVERYTHING) {
216       return null;
217     }
219     // PRTime is microseconds while JS time is milliseconds
220     var endDate = Date.now() * 1000;
221     switch (ts) {
222       case Sanitizer.TIMESPAN_5MIN:
223         var startDate = endDate - 300000000; // 5*60*1000000
224         break;
225       case Sanitizer.TIMESPAN_HOUR:
226         startDate = endDate - 3600000000; // 1*60*60*1000000
227         break;
228       case Sanitizer.TIMESPAN_2HOURS:
229         startDate = endDate - 7200000000; // 2*60*60*1000000
230         break;
231       case Sanitizer.TIMESPAN_4HOURS:
232         startDate = endDate - 14400000000; // 4*60*60*1000000
233         break;
234       case Sanitizer.TIMESPAN_TODAY:
235         var d = new Date(); // Start with today
236         d.setHours(0); // zero us back to midnight...
237         d.setMinutes(0);
238         d.setSeconds(0);
239         d.setMilliseconds(0);
240         startDate = d.valueOf() * 1000; // convert to epoch usec
241         break;
242       case Sanitizer.TIMESPAN_24HOURS:
243         startDate = endDate - 86400000000; // 24*60*60*1000000
244         break;
245       default:
246         throw new Error("Invalid time span for clear private data: " + ts);
247     }
248     return [startDate, endDate];
249   },
251   /**
252    * Deletes privacy sensitive data in a batch, according to user preferences.
253    * Returns a promise which is resolved if no errors occurred.  If an error
254    * occurs, a message is reported to the console and all other items are still
255    * cleared before the promise is finally rejected.
256    *
257    * @param [optional] itemsToClear
258    *        Array of items to be cleared. if specified only those
259    *        items get cleared, irrespectively of the preference settings.
260    * @param [optional] options
261    *        Object whose properties are options for this sanitization:
262    *         - ignoreTimespan (default: true): Time span only makes sense in
263    *           certain cases.  Consumers who want to only clear some private
264    *           data can opt in by setting this to false, and can optionally
265    *           specify a specific range.
266    *           If timespan is not ignored, and range is not set, sanitize() will
267    *           use the value of the timespan pref to determine a range.
268    *         - range (default: null): array-tuple of [from, to] timestamps
269    *         - privateStateForNewWindow (default: "non-private"): when clearing
270    *           open windows, defines the private state for the newly opened window.
271    * @returns {object} An object containing debug information about the
272    *          sanitization progress. This state object is also used as
273    *          AsyncShutdown metadata.
274    */
275   async sanitize(itemsToClear = null, options = {}) {
276     let progress = options.progress;
277     // initialise the principals collector
278     gPrincipalsCollector = new lazy.PrincipalsCollector();
279     if (!progress) {
280       progress = options.progress = {};
281     }
283     if (!itemsToClear) {
284       itemsToClear = getItemsToClearFromPrefBranch(this.PREF_CPD_BRANCH);
285     }
286     let promise = sanitizeInternal(this.items, itemsToClear, options);
288     // Depending on preferences, the sanitizer may perform asynchronous
289     // work before it starts cleaning up the Places database (e.g. closing
290     // windows). We need to make sure that the connection to that database
291     // hasn't been closed by the time we use it.
292     // Though, if this is a sanitize on shutdown, we already have a blocker.
293     if (!progress.isShutdown) {
294       let shutdownClient = lazy.PlacesUtils.history.shutdownClient.jsclient;
295       shutdownClient.addBlocker("sanitize.js: Sanitize", promise, {
296         fetchState: () => ({ progress }),
297       });
298     }
300     try {
301       await promise;
302     } finally {
303       Services.obs.notifyObservers(null, "sanitizer-sanitization-complete");
304     }
305     return progress;
306   },
308   observe(subject, topic, data) {
309     if (topic == "nsPref:changed") {
310       if (
311         data.startsWith(this.PREF_SHUTDOWN_BRANCH) &&
312         this.shouldSanitizeOnShutdown
313       ) {
314         // Update the pending shutdown sanitization.
315         removePendingSanitization("shutdown");
316         let itemsToClear = getItemsToClearFromPrefBranch(
317           Sanitizer.PREF_SHUTDOWN_BRANCH
318         );
319         addPendingSanitization("shutdown", itemsToClear, {});
320       } else if (data == this.PREF_SANITIZE_ON_SHUTDOWN) {
321         this.shouldSanitizeOnShutdown = Services.prefs.getBoolPref(
322           Sanitizer.PREF_SANITIZE_ON_SHUTDOWN,
323           false
324         );
325         removePendingSanitization("shutdown");
326         if (this.shouldSanitizeOnShutdown) {
327           let itemsToClear = getItemsToClearFromPrefBranch(
328             Sanitizer.PREF_SHUTDOWN_BRANCH
329           );
330           addPendingSanitization("shutdown", itemsToClear, {});
331         }
332       } else if (data == this.PREF_NEWTAB_SEGREGATION) {
333         this.shouldSanitizeNewTabContainer = Services.prefs.getBoolPref(
334           this.PREF_NEWTAB_SEGREGATION,
335           false
336         );
337         removePendingSanitization("newtab-container");
338         if (this.shouldSanitizeNewTabContainer) {
339           addPendingSanitization("newtab-container", [], {});
340         }
341       }
342     }
343   },
345   QueryInterface: ChromeUtils.generateQI([
346     "nsIObserver",
347     "nsISupportsWeakReference",
348   ]),
350   // This method is meant to be used by tests.
351   async runSanitizeOnShutdown() {
352     // The collector needs to be reset for each test, as the collection only happens
353     // once and does not update after that.
354     // Pretend that it has never been initialized to mimic the actual browser behavior
355     // by setting it to null.
356     // The actually initialization will happen either via sanitize() or directly in
357     // sanitizeOnShutdown.
358     gPrincipalsCollector = null;
359     return sanitizeOnShutdown({
360       isShutdown: true,
361       clearHonoringExceptions: true,
362     });
363   },
365   // When making any changes to the sanitize implementations here,
366   // please check whether the changes are applicable to Android
367   // (mobile/android/modules/geckoview/GeckoViewStorageController.jsm) as well.
369   items: {
370     cache: {
371       async clear(range) {
372         let refObj = {};
373         TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
374         await clearData(range, Ci.nsIClearDataService.CLEAR_ALL_CACHES);
375         TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj);
376       },
377     },
379     cookies: {
380       async clear(range, { progress }, clearHonoringExceptions) {
381         let refObj = {};
382         TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
383         // This is true if called by sanitizeOnShutdown.
384         // On shutdown we clear by principal to be able to honor the users exceptions
385         if (clearHonoringExceptions) {
386           progress.step = "getAllPrincipals";
387           let principalsForShutdownClearing =
388             await gPrincipalsCollector.getAllPrincipals(progress);
389           await maybeSanitizeSessionPrincipals(
390             progress,
391             principalsForShutdownClearing,
392             Ci.nsIClearDataService.CLEAR_COOKIES |
393               Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD
394           );
395         } else {
396           // Not on shutdown
397           await clearData(
398             range,
399             Ci.nsIClearDataService.CLEAR_COOKIES |
400               Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD
401           );
402         }
403         await clearData(range, Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES);
404         TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
405       },
406     },
408     offlineApps: {
409       async clear(range, { progress }, clearHonoringExceptions) {
410         // This is true if called by sanitizeOnShutdown.
411         // On shutdown we clear by principal to be able to honor the users exceptions
412         if (clearHonoringExceptions) {
413           progress.step = "getAllPrincipals";
414           let principalsForShutdownClearing =
415             await gPrincipalsCollector.getAllPrincipals(progress);
416           await maybeSanitizeSessionPrincipals(
417             progress,
418             principalsForShutdownClearing,
419             Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
420               Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD
421           );
422         } else {
423           // Not on shutdown
424           await clearData(
425             range,
426             Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
427               Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD
428           );
429         }
430       },
431     },
433     history: {
434       async clear(range, { progress }) {
435         // TODO: This check is needed for the case that this method is invoked directly and not via the sanitizer.sanitize API.
436         // This can be removed once bug 1803799 has landed.
437         if (!gPrincipalsCollector) {
438           gPrincipalsCollector = new lazy.PrincipalsCollector();
439         }
440         progress.step = "getAllPrincipals";
441         let principals = await gPrincipalsCollector.getAllPrincipals(progress);
442         let refObj = {};
443         TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
444         progress.step = "clearing browsing history";
445         await clearData(
446           range,
447           Ci.nsIClearDataService.CLEAR_HISTORY |
448             Ci.nsIClearDataService.CLEAR_SESSION_HISTORY |
449             Ci.nsIClearDataService.CLEAR_CONTENT_BLOCKING_RECORDS
450         );
452         // storageAccessAPI permissions record every site that the user
453         // interacted with and thus mirror history quite closely. It makes
454         // sense to clear them when we clear history. However, since their absence
455         // indicates that we can purge cookies and site data for tracking origins without
456         // user interaction, we need to ensure that we only delete those permissions that
457         // do not have any existing storage.
458         progress.step = "clearing user interaction";
459         await new Promise(resolve => {
460           Services.clearData.deleteUserInteractionForClearingHistory(
461             principals,
462             range ? range[0] : 0,
463             resolve
464           );
465         });
466         TelemetryStopwatch.finish("FX_SANITIZE_HISTORY", refObj);
467       },
468     },
470     formdata: {
471       async clear(range) {
472         let seenException;
473         let refObj = {};
474         TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj);
475         try {
476           // Clear undo history of all search bars.
477           for (let currentWindow of Services.wm.getEnumerator(
478             "navigator:browser"
479           )) {
480             let currentDocument = currentWindow.document;
482             // searchBar may not exist if it's in the customize mode.
483             let searchBar = currentDocument.getElementById("searchbar");
484             if (searchBar) {
485               let input = searchBar.textbox;
486               input.value = "";
487               input.editor?.clearUndoRedo();
488             }
490             let tabBrowser = currentWindow.gBrowser;
491             if (!tabBrowser) {
492               // No tab browser? This means that it's too early during startup (typically,
493               // Session Restore hasn't completed yet). Since we don't have find
494               // bars at that stage and since Session Restore will not restore
495               // find bars further down during startup, we have nothing to clear.
496               continue;
497             }
498             for (let tab of tabBrowser.tabs) {
499               if (tabBrowser.isFindBarInitialized(tab)) {
500                 tabBrowser.getCachedFindBar(tab).clear();
501               }
502             }
503             // Clear any saved find value
504             tabBrowser._lastFindValue = "";
505           }
506         } catch (ex) {
507           seenException = ex;
508         }
510         try {
511           let change = { op: "remove" };
512           if (range) {
513             [change.firstUsedStart, change.firstUsedEnd] = range;
514           }
515           await lazy.FormHistory.update(change).catch(e => {
516             seenException = new Error("Error " + e.result + ": " + e.message);
517           });
518         } catch (ex) {
519           seenException = ex;
520         }
522         TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj);
523         if (seenException) {
524           throw seenException;
525         }
526       },
527     },
529     downloads: {
530       async clear(range) {
531         let refObj = {};
532         TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
533         await clearData(range, Ci.nsIClearDataService.CLEAR_DOWNLOADS);
534         TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
535       },
536     },
538     sessions: {
539       async clear(range) {
540         let refObj = {};
541         TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj);
542         await clearData(
543           range,
544           Ci.nsIClearDataService.CLEAR_AUTH_TOKENS |
545             Ci.nsIClearDataService.CLEAR_AUTH_CACHE
546         );
547         TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj);
548       },
549     },
551     siteSettings: {
552       async clear(range) {
553         let refObj = {};
554         TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS", refObj);
555         await clearData(
556           range,
557           Ci.nsIClearDataService.CLEAR_PERMISSIONS |
558             Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES |
559             Ci.nsIClearDataService.CLEAR_DOM_PUSH_NOTIFICATIONS |
560             Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE |
561             Ci.nsIClearDataService.CLEAR_CERT_EXCEPTIONS |
562             Ci.nsIClearDataService.CLEAR_CREDENTIAL_MANAGER_STATE |
563             Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXCEPTION
564         );
565         TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
566       },
567     },
569     openWindows: {
570       _canCloseWindow(win) {
571         if (win.CanCloseWindow()) {
572           // We already showed PermitUnload for the window, so let's
573           // make sure we don't do it again when we actually close the
574           // window.
575           win.skipNextCanClose = true;
576           return true;
577         }
578         return false;
579       },
580       _resetAllWindowClosures(windowList) {
581         for (let win of windowList) {
582           win.skipNextCanClose = false;
583         }
584       },
585       async clear(range, { privateStateForNewWindow = "non-private" }) {
586         // NB: this closes all *browser* windows, not other windows like the library, about window,
587         // browser console, etc.
589         // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
590         // dialogs
591         let startDate = Date.now();
593         // First check if all these windows are OK with being closed:
594         let windowList = [];
595         for (let someWin of Services.wm.getEnumerator("navigator:browser")) {
596           windowList.push(someWin);
597           // If someone says "no" to a beforeunload prompt, we abort here:
598           if (!this._canCloseWindow(someWin)) {
599             this._resetAllWindowClosures(windowList);
600             throw new Error(
601               "Sanitize could not close windows: cancelled by user"
602             );
603           }
605           // ...however, beforeunload prompts spin the event loop, and so the code here won't get
606           // hit until the prompt has been dismissed. If more than 1 minute has elapsed since we
607           // started prompting, stop, because the user might not even remember initiating the
608           // 'forget', and the timespans will be all wrong by now anyway:
609           if (Date.now() > startDate + 60 * 1000) {
610             this._resetAllWindowClosures(windowList);
611             throw new Error("Sanitize could not close windows: timeout");
612           }
613         }
615         if (!windowList.length) {
616           return;
617         }
619         // If/once we get here, we should actually be able to close all windows.
621         let refObj = {};
622         TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS", refObj);
624         // First create a new window. We do this first so that on non-mac, we don't
625         // accidentally close the app by closing all the windows.
626         let handler = Cc["@mozilla.org/browser/clh;1"].getService(
627           Ci.nsIBrowserHandler
628         );
629         let defaultArgs = handler.defaultArgs;
630         let features = "chrome,all,dialog=no," + privateStateForNewWindow;
631         let newWindow = windowList[0].openDialog(
632           AppConstants.BROWSER_CHROME_URL,
633           "_blank",
634           features,
635           defaultArgs
636         );
638         let onFullScreen = null;
639         if (AppConstants.platform == "macosx") {
640           onFullScreen = function (e) {
641             newWindow.removeEventListener("fullscreen", onFullScreen);
642             let docEl = newWindow.document.documentElement;
643             let sizemode = docEl.getAttribute("sizemode");
644             if (!newWindow.fullScreen && sizemode == "fullscreen") {
645               docEl.setAttribute("sizemode", "normal");
646               e.preventDefault();
647               e.stopPropagation();
648               return false;
649             }
650             return undefined;
651           };
652           newWindow.addEventListener("fullscreen", onFullScreen);
653         }
655         let promiseReady = new Promise(resolve => {
656           // Window creation and destruction is asynchronous. We need to wait
657           // until all existing windows are fully closed, and the new window is
658           // fully open, before continuing. Otherwise the rest of the sanitizer
659           // could run too early (and miss new cookies being set when a page
660           // closes) and/or run too late (and not have a fully-formed window yet
661           // in existence). See bug 1088137.
662           let newWindowOpened = false;
663           let onWindowOpened = function (subject, topic, data) {
664             if (subject != newWindow) {
665               return;
666             }
668             Services.obs.removeObserver(
669               onWindowOpened,
670               "browser-delayed-startup-finished"
671             );
672             if (AppConstants.platform == "macosx") {
673               newWindow.removeEventListener("fullscreen", onFullScreen);
674             }
675             newWindowOpened = true;
676             // If we're the last thing to happen, invoke callback.
677             if (numWindowsClosing == 0) {
678               TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
679               resolve();
680             }
681           };
683           let numWindowsClosing = windowList.length;
684           let onWindowClosed = function () {
685             numWindowsClosing--;
686             if (numWindowsClosing == 0) {
687               Services.obs.removeObserver(
688                 onWindowClosed,
689                 "xul-window-destroyed"
690               );
691               // If we're the last thing to happen, invoke callback.
692               if (newWindowOpened) {
693                 TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
694                 resolve();
695               }
696             }
697           };
698           Services.obs.addObserver(
699             onWindowOpened,
700             "browser-delayed-startup-finished"
701           );
702           Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
703         });
705         // Start the process of closing windows
706         while (windowList.length) {
707           windowList.pop().close();
708         }
709         newWindow.focus();
710         await promiseReady;
711       },
712     },
714     pluginData: {
715       async clear(range) {},
716     },
717   },
720 async function sanitizeInternal(items, aItemsToClear, options) {
721   let { ignoreTimespan = true, range, progress } = options;
722   let seenError = false;
723   // Shallow copy the array, as we are going to modify it in place later.
724   if (!Array.isArray(aItemsToClear)) {
725     throw new Error("Must pass an array of items to clear.");
726   }
727   let itemsToClear = [...aItemsToClear];
729   // Store the list of items to clear, in case we are killed before we
730   // get a chance to complete.
731   let uid = gPendingSanitizationSerial++;
732   // Shutdown sanitization is managed outside.
733   if (!progress.isShutdown) {
734     addPendingSanitization(uid, itemsToClear, options);
735   }
737   // Store the list of items to clear, for debugging/forensics purposes
738   for (let k of itemsToClear) {
739     progress[k] = "ready";
740     // Create a progress object specific to each cleaner. We'll pass down this
741     // to the cleaners instead of the main progress object, so they don't end
742     // up overriding properties each other.
743     // This specific progress is deleted if the cleaner completes successfully,
744     // so the metadata will only contain progress of unresolved cleaners.
745     progress[k + "Progress"] = {};
746   }
748   // Ensure open windows get cleared first, if they're in our list, so that
749   // they don't stick around in the recently closed windows list, and so we
750   // can cancel the whole thing if the user selects to keep a window open
751   // from a beforeunload prompt.
752   let openWindowsIndex = itemsToClear.indexOf("openWindows");
753   if (openWindowsIndex != -1) {
754     itemsToClear.splice(openWindowsIndex, 1);
755     await items.openWindows.clear(
756       null,
757       Object.assign(options, { progress: progress.openWindowsProgress })
758     );
759     progress.openWindows = "cleared";
760     delete progress.openWindowsProgress;
761   }
763   // If we ignore timespan, clear everything,
764   // otherwise, pick a range.
765   if (!ignoreTimespan && !range) {
766     range = Sanitizer.getClearRange();
767   }
769   // For performance reasons we start all the clear tasks at once, then wait
770   // for their promises later.
771   // Some of the clear() calls may raise exceptions (for example bug 265028),
772   // we catch and store them, but continue to sanitize as much as possible.
773   // Callers should check returned errors and give user feedback
774   // about items that could not be sanitized
775   let refObj = {};
776   TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
778   let annotateError = (name, ex) => {
779     progress[name] = "failed";
780     seenError = true;
781     console.error("Error sanitizing " + name, ex);
782   };
784   // Array of objects in form { name, promise }.
785   // `name` is the item's name and `promise` may be a promise, if the
786   // sanitization is asynchronous, or the function return value, otherwise.
787   let handles = [];
788   for (let name of itemsToClear) {
789     progress[name] = "blocking";
790     let item = items[name];
791     try {
792       // Catch errors here, so later we can just loop through these.
793       handles.push({
794         name,
795         promise: item
796           .clear(
797             range,
798             Object.assign(options, { progress: progress[name + "Progress"] }),
799             progress.clearHonoringExceptions
800           )
801           .then(
802             () => {
803               progress[name] = "cleared";
804               delete progress[name + "Progress"];
805             },
806             ex => annotateError(name, ex)
807           ),
808       });
809     } catch (ex) {
810       annotateError(name, ex);
811     }
812   }
813   await Promise.all(handles.map(h => h.promise));
815   // Sanitization is complete.
816   TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
817   if (!progress.isShutdown) {
818     removePendingSanitization(uid);
819   }
820   progress = {};
821   if (seenError) {
822     throw new Error("Error sanitizing");
823   }
826 async function sanitizeOnShutdown(progress) {
827   log("Sanitizing on shutdown");
828   progress.sanitizationPrefs = {
829     privacy_sanitize_sanitizeOnShutdown: Services.prefs.getBoolPref(
830       "privacy.sanitize.sanitizeOnShutdown"
831     ),
832     privacy_clearOnShutdown_cookies: Services.prefs.getBoolPref(
833       "privacy.clearOnShutdown.cookies"
834     ),
835     privacy_clearOnShutdown_history: Services.prefs.getBoolPref(
836       "privacy.clearOnShutdown.history"
837     ),
838     privacy_clearOnShutdown_formdata: Services.prefs.getBoolPref(
839       "privacy.clearOnShutdown.formdata"
840     ),
841     privacy_clearOnShutdown_downloads: Services.prefs.getBoolPref(
842       "privacy.clearOnShutdown.downloads"
843     ),
844     privacy_clearOnShutdown_cache: Services.prefs.getBoolPref(
845       "privacy.clearOnShutdown.cache"
846     ),
847     privacy_clearOnShutdown_sessions: Services.prefs.getBoolPref(
848       "privacy.clearOnShutdown.sessions"
849     ),
850     privacy_clearOnShutdown_offlineApps: Services.prefs.getBoolPref(
851       "privacy.clearOnShutdown.offlineApps"
852     ),
853     privacy_clearOnShutdown_siteSettings: Services.prefs.getBoolPref(
854       "privacy.clearOnShutdown.siteSettings"
855     ),
856     privacy_clearOnShutdown_openWindows: Services.prefs.getBoolPref(
857       "privacy.clearOnShutdown.openWindows"
858     ),
859   };
861   let needsSyncSavePrefs = false;
862   if (Sanitizer.shouldSanitizeOnShutdown) {
863     // Need to sanitize upon shutdown
864     progress.advancement = "shutdown-cleaner";
865     let itemsToClear = getItemsToClearFromPrefBranch(
866       Sanitizer.PREF_SHUTDOWN_BRANCH
867     );
868     await Sanitizer.sanitize(itemsToClear, { progress });
870     // We didn't crash during shutdown sanitization, so annotate it to avoid
871     // sanitizing again on startup.
872     removePendingSanitization("shutdown");
873     needsSyncSavePrefs = true;
874   }
876   if (Sanitizer.shouldSanitizeNewTabContainer) {
877     progress.advancement = "newtab-segregation";
878     sanitizeNewTabSegregation();
879     removePendingSanitization("newtab-container");
880     needsSyncSavePrefs = true;
881   }
883   if (needsSyncSavePrefs) {
884     Services.prefs.savePrefFile(null);
885   }
887   if (!Sanitizer.shouldSanitizeOnShutdown) {
888     // In case the user has not activated sanitizeOnShutdown but has explicitely set exceptions
889     // to always clear particular origins, we clear those here
891     progress.advancement = "session-permission";
893     let exceptions = 0;
894     let selectedPrincipals = [];
895     // Let's see if we have to forget some particular site.
896     for (let permission of Services.perms.all) {
897       if (
898         permission.type != "cookie" ||
899         permission.capability != Ci.nsICookiePermission.ACCESS_SESSION
900       ) {
901         continue;
902       }
904       // We consider just permissions set for http, https and file URLs.
905       if (!isSupportedPrincipal(permission.principal)) {
906         continue;
907       }
909       log(
910         "Custom session cookie permission detected for: " +
911           permission.principal.asciiSpec
912       );
913       exceptions++;
915       // We use just the URI here, because permissions ignore OriginAttributes.
916       // The principalsCollector is lazy, this is computed only once
917       if (!gPrincipalsCollector) {
918         gPrincipalsCollector = new lazy.PrincipalsCollector();
919       }
920       let principals = await gPrincipalsCollector.getAllPrincipals(progress);
921       selectedPrincipals.push(
922         ...extractMatchingPrincipals(principals, permission.principal.host)
923       );
924     }
925     await maybeSanitizeSessionPrincipals(
926       progress,
927       selectedPrincipals,
928       Ci.nsIClearDataService.CLEAR_ALL_CACHES |
929         Ci.nsIClearDataService.CLEAR_COOKIES |
930         Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
931         Ci.nsIClearDataService.CLEAR_EME
932     );
933     progress.sanitizationPrefs.session_permission_exceptions = exceptions;
934   }
935   progress.advancement = "done";
938 // Extracts the principals matching matchUri as root domain.
939 function extractMatchingPrincipals(principals, matchHost) {
940   return principals.filter(principal => {
941     return Services.eTLD.hasRootDomain(matchHost, principal.host);
942   });
945 /**  This method receives a list of principals and it checks if some of them or
946  * some of their sub-domain need to be sanitize.
947  * @param {Object} progress - Object to keep track of the sanitization progress, prefs and mode
948  * @param {nsIPrincipal[]} principals - The principals generated by the PrincipalsCollector
949  * @param {int} flags - The cleaning categories that need to be cleaned for the principals.
950  * @returns {Promise} - Resolves once the clearing of the principals to be cleared is done
951  */
952 async function maybeSanitizeSessionPrincipals(progress, principals, flags) {
953   log("Sanitizing " + principals.length + " principals");
955   let promises = [];
956   let permissions = new Map();
957   Services.perms.getAllWithTypePrefix("cookie").forEach(perm => {
958     permissions.set(perm.principal.origin, perm);
959   });
961   principals.forEach(principal => {
962     progress.step = "checking-principal";
963     let cookieAllowed = cookiesAllowedForDomainOrSubDomain(
964       principal,
965       permissions
966     );
967     progress.step = "principal-checked:" + cookieAllowed;
969     if (!cookieAllowed) {
970       promises.push(sanitizeSessionPrincipal(progress, principal, flags));
971     }
972   });
974   progress.step = "promises:" + promises.length;
975   if (promises.length) {
976     await Promise.all(promises);
977     await new Promise(resolve =>
978       Services.clearData.cleanupAfterDeletionAtShutdown(flags, resolve)
979     );
980   }
981   progress.step = "promises resolved";
984 function cookiesAllowedForDomainOrSubDomain(principal, permissions) {
985   log("Checking principal: " + principal.asciiSpec);
987   // If we have the 'cookie' permission for this principal, let's return
988   // immediately.
989   let cookiePermission = checkIfCookiePermissionIsSet(principal);
990   if (cookiePermission != null) {
991     return cookiePermission;
992   }
994   for (let perm of permissions.values()) {
995     if (perm.type != "cookie") {
996       permissions.delete(perm.principal.origin);
997       continue;
998     }
999     // We consider just permissions set for http, https and file URLs.
1000     if (!isSupportedPrincipal(perm.principal)) {
1001       permissions.delete(perm.principal.origin);
1002       continue;
1003     }
1005     // We don't care about scheme, port, and anything else.
1006     if (Services.eTLD.hasRootDomain(perm.principal.host, principal.host)) {
1007       log("Cookie check on principal: " + perm.principal.asciiSpec);
1008       let rootDomainCookiePermission = checkIfCookiePermissionIsSet(
1009         perm.principal
1010       );
1011       if (rootDomainCookiePermission != null) {
1012         return rootDomainCookiePermission;
1013       }
1014     }
1015   }
1017   log("Cookie not allowed.");
1018   return false;
1022  * Checks if a cookie permission is set for a given principal
1023  * @returns {boolean} - true: cookie permission "ACCESS_ALLOW", false: cookie permission "ACCESS_DENY"/"ACCESS_SESSION"
1024  * @returns {null} - No cookie permission is set for this principal
1025  */
1026 function checkIfCookiePermissionIsSet(principal) {
1027   let p = Services.perms.testPermissionFromPrincipal(principal, "cookie");
1029   if (p == Ci.nsICookiePermission.ACCESS_ALLOW) {
1030     log("Cookie allowed!");
1031     return true;
1032   }
1034   if (
1035     p == Ci.nsICookiePermission.ACCESS_DENY ||
1036     p == Ci.nsICookiePermission.ACCESS_SESSION
1037   ) {
1038     log("Cookie denied or session!");
1039     return false;
1040   }
1041   // This is an old profile with unsupported permission values
1042   if (p != Ci.nsICookiePermission.ACCESS_DEFAULT) {
1043     log("Not supported cookie permission: " + p);
1044     return false;
1045   }
1046   return null;
1049 async function sanitizeSessionPrincipal(progress, principal, flags) {
1050   log("Sanitizing principal: " + principal.asciiSpec);
1052   await new Promise(resolve => {
1053     progress.sanitizePrincipal = "started";
1054     Services.clearData.deleteDataFromPrincipal(
1055       principal,
1056       true /* user request */,
1057       flags,
1058       resolve
1059     );
1060   });
1061   progress.sanitizePrincipal = "completed";
1064 function sanitizeNewTabSegregation() {
1065   let identity = lazy.ContextualIdentityService.getPrivateIdentity(
1066     "userContextIdInternal.thumbnail"
1067   );
1068   if (identity) {
1069     Services.clearData.deleteDataFromOriginAttributesPattern({
1070       userContextId: identity.userContextId,
1071     });
1072   }
1076  * Gets an array of items to clear from the given pref branch.
1077  * @param branch The pref branch to fetch.
1078  * @return Array of items to clear
1079  */
1080 function getItemsToClearFromPrefBranch(branch) {
1081   branch = Services.prefs.getBranch(branch);
1082   return Object.keys(Sanitizer.items).filter(itemName => {
1083     try {
1084       return branch.getBoolPref(itemName);
1085     } catch (ex) {
1086       return false;
1087     }
1088   });
1092  * These functions are used to track pending sanitization on the next startup
1093  * in case of a crash before a sanitization could happen.
1094  * @param id A unique id identifying the sanitization
1095  * @param itemsToClear The items to clear
1096  * @param options The Sanitize options
1097  */
1098 function addPendingSanitization(id, itemsToClear, options) {
1099   let pendingSanitizations = safeGetPendingSanitizations();
1100   pendingSanitizations.push({ id, itemsToClear, options });
1101   Services.prefs.setStringPref(
1102     Sanitizer.PREF_PENDING_SANITIZATIONS,
1103     JSON.stringify(pendingSanitizations)
1104   );
1107 function removePendingSanitization(id) {
1108   let pendingSanitizations = safeGetPendingSanitizations();
1109   let i = pendingSanitizations.findIndex(s => s.id == id);
1110   let [s] = pendingSanitizations.splice(i, 1);
1111   Services.prefs.setStringPref(
1112     Sanitizer.PREF_PENDING_SANITIZATIONS,
1113     JSON.stringify(pendingSanitizations)
1114   );
1115   return s;
1118 function getAndClearPendingSanitizations() {
1119   let pendingSanitizations = safeGetPendingSanitizations();
1120   if (pendingSanitizations.length) {
1121     Services.prefs.clearUserPref(Sanitizer.PREF_PENDING_SANITIZATIONS);
1122   }
1123   return pendingSanitizations;
1126 function safeGetPendingSanitizations() {
1127   try {
1128     return JSON.parse(
1129       Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]")
1130     );
1131   } catch (ex) {
1132     console.error("Invalid JSON value for pending sanitizations: ", ex);
1133     return [];
1134   }
1137 async function clearData(range, flags) {
1138   if (range) {
1139     await new Promise(resolve => {
1140       Services.clearData.deleteDataInTimeRange(
1141         range[0],
1142         range[1],
1143         true /* user request */,
1144         flags,
1145         resolve
1146       );
1147     });
1148   } else {
1149     await new Promise(resolve => {
1150       Services.clearData.deleteData(flags, resolve);
1151     });
1152   }
1155 function isSupportedPrincipal(principal) {
1156   return ["http", "https", "file"].some(scheme => principal.schemeIs(scheme));