summaryrefslogtreecommitdiffstats
path: root/toolkit/components/cleardata/ClearDataService.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/cleardata/ClearDataService.jsm1397
1 files changed, 1397 insertions, 0 deletions
diff --git a/toolkit/components/cleardata/ClearDataService.jsm b/toolkit/components/cleardata/ClearDataService.jsm
new file mode 100644
index 0000000000..12da4e7b83
--- /dev/null
+++ b/toolkit/components/cleardata/ClearDataService.jsm
@@ -0,0 +1,1397 @@
+/* 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"];