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