/* 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/. */ "use strict"; const { ComponentUtils } = ChromeUtils.import( "resource://gre/modules/ComponentUtils.jsm" ); const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", setTimeout: "resource://gre/modules/Timer.jsm", Downloads: "resource://gre/modules/Downloads.jsm", OfflineAppCacheHelper: "resource://gre/modules/offlineAppCache.jsm", ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm", PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", }); XPCOMUtils.defineLazyServiceGetter( this, "sas", "@mozilla.org/storage/activity-service;1", "nsIStorageActivityService" ); XPCOMUtils.defineLazyServiceGetter( this, "TrackingDBService", "@mozilla.org/tracking-db-service;1", "nsITrackingDBService" ); // A Cleaner is an object with 5 methods. These methods must return a Promise // object. Here a description of these methods: // * deleteAll() - this method _must_ exist. When called, it deletes all the // data owned by the cleaner. // * deleteByPrincipal() - this method is implemented only if the cleaner knows // how to delete data by nsIPrincipal. If not // implemented, deleteByHost will be used instead. // * deleteByHost() - this method is implemented only if the cleaner knows // how to delete data by host + originAttributes pattern. If // not implemented, deleteAll() will be used as fallback. // * deleteByRange() - this method is implemented only if the cleaner knows how // to delete data by time range. It receives 2 time range // parameters: aFrom/aTo. If not implemented, deleteAll() is // used as fallback. // * deleteByLocalFiles() - this method removes data held for local files and // other hostless origins. If not implemented, // **no fallback is used**, as for a number of // cleaners, no such data will ever exist and // therefore clearing it does not make sense. // * deleteByOriginAttributes() - this method is implemented only if the cleaner // knows how to delete data by originAttributes // pattern. const CookieCleaner = { deleteByLocalFiles(aOriginAttributes) { return new Promise(aResolve => { Services.cookies.removeCookiesFromExactHost( "", JSON.stringify(aOriginAttributes) ); aResolve(); }); }, deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { Services.cookies.removeCookiesFromExactHost( aHost, JSON.stringify(aOriginAttributes) ); aResolve(); }); }, deleteByRange(aFrom, aTo) { return Services.cookies.removeAllSince(aFrom); }, deleteByOriginAttributes(aOriginAttributesString) { return new Promise(aResolve => { try { Services.cookies.removeCookiesWithOriginAttributes( aOriginAttributesString ); } catch (ex) {} aResolve(); }); }, deleteAll() { return new Promise(aResolve => { Services.cookies.removeAll(); aResolve(); }); }, }; const CertCleaner = { deleteByHost(aHost, aOriginAttributes) { let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( Ci.nsICertOverrideService ); return new Promise(aResolve => { overrideService.clearValidityOverride(aHost, -1); aResolve(); }); }, deleteAll() { let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( Ci.nsICertOverrideService ); return new Promise(aResolve => { overrideService.clearAllOverrides(); aResolve(); }); }, }; const NetworkCacheCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { // Delete data from both HTTP and HTTPS sites. let httpURI = Services.io.newURI("http://" + aHost); let httpsURI = Services.io.newURI("https://" + aHost); let httpPrincipal = Services.scriptSecurityManager.createContentPrincipal( httpURI, aOriginAttributes ); let httpsPrincipal = Services.scriptSecurityManager.createContentPrincipal( httpsURI, aOriginAttributes ); Services.cache2.clearOrigin(httpPrincipal); Services.cache2.clearOrigin(httpsPrincipal); aResolve(); }); }, deleteByPrincipal(aPrincipal) { return new Promise(aResolve => { Services.cache2.clearOrigin(aPrincipal); aResolve(); }); }, deleteByOriginAttributes(aOriginAttributesString) { return new Promise(aResolve => { Services.cache2.clearOriginAttributes(aOriginAttributesString); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { Services.cache2.clear(); aResolve(); }); }, }; const CSSCacheCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { // Delete data from both HTTP and HTTPS sites. let httpURI = Services.io.newURI("http://" + aHost); let httpsURI = Services.io.newURI("https://" + aHost); let httpPrincipal = Services.scriptSecurityManager.createContentPrincipal( httpURI, aOriginAttributes ); let httpsPrincipal = Services.scriptSecurityManager.createContentPrincipal( httpsURI, aOriginAttributes ); ChromeUtils.clearStyleSheetCache(httpPrincipal); ChromeUtils.clearStyleSheetCache(httpsPrincipal); aResolve(); }); }, deleteByPrincipal(aPrincipal) { return new Promise(aResolve => { ChromeUtils.clearStyleSheetCache(aPrincipal); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { ChromeUtils.clearStyleSheetCache(); aResolve(); }); }, }; const ImageCacheCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { let imageCache = Cc["@mozilla.org/image/tools;1"] .getService(Ci.imgITools) .getImgCacheForDocument(null); // Delete data from both HTTP and HTTPS sites. let httpURI = Services.io.newURI("http://" + aHost); let httpsURI = Services.io.newURI("https://" + aHost); let httpPrincipal = Services.scriptSecurityManager.createContentPrincipal( httpURI, aOriginAttributes ); let httpsPrincipal = Services.scriptSecurityManager.createContentPrincipal( httpsURI, aOriginAttributes ); imageCache.removeEntriesFromPrincipal(httpPrincipal); imageCache.removeEntriesFromPrincipal(httpsPrincipal); aResolve(); }); }, deleteByPrincipal(aPrincipal) { return new Promise(aResolve => { let imageCache = Cc["@mozilla.org/image/tools;1"] .getService(Ci.imgITools) .getImgCacheForDocument(null); imageCache.removeEntriesFromPrincipal(aPrincipal); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { let imageCache = Cc["@mozilla.org/image/tools;1"] .getService(Ci.imgITools) .getImgCacheForDocument(null); imageCache.clearCache(false); // true=chrome, false=content aResolve(); }); }, }; const PluginDataCleaner = { deleteByHost(aHost, aOriginAttributes) { return this._deleteInternal((aPh, aTag) => { return new Promise(aResolve => { try { aPh.clearSiteData( aTag, aHost, Ci.nsIPluginHost.FLAG_CLEAR_ALL, -1, aResolve ); } catch (e) { // Ignore errors from the plugin, but resolve the promise // We cannot check if something is a bailout or an error aResolve(); } }); }); }, deleteByRange(aFrom, aTo) { let age = Date.now() / 1000 - aFrom / 1000000; return this._deleteInternal((aPh, aTag) => { return new Promise(aResolve => { try { aPh.clearSiteData( aTag, null, Ci.nsIPluginHost.FLAG_CLEAR_ALL, age, aResolve ); } catch (e) { aResolve(Cr.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED); } }).then(aRv => { // If the plugin doesn't support clearing by age, clear everything. if (aRv == Cr.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) { return new Promise(aResolve => { try { aPh.clearSiteData( aTag, null, Ci.nsIPluginHost.FLAG_CLEAR_ALL, -1, aResolve ); } catch (e) { aResolve(); } }); } return true; }); }); }, deleteAll() { return this._deleteInternal((aPh, aTag) => { return new Promise(aResolve => { try { aPh.clearSiteData( aTag, null, Ci.nsIPluginHost.FLAG_CLEAR_ALL, -1, aResolve ); } catch (e) { aResolve(); } }); }); }, _deleteInternal(aCb) { let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); let promises = []; let tags = ph.getPluginTags(); for (let tag of tags) { if (tag.loaded) { promises.push(aCb(ph, tag)); } } // As evidenced in bug 1253204, clearing plugin data can sometimes be // very, very long, for mysterious reasons. Unfortunately, this is not // something actionable by Mozilla, so crashing here serves no purpose. // // For this reason, instead of waiting for sanitization to always // complete, we introduce a soft timeout. Once this timeout has // elapsed, we proceed with the shutdown of Firefox. return Promise.race([ Promise.all(promises), new Promise(aResolve => setTimeout(aResolve, 10000 /* 10 seconds */)), ]); }, }; const DownloadsCleaner = { deleteByHost(aHost, aOriginAttributes) { return Downloads.getList(Downloads.ALL).then(aList => { aList.removeFinished(aDownload => Services.eTLD.hasRootDomain( Services.io.newURI(aDownload.source.url).host, aHost ) ); }); }, deleteByRange(aFrom, aTo) { // Convert microseconds back to milliseconds for date comparisons. let rangeBeginMs = aFrom / 1000; let rangeEndMs = aTo / 1000; return Downloads.getList(Downloads.ALL).then(aList => { aList.removeFinished( aDownload => aDownload.startTime >= rangeBeginMs && aDownload.startTime <= rangeEndMs ); }); }, deleteAll() { return Downloads.getList(Downloads.ALL).then(aList => { aList.removeFinished(null); }); }, }; const PasswordsCleaner = { deleteByHost(aHost, aOriginAttributes) { return this._deleteInternal(aLogin => Services.eTLD.hasRootDomain(aLogin.hostname, aHost) ); }, deleteAll() { return this._deleteInternal(() => true); }, _deleteInternal(aCb) { return new Promise(aResolve => { try { let logins = Services.logins.getAllLogins(); for (let login of logins) { if (aCb(login)) { Services.logins.removeLogin(login); } } } catch (ex) { // XXXehsan: is there a better way to do this rather than this // hacky comparison? if ( !ex.message.includes("User canceled Master Password entry") && ex.result != Cr.NS_ERROR_NOT_IMPLEMENTED ) { throw new Error("Exception occured in clearing passwords: " + ex); } } aResolve(); }); }, }; const MediaDevicesCleaner = { deleteByRange(aFrom, aTo) { return new Promise(aResolve => { let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"].getService( Ci.nsIMediaManagerService ); mediaMgr.sanitizeDeviceIds(aFrom); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"].getService( Ci.nsIMediaManagerService ); mediaMgr.sanitizeDeviceIds(null); aResolve(); }); }, }; const AppCacheCleaner = { deleteByOriginAttributes(aOriginAttributesString) { return new Promise(aResolve => { let appCacheService = Cc[ "@mozilla.org/network/application-cache-service;1" ].getService(Ci.nsIApplicationCacheService); try { appCacheService.evictMatchingOriginAttributes(aOriginAttributesString); } catch (ex) {} aResolve(); }); }, deleteAll() { // AppCache: this doesn't wait for the cleanup to be complete. OfflineAppCacheHelper.clear(); return Promise.resolve(); }, }; const QuotaCleaner = { deleteByPrincipal(aPrincipal) { // localStorage: The legacy LocalStorage implementation that will // eventually be removed depends on this observer notification to clear by // principal. Services.obs.notifyObservers( null, "extension:purge-localStorage", aPrincipal.host ); // Clear sessionStorage Services.obs.notifyObservers( null, "browser:purge-sessionStorage", aPrincipal.host ); // ServiceWorkers: they must be removed before cleaning QuotaManager. return ServiceWorkerCleanUp.removeFromPrincipal(aPrincipal) .then( _ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true ) .then(exceptionThrown => { // QuotaManager: In the event of a failure, we call reject to propagate // the error upwards. return new Promise((aResolve, aReject) => { let req = Services.qms.clearStoragesForPrincipal(aPrincipal); req.callback = () => { if (exceptionThrown || req.resultCode != Cr.NS_OK) { aReject({ message: "Delete by principal failed" }); } else { aResolve(); } }; }); }); }, deleteByHost(aHost, aOriginAttributes) { // XXX: The aOriginAttributes is expected to always be empty({}). Maybe have // a debug assertion here to ensure that? // localStorage: The legacy LocalStorage implementation that will // eventually be removed depends on this observer notification to clear by // host. Some other subsystems like Reporting headers depend on this too. Services.obs.notifyObservers(null, "extension:purge-localStorage", aHost); // Clear sessionStorage Services.obs.notifyObservers(null, "browser:purge-sessionStorage", aHost); // ServiceWorkers: they must be removed before cleaning QuotaManager. return ServiceWorkerCleanUp.removeFromHost(aHost) .then( _ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true ) .then(exceptionThrown => { // QuotaManager: In the event of a failure, we call reject to propagate // the error upwards. // deleteByHost has the semantics that "foo.example.com" should be // wiped if we are provided an aHost of "example.com". return new Promise((aResolve, aReject) => { Services.qms.listOrigins().callback = aRequest => { if (aRequest.resultCode != Cr.NS_OK) { aReject({ message: "Delete by host failed" }); return; } let promises = []; for (const origin of aRequest.result) { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( origin ); let host; try { host = principal.host; } catch (e) { // There is no host for the given principal. continue; } if (Services.eTLD.hasRootDomain(host, aHost)) { promises.push( new Promise((aResolve, aReject) => { let clearRequest = Services.qms.clearStoragesForPrincipal( principal ); clearRequest.callback = () => { if (clearRequest.resultCode == Cr.NS_OK) { aResolve(); } else { aReject({ message: "Delete by host failed" }); } }; }) ); } } Promise.all(promises).then(exceptionThrown ? aReject : aResolve); }; }); }); }, deleteByRange(aFrom, aTo) { let principals = sas .getActiveOrigins(aFrom, aTo) .QueryInterface(Ci.nsIArray); let promises = []; for (let i = 0; i < principals.length; ++i) { let principal = principals.queryElementAt(i, Ci.nsIPrincipal); if ( !principal.schemeIs("http") && !principal.schemeIs("https") && !principal.schemeIs("file") ) { continue; } promises.push(this.deleteByPrincipal(principal)); } return Promise.all(promises); }, deleteByOriginAttributes(aOriginAttributesString) { // The legacy LocalStorage implementation that will eventually be removed. // And it should've been cleared while notifying observers with // clear-origin-attributes-data. return ServiceWorkerCleanUp.removeFromOriginAttributes( aOriginAttributesString ) .then( _ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true ) .then(exceptionThrown => { // QuotaManager: In the event of a failure, we call reject to propagate // the error upwards. return new Promise((aResolve, aReject) => { let req = Services.qms.clearStoragesForOriginAttributesPattern( aOriginAttributesString ); req.callback = () => { if (req.resultCode == Cr.NS_OK) { aResolve(); } else { aReject({ message: "Delete by origin attributes failed" }); } }; }); }); }, deleteAll() { // localStorage Services.obs.notifyObservers(null, "extension:purge-localStorage"); // sessionStorage Services.obs.notifyObservers(null, "browser:purge-sessionStorage"); // ServiceWorkers return ServiceWorkerCleanUp.removeAll() .then( _ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true ) .then(exceptionThrown => { // QuotaManager: In the event of a failure, we call reject to propagate // the error upwards. return new Promise((aResolve, aReject) => { Services.qms.getUsage(aRequest => { if (aRequest.resultCode != Cr.NS_OK) { aReject({ message: "Delete all failed" }); return; } let promises = []; for (let item of aRequest.result) { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( item.origin ); if ( principal.schemeIs("http") || principal.schemeIs("https") || principal.schemeIs("file") ) { promises.push( new Promise((aResolve, aReject) => { let req = Services.qms.clearStoragesForPrincipal(principal); req.callback = () => { if (req.resultCode == Cr.NS_OK) { aResolve(); } else { aReject({ message: "Delete all failed" }); } }; }) ); } } Promise.all(promises).then(exceptionThrown ? aReject : aResolve); }); }); }); }, }; const PredictorNetworkCleaner = { deleteAll() { // Predictive network data - like cache, no way to clear this per // domain, so just trash it all let np = Cc["@mozilla.org/network/predictor;1"].getService( Ci.nsINetworkPredictor ); np.reset(); return Promise.resolve(); }, }; const PushNotificationsCleaner = { deleteByHost(aHost, aOriginAttributes) { if (!Services.prefs.getBoolPref("dom.push.enabled", false)) { return Promise.resolve(); } return new Promise((aResolve, aReject) => { let push = Cc["@mozilla.org/push/Service;1"].getService( Ci.nsIPushService ); push.clearForDomain(aHost, aStatus => { if (!Components.isSuccessCode(aStatus)) { aReject(); } else { aResolve(); } }); }); }, deleteAll() { if (!Services.prefs.getBoolPref("dom.push.enabled", false)) { return Promise.resolve(); } return new Promise((aResolve, aReject) => { let push = Cc["@mozilla.org/push/Service;1"].getService( Ci.nsIPushService ); push.clearForDomain("*", aStatus => { if (!Components.isSuccessCode(aStatus)) { aReject(); } else { aResolve(); } }); }); }, }; const StorageAccessCleaner = { // This is a special function to implement deleteUserInteractionForClearingHistory. deleteExceptPrincipals(aPrincipalsWithStorage, aFrom) { // We compare by base domain in order to simulate the behavior // from purging, Consider a scenario where the user is logged // into sub.example.com but the cookies are on example.com. In this // case, we will remove the user interaction for sub.example.com // because its principal does not match the one with storage. let baseDomainsWithStorage = new Set(); for (let principal of aPrincipalsWithStorage) { baseDomainsWithStorage.add(principal.baseDomain); } return new Promise(aResolve => { for (let perm of Services.perms.getAllByTypeSince( "storageAccessAPI", aFrom )) { if (!baseDomainsWithStorage.has(perm.principal.baseDomain)) { Services.perms.removePermission(perm); } } aResolve(); }); }, deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { for (let perm of Services.perms.all) { if (perm.type == "storageAccessAPI") { let toBeRemoved = false; try { toBeRemoved = Services.eTLD.hasRootDomain( perm.principal.host, aHost ); } catch (ex) { continue; } if (!toBeRemoved) { continue; } try { Services.perms.removePermission(perm); } catch (ex) { Cu.reportError(ex); } } } aResolve(); }); }, deleteByRange(aFrom, aTo) { Services.perms.removeByTypeSince("storageAccessAPI", aFrom / 1000); return Promise.resolve(); }, deleteAll() { Services.perms.removeByType("storageAccessAPI"); return Promise.resolve(); }, }; const HistoryCleaner = { deleteByHost(aHost, aOriginAttributes) { if (!AppConstants.MOZ_PLACES) { return Promise.resolve(); } return PlacesUtils.history.removeByFilter({ host: "." + aHost }); }, deleteByRange(aFrom, aTo) { if (!AppConstants.MOZ_PLACES) { return Promise.resolve(); } return PlacesUtils.history.removeVisitsByFilter({ beginDate: new Date(aFrom / 1000), endDate: new Date(aTo / 1000), }); }, deleteAll() { if (!AppConstants.MOZ_PLACES) { return Promise.resolve(); } return PlacesUtils.history.clear(); }, }; const SessionHistoryCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { Services.obs.notifyObservers(null, "browser:purge-sessionStorage", aHost); Services.obs.notifyObservers( null, "browser:purge-session-history-for-domain", aHost ); aResolve(); }); }, deleteByRange(aFrom, aTo) { return new Promise(aResolve => { Services.obs.notifyObservers( null, "browser:purge-session-history", String(aFrom) ); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { Services.obs.notifyObservers(null, "browser:purge-session-history"); aResolve(); }); }, }; const AuthTokensCleaner = { deleteAll() { return new Promise(aResolve => { let sdr = Cc["@mozilla.org/security/sdr;1"].getService( Ci.nsISecretDecoderRing ); sdr.logoutAndTeardown(); aResolve(); }); }, }; const AuthCacheCleaner = { deleteAll() { return new Promise(aResolve => { Services.obs.notifyObservers(null, "net:clear-active-logins"); aResolve(); }); }, }; const PermissionsCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { for (let perm of Services.perms.all) { let toBeRemoved; try { toBeRemoved = Services.eTLD.hasRootDomain(perm.principal.host, aHost); } catch (ex) { continue; } if (!toBeRemoved && perm.type.startsWith("3rdPartyStorage^")) { let parts = perm.type.split("^"); let uri; try { uri = Services.io.newURI(parts[1]); } catch (ex) { continue; } toBeRemoved = Services.eTLD.hasRootDomain(uri.host, aHost); } if (!toBeRemoved) { continue; } try { Services.perms.removePermission(perm); } catch (ex) { // Ignore entry } } aResolve(); }); }, deleteByRange(aFrom, aTo) { Services.perms.removeAllSince(aFrom / 1000); return Promise.resolve(); }, deleteByOriginAttributes(aOriginAttributesString) { Services.perms.removePermissionsWithAttributes(aOriginAttributesString); return Promise.resolve(); }, deleteAll() { Services.perms.removeAll(); return Promise.resolve(); }, }; const PreferencesCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise((aResolve, aReject) => { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); cps2.removeBySubdomain(aHost, null, { handleCompletion: aReason => { if (aReason === cps2.COMPLETE_ERROR) { aReject(); } else { aResolve(); } }, handleError() {}, }); }); }, deleteByRange(aFrom, aTo) { return new Promise(aResolve => { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); cps2.removeAllDomainsSince(aFrom / 1000, null); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); cps2.removeAllDomains(null); aResolve(); }); }, }; const SecuritySettingsCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { let sss = Cc["@mozilla.org/ssservice;1"].getService( Ci.nsISiteSecurityService ); // Also remove HSTS information for subdomains by enumerating // the information in the site security service. for (let entry of sss.enumerate(Ci.nsISiteSecurityService.HEADER_HSTS)) { let hostname = entry.hostname; if (Services.eTLD.hasRootDomain(hostname, aHost)) { // This uri is used as a key to reset the state. let uri = Services.io.newURI("https://" + hostname); sss.resetState( Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0, entry.originAttributes ); } } let cars = Cc[ "@mozilla.org/security/clientAuthRememberService;1" ].getService(Ci.nsIClientAuthRememberService); cars.deleteDecisionsByHost(aHost, aOriginAttributes); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { // Clear site security settings - no support for ranges in this // interface either, so we clearAll(). let sss = Cc["@mozilla.org/ssservice;1"].getService( Ci.nsISiteSecurityService ); sss.clearAll(); let cars = Cc[ "@mozilla.org/security/clientAuthRememberService;1" ].getService(Ci.nsIClientAuthRememberService); cars.clearRememberedDecisions(); aResolve(); }); }, }; const EMECleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].getService( Ci.mozIGeckoMediaPluginChromeService ); mps.forgetThisSite(aHost, JSON.stringify(aOriginAttributes)); aResolve(); }); }, deleteAll() { // Not implemented. return Promise.resolve(); }, }; const ReportsCleaner = { deleteByHost(aHost, aOriginAttributes) { return new Promise(aResolve => { Services.obs.notifyObservers(null, "reporting:purge-host", aHost); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { Services.obs.notifyObservers(null, "reporting:purge-all"); aResolve(); }); }, }; const ContentBlockingCleaner = { deleteAll() { return TrackingDBService.clearAll(); }, deleteByRange(aFrom, aTo) { return TrackingDBService.clearSince(aFrom); }, }; /** * The about:home startup cache, if it exists, might contain information * about where the user has been, or what they've downloaded. */ const AboutHomeStartupCacheCleaner = { deleteAll() { // This cleaner only makes sense on Firefox desktop, which is the only // application that uses the about:home startup cache. if (!AppConstants.MOZ_BUILD_APP == "browser") { return Promise.resolve(); } return new Promise((aResolve, aReject) => { let lci = Services.loadContextInfo.default; let storage = Services.cache2.diskCacheStorage(lci, false); let uri = Services.io.newURI("about:home"); try { storage.asyncDoomURI(uri, "", { onCacheEntryDoomed(aResult) { if ( Components.isSuccessCode(aResult) || aResult == Cr.NS_ERROR_NOT_AVAILABLE ) { aResolve(); } else { aReject({ message: "asyncDoomURI for about:home failed", }); } }, }); } catch (e) { aReject({ message: "Failed to doom about:home startup cache entry", }); } }); }, }; // Here the map of Flags-Cleaners. const FLAGS_MAP = [ { flag: Ci.nsIClearDataService.CLEAR_CERT_EXCEPTIONS, cleaners: [CertCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_COOKIES, cleaners: [CookieCleaner] }, { flag: Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, cleaners: [NetworkCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_IMAGE_CACHE, cleaners: [ImageCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_CSS_CACHE, cleaners: [CSSCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_PLUGIN_DATA, cleaners: [PluginDataCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_DOWNLOADS, cleaners: [DownloadsCleaner, AboutHomeStartupCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_PASSWORDS, cleaners: [PasswordsCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES, cleaners: [MediaDevicesCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_APPCACHE, cleaners: [AppCacheCleaner] }, { flag: Ci.nsIClearDataService.CLEAR_DOM_QUOTA, cleaners: [QuotaCleaner] }, { flag: Ci.nsIClearDataService.CLEAR_PREDICTOR_NETWORK_DATA, cleaners: [PredictorNetworkCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_DOM_PUSH_NOTIFICATIONS, cleaners: [PushNotificationsCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_HISTORY, cleaners: [HistoryCleaner, AboutHomeStartupCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_SESSION_HISTORY, cleaners: [SessionHistoryCleaner, AboutHomeStartupCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_AUTH_TOKENS, cleaners: [AuthTokensCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_AUTH_CACHE, cleaners: [AuthCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_PERMISSIONS, cleaners: [PermissionsCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES, cleaners: [PreferencesCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS, cleaners: [SecuritySettingsCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_EME, cleaners: [EMECleaner] }, { flag: Ci.nsIClearDataService.CLEAR_REPORTS, cleaners: [ReportsCleaner] }, { flag: Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, cleaners: [StorageAccessCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_CONTENT_BLOCKING_RECORDS, cleaners: [ContentBlockingCleaner], }, ]; this.ClearDataService = function() { this._initialize(); }; ClearDataService.prototype = Object.freeze({ classID: Components.ID("{0c06583d-7dd8-4293-b1a5-912205f779aa}"), QueryInterface: ChromeUtils.generateQI(["nsIClearDataService"]), _xpcom_factory: ComponentUtils.generateSingletonFactory(ClearDataService), _initialize() { // Let's start all the service we need to cleanup data. // This is mainly needed for GeckoView that doesn't start QMS on startup // time. if (!Services.qms) { Cu.reportError("Failed initializiation of QuotaManagerService."); } }, deleteDataFromLocalFiles(aIsUserRequest, aFlags, aCallback) { if (!aCallback) { return Cr.NS_ERROR_INVALID_ARG; } return this._deleteInternal(aFlags, aCallback, aCleaner => { // Some of the 'Cleaners' do not support clearing data for // local files. Ignore those. if (aCleaner.deleteByLocalFiles) { // A generic originAttributes dictionary. return aCleaner.deleteByLocalFiles({}); } return Promise.resolve(); }); }, deleteDataFromHost(aHost, aIsUserRequest, aFlags, aCallback) { if (!aHost || !aCallback) { return Cr.NS_ERROR_INVALID_ARG; } return this._deleteInternal(aFlags, aCallback, aCleaner => { // Some of the 'Cleaners' do not support to delete by principal. Let's // use deleteAll() as fallback. if (aCleaner.deleteByHost) { // A generic originAttributes dictionary. return aCleaner.deleteByHost(aHost, {}); } // The user wants to delete data. Let's remove as much as we can. if (aIsUserRequest) { return aCleaner.deleteAll(); } // We don't want to delete more than what is strictly required. return Promise.resolve(); }); }, deleteDataFromPrincipal(aPrincipal, aIsUserRequest, aFlags, aCallback) { if (!aPrincipal || !aCallback) { return Cr.NS_ERROR_INVALID_ARG; } return this._deleteInternal(aFlags, aCallback, aCleaner => { if (aCleaner.deleteByPrincipal) { return aCleaner.deleteByPrincipal(aPrincipal); } // Some of the 'Cleaners' do not support to delete by principal. Fallback // is to delete by host. if (aCleaner.deleteByHost) { return aCleaner.deleteByHost( aPrincipal.host, aPrincipal.originAttributes ); } // Next fallback is to use deleteAll(), but only if this was a user request. if (aIsUserRequest) { return aCleaner.deleteAll(); } // We don't want to delete more than what is strictly required. return Promise.resolve(); }); }, deleteDataInTimeRange(aFrom, aTo, aIsUserRequest, aFlags, aCallback) { if (aFrom > aTo || !aCallback) { return Cr.NS_ERROR_INVALID_ARG; } return this._deleteInternal(aFlags, aCallback, aCleaner => { // Some of the 'Cleaners' do not support to delete by range. Let's use // deleteAll() as fallback. if (aCleaner.deleteByRange) { return aCleaner.deleteByRange(aFrom, aTo); } // The user wants to delete data. Let's remove as much as we can. if (aIsUserRequest) { return aCleaner.deleteAll(); } // We don't want to delete more than what is strictly required. return Promise.resolve(); }); }, deleteData(aFlags, aCallback) { if (!aCallback) { return Cr.NS_ERROR_INVALID_ARG; } return this._deleteInternal(aFlags, aCallback, aCleaner => { return aCleaner.deleteAll(); }); }, deleteDataFromOriginAttributesPattern(aPattern, aCallback) { if (!aPattern) { return Cr.NS_ERROR_INVALID_ARG; } let patternString = JSON.stringify(aPattern); // XXXtt remove clear-origin-attributes-data entirely Services.obs.notifyObservers( null, "clear-origin-attributes-data", patternString ); if (!aCallback) { aCallback = { onDataDeleted: () => {}, }; } return this._deleteInternal( Ci.nsIClearDataService.CLEAR_ALL, aCallback, aCleaner => { if (aCleaner.deleteByOriginAttributes) { return aCleaner.deleteByOriginAttributes(patternString); } // We don't want to delete more than what is strictly required. return Promise.resolve(); } ); }, deleteUserInteractionForClearingHistory( aPrincipalsWithStorage, aFrom, aCallback ) { if (!aCallback) { return Cr.NS_ERROR_INVALID_ARG; } StorageAccessCleaner.deleteExceptPrincipals( aPrincipalsWithStorage, aFrom ).then(() => { aCallback.onDataDeleted(0); }); return Cr.NS_OK; }, // This internal method uses aFlags against FLAGS_MAP in order to retrieve a // list of 'Cleaners'. For each of them, the aHelper callback retrieves a // promise object. All these promise objects are resolved before calling // onDataDeleted. _deleteInternal(aFlags, aCallback, aHelper) { let resultFlags = 0; let promises = FLAGS_MAP.filter(c => aFlags & c.flag).map(c => { return Promise.all( c.cleaners.map(cleaner => { return aHelper(cleaner).catch(e => { Cu.reportError(e); resultFlags |= c.flag; }); }) ); // Let's collect the failure in resultFlags. }); Promise.all(promises).then(() => { aCallback.onDataDeleted(resultFlags); }); return Cr.NS_OK; }, }); var EXPORTED_SYMBOLS = ["ClearDataService"];