summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/parent/ext-browsingData.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/parent/ext-browsingData.js')
-rw-r--r--toolkit/components/extensions/parent/ext-browsingData.js405
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 });
+ },
+ },
+ };
+ }
+};