diff options
Diffstat (limited to 'toolkit/components/extensions/parent/ext-browsingData.js')
-rw-r--r-- | toolkit/components/extensions/parent/ext-browsingData.js | 405 |
1 files changed, 405 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/ext-browsingData.js b/toolkit/components/extensions/parent/ext-browsingData.js new file mode 100644 index 0000000000..d06f7a3a1b --- /dev/null +++ b/toolkit/components/extensions/parent/ext-browsingData.js @@ -0,0 +1,405 @@ +/* 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 }); + }, + }, + }; + } +}; |