forked from mirrors/gecko-dev
Backed out changeset 2d264402493e (bug 1854940) Backed out changeset 144b76e6db22 (bug 1854940) Backed out changeset 5a9e45504f32 (bug 1854940) Backed out changeset c8c7ec382574 (bug 1854940)
1848 lines
52 KiB
JavaScript
1848 lines
52 KiB
JavaScript
/* 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/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
Downloads: "resource://gre/modules/Downloads.sys.mjs",
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
|
ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"sas",
|
|
"@mozilla.org/storage/activity-service;1",
|
|
"nsIStorageActivityService"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"TrackingDBService",
|
|
"@mozilla.org/tracking-db-service;1",
|
|
"nsITrackingDBService"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"IdentityCredentialStorageService",
|
|
"@mozilla.org/browser/identity-credential-storage-service;1",
|
|
"nsIIdentityCredentialStorageService"
|
|
);
|
|
|
|
/**
|
|
* Test if host, OriginAttributes or principal belong to a baseDomain. Also
|
|
* considers partitioned storage by inspecting OriginAttributes partitionKey.
|
|
* @param options
|
|
* @param {string} [options.host] - Optional host to compare to base domain.
|
|
* @param {object} [options.originAttributes] - Optional origin attributes to
|
|
* inspect for aBaseDomain. If omitted, partitionKey will not be matched.
|
|
* @param {nsIPrincipal} [options.principal] - Optional principal to compare to
|
|
* base domain.
|
|
* @param {string} aBaseDomain - Domain to check for. Must be a valid, non-empty
|
|
* baseDomain string.
|
|
* @returns {boolean} Whether the host, originAttributes or principal matches
|
|
* the base domain.
|
|
*/
|
|
function hasBaseDomain(
|
|
{ host = null, originAttributes = null, principal = null },
|
|
aBaseDomain
|
|
) {
|
|
if (!aBaseDomain) {
|
|
throw new Error("Missing baseDomain.");
|
|
}
|
|
if (!host && !originAttributes && !principal) {
|
|
throw new Error(
|
|
"Missing host, originAttributes or principal to match with baseDomain."
|
|
);
|
|
}
|
|
if (principal && (host || originAttributes)) {
|
|
throw new Error(
|
|
"Can only pass either principal or host and originAttributes."
|
|
);
|
|
}
|
|
|
|
if (host && Services.eTLD.hasRootDomain(host, aBaseDomain)) {
|
|
return true;
|
|
}
|
|
|
|
if (principal?.baseDomain == aBaseDomain) {
|
|
return true;
|
|
}
|
|
|
|
originAttributes = originAttributes || principal?.originAttributes;
|
|
if (!originAttributes) {
|
|
return false;
|
|
}
|
|
|
|
return ChromeUtils.originAttributesMatchPattern(originAttributes, {
|
|
partitionKeyPattern: { baseDomain: aBaseDomain },
|
|
});
|
|
}
|
|
|
|
// Here is a list of methods cleaners may implement. These methods must return a
|
|
// Promise object.
|
|
// * deleteAll() - this method _must_ exist. When called, it deletes all the
|
|
// data owned by the cleaner.
|
|
// * deleteByPrincipal() - this method _must_ exist.
|
|
// * deleteByBaseDomain() - this method _must_ exist.
|
|
// * 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.
|
|
// * cleanupAfterDeletionAtShutdown() - this method is implemented only if the
|
|
// cleaner needs a separate step after
|
|
// deletion. No-op if not implemented.
|
|
// Currently called via
|
|
// Sanitizer.maybeSanitizeSessionPrincipals().
|
|
|
|
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();
|
|
});
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
// Fall back to clearing by host and OA pattern. This will over-clear, since
|
|
// any properties that are not explicitly set in aPrincipal.originAttributes
|
|
// will be wildcard matched.
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
async deleteByBaseDomain(aDomain) {
|
|
Services.cookies.cookies
|
|
.filter(({ rawHost, originAttributes }) =>
|
|
hasBaseDomain({ host: rawHost, originAttributes }, aDomain)
|
|
)
|
|
.forEach(cookie => {
|
|
Services.cookies.removeCookiesFromExactHost(
|
|
cookie.rawHost,
|
|
JSON.stringify(cookie.originAttributes)
|
|
);
|
|
});
|
|
},
|
|
|
|
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 = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(
|
|
Ci.nsICertOverrideService
|
|
);
|
|
|
|
overrideService.clearValidityOverride(aHost, -1, aOriginAttributes);
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(
|
|
Ci.nsICertOverrideService
|
|
);
|
|
overrideService
|
|
.getOverrides()
|
|
.filter(({ asciiHost }) =>
|
|
hasBaseDomain({ host: asciiHost }, aBaseDomain)
|
|
)
|
|
.forEach(({ asciiHost, port }) =>
|
|
overrideService.clearValidityOverride(asciiHost, port, {})
|
|
);
|
|
},
|
|
|
|
async deleteAll() {
|
|
let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(
|
|
Ci.nsICertOverrideService
|
|
);
|
|
|
|
overrideService.clearAllOverrides();
|
|
},
|
|
};
|
|
|
|
const NetworkCacheCleaner = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
// 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);
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
Services.cache2.clearBaseDomain(aBaseDomain);
|
|
},
|
|
|
|
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 = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
// 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.clearStyleSheetCacheByPrincipal(httpPrincipal);
|
|
ChromeUtils.clearStyleSheetCacheByPrincipal(httpsPrincipal);
|
|
},
|
|
|
|
async deleteByPrincipal(aPrincipal) {
|
|
ChromeUtils.clearStyleSheetCacheByPrincipal(aPrincipal);
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
ChromeUtils.clearStyleSheetCacheByBaseDomain(aBaseDomain);
|
|
},
|
|
|
|
async deleteAll() {
|
|
ChromeUtils.clearStyleSheetCache();
|
|
},
|
|
};
|
|
|
|
const ImageCacheCleaner = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
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.removeEntriesFromPrincipalInAllProcesses(httpPrincipal);
|
|
imageCache.removeEntriesFromPrincipalInAllProcesses(httpsPrincipal);
|
|
},
|
|
|
|
async deleteByPrincipal(aPrincipal) {
|
|
let imageCache = Cc["@mozilla.org/image/tools;1"]
|
|
.getService(Ci.imgITools)
|
|
.getImgCacheForDocument(null);
|
|
imageCache.removeEntriesFromPrincipalInAllProcesses(aPrincipal);
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
let imageCache = Cc["@mozilla.org/image/tools;1"]
|
|
.getService(Ci.imgITools)
|
|
.getImgCacheForDocument(null);
|
|
imageCache.removeEntriesFromBaseDomainInAllProcesses(aBaseDomain);
|
|
},
|
|
|
|
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 DownloadsCleaner = {
|
|
async _deleteInternal({ hostOrBaseDomain, principal, originAttributes }) {
|
|
originAttributes = originAttributes || principal?.originAttributes || {};
|
|
|
|
let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
|
|
list.removeFinished(({ source }) => {
|
|
if (
|
|
"userContextId" in originAttributes &&
|
|
"userContextId" in source &&
|
|
originAttributes.userContextId != source.userContextId
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
"privateBrowsingId" in originAttributes &&
|
|
!!originAttributes.privateBrowsingId != source.isPrivate
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
let entryURI = Services.io.newURI(source.url);
|
|
if (hostOrBaseDomain) {
|
|
return Services.eTLD.hasRootDomain(entryURI.host, hostOrBaseDomain);
|
|
}
|
|
if (principal) {
|
|
return principal.equalsURI(entryURI);
|
|
}
|
|
return false;
|
|
});
|
|
},
|
|
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
// Clearing by host also clears associated subdomains.
|
|
return this._deleteInternal({
|
|
hostOrBaseDomain: aHost,
|
|
originAttributes: aOriginAttributes,
|
|
});
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this._deleteInternal({ principal: aPrincipal });
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
return this._deleteInternal({ hostOrBaseDomain: aBaseDomain });
|
|
},
|
|
|
|
deleteByRange(aFrom, aTo) {
|
|
// Convert microseconds back to milliseconds for date comparisons.
|
|
let rangeBeginMs = aFrom / 1000;
|
|
let rangeEndMs = aTo / 1000;
|
|
|
|
return lazy.Downloads.getList(lazy.Downloads.ALL).then(aList => {
|
|
aList.removeFinished(
|
|
aDownload =>
|
|
aDownload.startTime >= rangeBeginMs &&
|
|
aDownload.startTime <= rangeEndMs
|
|
);
|
|
});
|
|
},
|
|
|
|
deleteAll() {
|
|
return lazy.Downloads.getList(lazy.Downloads.ALL).then(aList => {
|
|
aList.removeFinished(null);
|
|
});
|
|
},
|
|
};
|
|
|
|
const PasswordsCleaner = {
|
|
deleteByHost(aHost, aOriginAttributes) {
|
|
// Clearing by host also clears associated subdomains.
|
|
return this._deleteInternal(aLogin =>
|
|
Services.eTLD.hasRootDomain(aLogin.hostname, aHost)
|
|
);
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
// Login origins don't contain any origin attributes.
|
|
return this._deleteInternal(
|
|
aLogin => aLogin.origin == aPrincipal.originNoSuffix
|
|
);
|
|
},
|
|
|
|
deleteByBaseDomain(aBaseDomain) {
|
|
return this._deleteInternal(aLogin =>
|
|
Services.eTLD.hasRootDomain(aLogin.hostname, aBaseDomain)
|
|
);
|
|
},
|
|
|
|
deleteAll() {
|
|
return this._deleteInternal(() => true);
|
|
},
|
|
|
|
async _deleteInternal(aCb) {
|
|
try {
|
|
let logins = await 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);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
const MediaDevicesCleaner = {
|
|
async deleteByRange(aFrom, aTo) {
|
|
let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"].getService(
|
|
Ci.nsIMediaManagerService
|
|
);
|
|
mediaMgr.sanitizeDeviceIds(aFrom);
|
|
},
|
|
|
|
// TODO: We should call the MediaManager to clear by principal, rather than
|
|
// over-clearing for user requests or bailing out for programmatic calls.
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
// TODO: Same as above, but for base domain.
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
async deleteAll() {
|
|
let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"].getService(
|
|
Ci.nsIMediaManagerService
|
|
);
|
|
mediaMgr.sanitizeDeviceIds(null);
|
|
},
|
|
};
|
|
|
|
const QuotaCleaner = {
|
|
/**
|
|
* Clear quota storage for matching principals.
|
|
* @param {function} filterFn - Filter function which is passed a principal.
|
|
* Return true to clear storage for given principal or false to skip it.
|
|
* @returns {Promise} - Resolves once all matching items have been cleared.
|
|
* Rejects on error.
|
|
*/
|
|
async _qmsClearStoragesForPrincipalsMatching(filterFn) {
|
|
// Clearing quota storage by first getting all entry origins and then
|
|
// iterating over them is not ideal, since we can not ensure an entirely
|
|
// consistent clearing state. Between fetching the origins and clearing
|
|
// them, additional entries could be added. This means we could end up with
|
|
// stray entries after the clearing operation. To fix this we would need to
|
|
// move the clearing code to the QuotaManager itself which could either
|
|
// prevent new writes while clearing or clean up any additional entries
|
|
// which get written during the clearing operation.
|
|
// Performance is also not ideal, since we iterate over storage multiple
|
|
// times for this two step process.
|
|
// See Bug 1719195.
|
|
let origins = await new Promise((resolve, reject) => {
|
|
Services.qms.listOrigins().callback = request => {
|
|
if (request.resultCode != Cr.NS_OK) {
|
|
reject({ message: "Deleting quota storages failed" });
|
|
return;
|
|
}
|
|
resolve(request.result);
|
|
};
|
|
});
|
|
|
|
let clearPromises = origins
|
|
// Parse origins into principals.
|
|
.map(Services.scriptSecurityManager.createContentPrincipalFromOrigin)
|
|
// Filter out principals that don't match the filterFn.
|
|
.filter(filterFn)
|
|
// Clear quota storage by principal and collect the promises.
|
|
.map(
|
|
principal =>
|
|
new Promise((resolve, reject) => {
|
|
let clearRequest =
|
|
Services.qms.clearStoragesForPrincipal(principal);
|
|
clearRequest.callback = () => {
|
|
if (clearRequest.resultCode != Cr.NS_OK) {
|
|
reject({ message: "Deleting quota storages failed" });
|
|
return;
|
|
}
|
|
resolve();
|
|
};
|
|
})
|
|
);
|
|
return Promise.all(clearPromises);
|
|
},
|
|
|
|
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.sessionStorage.clearStoragesForOrigin(aPrincipal);
|
|
|
|
// ServiceWorkers: they must be removed before cleaning QuotaManager.
|
|
return lazy.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();
|
|
}
|
|
};
|
|
});
|
|
});
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
// 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",
|
|
aBaseDomain
|
|
);
|
|
|
|
// Clear sessionStorage
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"browser:purge-sessionStorage",
|
|
aBaseDomain
|
|
);
|
|
|
|
// Clear third-party storage partitioned under aBaseDomain.
|
|
// This notification is forwarded via the StorageObserver and consumed only
|
|
// by the SessionStorageManager and (legacy) LocalStorageManager.
|
|
// There is a similar (legacy) notification "clear-origin-attributes-data"
|
|
// which additionally clears data across various other storages unrelated to
|
|
// the QuotaCleaner.
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"dom-storage:clear-origin-attributes-data",
|
|
JSON.stringify({ partitionKeyPattern: { baseDomain: aBaseDomain } })
|
|
);
|
|
|
|
// ServiceWorkers must be removed before cleaning QuotaManager. We store
|
|
// potential errors so we can re-throw later, once all operations have
|
|
// completed.
|
|
let swCleanupError;
|
|
try {
|
|
await lazy.ServiceWorkerCleanUp.removeFromBaseDomain(aBaseDomain);
|
|
} catch (error) {
|
|
swCleanupError = error;
|
|
}
|
|
|
|
await this._qmsClearStoragesForPrincipalsMatching(principal =>
|
|
hasBaseDomain({ principal }, aBaseDomain)
|
|
);
|
|
|
|
// Re-throw any service worker cleanup errors.
|
|
if (swCleanupError) {
|
|
throw swCleanupError;
|
|
}
|
|
},
|
|
|
|
async 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 must be removed before cleaning QuotaManager. We store any
|
|
// errors so we can re-throw later once all operations have completed.
|
|
let swCleanupError;
|
|
try {
|
|
await lazy.ServiceWorkerCleanUp.removeFromHost(aHost);
|
|
} catch (error) {
|
|
swCleanupError = error;
|
|
}
|
|
|
|
await this._qmsClearStoragesForPrincipalsMatching(principal => {
|
|
try {
|
|
// deleteByHost has the semantics that "foo.example.com" should be
|
|
// wiped if we are provided an aHost of "example.com".
|
|
return Services.eTLD.hasRootDomain(principal.host, aHost);
|
|
} catch (e) {
|
|
// There is no host for the given principal.
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Re-throw any service worker cleanup errors.
|
|
if (swCleanupError) {
|
|
throw swCleanupError;
|
|
}
|
|
},
|
|
|
|
deleteByRange(aFrom, aTo) {
|
|
let principals = lazy.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 lazy.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" });
|
|
}
|
|
};
|
|
});
|
|
});
|
|
},
|
|
|
|
async deleteAll() {
|
|
// localStorage
|
|
Services.obs.notifyObservers(null, "extension:purge-localStorage");
|
|
|
|
// sessionStorage
|
|
Services.obs.notifyObservers(null, "browser:purge-sessionStorage");
|
|
|
|
// ServiceWorkers must be removed before cleaning QuotaManager. We store any
|
|
// errors so we can re-throw later once all operations have completed.
|
|
let swCleanupError;
|
|
try {
|
|
await lazy.ServiceWorkerCleanUp.removeAll();
|
|
} catch (error) {
|
|
swCleanupError = error;
|
|
}
|
|
|
|
await this._qmsClearStoragesForPrincipalsMatching(
|
|
principal =>
|
|
principal.schemeIs("http") ||
|
|
principal.schemeIs("https") ||
|
|
principal.schemeIs("file")
|
|
);
|
|
|
|
// Re-throw any service worker cleanup errors.
|
|
if (swCleanupError) {
|
|
throw swCleanupError;
|
|
}
|
|
},
|
|
|
|
async cleanupAfterDeletionAtShutdown() {
|
|
const toBeRemovedDir = PathUtils.join(
|
|
PathUtils.profileDir,
|
|
Services.prefs.getStringPref("dom.quotaManager.storageName"),
|
|
"to-be-removed"
|
|
);
|
|
|
|
if (
|
|
!AppConstants.MOZ_BACKGROUNDTASKS ||
|
|
!Services.prefs.getBoolPref("dom.quotaManager.backgroundTask.enabled")
|
|
) {
|
|
await IOUtils.remove(toBeRemovedDir, { recursive: true });
|
|
return;
|
|
}
|
|
|
|
const runner = Cc["@mozilla.org/backgroundtasksrunner;1"].getService(
|
|
Ci.nsIBackgroundTasksRunner
|
|
);
|
|
|
|
runner.removeDirectoryInDetachedProcess(
|
|
toBeRemovedDir,
|
|
"",
|
|
"0",
|
|
"*", // wildcard
|
|
"Quota"
|
|
);
|
|
},
|
|
};
|
|
|
|
const PredictorNetworkCleaner = {
|
|
async 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();
|
|
},
|
|
|
|
// TODO: We should call the NetworkPredictor to clear by principal, rather
|
|
// than over-clearing for user requests or bailing out for programmatic calls.
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
// TODO: Same as above, but for base domain.
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
};
|
|
|
|
const PushNotificationsCleaner = {
|
|
/**
|
|
* Clear entries for aDomain including subdomains of aDomain.
|
|
* @param {string} aDomain - Domain to clear data for.
|
|
* @returns {Promise} a promise which resolves once data has been cleared.
|
|
*/
|
|
_deleteByRootDomain(aDomain) {
|
|
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
|
|
);
|
|
// ClearForDomain also clears subdomains.
|
|
push.clearForDomain(aDomain, aStatus => {
|
|
if (!Components.isSuccessCode(aStatus)) {
|
|
aReject();
|
|
} else {
|
|
aResolve();
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
deleteByHost(aHost, aOriginAttributes) {
|
|
// Will also clear entries for subdomains of aHost. Data is cleared across
|
|
// all origin attributes.
|
|
return this._deleteByRootDomain(aHost);
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
// Will also clear entries for subdomains of the principal host. Data is
|
|
// cleared across all origin attributes.
|
|
return this._deleteByRootDomain(aPrincipal.host);
|
|
},
|
|
|
|
deleteByBaseDomain(aBaseDomain) {
|
|
return this._deleteByRootDomain(aBaseDomain);
|
|
},
|
|
|
|
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.
|
|
async 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);
|
|
}
|
|
for (let perm of Services.perms.getAllByTypeSince(
|
|
"storageAccessAPI",
|
|
// The permission manager uses milliseconds instead of microseconds
|
|
aFrom / 1000
|
|
)) {
|
|
if (!baseDomainsWithStorage.has(perm.principal.baseDomain)) {
|
|
Services.perms.removePermission(perm);
|
|
}
|
|
}
|
|
},
|
|
|
|
async deleteByPrincipal(aPrincipal) {
|
|
return Services.perms.removeFromPrincipal(aPrincipal, "storageAccessAPI");
|
|
},
|
|
|
|
_deleteInternal(filter) {
|
|
Services.perms.all
|
|
.filter(({ type }) => type == "storageAccessAPI")
|
|
.filter(filter)
|
|
.forEach(perm => {
|
|
try {
|
|
Services.perms.removePermission(perm);
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
});
|
|
},
|
|
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
// Clearing by host also clears associated subdomains.
|
|
this._deleteInternal(({ principal }) => {
|
|
let toBeRemoved = false;
|
|
try {
|
|
toBeRemoved = Services.eTLD.hasRootDomain(principal.host, aHost);
|
|
} catch (ex) {}
|
|
return toBeRemoved;
|
|
});
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
this._deleteInternal(
|
|
({ principal }) => principal.baseDomain == aBaseDomain
|
|
);
|
|
},
|
|
|
|
async deleteByRange(aFrom, aTo) {
|
|
Services.perms.removeByTypeSince("storageAccessAPI", aFrom / 1000);
|
|
},
|
|
|
|
async deleteAll() {
|
|
Services.perms.removeByType("storageAccessAPI");
|
|
},
|
|
};
|
|
|
|
const HistoryCleaner = {
|
|
deleteByHost(aHost, aOriginAttributes) {
|
|
if (!AppConstants.MOZ_PLACES) {
|
|
return Promise.resolve();
|
|
}
|
|
return lazy.PlacesUtils.history.removeByFilter({ host: "." + aHost });
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
if (!AppConstants.MOZ_PLACES) {
|
|
return Promise.resolve();
|
|
}
|
|
return lazy.PlacesUtils.history.removeByFilter({ host: aPrincipal.host });
|
|
},
|
|
|
|
deleteByBaseDomain(aBaseDomain) {
|
|
return this.deleteByHost(aBaseDomain, {});
|
|
},
|
|
|
|
deleteByRange(aFrom, aTo) {
|
|
if (!AppConstants.MOZ_PLACES) {
|
|
return Promise.resolve();
|
|
}
|
|
return lazy.PlacesUtils.history.removeVisitsByFilter({
|
|
beginDate: new Date(aFrom / 1000),
|
|
endDate: new Date(aTo / 1000),
|
|
});
|
|
},
|
|
|
|
deleteAll() {
|
|
if (!AppConstants.MOZ_PLACES) {
|
|
return Promise.resolve();
|
|
}
|
|
return lazy.PlacesUtils.history.clear();
|
|
},
|
|
};
|
|
|
|
const SessionHistoryCleaner = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
// Session storage and history also clear subdomains of aHost.
|
|
Services.obs.notifyObservers(null, "browser:purge-sessionStorage", aHost);
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"browser:purge-session-history-for-domain",
|
|
aHost
|
|
);
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
deleteByBaseDomain(aBaseDomain) {
|
|
return this.deleteByHost(aBaseDomain, {});
|
|
},
|
|
|
|
async deleteByRange(aFrom, aTo) {
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"browser:purge-session-history",
|
|
String(aFrom)
|
|
);
|
|
},
|
|
|
|
async deleteAll() {
|
|
Services.obs.notifyObservers(null, "browser:purge-session-history");
|
|
},
|
|
};
|
|
|
|
const AuthTokensCleaner = {
|
|
// TODO: Bug 1726742
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
// TODO: Bug 1726742
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
async deleteAll() {
|
|
let sdr = Cc["@mozilla.org/security/sdr;1"].getService(
|
|
Ci.nsISecretDecoderRing
|
|
);
|
|
sdr.logoutAndTeardown();
|
|
},
|
|
};
|
|
|
|
const AuthCacheCleaner = {
|
|
// TODO: Bug 1726743
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
// TODO: Bug 1726743
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
deleteAll() {
|
|
return new Promise(aResolve => {
|
|
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
|
aResolve();
|
|
});
|
|
},
|
|
};
|
|
|
|
const PermissionsCleaner = {
|
|
/**
|
|
* Delete permissions by either base domain or host.
|
|
* Clearing by host also clears associated subdomains.
|
|
* For example, clearing "example.com" will also clear permissions for
|
|
* "test.example.com" and "another.test.example.com".
|
|
* @param options
|
|
* @param {string} options.baseDomain - Base domain to delete permissions for.
|
|
* @param {string} options.host - Host to delete permissions for.
|
|
*/
|
|
async _deleteInternal({ baseDomain, host }) {
|
|
for (let perm of Services.perms.all) {
|
|
let toBeRemoved;
|
|
|
|
if (baseDomain) {
|
|
toBeRemoved = perm.principal.baseDomain == baseDomain;
|
|
} else {
|
|
try {
|
|
toBeRemoved = Services.eTLD.hasRootDomain(perm.principal.host, host);
|
|
} catch (ex) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (
|
|
!toBeRemoved &&
|
|
(perm.type.startsWith("3rdPartyStorage^") ||
|
|
perm.type.startsWith("3rdPartyFrameStorage^"))
|
|
) {
|
|
let parts = perm.type.split("^");
|
|
let uri;
|
|
try {
|
|
uri = Services.io.newURI(parts[1]);
|
|
} catch (ex) {
|
|
continue;
|
|
}
|
|
|
|
toBeRemoved = Services.eTLD.hasRootDomain(uri.host, baseDomain || host);
|
|
}
|
|
|
|
if (!toBeRemoved) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
Services.perms.removePermission(perm);
|
|
} catch (ex) {
|
|
// Ignore entry
|
|
}
|
|
}
|
|
},
|
|
|
|
deleteByHost(aHost, aOriginAttributes) {
|
|
return this._deleteInternal({ host: aHost });
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
deleteByBaseDomain(aBaseDomain) {
|
|
return this._deleteInternal({ baseDomain: aBaseDomain });
|
|
},
|
|
|
|
async deleteByRange(aFrom, aTo) {
|
|
Services.perms.removeAllSince(aFrom / 1000);
|
|
},
|
|
|
|
async deleteByOriginAttributes(aOriginAttributesString) {
|
|
Services.perms.removePermissionsWithAttributes(aOriginAttributesString);
|
|
},
|
|
|
|
async deleteAll() {
|
|
Services.perms.removeAll();
|
|
},
|
|
};
|
|
|
|
const PreferencesCleaner = {
|
|
deleteByHost(aHost, aOriginAttributes) {
|
|
// Also clears subdomains of aHost.
|
|
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() {},
|
|
});
|
|
});
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
deleteByBaseDomain(aBaseDomain) {
|
|
return this.deleteByHost(aBaseDomain, {});
|
|
},
|
|
|
|
async deleteByRange(aFrom, aTo) {
|
|
let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
|
|
Ci.nsIContentPrefService2
|
|
);
|
|
cps2.removeAllDomainsSince(aFrom / 1000, null);
|
|
},
|
|
|
|
async deleteAll() {
|
|
let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
|
|
Ci.nsIContentPrefService2
|
|
);
|
|
cps2.removeAllDomains(null);
|
|
},
|
|
};
|
|
|
|
const ClientAuthRememberCleaner = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
let cars = Cc[
|
|
"@mozilla.org/security/clientAuthRememberService;1"
|
|
].getService(Ci.nsIClientAuthRememberService);
|
|
|
|
cars.deleteDecisionsByHost(aHost, aOriginAttributes);
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
async deleteByBaseDomain(aDomain) {
|
|
let cars = Cc[
|
|
"@mozilla.org/security/clientAuthRememberService;1"
|
|
].getService(Ci.nsIClientAuthRememberService);
|
|
|
|
cars
|
|
.getDecisions()
|
|
.filter(({ asciiHost, entryKey }) => {
|
|
// Get the origin attributes which are in the third component of the
|
|
// entryKey. ',' is used as the delimiter.
|
|
let originSuffixEncoded = entryKey.split(",")[2];
|
|
let originAttributes;
|
|
|
|
if (originSuffixEncoded) {
|
|
try {
|
|
// Decoding the suffix or parsing the origin attributes can fail. In
|
|
// this case we won't match the partitionKey, but we can still match
|
|
// the asciiHost.
|
|
let originSuffix = decodeURIComponent(originSuffixEncoded);
|
|
originAttributes =
|
|
ChromeUtils.CreateOriginAttributesFromOriginSuffix(originSuffix);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
return hasBaseDomain(
|
|
{
|
|
host: asciiHost,
|
|
originAttributes,
|
|
},
|
|
aDomain
|
|
);
|
|
})
|
|
.forEach(({ entryKey }) => cars.forgetRememberedDecision(entryKey));
|
|
},
|
|
|
|
async deleteAll() {
|
|
let cars = Cc[
|
|
"@mozilla.org/security/clientAuthRememberService;1"
|
|
].getService(Ci.nsIClientAuthRememberService);
|
|
cars.clearRememberedDecisions();
|
|
},
|
|
};
|
|
|
|
const HSTSCleaner = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
let sss = Cc["@mozilla.org/ssservice;1"].getService(
|
|
Ci.nsISiteSecurityService
|
|
);
|
|
let uri = Services.io.newURI("https://" + aHost);
|
|
sss.resetState(
|
|
uri,
|
|
aOriginAttributes,
|
|
Ci.nsISiteSecurityService.RootDomain
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Adds brackets to a site if it's an IPv6 address.
|
|
* @param {string} aSite - (schemeless) site which may be an IPv6.
|
|
* @returns {string} bracketed IPv6 or site if site is not an IPv6.
|
|
*/
|
|
_maybeFixIpv6Site(aSite) {
|
|
// Not an IPv6 or already has brackets.
|
|
if (!aSite.includes(":") || aSite[0] == "[") {
|
|
return aSite;
|
|
}
|
|
return `[${aSite}]`;
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
async deleteByBaseDomain(aDomain) {
|
|
let sss = Cc["@mozilla.org/ssservice;1"].getService(
|
|
Ci.nsISiteSecurityService
|
|
);
|
|
|
|
// Add brackets to IPv6 sites to ensure URI creation succeeds.
|
|
let uri = Services.io.newURI("https://" + this._maybeFixIpv6Site(aDomain));
|
|
sss.resetState(uri, {}, Ci.nsISiteSecurityService.BaseDomain);
|
|
},
|
|
|
|
async deleteAll() {
|
|
// 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();
|
|
},
|
|
};
|
|
|
|
const EMECleaner = {
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(
|
|
Ci.mozIGeckoMediaPluginChromeService
|
|
);
|
|
mps.forgetThisSite(aHost, JSON.stringify(aOriginAttributes));
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain) {
|
|
let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(
|
|
Ci.mozIGeckoMediaPluginChromeService
|
|
);
|
|
mps.forgetThisBaseDomain(aBaseDomain);
|
|
},
|
|
|
|
deleteAll() {
|
|
// Not implemented.
|
|
return Promise.resolve();
|
|
},
|
|
};
|
|
|
|
const ReportsCleaner = {
|
|
deleteByHost(aHost, aOriginAttributes) {
|
|
// Also clears subdomains of aHost.
|
|
return new Promise(aResolve => {
|
|
Services.obs.notifyObservers(null, "reporting:purge-host", aHost);
|
|
aResolve();
|
|
});
|
|
},
|
|
|
|
deleteByPrincipal(aPrincipal) {
|
|
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
|
|
},
|
|
|
|
deleteByBaseDomain(aBaseDomain) {
|
|
return this.deleteByHost(aBaseDomain, {});
|
|
},
|
|
|
|
deleteAll() {
|
|
return new Promise(aResolve => {
|
|
Services.obs.notifyObservers(null, "reporting:purge-all");
|
|
aResolve();
|
|
});
|
|
},
|
|
};
|
|
|
|
const ContentBlockingCleaner = {
|
|
deleteAll() {
|
|
return lazy.TrackingDBService.clearAll();
|
|
},
|
|
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
deleteByRange(aFrom, aTo) {
|
|
return lazy.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 = {
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
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);
|
|
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",
|
|
});
|
|
}
|
|
});
|
|
},
|
|
};
|
|
|
|
const PreflightCacheCleaner = {
|
|
// TODO: Bug 1727141: We should call the cache to clear by principal, rather
|
|
// than over-clearing for user requests or bailing out for programmatic calls.
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
// TODO: Bug 1727141 (see deleteByPrincipal).
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
await this.deleteAll();
|
|
},
|
|
|
|
async deleteAll() {
|
|
Cc[`@mozilla.org/network/protocol;1?name=http`]
|
|
.getService(Ci.nsIHttpProtocolHandler)
|
|
.clearCORSPreflightCache();
|
|
},
|
|
};
|
|
|
|
const IdentityCredentialStorageCleaner = {
|
|
async deleteAll() {
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"dom.security.credentialmanagement.identity.enabled",
|
|
false
|
|
)
|
|
) {
|
|
lazy.IdentityCredentialStorageService.clear();
|
|
}
|
|
},
|
|
|
|
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"dom.security.credentialmanagement.identity.enabled",
|
|
false
|
|
)
|
|
) {
|
|
lazy.IdentityCredentialStorageService.deleteFromPrincipal(aPrincipal);
|
|
}
|
|
},
|
|
|
|
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
|
|
if (!aIsUserRequest) {
|
|
return;
|
|
}
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"dom.security.credentialmanagement.identity.enabled",
|
|
false
|
|
)
|
|
) {
|
|
lazy.IdentityCredentialStorageService.deleteFromBaseDomain(aBaseDomain);
|
|
}
|
|
},
|
|
|
|
async deleteByRange(aFrom, aTo) {
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"dom.security.credentialmanagement.identity.enabled",
|
|
false
|
|
)
|
|
) {
|
|
lazy.IdentityCredentialStorageService.deleteFromTimeRange(aFrom, aTo);
|
|
}
|
|
},
|
|
|
|
async deleteByHost(aHost, aOriginAttributes) {
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"dom.security.credentialmanagement.identity.enabled",
|
|
false
|
|
)
|
|
) {
|
|
// 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
|
|
);
|
|
lazy.IdentityCredentialStorageService.deleteFromPrincipal(httpPrincipal);
|
|
lazy.IdentityCredentialStorageService.deleteFromPrincipal(httpsPrincipal);
|
|
}
|
|
},
|
|
|
|
async deleteByOriginAttributes(aOriginAttributesString) {
|
|
if (
|
|
Services.prefs.getBoolPref(
|
|
"dom.security.credentialmanagement.identity.enabled",
|
|
false
|
|
)
|
|
) {
|
|
lazy.IdentityCredentialStorageService.deleteFromOriginAttributesPattern(
|
|
aOriginAttributesString
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
// 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_CLIENT_AUTH_REMEMBER_SERVICE,
|
|
cleaners: [ClientAuthRememberCleaner],
|
|
},
|
|
|
|
{
|
|
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_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_HSTS,
|
|
cleaners: [HSTSCleaner],
|
|
},
|
|
|
|
{ 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],
|
|
},
|
|
|
|
{
|
|
flag: Ci.nsIClearDataService.CLEAR_PREFLIGHT_CACHE,
|
|
cleaners: [PreflightCacheCleaner],
|
|
},
|
|
|
|
{
|
|
flag: Ci.nsIClearDataService.CLEAR_CREDENTIAL_MANAGER_STATE,
|
|
cleaners: [IdentityCredentialStorageCleaner],
|
|
},
|
|
];
|
|
|
|
export function ClearDataService() {
|
|
this._initialize();
|
|
}
|
|
|
|
ClearDataService.prototype = Object.freeze({
|
|
classID: Components.ID("{0c06583d-7dd8-4293-b1a5-912205f779aa}"),
|
|
QueryInterface: ChromeUtils.generateQI(["nsIClearDataService"]),
|
|
|
|
_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) {
|
|
console.error("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();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Compute the base domain from a given host. This is a wrapper around
|
|
* Services.eTLD.getBaseDomainFromHost which also supports IP addresses and
|
|
* hosts such as "localhost" which are considered valid base domains for
|
|
* principals and data storage.
|
|
* @param {string} aDomainOrHost - Domain or host to be converted. May already
|
|
* be a valid base domain.
|
|
* @returns {string} Base domain of the given host. Returns aDomainOrHost if
|
|
* already a base domain.
|
|
*/
|
|
_getBaseDomainWithFallback(aDomainOrHost) {
|
|
let result = aDomainOrHost;
|
|
try {
|
|
result = Services.eTLD.getBaseDomainFromHost(aDomainOrHost);
|
|
} catch (e) {
|
|
if (
|
|
e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
|
|
e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
|
|
) {
|
|
// For these 2 expected errors, just take the host as the result.
|
|
// - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6.
|
|
// - NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: not enough domain parts to extract.
|
|
result = aDomainOrHost;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
deleteDataFromBaseDomain(aDomainOrHost, aIsUserRequest, aFlags, aCallback) {
|
|
if (!aDomainOrHost || !aCallback) {
|
|
return Cr.NS_ERROR_INVALID_ARG;
|
|
}
|
|
// We may throw here if aDomainOrHost can't be converted to a base domain.
|
|
let baseDomain;
|
|
|
|
try {
|
|
baseDomain = this._getBaseDomainWithFallback(aDomainOrHost);
|
|
} catch (e) {
|
|
return Cr.NS_ERROR_FAILURE;
|
|
}
|
|
|
|
return this._deleteInternal(aFlags, aCallback, aCleaner =>
|
|
aCleaner.deleteByBaseDomain(baseDomain, aIsUserRequest)
|
|
);
|
|
},
|
|
|
|
deleteDataFromPrincipal(aPrincipal, aIsUserRequest, aFlags, aCallback) {
|
|
if (!aPrincipal || !aCallback) {
|
|
return Cr.NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
return this._deleteInternal(aFlags, aCallback, aCleaner =>
|
|
aCleaner.deleteByPrincipal(aPrincipal, aIsUserRequest)
|
|
);
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
cleanupAfterDeletionAtShutdown(aFlags, aCallback) {
|
|
return this._deleteInternal(aFlags, aCallback, async aCleaner => {
|
|
if (aCleaner.cleanupAfterDeletionAtShutdown) {
|
|
await aCleaner.cleanupAfterDeletionAtShutdown();
|
|
}
|
|
});
|
|
},
|
|
|
|
// 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 => {
|
|
console.error(e);
|
|
resultFlags |= c.flag;
|
|
});
|
|
})
|
|
);
|
|
// Let's collect the failure in resultFlags.
|
|
});
|
|
Promise.all(promises).then(() => {
|
|
aCallback.onDataDeleted(resultFlags);
|
|
});
|
|
return Cr.NS_OK;
|
|
},
|
|
});
|