/* 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.defineESModuleGetters(this, { // This helper contains the platform-specific bits of browsingData. BrowsingDataDelegate: "resource:///modules/ExtensionBrowsingData.sys.mjs", LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", }); /** * A number of iterations after which to yield time back * to the system. */ const YIELD_PERIOD = 10; /** * Convert a Date object to a PRTime (microseconds). * * @param {Date} date * the Date object to convert. * @returns {integer} microseconds from the epoch. */ const toPRTime = date => { if (typeof date != "number" && date.constructor.name != "Date") { throw new Error("Invalid value passed to toPRTime"); } return date * 1000; }; const makeRange = options => { return options.since == null ? null : [toPRTime(options.since), toPRTime(Date.now())]; }; global.makeRange = makeRange; // General implementation for clearing data using Services.clearData. // Currently Sanitizer.items uses this under the hood. async function clearData(options, flags) { if (options.hostnames) { await Promise.all( options.hostnames.map( host => new Promise(resolve => { // Set aIsUserRequest to true. This means when the ClearDataService // "Cleaner" implementation doesn't support clearing by host // it will delete all data instead. // This is appropriate for cases like |cache|, which doesn't // support clearing by a time range. // In future when we use this for other data types, we have to // evaluate if that behavior is still acceptable. Services.clearData.deleteDataFromHost(host, true, flags, resolve); }) ) ); return; } if (options.since) { const range = makeRange(options); await new Promise(resolve => { Services.clearData.deleteDataInTimeRange(...range, true, flags, resolve); }); return; } // Don't return the promise here and above to prevent leaking the resolved // value. await new Promise(resolve => Services.clearData.deleteData(flags, resolve)); } const clearCache = options => { return clearData(options, Ci.nsIClearDataService.CLEAR_ALL_CACHES); }; const clearCookies = async function (options) { let cookieMgr = Services.cookies; // This code has been borrowed from Sanitizer.jsm. let yieldCounter = 0; if (options.since || options.hostnames || options.cookieStoreId) { // Iterate through the cookies and delete any created after our cutoff. let cookies = cookieMgr.cookies; if ( !options.cookieStoreId || isPrivateCookieStoreId(options.cookieStoreId) ) { // By default nsICookieManager.cookies doesn't contain private cookies. const privateCookies = cookieMgr.getCookiesWithOriginAttributes( JSON.stringify({ privateBrowsingId: 1, }) ); cookies = cookies.concat(privateCookies); } for (const cookie of cookies) { if ( (!options.since || cookie.creationTime >= toPRTime(options.since)) && (!options.hostnames || options.hostnames.includes(cookie.host.replace(/^\./, ""))) && (!options.cookieStoreId || getCookieStoreIdForOriginAttributes(cookie.originAttributes) === options.cookieStoreId) ) { // This cookie was created after our cutoff, clear it. cookieMgr.remove( cookie.host, cookie.name, cookie.path, cookie.originAttributes ); if (++yieldCounter % YIELD_PERIOD == 0) { await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long. } } } } else { // Remove everything. cookieMgr.removeAll(); } }; // Ideally we could reuse the logic in Sanitizer.jsm or nsIClearDataService, // but this API exposes an ability to wipe data at a much finger granularity // than those APIs. (See also Bug 1531276) async function clearQuotaManager(options, dataType) { // Can not clear localStorage/indexedDB in private browsing mode, // just ignore. if (options.cookieStoreId == PRIVATE_STORE) { return; } let promises = []; await new Promise((resolve, reject) => { Services.qms.getUsage(request => { if (request.resultCode != Cr.NS_OK) { reject({ message: `Clear ${dataType} failed` }); return; } for (let item of request.result) { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( item.origin ); // Consistently to removeIndexedDB and the API documentation for // removeLocalStorage, we should only clear the data stored by // regular websites, on the contrary we shouldn't clear data stored // by browser components (like about:newtab) or other extensions. if (!["http", "https", "file"].includes(principal.scheme)) { continue; } let host = principal.hostPort; if ( (!options.hostnames || options.hostnames.includes(host)) && (!options.cookieStoreId || getCookieStoreIdForOriginAttributes(principal.originAttributes) === options.cookieStoreId) ) { promises.push( new Promise((resolve, reject) => { let clearRequest; if (dataType === "indexedDB") { clearRequest = Services.qms.clearStoragesForPrincipal( principal, null, "idb" ); } else { clearRequest = Services.qms.clearStoragesForPrincipal( principal, "default", "ls" ); } clearRequest.callback = () => { if (clearRequest.resultCode == Cr.NS_OK) { resolve(); } else { reject({ message: `Clear ${dataType} failed` }); } }; }) ); } } resolve(); }); }); return Promise.all(promises); } const clearIndexedDB = async function (options) { return clearQuotaManager(options, "indexedDB"); }; const clearLocalStorage = async function (options) { if (options.since) { return Promise.reject({ message: "Firefox does not support clearing localStorage with 'since'.", }); } // The legacy LocalStorage implementation that will eventually be removed // depends on this observer notification. Some other subsystems like // Reporting headers depend on this too. // When NextGenLocalStorage is enabled these notifications are ignored. if (options.hostnames) { for (let hostname of options.hostnames) { Services.obs.notifyObservers( null, "extension:purge-localStorage", hostname ); } } else { Services.obs.notifyObservers(null, "extension:purge-localStorage"); } if (Services.domStorageManager.nextGenLocalStorageEnabled) { return clearQuotaManager(options, "localStorage"); } }; const clearPasswords = async function (options) { let yieldCounter = 0; // Iterate through the logins and delete any updated after our cutoff. for (let login of await LoginHelper.getAllUserFacingLogins()) { login.QueryInterface(Ci.nsILoginMetaInfo); if (!options.since || login.timePasswordChanged >= options.since) { Services.logins.removeLogin(login); if (++yieldCounter % YIELD_PERIOD == 0) { await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long. } } } }; const clearServiceWorkers = options => { if (!options.hostnames) { return ServiceWorkerCleanUp.removeAll(); } return Promise.all( options.hostnames.map(host => { return ServiceWorkerCleanUp.removeFromHost(host); }) ); }; class BrowsingDataImpl { constructor(extension) { this.extension = extension; // Some APIs cannot implement in a platform-independent way and they are // delegated to a platform-specific delegate. this.platformDelegate = new BrowsingDataDelegate(extension); } handleRemoval(dataType, options) { // First, let's see if the platform implements this let result = this.platformDelegate.handleRemoval(dataType, options); if (result !== undefined) { return result; } // ... if not, run the default behavior. switch (dataType) { case "cache": return clearCache(options); case "cookies": return clearCookies(options); case "indexedDB": return clearIndexedDB(options); case "localStorage": return clearLocalStorage(options); case "passwords": return clearPasswords(options); case "pluginData": this.extension?.logger.warn( "pluginData has been deprecated (along with Flash plugin support)" ); return Promise.resolve(); case "serviceWorkers": return clearServiceWorkers(options); default: return undefined; } } doRemoval(options, dataToRemove) { if ( options.originTypes && (options.originTypes.protectedWeb || options.originTypes.extension) ) { return Promise.reject({ message: "Firefox does not support protectedWeb or extension as originTypes.", }); } if (options.cookieStoreId) { const SUPPORTED_TYPES = ["cookies", "indexedDB"]; if (Services.domStorageManager.nextGenLocalStorageEnabled) { // Only the next-gen storage supports removal by cookieStoreId. SUPPORTED_TYPES.push("localStorage"); } for (let dataType in dataToRemove) { if (dataToRemove[dataType] && !SUPPORTED_TYPES.includes(dataType)) { return Promise.reject({ message: `Firefox does not support clearing ${dataType} with 'cookieStoreId'.`, }); } } if ( !isPrivateCookieStoreId(options.cookieStoreId) && !isDefaultCookieStoreId(options.cookieStoreId) && !getContainerForCookieStoreId(options.cookieStoreId) ) { return Promise.reject({ message: `Invalid cookieStoreId: ${options.cookieStoreId}`, }); } } let removalPromises = []; let invalidDataTypes = []; for (let dataType in dataToRemove) { if (dataToRemove[dataType]) { let result = this.handleRemoval(dataType, options); if (result === undefined) { invalidDataTypes.push(dataType); } else { removalPromises.push(result); } } } if (invalidDataTypes.length) { this.extension.logger.warn( `Firefox does not support dataTypes: ${invalidDataTypes.toString()}.` ); } return Promise.all(removalPromises); } settings() { return this.platformDelegate.settings(); } } this.browsingData = class extends ExtensionAPI { getAPI(context) { const impl = new BrowsingDataImpl(context.extension); return { browsingData: { settings() { return impl.settings(); }, remove(options, dataToRemove) { return impl.doRemoval(options, dataToRemove); }, removeCache(options) { return impl.doRemoval(options, { cache: true }); }, removeCookies(options) { return impl.doRemoval(options, { cookies: true }); }, removeDownloads(options) { return impl.doRemoval(options, { downloads: true }); }, removeFormData(options) { return impl.doRemoval(options, { formData: true }); }, removeHistory(options) { return impl.doRemoval(options, { history: true }); }, removeIndexedDB(options) { return impl.doRemoval(options, { indexedDB: true }); }, removeLocalStorage(options) { return impl.doRemoval(options, { localStorage: true }); }, removePasswords(options) { return impl.doRemoval(options, { passwords: true }); }, removePluginData(options) { return impl.doRemoval(options, { pluginData: true }); }, }, }; } };