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 });