tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

sanitizeDialog.js (18700B)


      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 
      6 /* import-globals-from /toolkit/content/preferencesBindings.js */
      7 
      8 var { Sanitizer } = ChromeUtils.importESModule(
      9  "resource:///modules/Sanitizer.sys.mjs"
     10 );
     11 
     12 const { XPCOMUtils } = ChromeUtils.importESModule(
     13  "resource://gre/modules/XPCOMUtils.sys.mjs"
     14 );
     15 
     16 const lazy = {};
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
     20  SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs",
     21 });
     22 
     23 XPCOMUtils.defineLazyPreferenceGetter(
     24  lazy,
     25  "USE_OLD_DIALOG",
     26  "privacy.sanitize.useOldClearHistoryDialog",
     27  false
     28 );
     29 
     30 Preferences.addAll([
     31  { id: "privacy.cpd.history", type: "bool" },
     32  { id: "privacy.cpd.formdata", type: "bool" },
     33  { id: "privacy.cpd.downloads", type: "bool", disabled: true },
     34  { id: "privacy.cpd.cookies", type: "bool" },
     35  { id: "privacy.cpd.cache", type: "bool" },
     36  { id: "privacy.cpd.sessions", type: "bool" },
     37  { id: "privacy.cpd.offlineApps", type: "bool" },
     38  { id: "privacy.cpd.siteSettings", type: "bool" },
     39  { id: "privacy.sanitize.timeSpan", type: "int" },
     40  { id: "privacy.clearOnShutdown.history", type: "bool" },
     41  { id: "privacy.clearHistory.browsingHistoryAndDownloads", type: "bool" },
     42  { id: "privacy.clearHistory.cookiesAndStorage", type: "bool" },
     43  { id: "privacy.clearHistory.cache", type: "bool" },
     44  { id: "privacy.clearHistory.siteSettings", type: "bool" },
     45  { id: "privacy.clearHistory.formdata", type: "bool" },
     46  { id: "privacy.clearSiteData.browsingHistoryAndDownloads", type: "bool" },
     47  { id: "privacy.clearSiteData.cookiesAndStorage", type: "bool" },
     48  { id: "privacy.clearSiteData.cache", type: "bool" },
     49  { id: "privacy.clearSiteData.siteSettings", type: "bool" },
     50  { id: "privacy.clearSiteData.formdata", type: "bool" },
     51  {
     52    id: "privacy.clearOnShutdown_v2.browsingHistoryAndDownloads",
     53    type: "bool",
     54  },
     55  { id: "privacy.clearOnShutdown.formdata", type: "bool" },
     56  { id: "privacy.clearOnShutdown_v2.formdata", type: "bool" },
     57  { id: "privacy.clearOnShutdown.downloads", type: "bool" },
     58  { id: "privacy.clearOnShutdown_v2.downloads", type: "bool" },
     59  { id: "privacy.clearOnShutdown.cookies", type: "bool" },
     60  { id: "privacy.clearOnShutdown_v2.cookiesAndStorage", type: "bool" },
     61  { id: "privacy.clearOnShutdown.cache", type: "bool" },
     62  { id: "privacy.clearOnShutdown_v2.cache", type: "bool" },
     63  { id: "privacy.clearOnShutdown.offlineApps", type: "bool" },
     64  { id: "privacy.clearOnShutdown.sessions", type: "bool" },
     65  { id: "privacy.clearOnShutdown.siteSettings", type: "bool" },
     66  { id: "privacy.clearOnShutdown_v2.siteSettings", type: "bool" },
     67 ]);
     68 
     69 var gSanitizePromptDialog = {
     70  get selectedTimespan() {
     71    var durList = document.getElementById("sanitizeDurationChoice");
     72    return parseInt(durList.value);
     73  },
     74 
     75  get warningBox() {
     76    return document.getElementById("sanitizeEverythingWarningBox");
     77  },
     78 
     79  async init() {
     80    // This is used by selectByTimespan() to determine if the window has loaded.
     81    this._inited = true;
     82    this._dialog = document.querySelector("dialog");
     83    /**
     84     * Variables to store data sizes to display to user
     85     * for different timespans
     86     */
     87    this.siteDataSizes = {};
     88    this.cacheSize = [];
     89 
     90    let arg = window.arguments?.[0] || {};
     91 
     92    // These variables decide which context the dialog has been opened in
     93    this._inClearOnShutdownNewDialog = false;
     94    this._inClearSiteDataNewDialog = false;
     95    this._inBrowserWindow = !!arg.inBrowserWindow;
     96    if (arg.mode && !lazy.USE_OLD_DIALOG) {
     97      this._inClearOnShutdownNewDialog = arg.mode == "clearOnShutdown";
     98      this._inClearSiteDataNewDialog = arg.mode == "clearSiteData";
     99    }
    100 
    101    if (arg.inBrowserWindow) {
    102      this._dialog.setAttribute("inbrowserwindow", "true");
    103      this._observeTitleForChanges();
    104    } else if (arg.wrappedJSObject?.needNativeUI) {
    105      document
    106        .getElementById("sanitizeDurationChoice")
    107        .setAttribute("native", "true");
    108      for (let cb of document.querySelectorAll("checkbox")) {
    109        cb.setAttribute("native", "true");
    110      }
    111    }
    112 
    113    if (!lazy.USE_OLD_DIALOG) {
    114      this._dataSizesUpdated = false;
    115      this.dataSizesFinishedUpdatingPromise = this.getAndUpdateDataSizes(); // this promise is still used in tests
    116    }
    117 
    118    let OKButton = this._dialog.getButton("accept");
    119    let clearOnShutdownGroupbox = document.getElementById(
    120      "clearOnShutdownGroupbox"
    121    );
    122    let clearPrivateDataGroupbox = document.getElementById(
    123      "clearPrivateDataGroupbox"
    124    );
    125    let clearSiteDataGroupbox = document.getElementById(
    126      "clearSiteDataGroupbox"
    127    );
    128 
    129    let okButtonl10nID = "sanitize-button-ok";
    130    if (this._inClearOnShutdownNewDialog) {
    131      okButtonl10nID = "sanitize-button-ok-on-shutdown";
    132      this._dialog.setAttribute("inClearOnShutdown", "true");
    133 
    134      // remove the other groupbox elements that aren't related to the context
    135      // the dialog is opened in
    136      clearPrivateDataGroupbox.remove();
    137      clearSiteDataGroupbox.remove();
    138      // If this is the first time the user is opening the new clear on shutdown
    139      // dialog, migrate their prefs
    140      Sanitizer.maybeMigratePrefs("clearOnShutdown");
    141    } else if (!lazy.USE_OLD_DIALOG) {
    142      okButtonl10nID = "sanitize-button-ok2";
    143      clearOnShutdownGroupbox.remove();
    144      if (this._inClearSiteDataNewDialog) {
    145        clearPrivateDataGroupbox.remove();
    146        // we do not need to migrate prefs for clear site data,
    147        // since we decided to keep the default options for
    148        // privacy.clearSiteData.* to stay consistent with old behaviour
    149        // of the clear site data dialog box
    150      } else {
    151        clearSiteDataGroupbox.remove();
    152        Sanitizer.maybeMigratePrefs("cpd");
    153      }
    154    }
    155    document.l10n.setAttributes(OKButton, okButtonl10nID);
    156 
    157    if (!lazy.USE_OLD_DIALOG) {
    158      this._sinceMidnightSanitizeDurationOption = document.getElementById(
    159        "sanitizeSinceMidnight"
    160      );
    161      this._cookiesAndSiteDataCheckbox =
    162        document.getElementById("cookiesAndStorage");
    163      this._cacheCheckbox = document.getElementById("cache");
    164 
    165      let midnightTime = Intl.DateTimeFormat(navigator.language, {
    166        hour: "numeric",
    167        minute: "numeric",
    168      }).format(new Date().setHours(0, 0, 0, 0));
    169      document.l10n.setAttributes(
    170        this._sinceMidnightSanitizeDurationOption,
    171        "clear-time-duration-value-since-midnight",
    172        { midnightTime }
    173      );
    174    }
    175 
    176    document
    177      .getElementById("sanitizeDurationChoice")
    178      .addEventListener("select", () => this.selectByTimespan());
    179 
    180    document.addEventListener("dialogaccept", e => {
    181      if (this._inClearOnShutdownNewDialog) {
    182        this.updatePrefs();
    183      } else {
    184        this.sanitize(e);
    185      }
    186    });
    187 
    188    this._allCheckboxes = document.querySelectorAll("checkbox[preference]");
    189 
    190    this.registerSyncFromPrefListeners();
    191 
    192    // we want to show the warning box for all cases except clear on shutdown
    193    if (
    194      this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING &&
    195      !this._inClearOnShutdownNewDialog
    196    ) {
    197      this.prepareWarning();
    198      this.warningBox.hidden = false;
    199      if (lazy.USE_OLD_DIALOG) {
    200        document.l10n.setAttributes(
    201          document.documentElement,
    202          "sanitize-dialog-title-everything"
    203        );
    204      }
    205      let warningDesc = document.getElementById("sanitizeEverythingWarning");
    206      // Ensure we've translated and sized the warning.
    207      await document.l10n.translateFragment(warningDesc);
    208      let rootWin = window.browsingContext.topChromeWindow;
    209      await rootWin.promiseDocumentFlushed(() => {});
    210    } else {
    211      this.warningBox.hidden = true;
    212    }
    213  },
    214 
    215  updateAcceptButtonState() {
    216    // Check if none of the checkboxes are checked
    217    let noneChecked = Array.from(this._allCheckboxes).every(cb => !cb.checked);
    218    let acceptButton = this._dialog.getButton("accept");
    219 
    220    acceptButton.disabled = noneChecked;
    221  },
    222 
    223  async selectByTimespan() {
    224    // This method is the onselect handler for the duration dropdown.  As a
    225    // result it's called a couple of times before onload calls init().
    226    if (!this._inited) {
    227      return;
    228    }
    229 
    230    var warningBox = this.warningBox;
    231 
    232    // If clearing everything
    233    if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
    234      this.prepareWarning();
    235      if (warningBox.hidden) {
    236        warningBox.hidden = false;
    237        let diff =
    238          warningBox.nextElementSibling.getBoundingClientRect().top -
    239          warningBox.previousElementSibling.getBoundingClientRect().bottom;
    240        window.resizeBy(0, diff);
    241      }
    242 
    243      // update title for the old dialog
    244      if (lazy.USE_OLD_DIALOG) {
    245        document.l10n.setAttributes(
    246          document.documentElement,
    247          "sanitize-dialog-title-everything"
    248        );
    249      }
    250      // make sure the sizes are updated in the new dialog
    251      else {
    252        await this.updateDataSizesInUI();
    253      }
    254      return;
    255    }
    256 
    257    // If clearing a specific time range
    258    if (!warningBox.hidden) {
    259      let diff =
    260        warningBox.nextElementSibling.getBoundingClientRect().top -
    261        warningBox.previousElementSibling.getBoundingClientRect().bottom;
    262      window.resizeBy(0, -diff);
    263      warningBox.hidden = true;
    264    }
    265    let datal1OnId = lazy.USE_OLD_DIALOG
    266      ? "sanitize-dialog-title"
    267      : "sanitize-dialog-title2";
    268    document.l10n.setAttributes(document.documentElement, datal1OnId);
    269 
    270    if (!lazy.USE_OLD_DIALOG) {
    271      // We only update data sizes to display on the new dialog
    272      await this.updateDataSizesInUI();
    273    }
    274  },
    275 
    276  sanitize(event) {
    277    // Update pref values before handing off to the sanitizer (bug 453440)
    278    this.updatePrefs();
    279 
    280    // As the sanitize is async, we disable the buttons, update the label on
    281    // the 'accept' button to indicate things are happening and return false -
    282    // once the async operation completes (either with or without errors)
    283    // we close the window.
    284    let acceptButton = this._dialog.getButton("accept");
    285    acceptButton.disabled = true;
    286    document.l10n.setAttributes(acceptButton, "sanitize-button-clearing");
    287    this._dialog.getButton("cancel").disabled = true;
    288 
    289    try {
    290      let range = Sanitizer.getClearRange(this.selectedTimespan);
    291      let options = {
    292        ignoreTimespan: !range,
    293        range,
    294      };
    295 
    296      let itemsToClear = this.getItemsToClear();
    297      Sanitizer.sanitize(itemsToClear, options)
    298        .catch(console.error)
    299        .then(() => {
    300          // we don't need to update data sizes in settings when the dialog is opened
    301          // in the browser context
    302          if (!this._inBrowserWindow) {
    303            // call update sites to ensure the data sizes displayed
    304            // in settings is updated.
    305            lazy.SiteDataManager.updateSites();
    306          }
    307          window.close();
    308        })
    309        .catch(console.error);
    310      event.preventDefault();
    311    } catch (er) {
    312      console.error("Exception during sanitize: ", er);
    313    }
    314  },
    315 
    316  /**
    317   * If the panel that displays a warning when the duration is "Everything" is
    318   * not set up, sets it up.  Otherwise does nothing.
    319   */
    320  prepareWarning() {
    321    // If the date and time-aware locale warning string is ever used again,
    322    // initialize it here.  Currently we use the no-visits warning string,
    323    // which does not include date and time.  See bug 480169 comment 48.
    324 
    325    var warningDesc = document.getElementById("sanitizeEverythingWarning");
    326    if (this.hasNonSelectedItems()) {
    327      document.l10n.setAttributes(warningDesc, "sanitize-selected-warning");
    328    } else {
    329      document.l10n.setAttributes(warningDesc, "sanitize-everything-warning");
    330    }
    331  },
    332 
    333  /**
    334   * Return the boolean prefs that correspond to the checkboxes on the dialog.
    335   */
    336  _getItemPrefs() {
    337    return Array.from(this._allCheckboxes).map(checkbox =>
    338      checkbox.getAttribute("preference")
    339    );
    340  },
    341 
    342  /**
    343   * Called when the value of a preference element is synced from the actual
    344   * pref.  Enables or disables the OK button appropriately.
    345   */
    346  onReadGeneric() {
    347    // Find any other pref that's checked and enabled (except for
    348    // privacy.sanitize.timeSpan, which doesn't affect the button's status.
    349    // and (in the old dialog) privacy.cpd.downloads which is not controlled
    350    // directly by a checkbox).
    351    var found = this._getItemPrefs().some(
    352      pref => Preferences.get(pref).value === true
    353    );
    354 
    355    try {
    356      this._dialog.getButton("accept").disabled = !found;
    357    } catch (e) {}
    358 
    359    // Update the warning prompt if needed
    360    this.prepareWarning();
    361 
    362    return undefined;
    363  },
    364 
    365  /**
    366   * Gets the latest usage data and then updates the UI
    367   *
    368   * @returns {Promise} resolves when updating the UI is complete
    369   */
    370  async getAndUpdateDataSizes() {
    371    if (lazy.USE_OLD_DIALOG) {
    372      return;
    373    }
    374 
    375    // We have to update sites before displaying data sizes
    376    // when the dialog is opened in the browser context, since users
    377    // can open the dialog in this context without opening about:preferences.
    378    // When a user opens about:preferences, updateSites is called on load.
    379    if (this._inBrowserWindow) {
    380      await lazy.SiteDataManager.updateSites();
    381    }
    382    // Current timespans used in the dialog box
    383    const ALL_TIMESPANS = [
    384      "TIMESPAN_HOUR",
    385      "TIMESPAN_2HOURS",
    386      "TIMESPAN_4HOURS",
    387      "TIMESPAN_TODAY",
    388      "TIMESPAN_EVERYTHING",
    389    ];
    390 
    391    let [quotaUsage, cacheSize] = await Promise.all([
    392      lazy.SiteDataManager.getQuotaUsageForTimeRanges(ALL_TIMESPANS),
    393      lazy.SiteDataManager.getCacheSize(),
    394    ]);
    395    // Convert sizes to [amount, unit]
    396    for (const timespan in quotaUsage) {
    397      this.siteDataSizes[timespan] = lazy.DownloadUtils.convertByteUnits(
    398        quotaUsage[timespan]
    399      );
    400    }
    401    this.cacheSize = lazy.DownloadUtils.convertByteUnits(cacheSize);
    402 
    403    this._dataSizesUpdated = true;
    404    await this.updateDataSizesInUI();
    405  },
    406 
    407  /**
    408   * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
    409   * Because the type of this prefwindow is "child" -- and that's needed because
    410   * without it the dialog has no OK and Cancel buttons -- the prefs are not
    411   * updated on dialogaccept.  We must therefore manually set the prefs
    412   * from their corresponding preference elements.
    413   */
    414  updatePrefs() {
    415    Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, this.selectedTimespan);
    416 
    417    if (lazy.USE_OLD_DIALOG) {
    418      let historyValue = Preferences.get(`privacy.cpd.history`).value;
    419      // Keep the pref for the download history in sync with the history pref.
    420      Preferences.get("privacy.cpd.downloads").value = historyValue;
    421      Services.prefs.setBoolPref("privacy.cpd.downloads", historyValue);
    422    }
    423 
    424    // Now manually set the prefs from their corresponding preference
    425    // elements.
    426    var prefs = this._getItemPrefs();
    427    for (let i = 0; i < prefs.length; ++i) {
    428      var p = Preferences.get(prefs[i]);
    429      Services.prefs.setBoolPref(p.id, p.value);
    430    }
    431  },
    432 
    433  /**
    434   * Check if all of the history items have been selected like the default status.
    435   */
    436  hasNonSelectedItems() {
    437    let checkboxes = document.querySelectorAll("checkbox[preference]");
    438    for (let i = 0; i < checkboxes.length; ++i) {
    439      let pref = Preferences.get(checkboxes[i].getAttribute("preference"));
    440      if (!pref.value) {
    441        return true;
    442      }
    443    }
    444    return false;
    445  },
    446 
    447  /**
    448   * Register syncFromPref listener functions.
    449   */
    450  registerSyncFromPrefListeners() {
    451    let checkboxes = document.querySelectorAll("checkbox[preference]");
    452    for (let checkbox of checkboxes) {
    453      Preferences.addSyncFromPrefListener(checkbox, () => this.onReadGeneric());
    454    }
    455  },
    456 
    457  _titleChanged() {
    458    let title = document.documentElement.getAttribute("title");
    459    if (title) {
    460      document.getElementById("titleText").textContent = title;
    461    }
    462  },
    463 
    464  _observeTitleForChanges() {
    465    this._titleChanged();
    466    this._mutObs = new MutationObserver(() => {
    467      this._titleChanged();
    468    });
    469    this._mutObs.observe(document.documentElement, {
    470      attributes: true,
    471      attributeFilter: ["title"],
    472    });
    473  },
    474 
    475  /**
    476   * Updates data sizes displayed based on new selected timespan
    477   */
    478  async updateDataSizesInUI() {
    479    if (!this._dataSizesUpdated) {
    480      return;
    481    }
    482 
    483    const TIMESPAN_SELECTION_MAP = {
    484      0: "TIMESPAN_EVERYTHING",
    485      1: "TIMESPAN_HOUR",
    486      2: "TIMESPAN_2HOURS",
    487      3: "TIMESPAN_4HOURS",
    488      4: "TIMESPAN_TODAY",
    489      5: "TIMESPAN_5MINS",
    490      6: "TIMESPAN_24HOURS",
    491    };
    492    let index = this.selectedTimespan;
    493    let timeSpanSelected = TIMESPAN_SELECTION_MAP[index];
    494    let [amount, unit] = this.siteDataSizes[timeSpanSelected];
    495 
    496    document.l10n.pauseObserving();
    497    document.l10n.setAttributes(
    498      this._cookiesAndSiteDataCheckbox,
    499      "item-cookies-site-data-with-size",
    500      { amount, unit }
    501    );
    502 
    503    [amount, unit] = this.cacheSize;
    504    document.l10n.setAttributes(
    505      this._cacheCheckbox,
    506      "item-cached-content-with-size",
    507      { amount, unit }
    508    );
    509 
    510    // make sure l10n updates are completed
    511    await document.l10n.translateElements([
    512      this._sinceMidnightSanitizeDurationOption,
    513      this._cookiesAndSiteDataCheckbox,
    514      this._cacheCheckbox,
    515    ]);
    516 
    517    document.l10n.resumeObserving();
    518 
    519    // the data sizes may have led to wrapping, resize dialog to make sure the buttons
    520    // don't move out of view
    521    await window.resizeDialog();
    522  },
    523 
    524  /**
    525   * Get all items to clear based on checked boxes
    526   *
    527   * @returns {string[]} array of items ["cache", "browsingHistoryAndDownloads"...]
    528   */
    529  getItemsToClear() {
    530    // the old dialog uses the preferences to decide what to clear
    531    if (lazy.USE_OLD_DIALOG) {
    532      return null;
    533    }
    534 
    535    let items = [];
    536    for (let cb of this._allCheckboxes) {
    537      if (cb.checked) {
    538        items.push(cb.id);
    539      }
    540    }
    541    return items;
    542  },
    543 };
    544 
    545 // We need to give the dialog an opportunity to set up the DOM
    546 // before its measured for the SubDialog it will be embedded in.
    547 // This is because the sanitizeEverythingWarningBox may or may
    548 // not be visible, depending on whether "Everything" is the default
    549 // choice in the menulist.
    550 document.mozSubdialogReady = new Promise(resolve => {
    551  window.addEventListener(
    552    "load",
    553    function () {
    554      gSanitizePromptDialog.init().then(resolve);
    555    },
    556    {
    557      once: true,
    558    }
    559  );
    560 });