/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* import-globals-from /toolkit/content/preferencesBindings.js */ var { Sanitizer } = ChromeUtils.importESModule( "resource:///modules/Sanitizer.sys.mjs" ); const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "USE_OLD_DIALOG", "privacy.sanitize.useOldClearHistoryDialog", false ); Preferences.addAll([ { id: "privacy.cpd.history", type: "bool" }, { id: "privacy.cpd.formdata", type: "bool" }, { id: "privacy.cpd.downloads", type: "bool", disabled: true }, { id: "privacy.cpd.cookies", type: "bool" }, { id: "privacy.cpd.cache", type: "bool" }, { id: "privacy.cpd.sessions", type: "bool" }, { id: "privacy.cpd.offlineApps", type: "bool" }, { id: "privacy.cpd.siteSettings", type: "bool" }, { id: "privacy.sanitize.timeSpan", type: "int" }, { id: "privacy.clearOnShutdown.history", type: "bool" }, { id: "privacy.clearHistory.historyFormDataAndDownloads", type: "bool" }, { id: "privacy.clearHistory.cookiesAndStorage", type: "bool" }, { id: "privacy.clearHistory.cache", type: "bool" }, { id: "privacy.clearHistory.siteSettings", type: "bool" }, { id: "privacy.clearSiteData.historyFormDataAndDownloads", type: "bool" }, { id: "privacy.clearSiteData.cookiesAndStorage", type: "bool" }, { id: "privacy.clearSiteData.cache", type: "bool" }, { id: "privacy.clearSiteData.siteSettings", type: "bool" }, { id: "privacy.clearOnShutdown_v2.historyFormDataAndDownloads", type: "bool", }, { id: "privacy.clearOnShutdown.formdata", type: "bool" }, { id: "privacy.clearOnShutdown.downloads", type: "bool" }, { id: "privacy.clearOnShutdown_v2.downloads", type: "bool" }, { id: "privacy.clearOnShutdown.cookies", type: "bool" }, { id: "privacy.clearOnShutdown_v2.cookiesAndStorage", type: "bool" }, { id: "privacy.clearOnShutdown.cache", type: "bool" }, { id: "privacy.clearOnShutdown_v2.cache", type: "bool" }, { id: "privacy.clearOnShutdown.offlineApps", type: "bool" }, { id: "privacy.clearOnShutdown.sessions", type: "bool" }, { id: "privacy.clearOnShutdown.siteSettings", type: "bool" }, { id: "privacy.clearOnShutdown_v2.siteSettings", type: "bool" }, ]); var gSanitizePromptDialog = { get selectedTimespan() { var durList = document.getElementById("sanitizeDurationChoice"); return parseInt(durList.value); }, get warningBox() { return document.getElementById("sanitizeEverythingWarningBox"); }, async init() { // This is used by selectByTimespan() to determine if the window has loaded. this._inited = true; this._dialog = document.querySelector("dialog"); /** * Variables to store data sizes to display to user * for different timespans */ this.siteDataSizes = {}; this.cacheSize = []; let arg = window.arguments?.[0] || {}; // These variables decide which context the dialog has been opened in this._inClearOnShutdownNewDialog = false; this._inClearSiteDataNewDialog = false; this._inBrowserWindow = !!arg.inBrowserWindow; if (arg.mode && !lazy.USE_OLD_DIALOG) { this._inClearOnShutdownNewDialog = arg.mode == "clearOnShutdown"; this._inClearSiteDataNewDialog = arg.mode == "clearSiteData"; } if (arg.inBrowserWindow) { this._dialog.setAttribute("inbrowserwindow", "true"); this._observeTitleForChanges(); } else if (arg.wrappedJSObject?.needNativeUI) { document .getElementById("sanitizeDurationChoice") .setAttribute("native", "true"); for (let cb of document.querySelectorAll("checkbox")) { cb.setAttribute("native", "true"); } } if (!lazy.USE_OLD_DIALOG) { // Begin collecting how long it takes to load from here let timerId = Glean.privacySanitize.loadTime.start(); this._dataSizesUpdated = false; this.dataSizesFinishedUpdatingPromise = this.getAndUpdateDataSizes() .then(() => { // We're done loading, stop telemetry here Glean.privacySanitize.loadTime.stopAndAccumulate(timerId); }) .catch(() => { // We're done loading, stop telemetry here Glean.privacySanitize.loadTime.cancel(timerId); }); } let OKButton = this._dialog.getButton("accept"); let clearOnShutdownGroupbox = document.getElementById( "clearOnShutdownGroupbox" ); let clearPrivateDataGroupbox = document.getElementById( "clearPrivateDataGroupbox" ); let clearSiteDataGroupbox = document.getElementById( "clearSiteDataGroupbox" ); let okButtonl10nID = "sanitize-button-ok"; if (this._inClearOnShutdownNewDialog) { okButtonl10nID = "sanitize-button-ok-on-shutdown"; this._dialog.setAttribute("inClearOnShutdown", "true"); // remove the other groupbox elements that aren't related to the context // the dialog is opened in clearPrivateDataGroupbox.remove(); clearSiteDataGroupbox.remove(); // If this is the first time the user is opening the new clear on shutdown // dialog, migrate their prefs Sanitizer.maybeMigratePrefs("clearOnShutdown"); } else if (!lazy.USE_OLD_DIALOG) { okButtonl10nID = "sanitize-button-ok2"; clearOnShutdownGroupbox.remove(); if (this._inClearSiteDataNewDialog) { clearPrivateDataGroupbox.remove(); // we do not need to migrate prefs for clear site data, // since we decided to keep the default options for // privacy.clearSiteData.* to stay consistent with old behaviour // of the clear site data dialog box } else { clearSiteDataGroupbox.remove(); Sanitizer.maybeMigratePrefs("cpd"); } } document.l10n.setAttributes(OKButton, okButtonl10nID); if (!lazy.USE_OLD_DIALOG) { this._cookiesAndSiteDataCheckbox = document.getElementById("cookiesAndStorage"); this._cacheCheckbox = document.getElementById("cache"); } document.addEventListener("dialogaccept", e => { if (this._inClearOnShutdownNewDialog) { this.updatePrefs(); } else { if (!lazy.USE_OLD_DIALOG) { this.reportTelemetry("clear"); } this.sanitize(e); } }); this._allCheckboxes = document.querySelectorAll("checkbox[preference]"); this.registerSyncFromPrefListeners(); // we want to show the warning box for all cases except clear on shutdown if ( this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING && !this._inClearOnShutdownNewDialog ) { this.prepareWarning(); this.warningBox.hidden = false; if (lazy.USE_OLD_DIALOG) { document.l10n.setAttributes( document.documentElement, "sanitize-dialog-title-everything" ); } let warningDesc = document.getElementById("sanitizeEverythingWarning"); // Ensure we've translated and sized the warning. await document.l10n.translateFragment(warningDesc); let rootWin = window.browsingContext.topChromeWindow; await rootWin.promiseDocumentFlushed(() => {}); } else { this.warningBox.hidden = true; } if (!lazy.USE_OLD_DIALOG) { this.reportTelemetry("open"); } }, updateAcceptButtonState() { // Check if none of the checkboxes are checked let noneChecked = Array.from(this._allCheckboxes).every(cb => !cb.checked); let acceptButton = this._dialog.getButton("accept"); acceptButton.disabled = noneChecked; }, async selectByTimespan() { // This method is the onselect handler for the duration dropdown. As a // result it's called a couple of times before onload calls init(). if (!this._inited) { return; } var warningBox = this.warningBox; // If clearing everything if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) { this.prepareWarning(); if (warningBox.hidden) { warningBox.hidden = false; let diff = warningBox.nextElementSibling.getBoundingClientRect().top - warningBox.previousElementSibling.getBoundingClientRect().bottom; window.resizeBy(0, diff); } // update title for the old dialog if (lazy.USE_OLD_DIALOG) { document.l10n.setAttributes( document.documentElement, "sanitize-dialog-title-everything" ); } // make sure the sizes are updated in the new dialog else { await this.updateDataSizesInUI(); } return; } // If clearing a specific time range if (!warningBox.hidden) { let diff = warningBox.nextElementSibling.getBoundingClientRect().top - warningBox.previousElementSibling.getBoundingClientRect().bottom; window.resizeBy(0, -diff); warningBox.hidden = true; } let datal1OnId = lazy.USE_OLD_DIALOG ? "sanitize-dialog-title" : "sanitize-dialog-title2"; document.l10n.setAttributes(document.documentElement, datal1OnId); if (!lazy.USE_OLD_DIALOG) { // We only update data sizes to display on the new dialog await this.updateDataSizesInUI(); } }, sanitize(event) { // Update pref values before handing off to the sanitizer (bug 453440) this.updatePrefs(); // As the sanitize is async, we disable the buttons, update the label on // the 'accept' button to indicate things are happening and return false - // once the async operation completes (either with or without errors) // we close the window. let acceptButton = this._dialog.getButton("accept"); acceptButton.disabled = true; document.l10n.setAttributes(acceptButton, "sanitize-button-clearing"); this._dialog.getButton("cancel").disabled = true; try { let range = Sanitizer.getClearRange(this.selectedTimespan); let options = { ignoreTimespan: !range, range, }; let itemsToClear = this.getItemsToClear(); Sanitizer.sanitize(itemsToClear, options) .catch(console.error) .then(() => { // we don't need to update data sizes in settings when the dialog is opened // in the browser context if (!this._inBrowserWindow) { // call update sites to ensure the data sizes displayed // in settings is updated. lazy.SiteDataManager.updateSites(); } window.close(); }) .catch(console.error); event.preventDefault(); } catch (er) { console.error("Exception during sanitize: ", er); } }, /** * If the panel that displays a warning when the duration is "Everything" is * not set up, sets it up. Otherwise does nothing. */ prepareWarning() { // If the date and time-aware locale warning string is ever used again, // initialize it here. Currently we use the no-visits warning string, // which does not include date and time. See bug 480169 comment 48. var warningDesc = document.getElementById("sanitizeEverythingWarning"); if (this.hasNonSelectedItems()) { document.l10n.setAttributes(warningDesc, "sanitize-selected-warning"); } else { document.l10n.setAttributes(warningDesc, "sanitize-everything-warning"); } }, /** * Return the boolean prefs that correspond to the checkboxes on the dialog. */ _getItemPrefs() { return Array.from(this._allCheckboxes).map(checkbox => checkbox.getAttribute("preference") ); }, /** * Called when the value of a preference element is synced from the actual * pref. Enables or disables the OK button appropriately. */ onReadGeneric() { // Find any other pref that's checked and enabled (except for // privacy.sanitize.timeSpan, which doesn't affect the button's status. // and (in the old dialog) privacy.cpd.downloads which is not controlled // directly by a checkbox). var found = this._getItemPrefs().some( pref => Preferences.get(pref).value === true ); try { this._dialog.getButton("accept").disabled = !found; } catch (e) {} // Update the warning prompt if needed this.prepareWarning(); return undefined; }, /** * Gets the latest usage data and then updates the UI * * @returns {Promise} resolves when updating the UI is complete */ async getAndUpdateDataSizes() { if (lazy.USE_OLD_DIALOG) { return; } // We have to update sites before displaying data sizes // when the dialog is opened in the browser context, since users // can open the dialog in this context without opening about:preferences. // When a user opens about:preferences, updateSites is called on load. if (this._inBrowserWindow) { await lazy.SiteDataManager.updateSites(); } // Current timespans used in the dialog box const ALL_TIMESPANS = [ "TIMESPAN_HOUR", "TIMESPAN_2HOURS", "TIMESPAN_4HOURS", "TIMESPAN_TODAY", "TIMESPAN_EVERYTHING", ]; let [quotaUsage, cacheSize] = await Promise.all([ lazy.SiteDataManager.getQuotaUsageForTimeRanges(ALL_TIMESPANS), lazy.SiteDataManager.getCacheSize(), ]); // Convert sizes to [amount, unit] for (const timespan in quotaUsage) { this.siteDataSizes[timespan] = lazy.DownloadUtils.convertByteUnits( quotaUsage[timespan] ); } this.cacheSize = lazy.DownloadUtils.convertByteUnits(cacheSize); this._dataSizesUpdated = true; await this.updateDataSizesInUI(); }, /** * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date. * Because the type of this prefwindow is "child" -- and that's needed because * without it the dialog has no OK and Cancel buttons -- the prefs are not * updated on dialogaccept. We must therefore manually set the prefs * from their corresponding preference elements. */ updatePrefs() { Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, this.selectedTimespan); if (lazy.USE_OLD_DIALOG) { let historyValue = Preferences.get(`privacy.cpd.history`).value; // Keep the pref for the download history in sync with the history pref. Preferences.get("privacy.cpd.downloads").value = historyValue; Services.prefs.setBoolPref("privacy.cpd.downloads", historyValue); } // Now manually set the prefs from their corresponding preference // elements. var prefs = this._getItemPrefs(); for (let i = 0; i < prefs.length; ++i) { var p = Preferences.get(prefs[i]); Services.prefs.setBoolPref(p.id, p.value); } }, /** * Check if all of the history items have been selected like the default status. */ hasNonSelectedItems() { let checkboxes = document.querySelectorAll("checkbox[preference]"); for (let i = 0; i < checkboxes.length; ++i) { let pref = Preferences.get(checkboxes[i].getAttribute("preference")); if (!pref.value) { return true; } } return false; }, /** * Register syncFromPref listener functions. */ registerSyncFromPrefListeners() { let checkboxes = document.querySelectorAll("checkbox[preference]"); for (let checkbox of checkboxes) { Preferences.addSyncFromPrefListener(checkbox, () => this.onReadGeneric()); } }, _titleChanged() { let title = document.documentElement.getAttribute("title"); if (title) { document.getElementById("titleText").textContent = title; } }, _observeTitleForChanges() { this._titleChanged(); this._mutObs = new MutationObserver(() => { this._titleChanged(); }); this._mutObs.observe(document.documentElement, { attributes: true, attributeFilter: ["title"], }); }, /** * Updates data sizes displayed based on new selected timespan */ async updateDataSizesInUI() { if (!this._dataSizesUpdated) { return; } const TIMESPAN_SELECTION_MAP = { 0: "TIMESPAN_EVERYTHING", 1: "TIMESPAN_HOUR", 2: "TIMESPAN_2HOURS", 3: "TIMESPAN_4HOURS", 4: "TIMESPAN_TODAY", 5: "TIMESPAN_5MINS", 6: "TIMESPAN_24HOURS", }; let index = this.selectedTimespan; let timeSpanSelected = TIMESPAN_SELECTION_MAP[index]; let [amount, unit] = this.siteDataSizes[timeSpanSelected]; document.l10n.pauseObserving(); document.l10n.setAttributes( this._cookiesAndSiteDataCheckbox, "item-cookies-site-data-with-size", { amount, unit } ); [amount, unit] = this.cacheSize; document.l10n.setAttributes( this._cacheCheckbox, "item-cached-content-with-size", { amount, unit } ); // make sure l10n updates are completed await document.l10n.translateElements([ this._cookiesAndSiteDataCheckbox, this._cacheCheckbox, ]); document.l10n.resumeObserving(); // the data sizes may have led to wrapping, resize dialog to make sure the buttons // don't move out of view await window.resizeDialog(); }, /** * Get all items to clear based on checked boxes * * @returns {string[]} array of items ["cache", "historyFormDataAndDownloads"...] */ getItemsToClear() { // the old dialog uses the preferences to decide what to clear if (lazy.USE_OLD_DIALOG) { return null; } let items = []; for (let cb of this._allCheckboxes) { if (cb.checked) { items.push(cb.id); } } return items; }, reportTelemetry(event) { let contextOpenedIn; if (this._inClearSiteDataNewDialog) { contextOpenedIn = "clearSiteData"; } else if (this._inBrowserWindow) { contextOpenedIn = "browser"; } else { contextOpenedIn = "clearHistory"; } // Report time span and clearing options after sanitize is clicked if (event == "clear") { Glean.privacySanitize.clearingTimeSpanSelected.record({ time_span: this.selectedTimespan.toString(), }); let selectedOptions = this.getItemsToClear(); Glean.privacySanitize.clear.record({ context: contextOpenedIn, history_form_data_downloads: selectedOptions.includes( "historyFormDataAndDownloads" ), cookies_and_storage: selectedOptions.includes("cookiesAndStorage"), cache: selectedOptions.includes("cache"), site_settings: selectedOptions.includes("siteSettings"), }); } // if the dialog was just opened, just report which context it was opened in else { Glean.privacySanitize.dialogOpen.record({ context: contextOpenedIn, }); } }, }; // We need to give the dialog an opportunity to set up the DOM // before its measured for the SubDialog it will be embedded in. // This is because the sanitizeEverythingWarningBox may or may // not be visible, depending on whether "Everything" is the default // choice in the menulist. document.mozSubdialogReady = new Promise(resolve => { window.addEventListener( "load", function () { gSanitizePromptDialog.init().then(resolve); }, { once: true, } ); });