fune/toolkit/components/cleardata/ClearDataService.js

794 lines
24 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/. */
"use strict";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
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, "eTLDService",
"@mozilla.org/network/effective-tld-service;1",
"nsIEffectiveTLDService");
// A Cleaner is an object with 3 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.
const CookieCleaner = {
deleteByHost(aHost, aOriginAttributes) {
return new Promise(aResolve => {
Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify(aOriginAttributes),
aHost);
aResolve();
});
},
deleteByRange(aFrom, aTo) {
let enumerator = Services.cookies.enumerator;
return this._deleteInternal(enumerator, aCookie => aCookie.creationTime > aFrom);
},
deleteAll() {
return new Promise(aResolve => {
Services.cookies.removeAll();
aResolve();
});
},
_deleteInternal(aEnumerator, aCb) {
// A number of iterations after which to yield time back to the system.
const YIELD_PERIOD = 10;
return new Promise((aResolve, aReject) => {
let count = 0;
while (aEnumerator.hasMoreElements()) {
let cookie = aEnumerator.getNext().QueryInterface(Ci.nsICookie2);
if (aCb(cookie)) {
Services.cookies.remove(cookie.host, cookie.name, cookie.path,
false, cookie.originAttributes);
// We don't want to block the main-thread.
if (++count % YIELD_PERIOD == 0) {
setTimeout(() => {
this._deleteInternal(aEnumerator, aCb).then(aResolve, aReject);
}, 0);
return;
}
}
}
aResolve();
});
},
};
const NetworkCacheCleaner = {
deleteByPrincipal(aPrincipal) {
return new Promise(aResolve => {
Services.cache2.asyncClearOrigin(aPrincipal);
aResolve();
});
},
deleteAll() {
return new Promise(aResolve => {
Services.cache2.clear();
aResolve();
});
},
};
const ImageCacheCleaner = {
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) {
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 => eTLDService.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 => eTLDService.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")) {
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 = {
deleteAll() {
// AppCache: this doesn't wait for the cleanup to be complete.
OfflineAppCacheHelper.clear();
return Promise.resolve();
},
};
const QuotaCleaner = {
deleteByPrincipal(aPrincipal) {
// localStorage
Services.obs.notifyObservers(null, "browser:purge-domain-data",
aPrincipal.URI.host);
// ServiceWorkers: they must be removed before cleaning QuotaManager.
return ServiceWorkerCleanUp.removeFromPrincipal(aPrincipal)
.then(_ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true)
.then(exceptionThrown => {
// QuotaManager
return new Promise((aResolve, aReject) => {
let req = Services.qms.clearStoragesForPrincipal(aPrincipal, null, false);
req.callback = () => {
if (exceptionThrown) {
aReject();
} else {
aResolve();
}
};
});
});
},
deleteByHost(aHost, aOriginAttributes) {
// localStorage
Services.obs.notifyObservers(null, "browser:purge-domain-data", aHost);
let exceptionThrown = false;
// ServiceWorkers: they must be removed before cleaning QuotaManager.
return Promise.all([
ServiceWorkerCleanUp.removeFromHost("http://" + aHost).catch(_ => { exceptionThrown = true; }),
ServiceWorkerCleanUp.removeFromHost("https://" + aHost).catch(_ => { exceptionThrown = true; }),
]).then(() => {
// QuotaManager
// 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
.createCodebasePrincipal(httpURI, aOriginAttributes);
let httpsPrincipal = Services.scriptSecurityManager
.createCodebasePrincipal(httpsURI, aOriginAttributes);
return Promise.all([
new Promise(aResolve => {
let req = Services.qms.clearStoragesForPrincipal(httpPrincipal, null, true);
req.callback = () => { aResolve(); };
}),
new Promise(aResolve => {
let req = Services.qms.clearStoragesForPrincipal(httpsPrincipal, null, true);
req.callback = () => { aResolve(); };
}),
]).then(() => {
return exceptionThrown ? Promise.reject() : Promise.resolve();
});
});
},
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.URI.scheme != "http" &&
principal.URI.scheme != "https" &&
principal.URI.scheme != "file") {
continue;
}
promises.push(this.deleteByPrincipal(principal));
}
return Promise.all(promises);
},
deleteAll() {
// localStorage
Services.obs.notifyObservers(null, "extension:purge-localStorage");
// ServiceWorkers
return ServiceWorkerCleanUp.removeAll()
.then(_ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true)
.then(exceptionThrown => {
// QuotaManager
return new Promise((aResolve, aReject) => {
Services.qms.getUsage(aRequest => {
if (aRequest.resultCode != Cr.NS_OK) {
// We are probably shutting down.
if (exceptionThrown) {
aReject();
} else {
aResolve();
}
return;
}
let promises = [];
for (let item of aRequest.result) {
let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
if (principal.URI.scheme == "http" ||
principal.URI.scheme == "https" ||
principal.URI.scheme == "file") {
promises.push(new Promise(aResolve => {
let req = Services.qms.clearStoragesForPrincipal(principal, null, false);
req.callback = () => { aResolve(); };
}));
}
}
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 HistoryCleaner = {
deleteByHost(aHost, aOriginAttributes) {
return PlacesUtils.history.removeByFilter({ host: "." + aHost });
},
deleteByRange(aFrom, aTo) {
return PlacesUtils.history.removeVisitsByFilter({
beginDate: new Date(aFrom / 1000),
endDate: new Date(aTo / 1000)
});
},
deleteAll() {
return PlacesUtils.history.clear();
},
};
const SessionHistoryCleaner = {
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 => {
let enumerator = Services.perms.enumerator;
while (enumerator.hasMoreElements()) {
let perm = enumerator.getNext().QueryInterface(Ci.nsIPermission);
try {
if (eTLDService.hasRootDomain(perm.principal.URI.host, aHost)) {
Services.perms.removePermission(perm);
}
} catch (ex) {
// Ignore entry
}
}
aResolve();
});
},
deleteByRange(aFrom, aTo) {
Services.perms.removeAllSince(aFrom / 1000);
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 => {
// Notify other consumers, including extensions
Services.obs.notifyObservers(null, "browser:purge-domain-data",
aHost);
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);
for (let type of [Ci.nsISiteSecurityService.HEADER_HSTS,
Ci.nsISiteSecurityService.HEADER_HPKP]) {
// Also remove HSTS/HPKP/OMS information for subdomains by enumerating
// the information in the site security service.
let enumerator = sss.enumerate(type);
while (enumerator.hasMoreElements()) {
let entry = enumerator.getNext();
let hostname = entry.QueryInterface(Ci.nsISiteSecurityState).hostname;
if (eTLDService.hasRootDomain(hostname, aHost)) {
// This uri is used as a key to remove the state.
let uri = Services.io.newURI("https://" + hostname);
sss.removeState(type, uri, 0, entry.originAttributes);
}
}
}
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();
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();
},
};
// Here the map of Flags-Cleaner.
const FLAGS_MAP = [
{ flag: Ci.nsIClearDataService.CLEAR_COOKIES,
cleaner: CookieCleaner },
{ flag: Ci.nsIClearDataService.CLEAR_NETWORK_CACHE,
cleaner: NetworkCacheCleaner },
{ flag: Ci.nsIClearDataService.CLEAR_IMAGE_CACHE,
cleaner: ImageCacheCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_PLUGIN_DATA,
cleaner: PluginDataCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_DOWNLOADS,
cleaner: DownloadsCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_PASSWORDS,
cleaner: PasswordsCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES,
cleaner: MediaDevicesCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_APPCACHE,
cleaner: AppCacheCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_DOM_QUOTA,
cleaner: QuotaCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_PREDICTOR_NETWORK_DATA,
cleaner: PredictorNetworkCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_DOM_PUSH_NOTIFICATIONS,
cleaner: PushNotificationsCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_HISTORY,
cleaner: HistoryCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_SESSION_HISTORY,
cleaner: SessionHistoryCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_AUTH_TOKENS,
cleaner: AuthTokensCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
cleaner: AuthCacheCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_PERMISSIONS,
cleaner: PermissionsCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES,
cleaner: PreferencesCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS,
cleaner: SecuritySettingsCleaner, },
{ flag: Ci.nsIClearDataService.CLEAR_EME,
cleaner: EMECleaner, },
];
this.ClearDataService = function() {};
ClearDataService.prototype = Object.freeze({
classID: Components.ID("{0c06583d-7dd8-4293-b1a5-912205f779aa}"),
QueryInterface: ChromeUtils.generateQI([Ci.nsIClearDataService]),
_xpcom_factory: XPCOMUtils.generateSingletonFactory(ClearDataService),
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.URI.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();
});
},
// 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 => {
// Let's collect the failure in resultFlags.
return aHelper(c.cleaner).catch(() => { resultFlags |= c.flag; });
});
Promise.all(promises).then(() => { aCallback.onDataDeleted(resultFlags); });
return Cr.NS_OK;
},
});
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ClearDataService]);