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