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/. */
5
6var EXPORTED_SYMBOLS = ["Sanitizer"];
7
8const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
9const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
10
11XPCOMUtils.defineLazyModuleGetters(this, {
12  AppConstants: "resource://gre/modules/AppConstants.jsm",
13  console: "resource://gre/modules/Console.jsm",
14  Downloads: "resource://gre/modules/Downloads.jsm",
15  DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
16  FormHistory: "resource://gre/modules/FormHistory.jsm",
17  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
18  setTimeout: "resource://gre/modules/Timer.jsm",
19});
20
21XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
22                                   "@mozilla.org/serviceworkers/manager;1",
23                                   "nsIServiceWorkerManager");
24XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
25                                   "@mozilla.org/dom/quota-manager-service;1",
26                                   "nsIQuotaManagerService");
27
28// Used as unique id for pending sanitizations.
29var gPendingSanitizationSerial = 0;
30
31/**
32 * A number of iterations after which to yield time back
33 * to the system.
34 */
35const YIELD_PERIOD = 10;
36
37var Sanitizer = {
38  /**
39   * Whether we should sanitize on shutdown.
40   */
41  PREF_SANITIZE_ON_SHUTDOWN: "privacy.sanitize.sanitizeOnShutdown",
42
43  /**
44   * During a sanitization this is set to a JSON containing an array of the
45   * pending sanitizations. This allows to retry sanitizations on startup in
46   * case they dind't run or were interrupted by a crash.
47   * Use addPendingSanitization and removePendingSanitization to manage it.
48   */
49  PREF_PENDING_SANITIZATIONS: "privacy.sanitize.pending",
50
51  /**
52   * Pref branches to fetch sanitization options from.
53   */
54  PREF_CPD_BRANCH: "privacy.cpd.",
55  PREF_SHUTDOWN_BRANCH: "privacy.clearOnShutdown.",
56
57  /**
58   * The fallback timestamp used when no argument is given to
59   * Sanitizer.getClearRange.
60   */
61  PREF_TIMESPAN: "privacy.sanitize.timeSpan",
62
63  /**
64   * Time span constants corresponding to values of the preference
65   * privacy.sanitize.timeSpan It is used to determine how much history
66   * to clear, for various items.
67   */
68  TIMESPAN_EVERYTHING: 0,
69  TIMESPAN_HOUR:       1,
70  TIMESPAN_2HOURS:     2,
71  TIMESPAN_4HOURS:     3,
72  TIMESPAN_TODAY:      4,
73  TIMESPAN_5MIN:       5,
74  TIMESPAN_24HOURS:    6,
75
76  /**
77   * Whether we should sanitize on shutdown.
78   * When this is set, a pending sanitization should also be added and removed
79   * when shutdown sanitization is complete. This allows to retry incomplete
80   * sanitizations on startup.
81   */
82  shouldSanitizeOnShutdown: false,
83
84  /**
85   * Shows a sanitization dialog to the user.
86   *
87   * @param [optional] parentWindow the window to use as
88   *                   parent for the created dialog.
89   */
90  showUI(parentWindow) {
91    let win = AppConstants.platform == "macosx" ?
92      null : // make this an app-modal window on Mac
93      parentWindow;
94    Services.ww.openWindow(win,
95                           "chrome://communicator/content/sanitizeDialog.xul",
96                           "Sanitize",
97                           "chrome,titlebar,centerscreen,dialog,modal",
98                           null);
99  },
100
101  /**
102   * Performs startup tasks:
103   *  - Checks if sanitizations were not completed during the last session.
104   *  - Registers sanitize-on-shutdown.
105   */
106  async onStartup() {
107    // First, collect pending sanitizations from the last session, before we
108    // add pending sanitizations for this session.
109    let pendingSanitizations = getAndClearPendingSanitizations();
110
111    // Check if we should sanitize on shutdown.
112    this.shouldSanitizeOnShutdown =
113      Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
114    Services.prefs.addObserver(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, this,
115                               true);
116    // Add a pending shutdown sanitization, if necessary.
117    if (this.shouldSanitizeOnShutdown) {
118      let itemsToClear =
119        getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
120      addPendingSanitization("shutdown", itemsToClear, {});
121    }
122    // Shutdown sanitization is always pending, but the user may change the
123    // sanitize on shutdown prefs during the session. Then the pending
124    // sanitization would become stale and must be updated.
125    Services.prefs.addObserver(Sanitizer.PREF_SHUTDOWN_BRANCH, this, true);
126
127    // Make sure that we are triggered during shutdown.
128    let shutdownClient = PlacesUtils.history.shutdownClient.jsclient;
129    // We need to pass to sanitize() (through sanitizeOnShutdown) a state
130    // object that tracks the status of the shutdown blocker. This 'progress'
131    // object will be updated during sanitization and reported with the crash
132    // in case of a shutdown timeout.
133    // We use the `options` argument to pass the `progress` object to
134    // sanitize().
135    let progress = { isShutdown: true };
136    shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
137      () => sanitizeOnShutdown(progress),
138      {fetchState: () => ({ progress })}
139    );
140
141    // Finally, run the sanitizations that were left pending, because we
142    // crashed before completing them.
143    for (let {itemsToClear, options} of pendingSanitizations) {
144      try {
145        await this.sanitize(itemsToClear, options);
146      } catch (ex) {
147        Cu.reportError("A previously pending sanitization failed: " +
148                       itemsToClear + "\n" + ex);
149      }
150    }
151  },
152
153  /**
154   * Returns a 2 element array representing the start and end times,
155   * in the uSec-since-epoch format that PRTime likes. If we should
156   * clear everything, this function returns null.
157   *
158   * @param ts [optional] a timespan to convert to start and end time.
159   *                      Falls back to the privacy.sanitize.timeSpan
160   *                      preference if this argument is omitted.
161   *                      If this argument is provided, it has to be one of the
162   *                      Sanitizer.TIMESPAN_* constants. This function will
163   *                      throw an error otherwise.
164   *
165   * @return {Array} a 2-element Array containing the start and end times.
166   */
167  getClearRange(ts) {
168    if (ts === undefined)
169      ts = Services.prefs.getIntPref(Sanitizer.PREF_TIMESPAN);
170    if (ts === Sanitizer.TIMESPAN_EVERYTHING)
171      return null;
172
173    // PRTime is microseconds while JS time is milliseconds
174    var endDate = Date.now() * 1000;
175    switch (ts) {
176      case Sanitizer.TIMESPAN_5MIN :
177        // 5*60*1000000
178        var startDate = endDate - 300000000;
179        break;
180      case Sanitizer.TIMESPAN_HOUR :
181        // 1*60*60*1000000
182        startDate = endDate - 3600000000;
183        break;
184      case Sanitizer.TIMESPAN_2HOURS :
185        // 2*60*60*1000000
186        startDate = endDate - 7200000000;
187        break;
188      case Sanitizer.TIMESPAN_4HOURS :
189        // 4*60*60*1000000
190        startDate = endDate - 14400000000;
191        break;
192      case Sanitizer.TIMESPAN_TODAY :
193        // Start with today
194        var d = new Date();
195        // zero us back to midnight...
196        d.setHours(0);
197        d.setMinutes(0);
198        d.setSeconds(0);
199        // convert to epoch usec
200        startDate = d.valueOf() * 1000;
201        break;
202      case Sanitizer.TIMESPAN_24HOURS :
203        // 24*60*60*1000000
204        startDate = endDate - 86400000000;
205        break;
206      default:
207        throw "Invalid time span for clear private data: " + ts;
208    }
209    return [startDate, endDate];
210  },
211
212  /**
213   * Deletes privacy sensitive data in a batch, according to user preferences.
214   * Returns a promise which is resolved if no errors occurred.  If an error
215   * occurs, a message is reported to the console and all other items are still
216   * cleared before the promise is finally rejected.
217   *
218   * @param [optional] itemsToClear
219   *        Array of items to be cleared. if specified only those
220   *        items get cleared, irrespectively of the preference settings.
221   * @param [optional] options
222   *        Object whose properties are options for this sanitization:
223   *         - ignoreTimespan (default: true): Time span only makes sense in
224   *           certain cases.  Consumers who want to only clear some private
225   *           data can opt in by setting this to false, and can optionally
226   *           specify a specific range.
227   *           If timespan is not ignored, and range is not set, sanitize()
228   *           will use the value of the timespan pref to determine a range.
229   *         - range (default: null)
230   *         - privateStateForNewWindow (default: "non-private"): when clearing
231   *           open windows, defines the private state for the newly opened
232   *           window.
233   */
234  async sanitize(itemsToClear = null, options = {}) {
235    let progress = options.progress || {};
236    if (!itemsToClear)
237      itemsToClear = getItemsToClearFromPrefBranch(this.PREF_CPD_BRANCH);
238    let promise = sanitizeInternal(this.items, itemsToClear, progress,
239                                   options);
240
241    // Depending on preferences, the sanitizer may perform asynchronous
242    // work before it starts cleaning up the Places database (e.g. closing
243    // windows). We need to make sure that the connection to that database
244    // hasn't been closed by the time we use it.
245    // Though, if this is a sanitize on shutdown, we already have a blocker.
246    if (!progress.isShutdown) {
247      let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
248                             .getService(Ci.nsPIPlacesDatabase)
249                             .shutdownClient
250                             .jsclient;
251      shutdownClient.addBlocker("sanitize.js: Sanitize",
252        promise,
253        {
254          fetchState: () => ({ progress })
255        }
256      );
257    }
258
259    try {
260      await promise;
261    } finally {
262      Services.obs.notifyObservers(null, "sanitizer-sanitization-complete");
263    }
264  },
265
266  observe(subject, topic, data) {
267    if (topic == "nsPref:changed") {
268      if (data.startsWith(this.PREF_SHUTDOWN_BRANCH) &&
269          this.shouldSanitizeOnShutdown) {
270        // Update the pending shutdown sanitization.
271        removePendingSanitization("shutdown");
272        let itemsToClear =
273          getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
274        addPendingSanitization("shutdown", itemsToClear, {});
275      } else if (data == this.PREF_SANITIZE_ON_SHUTDOWN) {
276        this.shouldSanitizeOnShutdown =
277          Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN,
278                                     false);
279        removePendingSanitization("shutdown");
280        if (this.shouldSanitizeOnShutdown) {
281          let itemsToClear =
282            getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
283          addPendingSanitization("shutdown", itemsToClear, {});
284        }
285      }
286    }
287  },
288
289  QueryInterface: XPCOMUtils.generateQI([
290    Ci.nsiObserver,
291    Ci.nsISupportsWeakReference
292  ]),
293
294  items: {
295    cache: {
296      async clear(range) {
297        let seenException;
298
299        try {
300          // Cache doesn't consult timespan, nor does it have the
301          // facility for timespan-based eviction.  Wipe it.
302          Services.cache2.clear();
303        } catch (ex) {
304          seenException = ex;
305        }
306
307        try {
308          let imageCache = Cc["@mozilla.org/image/tools;1"]
309                             .getService(Ci.imgITools)
310                             .getImgCacheForDocument(null);
311          // clearCache: true=chrome, false=content.
312          imageCache.clearCache(false);
313        } catch (ex) {
314          seenException = ex;
315        }
316
317        if (seenException) {
318          throw seenException;
319        }
320      }
321    },
322
323    cookies: {
324      async clear(range) {
325        let seenException;
326        let yieldCounter = 0;
327
328        // Clear cookies.
329        try {
330          if (range) {
331            // Iterate through the cookies and delete any created after our
332            // cutoff.
333            let cookiesEnum = Services.cookies.enumerator;
334            while (cookiesEnum.hasMoreElements()) {
335              let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
336
337              if (cookie.creationTime > range[0]) {
338                // This cookie was created after our cutoff, clear it
339                Services.cookies.remove(cookie.host, cookie.name, cookie.path,
340                                        false, cookie.originAttributes);
341
342                if (++yieldCounter % YIELD_PERIOD == 0) {
343                  // Don't block the main thread too long
344                  await new Promise(resolve => setTimeout(resolve, 0));
345                }
346              }
347            }
348          } else {
349            // Remove everything
350            Services.cookies.removeAll();
351            // Don't block the main thread too long
352            await new Promise(resolve => setTimeout(resolve, 0));
353          }
354        } catch (ex) {
355          seenException = ex;
356        }
357
358        // Clear deviceIds. Done asynchronously (returns before complete).
359        try {
360          let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"]
361                           .getService(Ci.nsIMediaManagerService);
362          mediaMgr.sanitizeDeviceIds(range && range[0]);
363        } catch (ex) {
364          seenException = ex;
365        }
366
367        // Clear plugin data.
368        try {
369          await clearPluginData(range);
370        } catch (ex) {
371          seenException = ex;
372        }
373
374        if (seenException) {
375          throw seenException;
376        }
377      },
378    },
379
380    offlineApps: {
381      async clear(range) {
382        // AppCache
383        ChromeUtils.import("resource:///modules/OfflineAppCacheHelper.jsm");
384        // This doesn't wait for the cleanup to be complete.
385        OfflineAppCacheHelper.clear();
386
387        // LocalStorage
388        Services.obs.notifyObservers(null, "extension:purge-localStorage");
389
390        // ServiceWorkers
391        let promises = [];
392        let serviceWorkers = serviceWorkerManager.getAllRegistrations();
393        for (let i = 0; i < serviceWorkers.length; i++) {
394          let sw = serviceWorkers
395            .queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
396
397          promises.push(new Promise(resolve => {
398            let unregisterCallback = {
399              unregisterSucceeded: () => { resolve(true); },
400              // We don't care about failures.
401              unregisterFailed: () => { resolve(true); },
402              QueryInterface: XPCOMUtils.generateQI(
403                [Ci.nsIServiceWorkerUnregisterCallback])
404            };
405
406            serviceWorkerManager.propagateUnregister(sw.principal,
407                                                     unregisterCallback,
408                                                     sw.scope);
409          }));
410        }
411
412        await Promise.all(promises);
413
414        // QuotaManager
415        promises = [];
416        await new Promise(resolve => {
417          quotaManagerService.getUsage(request => {
418            if (request.resultCode != Cr.NS_OK) {
419              // We are probably shutting down. We don't want to propagate the
420              // error, rejecting the promise.
421              resolve();
422              return;
423            }
424
425            for (let item of request.result) {
426              let principal =
427                Services.scriptSecurityManager
428                        .createCodebasePrincipalFromOrigin(item.origin);
429              let uri = principal.URI;
430              if (uri.scheme == "http" || uri.scheme == "https" ||
431                  uri.scheme == "file") {
432                promises.push(new Promise(r => {
433                  let req =
434                    quotaManagerService.clearStoragesForPrincipal(principal,
435                                                                  null, false);
436                  req.callback = () => { r(); };
437                }));
438              }
439            }
440            resolve();
441          });
442        });
443
444        return Promise.all(promises);
445      }
446    },
447
448    history: {
449      async clear(range) {
450        let seenException;
451        try {
452          if (range) {
453            await PlacesUtils.history.removeVisitsByFilter({
454              beginDate: new Date(range[0] / 1000),
455              endDate: new Date(range[1] / 1000)
456            });
457          } else {
458            // Remove everything.
459            await PlacesUtils.history.clear();
460          }
461        } catch (ex) {
462          seenException = ex;
463        }
464
465        try {
466          let clearStartingTime = range ? String(range[0]) : "";
467          Services.obs.notifyObservers(null, "browser:purge-session-history",
468                                       clearStartingTime);
469        } catch (ex) {
470          seenException = ex;
471        }
472
473        try {
474          let predictor = Cc["@mozilla.org/network/predictor;1"]
475                            .getService(Ci.nsINetworkPredictor);
476          predictor.reset();
477        } catch (ex) {
478          seenException = ex;
479        }
480
481        if (seenException) {
482          throw seenException;
483        }
484      }
485    },
486
487    urlbar: {
488      async clear(range) {
489        let seenException;
490        // Clear last URL of the Open Web Location dialog
491        try {
492          Services.prefs.clearUserPref("general.open_location.last_url");
493        } catch(ex) {}
494
495        try {
496          // Clear URLbar history (see also pref-history.js)
497          let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
498          file.append("urlbarhistory.sqlite");
499          if (file.exists()) {
500            file.remove(false);
501          }
502        } catch (ex) {
503          seenException = ex;
504        }
505        if (seenException) {
506          throw seenException;
507        }
508      }
509    },
510
511    formdata: {
512      async clear(range) {
513        let seenException;
514        try {
515          // Clear undo history of all search and find bars.
516          let windows = Services.wm.getEnumerator("navigator:browser");
517          while (windows.hasMoreElements()) {
518            let win = windows.getNext();
519            let currentDocument = win.document;
520
521            let findBar = currentDocument.getElementById("FindToolbar");
522            if (findBar) {
523              findBar.clear();
524            }
525            // searchBar.textbox may not exist due to the search bar binding
526            // not having been constructed yet if the search bar is in the
527            // overflow or menu panel. It won't have a value or edit history in
528            // that case.
529            let searchBar = currentDocument.getElementById("searchbar");
530            if (searchBar && searchBar.textbox) {
531              searchBar.textbox.reset();
532            }
533
534            let sideSearchBar = win.BrowserSearch.searchSidebar;
535            if (sideSearchBar) {
536              sideSearchBar.reset();
537            }
538          }
539        } catch (ex) {
540          seenException = ex;
541        }
542
543        try {
544          let change = { op: "remove" };
545          if (range) {
546            [ change.firstUsedStart, change.firstUsedEnd ] = range;
547          }
548          await new Promise(resolve => {
549            FormHistory.update(change, {
550              handleError(e) {
551                seenException = new Error("Error " + e.result + ": " +
552                                          e.message);
553              },
554              handleCompletion() {
555                resolve();
556              }
557            });
558          });
559        } catch (ex) {
560          seenException = ex;
561        }
562
563        if (seenException) {
564          throw seenException;
565        }
566      }
567    },
568
569    downloads: {
570      async clear(range) {
571        try {
572          let filterByTime = null;
573          if (range) {
574            // Convert microseconds back to milliseconds for date comparisons.
575            let rangeBeginMs = range[0] / 1000;
576            let rangeEndMs = range[1] / 1000;
577            filterByTime = download => download.startTime >= rangeBeginMs &&
578                                       download.startTime <= rangeEndMs;
579          }
580
581          // Clear all completed/cancelled downloads
582          let list = await Downloads.getList(Downloads.ALL);
583          list.removeFinished(filterByTime);
584        } catch (ex) {}
585      }
586    },
587
588    passwords: {
589      async clear(range) {
590        try {
591          Services.logins.removeAllLogins();
592        } catch (ex) {}
593      }
594    },
595
596    sessions: {
597      async clear(range) {
598        try {
599          // clear all auth tokens
600          let sdr = Cc["@mozilla.org/security/sdr;1"]
601                      .getService(Ci.nsISecretDecoderRing);
602          sdr.logoutAndTeardown();
603
604          // clear FTP and plain HTTP auth sessions
605          Services.obs.notifyObservers(null, "net:clear-active-logins");
606        } catch (ex) {}
607      }
608    },
609
610    siteSettings: {
611      async clear(range) {
612        let seenException;
613
614        let startDateMS = range ? range[0] / 1000 : null;
615
616        try {
617          // Clear site-specific permissions like
618          // "Allow this site to open popups".
619          // We ignore the "end" range and hope it is now() - none of the
620          // interfaces used here support a true range anyway.
621          if (startDateMS == null) {
622            Services.perms.removeAll();
623          } else {
624            Services.perms.removeAllSince(startDateMS);
625          }
626        } catch (ex) {
627          seenException = ex;
628        }
629
630        try {
631          // Clear site-specific settings like page-zoom level
632          let cps = Cc["@mozilla.org/content-pref/service;1"]
633                      .getService(Ci.nsIContentPrefService2);
634          if (startDateMS == null) {
635            cps.removeAllDomains(null);
636          } else {
637            cps.removeAllDomainsSince(startDateMS, null);
638          }
639        } catch (ex) {
640          seenException = ex;
641        }
642
643        try {
644          // Clear site security settings - no support for ranges in this
645          // interface either, so we clearAll().
646          let sss = Cc["@mozilla.org/ssservice;1"]
647                      .getService(Ci.nsISiteSecurityService);
648          sss.clearAll();
649        } catch (ex) {
650          seenException = ex;
651        }
652
653        // Clear all push notification subscriptions
654        try {
655          await new Promise((resolve, reject) => {
656            let push = Cc["@mozilla.org/push/Service;1"]
657                         .getService(Ci.nsIPushService);
658            push.clearForDomain("*", status => {
659              if (Components.isSuccessCode(status)) {
660                resolve();
661              } else {
662                reject(new Error("Error clearing push subscriptions: " +
663                                 status));
664              }
665            });
666          });
667        } catch (ex) {
668          seenException = ex;
669        }
670
671        if (seenException) {
672          throw seenException;
673        }
674      }
675    },
676
677    openWindows: {
678      _canCloseWindow(win) {
679        if (win.CanCloseWindow()) {
680          // We already showed PermitUnload for the window, so let's
681          // make sure we don't do it again when we actually close the
682          // window.
683          win.skipNextCanClose = true;
684          return true;
685        }
686        return false;
687      },
688      _resetAllWindowClosures(windowList) {
689        for (let win of windowList) {
690          win.skipNextCanClose = false;
691        }
692      },
693      async clear(range, privateStateForNewWindow = "non-private") {
694        // NB: this closes all *browser* windows, not other windows like the
695        // library, about window,  browser console, etc.
696
697        // Keep track of the time in case we get stuck in la-la-land because of
698        // onbeforeunload dialogs.
699        let existingWindow = Services.appShell.hiddenDOMWindow;
700        let startDate = existingWindow.performance.now();
701
702        // First check if all these windows are OK with being closed:
703        let windowEnumerator = Services.wm.getEnumerator("navigator:browser");
704        let windowList = [];
705        while (windowEnumerator.hasMoreElements()) {
706          let someWin = windowEnumerator.getNext();
707          windowList.push(someWin);
708          // If someone says "no" to a beforeunload prompt, we abort here:
709          if (!this._canCloseWindow(someWin)) {
710            this._resetAllWindowClosures(windowList);
711            throw new Error("Sanitize could not close windows: " +
712                            "cancelled by user");
713          }
714
715          // ...however, beforeunload prompts spin the event loop, and so the
716          // code here won't get hit until the prompt has been dismissed.
717          // If more than 1 minute has elapsed since we started prompting,
718          // stop, because the user might not even remember initiating the
719          // 'forget', and the timespans will be all wrong by now anyway:
720          if (existingWindow.performance.now() > (startDate + 60 * 1000)) {
721            this._resetAllWindowClosures(windowList);
722            throw new Error("Sanitize could not close windows: timeout");
723          }
724        }
725
726        // If/once we get here, we should actually be able to close all
727        // windows.
728
729        // First create a new window. We do this first so that on non-mac, we
730        // don't accidentally close the app by closing all the windows.
731        let handler = Cc["@mozilla.org/browser/clh;1"]
732                        .getService(Ci.nsIBrowserHandler);
733        let defaultArgs = handler.defaultArgs;
734        let features = "chrome,all,dialog=no," + privateStateForNewWindow;
735        let newWindow = existingWindow.openDialog("chrome://browser/content/",
736                                                 "_blank", features,
737                                                 defaultArgs);
738
739        let onFullScreen = null;
740        if (AppConstants.platform == "macosx") {
741          onFullScreen = function(e) {
742            newWindow.removeEventListener("fullscreen", onFullScreen);
743            let docEl = newWindow.document.documentElement;
744            let sizemode = docEl.getAttribute("sizemode");
745            if (!newWindow.fullScreen && sizemode == "fullscreen") {
746              docEl.setAttribute("sizemode", "normal");
747              e.preventDefault();
748              e.stopPropagation();
749              return false;
750            }
751            return undefined;
752          };
753          newWindow.addEventListener("fullscreen", onFullScreen);
754        }
755
756        let promiseReady = new Promise(resolve => {
757          // Window creation and destruction is asynchronous. We need to wait
758          // until all existing windows are fully closed, and the new window is
759          // fully open, before continuing. Otherwise the rest of the sanitizer
760          // could run too early (and miss new cookies being set when a page
761          // closes) and/or run too late (and not have a fully-formed window
762          //  yet in existence). See bug 1088137.
763          let newWindowOpened = false;
764          let onWindowOpened = function(subject, topic, data) {
765            if (subject != newWindow)
766              return;
767
768            Services.obs.removeObserver(onWindowOpened,
769                                        "browser-delayed-startup-finished");
770            if (AppConstants.platform == "macosx") {
771              newWindow.removeEventListener("fullscreen", onFullScreen);
772            }
773            newWindowOpened = true;
774            // If we're the last thing to happen, invoke callback.
775            if (numWindowsClosing == 0) {
776              resolve();
777            }
778          };
779
780          let numWindowsClosing = windowList.length;
781          let onWindowClosed = function() {
782            numWindowsClosing--;
783            if (numWindowsClosing == 0) {
784              Services.obs.removeObserver(onWindowClosed,
785                                          "xul-window-destroyed");
786              // If we're the last thing to happen, invoke callback.
787              if (newWindowOpened) {
788                resolve();
789              }
790            }
791          };
792          Services.obs.addObserver(onWindowOpened,
793                                   "browser-delayed-startup-finished");
794          Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
795        });
796
797        // Start the process of closing windows
798        while (windowList.length) {
799          windowList.pop().close();
800        }
801        newWindow.focus();
802        await promiseReady;
803      }
804    },
805
806    pluginData: {
807      async clear(range) {
808        await clearPluginData(range);
809      },
810    },
811  },
812};
813
814async function sanitizeInternal(items, aItemsToClear, progress, options = {}) {
815  let { ignoreTimespan = true, range } = options;
816  let seenError = false;
817  // Shallow copy the array, as we are going to modify it in place later.
818  if (!Array.isArray(aItemsToClear))
819    throw new Error("Must pass an array of items to clear.");
820  let itemsToClear = [...aItemsToClear];
821
822  // Store the list of items to clear, in case we are killed before we
823  // get a chance to complete.
824  let uid = gPendingSanitizationSerial++;
825  // Shutdown sanitization is managed outside.
826  if (!progress.isShutdown)
827    addPendingSanitization(uid, itemsToClear, options);
828
829  // Store the list of items to clear, for debugging/forensics purposes
830  for (let k of itemsToClear) {
831    progress[k] = "ready";
832  }
833
834  // Ensure open windows get cleared first, if they're in our list, so that
835  // they don't stick around in the recently closed windows list, and so we
836  // can cancel the whole thing if the user selects to keep a window open
837  // from a beforeunload prompt.
838  let openWindowsIndex = itemsToClear.indexOf("openWindows");
839  if (openWindowsIndex != -1) {
840    itemsToClear.splice(openWindowsIndex, 1);
841    await items.openWindows.clear(null, options);
842    progress.openWindows = "cleared";
843  }
844
845  // If we ignore timespan, clear everything,
846  // otherwise, pick a range.
847  if (!ignoreTimespan && !range) {
848    range = Sanitizer.getClearRange();
849  }
850
851  // For performance reasons we start all the clear tasks at once, then wait
852  // for their promises later.
853  // Some of the clear() calls may raise exceptions (for example bug 265028),
854  // we catch and store them, but continue to sanitize as much as possible.
855  // Callers should check returned errors and give user feedback
856  // about items that could not be sanitized
857  let annotateError = (name, ex) => {
858    progress[name] = "failed";
859    seenError = true;
860    console.error("Error sanitizing " + name, ex);
861  };
862
863  // Array of objects in form { name, promise }.
864  // `name` is the item's name and `promise` may be a promise, if the
865  // sanitization is asynchronous, or the function return value, otherwise.
866  let handles = [];
867  for (let name of itemsToClear) {
868    let item = items[name];
869    try {
870      // Catch errors here, so later we can just loop through these.
871      handles.push({ name,
872                     promise: item.clear(range, options)
873                                  .then(() => progress[name] = "cleared",
874                                        ex => annotateError(name, ex))
875                   });
876    } catch (ex) {
877      annotateError(name, ex);
878    }
879  }
880  for (let handle of handles) {
881    progress[handle.name] = "blocking";
882    await handle.promise;
883  }
884
885  // Sanitization is complete.
886  if (!progress.isShutdown)
887    removePendingSanitization(uid);
888  progress = {};
889  if (seenError) {
890    throw new Error("Error sanitizing");
891  }
892}
893
894async function clearPluginData(range) {
895  // Clear plugin data.
896  // As evidenced in bug 1253204, clearing plugin data can sometimes be
897  // very, very long, for mysterious reasons. Unfortunately, this is not
898  // something actionable by Mozilla, so crashing here serves no purpose.
899  //
900  // For this reason, instead of waiting for sanitization to always
901  // complete, we introduce a soft timeout. Once this timeout has
902  // elapsed, we proceed with the shutdown of Firefox.
903  let seenException;
904
905  let promiseClearPluginData = async function() {
906    const FLAG_CLEAR_ALL = Ci.nsIPluginHost.FLAG_CLEAR_ALL;
907    let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
908
909    // Determine age range in seconds. (-1 means clear all.) We don't know
910    // that range[1] is actually now, so we compute age range based
911    // on the lower bound. If range results in a negative age, do nothing.
912    let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1;
913    if (!range || age >= 0) {
914      let tags = ph.getPluginTags();
915      for (let tag of tags) {
916        try {
917          let rv = await new Promise(resolve =>
918            ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
919          );
920          // If the plugin doesn't support clearing by age, clear everything.
921          if (rv == Cr.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
922            await new Promise(resolve =>
923              ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve)
924            );
925          }
926        } catch (ex) {
927          // Ignore errors from plug-ins
928        }
929      }
930    }
931  };
932
933  try {
934    // We don't want to wait for this operation to complete...
935    promiseClearPluginData = promiseClearPluginData(range);
936
937    // ... at least, not for more than 10 seconds.
938    await Promise.race([
939      promiseClearPluginData,
940      new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
941    ]);
942  } catch (ex) {
943    seenException = ex;
944  }
945
946  // Detach waiting for plugin data to be cleared.
947  promiseClearPluginData.catch(() => {
948    // If this exception is raised before the soft timeout, it
949    // will appear in `seenException`. Otherwise, it's too late
950    // to do anything about it.
951  });
952
953  if (seenException) {
954    throw seenException;
955  }
956}
957
958async function sanitizeOnShutdown(progress) {
959  if (!Sanitizer.shouldSanitizeOnShutdown) {
960    return;
961  }
962  // Need to sanitize upon shutdown
963  let itemsToClear =
964    getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
965  await Sanitizer.sanitize(itemsToClear, { progress });
966  // We didn't crash during shutdown sanitization, so annotate it to avoid
967  // sanitizing again on startup.
968  removePendingSanitization("shutdown");
969  Services.prefs.savePrefFile(null);
970}
971
972/**
973 * Gets an array of items to clear from the given pref branch.
974 * @param branch The pref branch to fetch.
975 * @return Array of items to clear
976 */
977function getItemsToClearFromPrefBranch(branch) {
978  branch = Services.prefs.getBranch(branch);
979  return Object.keys(Sanitizer.items).filter(itemName => {
980    try {
981      return branch.getBoolPref(itemName);
982    } catch (ex) {
983      return false;
984    }
985  });
986}
987
988/**
989 * These functions are used to track pending sanitization on the next
990 * startup in case of a crash before a sanitization could happen.
991 * @param id A unique id identifying the sanitization
992 * @param itemsToClear The items to clear
993 * @param options The Sanitize options
994 */
995function addPendingSanitization(id, itemsToClear, options) {
996  let pendingSanitizations = safeGetPendingSanitizations();
997  pendingSanitizations.push({id, itemsToClear, options});
998  Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
999                               JSON.stringify(pendingSanitizations));
1000}
1001function removePendingSanitization(id) {
1002  let pendingSanitizations = safeGetPendingSanitizations();
1003  let i = pendingSanitizations.findIndex(s => s.id == id);
1004  let [s] = pendingSanitizations.splice(i, 1);
1005  Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
1006    JSON.stringify(pendingSanitizations));
1007  return s;
1008}
1009function getAndClearPendingSanitizations() {
1010  let pendingSanitizations = safeGetPendingSanitizations();
1011  if (pendingSanitizations.length)
1012    Services.prefs.clearUserPref(Sanitizer.PREF_PENDING_SANITIZATIONS);
1013  return pendingSanitizations;
1014}
1015function safeGetPendingSanitizations() {
1016  try {
1017    return JSON.parse(
1018      Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
1019                                   "[]"));
1020  } catch (ex) {
1021    Cu.reportError("Invalid JSON value for pending sanitizations: " + ex);
1022    return [];
1023  }
1024}
1025