404 lines
12 KiB
JavaScript
404 lines
12 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.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.sys.mjs.
|
|
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.sys.mjs 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.clearStoragesForClient(
|
|
principal,
|
|
"idb"
|
|
);
|
|
} else {
|
|
clearRequest = Services.qms.clearStoragesForClient(
|
|
principal,
|
|
"ls",
|
|
"default"
|
|
);
|
|
}
|
|
|
|
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 });
|
|
},
|
|
},
|
|
};
|
|
}
|
|
};
|