diff options
Diffstat (limited to 'toolkit/components/cleardata')
23 files changed, 4163 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"]; diff --git a/toolkit/components/cleardata/PrincipalsCollector.jsm b/toolkit/components/cleardata/PrincipalsCollector.jsm new file mode 100644 index 0000000000..d604d5afd7 --- /dev/null +++ b/toolkit/components/cleardata/PrincipalsCollector.jsm @@ -0,0 +1,156 @@ +/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "quotaManagerService", + "@mozilla.org/dom/quota-manager-service;1", + "nsIQuotaManagerService" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "serviceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +/** + * A helper module to collect all principals that have any of the following: + * * cookies + * * quota storage (indexedDB, localStorage) + * * service workers + * + * Note that in case of cookies, because these are not strictly associated with a + * full origin (including scheme), the https:// scheme will be used as a convention, + * so when the cookie hostname is .example.com the principal will have the origin + * https://example.com. Origin Attributes from cookies are copied to the principal. + * + * This class is not a singleton and needs to be instantiated using the constructor + * before usage. The class instance will cache the last list of principals. + * + * There is currently no `refresh` method, though you are free to add one. + */ +class PrincipalsCollector { + /** + * Creates a new PrincipalsCollector. + */ + constructor() { + this.principals = null; + } + + /** + * Checks whether the passed in principal has a scheme that is considered by the + * PrincipalsCollector. This is used to avoid including principals for non-web + * entities such as moz-extension. + * + * @param {nsIPrincipal} the principal to check + * @returns {boolean} + */ + static isSupportedPrincipal(principal) { + return ["http", "https", "file"].some(scheme => principal.schemeIs(scheme)); + } + + /** + * Fetches and collects all principals with cookies and/or site data (see module + * description). Originally for usage in Sanitizer.jsm to compute principals to be + * cleared on shutdown based on user settings. + * + * This operation might take a while to complete on big profiles. + * DO NOT call or await this in a way that makes it block user interaction, or you + * risk several painful seconds or possibly even minutes of lag. + * + * This function will cache its result and return the same list on second call, + * even if the actual number of principals with cookies and site data changed. + * + * @param {Object} [optional] progress A Sanitizer.jsm progress object that will be + * updated to reflect the current step of fetching principals. + * @returns {Array<nsIPrincipal>} the list of principals + */ + async getAllPrincipals(progress = {}) { + if (this.principals == null) { + // Here is the list of principals with site data. + this.principals = await this._getAllPrincipalsInternal(progress); + } + + return this.principals; + } + + async _getAllPrincipalsInternal(progress = {}) { + progress.step = "principals-quota-manager"; + let principals = await new Promise(resolve => { + quotaManagerService.listOrigins().callback = request => { + progress.step = "principals-quota-manager-listOrigins"; + if (request.resultCode != Cr.NS_OK) { + // We are probably shutting down. We don't want to propagate the + // error, rejecting the promise. + resolve([]); + return; + } + + let list = []; + for (const origin of request.result) { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + if (PrincipalsCollector.isSupportedPrincipal(principal)) { + list.push(principal); + } + } + + progress.step = "principals-quota-manager-completed"; + resolve(list); + }; + }).catch(ex => { + Cu.reportError("QuotaManagerService promise failed: " + ex); + return []; + }); + + progress.step = "principals-service-workers"; + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + // We don't need to check the scheme. SW are just exposed to http/https URLs. + principals.push(sw.principal); + } + + // Let's take the list of unique hosts+OA from cookies. + progress.step = "principals-cookies"; + let cookies = Services.cookies.cookies; + let hosts = new Set(); + for (let cookie of cookies) { + hosts.add( + cookie.rawHost + + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + } + + progress.step = "principals-host-cookie"; + hosts.forEach(host => { + // Cookies and permissions are handled by origin/host. Doesn't matter if we + // use http: or https: schema here. + principals.push( + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://" + host + ) + ); + }); + + progress.step = "total-principals:" + principals.length; + return principals; + } +} + +this.EXPORTED_SYMBOLS = ["PrincipalsCollector"]; diff --git a/toolkit/components/cleardata/ServiceWorkerCleanUp.jsm b/toolkit/components/cleardata/ServiceWorkerCleanUp.jsm new file mode 100644 index 0000000000..00b6c04f07 --- /dev/null +++ b/toolkit/components/cleardata/ServiceWorkerCleanUp.jsm @@ -0,0 +1,94 @@ +/* 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 { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyServiceGetter( + this, + "serviceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + throw new Error( + "ServiceWorkerCleanUp.jsm can only be used in the parent process" + ); +} + +this.EXPORTED_SYMBOLS = ["ServiceWorkerCleanUp"]; + +function unregisterServiceWorker(aSW) { + return new Promise(resolve => { + let unregisterCallback = { + unregisterSucceeded: resolve, + unregisterFailed: resolve, // We don't care about failures. + QueryInterface: ChromeUtils.generateQI([ + "nsIServiceWorkerUnregisterCallback", + ]), + }; + serviceWorkerManager.propagateUnregister( + aSW.principal, + unregisterCallback, + aSW.scope + ); + }); +} + +this.ServiceWorkerCleanUp = { + removeFromHost(aHost) { + let promises = []; + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.principal.host == aHost) { + promises.push(unregisterServiceWorker(sw)); + } + } + return Promise.all(promises); + }, + + removeFromPrincipal(aPrincipal) { + let promises = []; + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.principal.equals(aPrincipal)) { + promises.push(unregisterServiceWorker(sw)); + } + } + return Promise.all(promises); + }, + + removeFromOriginAttributes(aOriginAttributesString) { + serviceWorkerManager.removeRegistrationsByOriginAttributes( + aOriginAttributesString + ); + return Promise.resolve(); + }, + + removeAll() { + let promises = []; + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + promises.push(unregisterServiceWorker(sw)); + } + return Promise.all(promises); + }, +}; diff --git a/toolkit/components/cleardata/SiteDataTestUtils.jsm b/toolkit/components/cleardata/SiteDataTestUtils.jsm new file mode 100644 index 0000000000..f971e8d5c3 --- /dev/null +++ b/toolkit/components/cleardata/SiteDataTestUtils.jsm @@ -0,0 +1,397 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["SiteDataTestUtils"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { BrowserTestUtils } = ChromeUtils.import( + "resource://testing-common/BrowserTestUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +XPCOMUtils.defineLazyGlobalGetters(this, ["indexedDB", "Blob"]); + +/** + * This module assists with tasks around testing functionality that shows + * or clears site data. + * + * Please note that you will have to clean up changes made manually, for + * example using SiteDataTestUtils.clear(). + */ +var SiteDataTestUtils = { + /** + * Makes an origin have persistent data storage. + * + * @param {String} origin - the origin of the site to give persistent storage + * + * @returns a Promise that resolves when storage was persisted + */ + persist(origin, value = Services.perms.ALLOW_ACTION) { + return new Promise(resolve => { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + Services.perms.addFromPrincipal(principal, "persistent-storage", value); + Services.qms.persist(principal).callback = () => resolve(); + }); + }, + + /** + * Adds a new blob entry to a dummy indexedDB database for the specified origin. + * + * @param {String} origin - the origin of the site to add test data for + * @param {Number} size [optional] - the size of the entry in bytes + * + * @returns a Promise that resolves when the data was added successfully. + */ + addToIndexedDB(origin, size = 1024) { + return new Promise(resolve => { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1); + request.onupgradeneeded = function(e) { + let db = e.target.result; + db.createObjectStore("TestStore"); + }; + request.onsuccess = function(e) { + let db = e.target.result; + let tx = db.transaction("TestStore", "readwrite"); + let store = tx.objectStore("TestStore"); + tx.oncomplete = resolve; + let buffer = new ArrayBuffer(size); + let blob = new Blob([buffer]); + store.add(blob, Cu.now()); + }; + }); + }, + + /** + * Adds a new cookie for the specified origin, with the specified contents. + * The cookie will be valid for one day. + * + * @param {String} origin - the origin of the site to add test data for + * @param {String} name [optional] - the cookie name + * @param {String} value [optional] - the cookie value + */ + addToCookies(origin, name = "foo", value = "bar") { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + Services.cookies.add( + principal.host, + principal.URI.pathQueryRef, + name, + value, + false, + false, + false, + Date.now() + 24000 * 60 * 60, + principal.originAttributes, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_UNSET + ); + }, + + /** + * Adds a new localStorage entry for the specified origin, with the specified contents. + * + * @param {String} origin - the origin of the site to add test data for + * @param {String} [key] - the localStorage key + * @param {String} [value] - the localStorage value + */ + addToLocalStorage(origin, key = "foo", value = "bar") { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + let storage = Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + storage.setItem(key, value); + }, + + /** + * Checks whether the given origin is storing data in localStorage + * + * @param {String} origin - the origin of the site to check + * @param {{key: String, value: String}[]} [testEntries] - An array of entries + * to test for. + * + * @returns {Boolean} whether the origin has localStorage data + */ + hasLocalStorage(origin, testEntries) { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + let storage = Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + if (!storage.length) { + return false; + } + if (!testEntries) { + return true; + } + return ( + storage.length >= testEntries.length && + testEntries.every(({ key, value }) => storage.getItem(key) == value) + ); + }, + + /** + * Adds a new serviceworker with the specified path. Note that this + * method will open a new tab at the domain of the SW path to that effect. + * + * @param {String} path - the path to the service worker to add. + * + * @returns a Promise that resolves when the service worker was registered + */ + addServiceWorker(path) { + let uri = Services.io.newURI(path); + // Register a dummy ServiceWorker. + return BrowserTestUtils.withNewTab(uri.prePath, async function(browser) { + return browser.ownerGlobal.SpecialPowers.spawn( + browser, + [{ path }], + async ({ path: p }) => { + // eslint-disable-next-line no-undef + let r = await content.navigator.serviceWorker.register(p); + return new Promise(resolve => { + let worker = r.installing || r.waiting || r.active; + if (worker.state == "activated") { + resolve(); + } else { + worker.addEventListener("statechange", () => { + if (worker.state == "activated") { + resolve(); + } + }); + } + }); + } + ); + }); + }, + + hasCookies(origin, testEntries) { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + + let filterFn = cookie => { + return ( + ChromeUtils.isOriginAttributesEqual( + principal.originAttributes, + cookie.originAttributes + ) && cookie.host.includes(principal.host) + ); + }; + + // Return on first cookie found for principal. + if (!testEntries) { + return Services.cookies.cookies.some(filterFn); + } + + // Collect all cookies that match the principal + let cookies = Services.cookies.cookies.filter(filterFn); + + if (cookies.length < testEntries.length) { + return false; + } + + // This code isn't very efficient. It should only be used for testing + // a small amount of cookies. + return testEntries.every(({ key, value }) => + cookies.some(cookie => cookie.name == key && cookie.value == value) + ); + }, + + hasIndexedDB(origin) { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + return new Promise(resolve => { + let data = true; + let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1); + request.onupgradeneeded = function(e) { + data = false; + }; + request.onsuccess = function(e) { + resolve(data); + }; + }); + }, + + _getCacheStorage(where, lci) { + switch (where) { + case "disk": + return Services.cache2.diskCacheStorage(lci, false); + case "memory": + return Services.cache2.memoryCacheStorage(lci); + case "appcache": + return Services.cache2.appCacheStorage(lci, null); + case "pin": + return Services.cache2.pinningCacheStorage(lci); + } + return null; + }, + + hasCacheEntry(path, where, lci = Services.loadContextInfo.default) { + let storage = this._getCacheStorage(where, lci); + return storage.exists(Services.io.newURI(path), ""); + }, + + addCacheEntry(path, where, lci = Services.loadContextInfo.default) { + return new Promise(resolve => { + function CacheListener() {} + CacheListener.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + + onCacheEntryCheck(entry, appCache) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isnew, appCache, status) { + resolve(); + }, + }; + + let storage = this._getCacheStorage(where, lci); + storage.asyncOpenURI( + Services.io.newURI(path), + "", + Ci.nsICacheStorage.OPEN_NORMALLY, + new CacheListener() + ); + }); + }, + + /** + * Checks whether the specified origin has registered ServiceWorkers. + * + * @param {String} origin - the origin of the site to check + * + * @returns {Boolean} whether or not the site has ServiceWorkers. + */ + hasServiceWorkers(origin) { + let serviceWorkers = swm.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.principal.origin == origin) { + return true; + } + } + return false; + }, + + /** + * Waits for a ServiceWorker to be registered. + * + * @param {String} the url of the ServiceWorker to wait for + * + * @returns a Promise that resolves when a ServiceWorker at the + * specified location has been registered. + */ + promiseServiceWorkerRegistered(url) { + if (!(url instanceof Ci.nsIURI)) { + url = Services.io.newURI(url); + } + + return new Promise(resolve => { + let listener = { + onRegister: registration => { + if (registration.principal.host != url.host) { + return; + } + swm.removeListener(listener); + resolve(registration); + }, + }; + swm.addListener(listener); + }); + }, + + /** + * Waits for a ServiceWorker to be unregistered. + * + * @param {String} the url of the ServiceWorker to wait for + * + * @returns a Promise that resolves when a ServiceWorker at the + * specified location has been unregistered. + */ + promiseServiceWorkerUnregistered(url) { + if (!(url instanceof Ci.nsIURI)) { + url = Services.io.newURI(url); + } + + return new Promise(resolve => { + let listener = { + onUnregister: registration => { + if (registration.principal.host != url.host) { + return; + } + swm.removeListener(listener); + resolve(registration); + }, + }; + swm.addListener(listener); + }); + }, + + /** + * Gets the current quota usage for the specified origin. + * + * @returns a Promise that resolves to an integer with the total + * amount of disk usage by a given origin. + */ + getQuotaUsage(origin) { + return new Promise(resolve => { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + Services.qms.getUsageForPrincipal(principal, request => + resolve(request.result.usage) + ); + }); + }, + + /** + * Cleans up all site data. + */ + clear() { + return new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_COOKIES | + Ci.nsIClearDataService.CLEAR_ALL_CACHES | + Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES | + Ci.nsIClearDataService.CLEAR_DOM_STORAGES | + Ci.nsIClearDataService.CLEAR_PREDICTOR_NETWORK_DATA | + Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS | + Ci.nsIClearDataService.CLEAR_EME | + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, + resolve + ); + }); + }, +}; diff --git a/toolkit/components/cleardata/components.conf b/toolkit/components/cleardata/components.conf new file mode 100644 index 0000000000..40bd3960c1 --- /dev/null +++ b/toolkit/components/cleardata/components.conf @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'js_name': 'clearData', + 'cid': '{0c06583d-7dd8-4293-b1a5-912205f779aa}', + 'contract_ids': ['@mozilla.org/clear-data-service;1'], + 'interfaces': ['nsIClearDataService'], + 'jsm': 'resource://gre/modules/ClearDataService.jsm', + 'constructor': 'ClearDataService', + }, +] diff --git a/toolkit/components/cleardata/moz.build b/toolkit/components/cleardata/moz.build new file mode 100644 index 0000000000..37e69f4fd9 --- /dev/null +++ b/toolkit/components/cleardata/moz.build @@ -0,0 +1,36 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +TESTING_JS_MODULES += [ + "SiteDataTestUtils.jsm", +] + +XPIDL_SOURCES += [ + "nsIClearDataService.idl", +] + +XPIDL_MODULE = "toolkit_cleardata" + +EXTRA_JS_MODULES += [ + "ClearDataService.jsm", + "PrincipalsCollector.jsm", + "ServiceWorkerCleanUp.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] + +include("/ipc/chromium/chromium-config.mozbuild") + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Data Sanitization") + +FINAL_LIBRARY = "xul" diff --git a/toolkit/components/cleardata/nsIClearDataService.idl b/toolkit/components/cleardata/nsIClearDataService.idl new file mode 100644 index 0000000000..4db1dc5ad4 --- /dev/null +++ b/toolkit/components/cleardata/nsIClearDataService.idl @@ -0,0 +1,295 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIPrincipal; +interface nsIClearDataCallback; + +/** + * nsIClearDataService + * + * Provides methods for cleaning data from a nsIPrincipal and/or from a time + * range. + */ +[scriptable, uuid(6ef3ef16-a502-4576-9fb4-919f1c40bf61)] +interface nsIClearDataService : nsISupports +{ + /** + * Delete data owned by local files or other hostless schemes. + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + Note that not all flags will make sense (e.g. we can't clear + certificates for local files). Nonsensical flags will be + ignored. + * @param aCallback this callback will be executed when the operation is + * completed. + */ + void deleteDataFromLocalFiles(in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + /** + * Delete data owned by a host. For instance: mozilla.org. Data from any + * possible originAttributes will be deleted. + * @param aHost the host to be used. + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback this callback will be executed when the operation is + * completed. + */ + void deleteDataFromHost(in AUTF8String aHost, + in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete data owned by a principal. + * @param aPrincipal the nsIPrincipal to be used. + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback ths callback will be executed when the operation is + * completed. + */ + void deleteDataFromPrincipal(in nsIPrincipal aPrincipal, + in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete all data in a time range. Limit excluded. + * @param aFrom microseconds from the epoch + * @param aTo microseconds from the epoch + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback ths callback will be executed when the operation is + * completed. + */ + void deleteDataInTimeRange(in PRTime aFrom, in PRTime aTo, + in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete all data from any host, in any time range. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback ths callback will be executed when the operation is + * completed. + */ + void deleteData(in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete all data from an OriginAttributesPatternDictionary. + * @param aOriginAttributesPattern the originAttributes dictionary. + * @param aCallback the optional callback will be executed when the operation + * is completed. + */ + void deleteDataFromOriginAttributesPattern(in jsval aOriginAttributesPattern, + [optional] in nsIClearDataCallback aCallback); + + /** + * This is a helper function to clear storageAccessAPI permissions + * in a way that will not result in users getting logged out by + * cookie purging. To that end we only clear permissions for principals + * whose base domain does not have any storage associated with it. + * + * The principals to be considered will need to be passed by the API consumer. + * It is recommended to use PrincipalsCollector.jsm for that. + * + * @param aPrincipalsWithStorage principals to be excluded from clearing + * @param aFrom microseconds from the epoch + * @param aCallback the optional callback will be executed when the operation + * is completed. + */ + void deleteUserInteractionForClearingHistory(in Array<nsIPrincipal> aPrincipalsWithStorage, + [optional] in PRTime aFrom, + [optional] in nsIClearDataCallback aCallback); + + /************************************************************************** + * Listed below are the various flags which may be or'd together. + */ + + /** + * Delete cookies. + */ + const uint32_t CLEAR_COOKIES = 1 << 0; + + /** + * Network Cache. + */ + const uint32_t CLEAR_NETWORK_CACHE = 1 << 1; + + /** + * Image cache. + */ + const uint32_t CLEAR_IMAGE_CACHE = 1 << 2; + + /** + * Data stored by external plugins. + */ + const uint32_t CLEAR_PLUGIN_DATA = 1 << 3; + + /** + * Completed downloads. + */ + const uint32_t CLEAR_DOWNLOADS = 1 << 4; + + /** + * Stored passwords. + */ + const uint32_t CLEAR_PASSWORDS = 1 << 5; + + /** + * Media devices. + */ + const uint32_t CLEAR_MEDIA_DEVICES = 1 << 6; + + /** + * AppCache. + */ + const uint32_t CLEAR_APPCACHE = 1 << 7; + + /** + * LocalStorage, IndexedDB, ServiceWorkers, DOM Cache and so on. + */ + const uint32_t CLEAR_DOM_QUOTA = 1 << 8; + + /** + * Predictor network data + */ + const uint32_t CLEAR_PREDICTOR_NETWORK_DATA = 1 << 9; + + /** + * DOM Push notifications + */ + const uint32_t CLEAR_DOM_PUSH_NOTIFICATIONS = 1 << 10; + + /** + * Places history + */ + const uint32_t CLEAR_HISTORY = 1 << 11; + + /** + * Session history + */ + const uint32_t CLEAR_SESSION_HISTORY = 1 << 12; + + /** + * Auth tokens + */ + const uint32_t CLEAR_AUTH_TOKENS = 1 << 13; + + /** + * Login cache + */ + const uint32_t CLEAR_AUTH_CACHE = 1 << 14; + + /** + * Site permissions + */ + const uint32_t CLEAR_PERMISSIONS = 1 << 15; + + /** + * Site preferences + */ + const uint32_t CLEAR_CONTENT_PREFERENCES = 1 << 16; + + /** + * Secure site settings + */ + const uint32_t CLEAR_SECURITY_SETTINGS = 1 << 17; + + /** + * Media plugin data + */ + const uint32_t CLEAR_EME = 1 << 18; + + /** + * Reporting API reports. + */ + const uint32_t CLEAR_REPORTS = 1 << 19; + + /** + * StorageAccessAPI flag, which indicates user interaction. + */ + const uint32_t CLEAR_STORAGE_ACCESS = 1 << 20; + + /** + * Clear Cert Exceptions. + */ + const uint32_t CLEAR_CERT_EXCEPTIONS = 1 << 21; + + /** + * Clear entries in the content blocking database. + */ + const uint32_t CLEAR_CONTENT_BLOCKING_RECORDS = 1 << 22; + + /** + * Clear the in-memory CSS cache. + */ + const uint32_t CLEAR_CSS_CACHE = 1 << 23; + + /** + * Use this value to delete all the data. + */ + const uint32_t CLEAR_ALL = 0xFFFFFF; + + /************************************************************************** + * The following flags are helpers: they combine some of the previous flags + * in a more convenient way. + */ + + /** + * Delete all the possible caches. + */ + const uint32_t CLEAR_ALL_CACHES = CLEAR_NETWORK_CACHE | CLEAR_IMAGE_CACHE | CLEAR_CSS_CACHE; + + /** + * Delete all DOM storages + */ + const uint32_t CLEAR_DOM_STORAGES = CLEAR_APPCACHE | CLEAR_DOM_QUOTA | + CLEAR_DOM_PUSH_NOTIFICATIONS | CLEAR_REPORTS; + + /** + * Helper flag for forget about site + */ + const uint32_t CLEAR_FORGET_ABOUT_SITE = + CLEAR_HISTORY | CLEAR_SESSION_HISTORY | CLEAR_ALL_CACHES | + CLEAR_COOKIES | CLEAR_EME | CLEAR_PLUGIN_DATA | CLEAR_DOWNLOADS | CLEAR_PASSWORDS | + CLEAR_PERMISSIONS | CLEAR_DOM_STORAGES | CLEAR_CONTENT_PREFERENCES | + CLEAR_PREDICTOR_NETWORK_DATA | CLEAR_DOM_PUSH_NOTIFICATIONS | + CLEAR_SECURITY_SETTINGS | CLEAR_REPORTS | CLEAR_CERT_EXCEPTIONS; +}; + +/** + * This is a companion interface for + * nsIClearDataService::deleteDataFromPrincipal(). + */ +[function, scriptable, uuid(e225517b-24c5-498a-b9fb-9993e341a398)] +interface nsIClearDataCallback : nsISupports +{ + /** + * Called to indicate that the data cleaning is completed. + * @param aFailedFlags this value contains the flags that failed during the + * cleanup. If nothing failed, aFailedFlags will be 0. + */ + void onDataDeleted(in uint32_t aFailedFlags); +}; diff --git a/toolkit/components/cleardata/tests/browser/browser.ini b/toolkit/components/cleardata/tests/browser/browser.ini new file mode 100644 index 0000000000..92373c3ec7 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser.ini @@ -0,0 +1,3 @@ +[browser_serviceworkers.js] +[browser_quota.js] +support-files = worker.js diff --git a/toolkit/components/cleardata/tests/browser/browser_quota.js b/toolkit/components/cleardata/tests/browser/browser_quota.js new file mode 100644 index 0000000000..bbbe986fa7 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_quota.js @@ -0,0 +1,238 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This function adds the quota storage by simpleDB (one of quota clients +// managed by the QuotaManager). In this function, a directory +// ${profile}/storage/default/${origin}/sdb/ and a file inside are expected to +// be added. +async function addQuotaStorage(principal) { + let connection = Cc["@mozilla.org/dom/sdb-connection;1"].createInstance( + Ci.nsISDBConnection + ); + + connection.init(principal); + + await new Promise((aResolve, aReject) => { + let request = connection.open("db"); + request.callback = request => { + if (request.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject(request.resultCode); + } + }; + }); + + await new Promise((aResolve, aReject) => { + let request = connection.write(new ArrayBuffer(1)); + request.callback = request => { + if (request.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject(request.resultCode); + } + }; + }); +} + +function getPrincipal(url, attr = {}) { + let uri = Services.io.newURI(url); + let ssm = Services.scriptSecurityManager; + + return ssm.createContentPrincipal(uri, attr); +} + +function getProfileDir() { + let directoryService = Services.dirsvc; + + return directoryService.get("ProfD", Ci.nsIFile); +} + +function getRelativeFile(relativePath) { + let profileDir = getProfileDir(); + + let file = profileDir.clone(); + relativePath.split("/").forEach(function(component) { + file.append(component); + }); + + return file; +} + +function getPath(origin) { + // Santizing + let regex = /[:\/]/g; + return "storage/default/" + origin.replace(regex, "+"); +} + +// This function checks if the origin has the quota storage by checking whether +// the origin directory of that origin exists or not. +function hasQuotaStorage(origin, attr) { + let path = getPath(origin); + if (attr) { + path = path + "^userContextId=" + attr.userContextId; + } + + let file = getRelativeFile(path); + return file.exists(); +} + +async function runTest(sites, deleteDataFunc) { + info(`Adding quota storage`); + for (let site of sites) { + const principal = getPrincipal(site.origin, site.originAttributes); + await addQuotaStorage(principal); + } + + info(`Verifying ${deleteDataFunc.name}`); + let site; + while ((site = sites.shift())) { + await new Promise(aResolve => { + deleteDataFunc(...site.args, value => { + Assert.equal(value, 0); + aResolve(); + }); + }); + + ok( + !hasQuotaStorage(site.origin, site.originAttributes), + `${site.origin} has no quota storage` + ); + sites.forEach(remainSite => + ok( + hasQuotaStorage(remainSite.origin, remainSite.originAttributes), + `${remainSite.origin} has quota storage` + ) + ); + } +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.quotaManager.testing", true], + ["dom.simpleDB.enabled", true], + ], + }); +}); + +const ORG_DOMAIN = "example.com"; +const ORG_ORIGIN = `https://${ORG_DOMAIN}`; +const COM_DOMAIN = "example.org"; +const COM_ORIGIN = `https://${COM_DOMAIN}`; +const LH_DOMAIN = "localhost"; +const FOO_DOMAIN = "foo.com"; + +add_task(async function test_deleteFromHost() { + const sites = [ + { + args: [ORG_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: ORG_ORIGIN, + }, + { + args: [COM_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: COM_ORIGIN, + }, + { + args: [LH_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: `http://${LH_DOMAIN}:8000`, + }, + { + args: [FOO_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: `http://${FOO_DOMAIN}`, + originAttributes: { userContextId: 1 }, + }, + ]; + + await runTest(sites, Services.clearData.deleteDataFromHost); +}); + +add_task(async function test_deleteFromPrincipal() { + const sites = [ + { + args: [ + getPrincipal(ORG_ORIGIN), + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + ], + origin: ORG_ORIGIN, + }, + { + args: [ + getPrincipal(COM_ORIGIN), + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + ], + origin: COM_ORIGIN, + }, + ]; + + await runTest(sites, Services.clearData.deleteDataFromPrincipal); +}); + +add_task(async function test_deleteFromOriginAttributes() { + const ORG_OA = { userContextId: 1 }; + const COM_OA = { userContextId: 2 }; + const sites = [ + { + args: [ORG_OA], + origin: ORG_ORIGIN, + originAttributes: ORG_OA, + }, + { + args: [COM_OA], + origin: COM_ORIGIN, + originAttributes: COM_OA, + }, + ]; + + await runTest( + sites, + Services.clearData.deleteDataFromOriginAttributesPattern + ); +}); + +add_task(async function test_deleteAll() { + info(`Adding quota storage`); + await addQuotaStorage(getPrincipal(ORG_ORIGIN)); + await addQuotaStorage(getPrincipal(COM_ORIGIN)); + + info(`Verifying deleteData`); + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + ok(!hasQuotaStorage(ORG_ORIGIN), `${ORG_ORIGIN} has no quota storage`); + ok(!hasQuotaStorage(COM_ORIGIN), `${COM_ORIGIN} has no quota storage`); +}); + +add_task(async function test_deleteSubdomain() { + const ANOTHER_ORIGIN = `https://wwww.${ORG_DOMAIN}`; + info(`Adding quota storage`); + await addQuotaStorage(getPrincipal(ORG_ORIGIN)); + await addQuotaStorage(getPrincipal(ANOTHER_ORIGIN)); + + info(`Verifying deleteDataFromHost for subdomain`); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + ORG_DOMAIN, + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + ok(!hasQuotaStorage(ORG_ORIGIN), `${ORG_ORIGIN} has no quota storage`); + ok(!hasQuotaStorage(COM_ORIGIN), `${ANOTHER_ORIGIN} has no quota storage`); +}); diff --git a/toolkit/components/cleardata/tests/browser/browser_serviceworkers.js b/toolkit/components/cleardata/tests/browser/browser_serviceworkers.js new file mode 100644 index 0000000000..19a44cc11b --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_serviceworkers.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" +); + +async function addServiceWorker(origin) { + let swURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "worker.js"; + + let registered = SiteDataTestUtils.promiseServiceWorkerRegistered(swURL); + await SiteDataTestUtils.addServiceWorker(swURL); + await registered; + + ok(true, `${origin} has a service worker`); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +add_task(async function test_deleteFromHost() { + await addServiceWorker("https://example.com"); + await addServiceWorker("https://example.org"); + + let unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://example.com" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.com", + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.com"), + "example.com has no service worker" + ); + ok( + SiteDataTestUtils.hasServiceWorkers("https://example.org"), + "example.org has a service worker" + ); + + unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://example.org" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.org", + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.org"), + "example.org has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.com"), + "example.com has no service worker" + ); +}); + +add_task(async function test_deleteFromPrincipal() { + await addServiceWorker("https://test1.example.com"); + await addServiceWorker("https://test1.example.org"); + + let unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test1.example.com" + ); + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://test1.example.com/" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.com"), + "test1.example.com has no service worker" + ); + ok( + SiteDataTestUtils.hasServiceWorkers("https://test1.example.org"), + "test1.example.org has a service worker" + ); + + unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test1.example.org" + ); + principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://test1.example.org/" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.org"), + "test1.example.org has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.com"), + "test1.example.com has no service worker" + ); +}); + +add_task(async function test_deleteAll() { + await addServiceWorker("https://test2.example.com"); + await addServiceWorker("https://test2.example.org"); + + let unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test2.example.com" + ); + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test2.example.com"), + "test2.example.com has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test2.example.org"), + "test2.example.org has no service worker" + ); + + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/cleardata/tests/browser/worker.js b/toolkit/components/cleardata/tests/browser/worker.js new file mode 100644 index 0000000000..aa8a83a4ce --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/worker.js @@ -0,0 +1 @@ +// Empty script for testing service workers diff --git a/toolkit/components/cleardata/tests/marionette/manifest.ini b/toolkit/components/cleardata/tests/marionette/manifest.ini new file mode 100644 index 0000000000..db5bb69d95 --- /dev/null +++ b/toolkit/components/cleardata/tests/marionette/manifest.ini @@ -0,0 +1 @@ +[test_service_worker_at_shutdown.py] diff --git a/toolkit/components/cleardata/tests/marionette/test_service_worker_at_shutdown.py b/toolkit/components/cleardata/tests/marionette/test_service_worker_at_shutdown.py new file mode 100644 index 0000000000..e9fe35bfd0 --- /dev/null +++ b/toolkit/components/cleardata/tests/marionette/test_service_worker_at_shutdown.py @@ -0,0 +1,58 @@ +# 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/. + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + + +class ServiceWorkerAtShutdownTestCase(MarionetteTestCase): + def setUp(self): + super(ServiceWorkerAtShutdownTestCase, self).setUp() + self.install_service_worker() + self.set_pref_to_delete_site_data_on_shutdown() + + def tearDown(self): + self.marionette.restart(clean=True) + super(ServiceWorkerAtShutdownTestCase, self).tearDown() + + def install_service_worker(self): + install_url = self.marionette.absolute_url( + "serviceworker/install_serviceworker.html" + ) + self.marionette.navigate(install_url) + Wait(self.marionette).until(lambda _: self.is_service_worker_registered) + + def set_pref_to_delete_site_data_on_shutdown(self): + self.marionette.set_pref("network.cookie.lifetimePolicy", 2) + + def test_unregistering_service_worker_when_clearing_data(self): + self.marionette.restart(clean=False, in_app=True) + self.assertFalse(self.is_service_worker_registered) + + @property + def is_service_worker_registered(self): + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + """ + let serviceWorkerManager = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(arguments[0]); + + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.principal.origin == principal.origin) { + return true; + } + } + return false; + """, + script_args=(self.marionette.absolute_url(""),), + ) diff --git a/toolkit/components/cleardata/tests/unit/head.js b/toolkit/components/cleardata/tests/unit/head.js new file mode 100644 index 0000000000..88c6eda2f7 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/head.js @@ -0,0 +1,14 @@ +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" +); +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +function run_test() { + do_get_profile(); + run_next_test(); +} diff --git a/toolkit/components/cleardata/tests/unit/test_basic.js b/toolkit/components/cleardata/tests/unit/test_basic.js new file mode 100644 index 0000000000..3634483ee4 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_basic.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Basic test for nsIClearDataService module. + */ + +"use strict"; + +add_task(async function test_basic() { + Assert.ok(!!Services.clearData); + + await new Promise(aResolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => { + Assert.equal(value, 0); + aResolve(); + }); + }); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_certs.js b/toolkit/components/cleardata/tests/unit/test_certs.js new file mode 100644 index 0000000000..e724466e53 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_certs.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const certService = Cc["@mozilla.org/security/local-cert-service;1"].getService( + Ci.nsILocalCertService +); +const overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService +); +const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +const CERT_TEST = + "MIHhMIGcAgEAMA0GCSqGSIb3DQEBBQUAMAwxCjAIBgNVBAMTAUEwHhcNMTEwMzIzMjMyNTE3WhcNMTEwNDIyMjMyNTE3WjAMMQowCAYDVQQDEwFBMEwwDQYJKoZIhvcNAQEBBQADOwAwOAIxANFm7ZCfYNJViaDWTFuMClX3+9u18VFGiyLfM6xJrxir4QVtQC7VUC/WUGoBUs9COQIDAQABMA0GCSqGSIb3DQEBBQUAAzEAx2+gIwmuYjJO5SyabqIm4lB1MandHH1HQc0y0tUFshBOMESTzQRPSVwPn77a6R9t"; + +add_task(async function() { + Assert.ok(Services.clearData); + + const TEST_URI = Services.io.newURI("http://test.com/"); + const ANOTHER_TEST_URI = Services.io.newURI("https://example.com/"); + const YET_ANOTHER_TEST_URI = Services.io.newURI("https://example.test/"); + let cert = certDB.constructX509FromBase64(CERT_TEST); + let flags = Ci.nsIClearDataService.CLEAR_CERT_EXCEPTIONS; + + ok(cert, "Cert was created"); + + Assert.equal( + overrideService.isCertUsedForOverrides(cert, true, true), + 0, + "Cert should not be used for override yet" + ); + + overrideService.rememberValidityOverride( + TEST_URI.asciiHost, + TEST_URI.port, + cert, + flags, + false + ); + + Assert.equal( + overrideService.isCertUsedForOverrides(cert, true, true), + 1, + "Cert should be used for override now" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + TEST_URI.asciiHostPort, + true /* user request */, + flags, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + overrideService.isCertUsedForOverrides(cert, true, true), + 0, + "Cert should not be used for override now" + ); + + for (let uri of [TEST_URI, ANOTHER_TEST_URI, YET_ANOTHER_TEST_URI]) { + overrideService.rememberValidityOverride( + uri.asciiHost, + uri.port, + cert, + flags, + false + ); + Assert.ok( + overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + cert, + {}, + {} + ), + `Should have added override for ${uri.asciiHost}:${uri.port}` + ); + } + + await new Promise(aResolve => { + Services.clearData.deleteData(flags, value => { + Assert.equal(value, 0); + aResolve(); + }); + }); + + for (let uri of [TEST_URI, ANOTHER_TEST_URI, YET_ANOTHER_TEST_URI]) { + Assert.ok( + !overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + cert, + {}, + {} + ), + `Should have removed override for ${uri.asciiHost}:${uri.port}` + ); + } +}); diff --git a/toolkit/components/cleardata/tests/unit/test_cookies.js b/toolkit/components/cleardata/tests/unit/test_cookies.js new file mode 100644 index 0000000000..773f2275c9 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_cookies.js @@ -0,0 +1,172 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for cookies. + */ + +"use strict"; + +add_task(async function test_all_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "example.net", + "path", + "name", + "value", + true /* secure */, + true /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0); +}); + +add_task(async function test_range_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "example.net", + "path", + "name", + "value", + true /* secure */, + true /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + // The cookie is out of time range here. + let from = Date.now() + 60 * 60; + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + from * 1000, + expiry * 2000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + // Now we delete all. + from = Date.now() - 60 * 60; + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + from * 1000, + expiry * 2000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0); +}); + +add_task(async function test_principal_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "example.net", + "path", + "name", + "value", + true /* secure */, + true /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + let uri = Services.io.newURI("http://example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + // Now we delete all. + uri = Services.io.newURI("http://example.net"); + principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0); +}); + +add_task(async function test_localfile_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "", // local file + "path", + "name", + "value", + false /* secure */, + false /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + + Assert.notEqual(Services.cookies.countCookiesFromHost(""), 0); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromLocalFiles( + true, + Ci.nsIClearDataService.CLEAR_COOKIES, + aResolve + ); + }); + Assert.equal(Services.cookies.countCookiesFromHost(""), 0); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_downloads.js b/toolkit/components/cleardata/tests/unit/test_downloads.js new file mode 100644 index 0000000000..de37bfd48a --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_downloads.js @@ -0,0 +1,218 @@ +/** + * Tests for downloads. + */ + +"use strict"; + +const { Downloads } = ChromeUtils.import( + "resource://gre/modules/Downloads.jsm" +); +const { FileTestUtils } = ChromeUtils.import( + "resource://testing-common/FileTestUtils.jsm" +); + +const TEST_TARGET_FILE_NAME = "test-download.txt"; +let fileURL; +let downloadList; + +function createFileURL() { + if (!fileURL) { + const file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("foo.txt"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + fileURL = Services.io.newFileURI(file); + } + + return fileURL; +} + +async function createDownloadList() { + if (!downloadList) { + Downloads._promiseListsInitialized = null; + Downloads._lists = {}; + Downloads._summaries = {}; + + downloadList = await Downloads.getList(Downloads.ALL); + } + + return downloadList; +} + +add_task(async function test_all_downloads() { + const url = createFileURL(); + const list = await createDownloadList(); + + // First download. + let download = await Downloads.createDownload({ + source: { url: url.spec, isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + let view; + let removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + let items = await list.getAll(); + Assert.equal(items.length, 1); + + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 0); +}); + +add_task(async function test_range_downloads() { + const url = createFileURL(); + const list = await createDownloadList(); + + let download = await Downloads.createDownload({ + source: { url: url.spec, isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + // Start + cancel. I need to have a startTime value. + await download.start(); + await download.cancel(); + + let items = await list.getAll(); + Assert.equal(items.length, 1); + + let view; + let removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + await new Promise(resolve => { + Services.clearData.deleteDataInTimeRange( + download.startTime.getTime() * 1000, + download.startTime.getTime() * 1000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 0); +}); + +add_task(async function test_principal_downloads() { + const list = await createDownloadList(); + + let download = await Downloads.createDownload({ + source: { url: "http://example.net", isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + download = await Downloads.createDownload({ + source: { url: "http://example.com", isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + let items = await list.getAll(); + Assert.equal(items.length, 2); + + let view; + let removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + let uri = Services.io.newURI("http://example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 1); + + removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 0); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_network_cache.js b/toolkit/components/cleardata/tests/unit/test_network_cache.js new file mode 100644 index 0000000000..e2ffb441c3 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_network_cache.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test clearing cache. + */ + +"use strict"; + +add_task(async function test_deleteFromHost() { + await SiteDataTestUtils.addCacheEntry("http://example.com/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.com/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.addCacheEntry("http://example.org/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.org/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.com", + true, + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache is cleared" + ); + + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteFromPrincipal() { + await SiteDataTestUtils.addCacheEntry("http://example.com/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.com/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.addCacheEntry("http://example.org/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.org/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.com/" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true, + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache is cleared" + ); + + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteAll() { + await SiteDataTestUtils.addCacheEntry("http://example.com/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.com/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.addCacheEntry("http://example.org/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.org/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache is cleared" + ); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache is cleared" + ); + + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_passwords.js b/toolkit/components/cleardata/tests/unit/test_passwords.js new file mode 100644 index 0000000000..850b0014db --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_passwords.js @@ -0,0 +1,89 @@ +/** + * Tests for passwords. + */ + +"use strict"; + +const URL = "http://example.com"; + +const { LoginTestUtils } = ChromeUtils.import( + "resource://testing-common/LoginTestUtils.jsm" +); + +add_task(async function test_principal_downloads() { + // Store the strings "user" and "pass" using similarly looking glyphs. + let loginInfo = LoginTestUtils.testData.formLogin({ + origin: URL, + formActionOrigin: URL, + username: "admin", + password: "12345678", + usernameField: "field_username", + passwordField: "field_password", + }); + Services.logins.addLogin(loginInfo); + + Assert.equal(countLogins(URL), 1); + + let uri = Services.io.newURI(URL); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_PASSWORDS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + Assert.equal(countLogins(URL), 0); + + LoginTestUtils.clearData(); +}); + +add_task(async function test_all() { + // Store the strings "user" and "pass" using similarly looking glyphs. + let loginInfo = LoginTestUtils.testData.formLogin({ + origin: URL, + formActionOrigin: URL, + username: "admin", + password: "12345678", + usernameField: "field_username", + passwordField: "field_password", + }); + Services.logins.addLogin(loginInfo); + + Assert.equal(countLogins(URL), 1); + + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PASSWORDS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + Assert.equal(countLogins(URL), 0); + + LoginTestUtils.clearData(); +}); + +function countLogins(origin) { + let count = 0; + const logins = Services.logins.getAllLogins(); + for (const login of logins) { + if (login.origin == origin) { + ++count; + } + } + + return count; +} diff --git a/toolkit/components/cleardata/tests/unit/test_permissions.js b/toolkit/components/cleardata/tests/unit/test_permissions.js new file mode 100644 index 0000000000..52f33d1443 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_permissions.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for permissions + */ + +"use strict"; + +add_task(async function test_all_permissions() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) == null + ); +}); + +add_task(async function test_principal_permissions() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + const anotherUri = Services.io.newURI("https://example.com"); + const anotherPrincipal = Services.scriptSecurityManager.createContentPrincipal( + anotherUri, + {} + ); + + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) == null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); + +add_task(async function test_3rdpartystorage_permissions() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + + const anotherUri = Services.io.newURI("https://example.com"); + const anotherPrincipal = Services.scriptSecurityManager.createContentPrincipal( + anotherUri, + {} + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "3rdPartyStorage^https://example.net", + Services.perms.ALLOW_ACTION + ); + + const oneMoreUri = Services.io.newURI("https://example.org"); + const oneMorePrincipal = Services.scriptSecurityManager.createContentPrincipal( + oneMoreUri, + {} + ); + Services.perms.addFromPrincipal( + oneMorePrincipal, + "cookie", + Services.perms.ALLOW_ACTION + ); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject( + anotherPrincipal, + "3rdPartyStorage^https://example.net", + true + ) != null + ); + Assert.ok( + Services.perms.getPermissionObject(oneMorePrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) == null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject( + anotherPrincipal, + "3rdPartyStorage^https://example.net", + true + ) == null + ); + Assert.ok( + Services.perms.getPermissionObject(oneMorePrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_storage_permission.js b/toolkit/components/cleardata/tests/unit/test_storage_permission.js new file mode 100644 index 0000000000..edd8707e9d --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_storage_permission.js @@ -0,0 +1,296 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for permissions + */ + +"use strict"; + +// Test that only the storageAccessAPI gets removed. +add_task(async function test_removing_storage_permission() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + Services.perms.addFromPrincipal( + principal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "There is a storageAccessAPI permission set" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION, + "the storageAccessAPI permission has been removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal(principal, "cookie"), + Services.perms.ALLOW_ACTION, + "the cookie permission has not been removed" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); + +// Test that the storageAccessAPI gets removed from a particular principal +add_task(async function test_removing_storage_permission_from_principal() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + const anotherUri = Services.io.newURI("https://example.com"); + const anotherPrincipal = Services.scriptSecurityManager.createContentPrincipal( + anotherUri, + {} + ); + + Services.perms.addFromPrincipal( + principal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has been added to the first principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + anotherPrincipal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has been added to the second principal" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION, + "storageAccessAPI permission has been removed from the first principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + anotherPrincipal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has not been removed from the second principal" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); + +// Tests the deleteUserInteractionForClearingHistory function. +add_task(async function test_deleteUserInteractionForClearingHistory() { + // These should be retained. + PermissionTestUtils.add( + "https://example.com", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.com", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.com^userContextId=3", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + // These should be removed. + PermissionTestUtils.add( + "https://example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.org^userContextId=3", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + let principalWithStorage = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://sub.example.com" + ); + + await new Promise(resolve => { + return Services.clearData.deleteUserInteractionForClearingHistory( + [principalWithStorage], + 0, + resolve + ); + }); + + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.org", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.org", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.org^userContextId=3", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.com", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.com", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.com^userContextId=3", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + + // This permission is set earlier than the timestamp and should be retained. + PermissionTestUtils.add( + "https://example.net", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + // Add some time in between taking the snapshot of the timestamp + // to avoid flakyness. + await new Promise(c => do_timeout(100, c)); + let timestamp = Date.now(); + await new Promise(c => do_timeout(100, c)); + + // This permission is set later than the timestamp and should be removed. + PermissionTestUtils.add( + "https://example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + await new Promise(resolve => { + return Services.clearData.deleteUserInteractionForClearingHistory( + [principalWithStorage], + timestamp, + resolve + ); + }); + + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.org", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.net", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.com", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); diff --git a/toolkit/components/cleardata/tests/unit/xpcshell.ini b/toolkit/components/cleardata/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..d33d0c2260 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/xpcshell.ini @@ -0,0 +1,14 @@ +[DEFAULT] +firefox-appdir = browser +head = head.js +skip-if = toolkit == 'android' +support-files = + +[test_basic.js] +[test_certs.js] +[test_cookies.js] +[test_downloads.js] +[test_network_cache.js] +[test_passwords.js] +[test_permissions.js] +[test_storage_permission.js] |