forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D45685 --HG-- extra : moz-landing-system : lando
385 lines
11 KiB
JavaScript
385 lines
11 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
/* 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";
|
|
|
|
var { PlacesUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/PlacesUtils.jsm"
|
|
);
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"Preferences",
|
|
"resource://gre/modules/Preferences.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"Sanitizer",
|
|
"resource:///modules/Sanitizer.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"Services",
|
|
"resource://gre/modules/Services.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"setTimeout",
|
|
"resource://gre/modules/Timer.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"ServiceWorkerCleanUp",
|
|
"resource://gre/modules/ServiceWorkerCleanUp.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"quotaManagerService",
|
|
"@mozilla.org/dom/quota-manager-service;1",
|
|
"nsIQuotaManagerService"
|
|
);
|
|
|
|
/**
|
|
* A number of iterations after which to yield time back
|
|
* to the system.
|
|
*/
|
|
const YIELD_PERIOD = 10;
|
|
|
|
const makeRange = options => {
|
|
return options.since == null
|
|
? null
|
|
: [PlacesUtils.toPRTime(options.since), PlacesUtils.toPRTime(Date.now())];
|
|
};
|
|
|
|
const clearCache = () => {
|
|
// Clearing the cache does not support timestamps.
|
|
return Sanitizer.items.cache.clear();
|
|
};
|
|
|
|
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) {
|
|
// Iterate through the cookies and delete any created after our cutoff.
|
|
for (const cookie of cookieMgr.enumerator) {
|
|
if (
|
|
(!options.since ||
|
|
cookie.creationTime >= PlacesUtils.toPRTime(options.since)) &&
|
|
(!options.hostnames ||
|
|
options.hostnames.includes(cookie.host.replace(/^\./, "")))
|
|
) {
|
|
// 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();
|
|
}
|
|
};
|
|
|
|
const clearDownloads = options => {
|
|
return Sanitizer.items.downloads.clear(makeRange(options));
|
|
};
|
|
|
|
const clearFormData = options => {
|
|
return Sanitizer.items.formdata.clear(makeRange(options));
|
|
};
|
|
|
|
const clearHistory = options => {
|
|
return Sanitizer.items.history.clear(makeRange(options));
|
|
};
|
|
|
|
const clearIndexedDB = async function(options) {
|
|
let promises = [];
|
|
|
|
await new Promise((resolve, reject) => {
|
|
quotaManagerService.getUsage(request => {
|
|
if (request.resultCode != Cr.NS_OK) {
|
|
reject({ message: "Clear indexedDB failed" });
|
|
return;
|
|
}
|
|
|
|
for (let item of request.result) {
|
|
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
|
item.origin
|
|
);
|
|
if (
|
|
principal.schemeIs("http") ||
|
|
principal.schemeIs("https") ||
|
|
principal.schemeIs("file")
|
|
) {
|
|
promises.push(
|
|
new Promise((resolve, reject) => {
|
|
let clearRequest = quotaManagerService.clearStoragesForPrincipal(
|
|
principal,
|
|
null,
|
|
"idb"
|
|
);
|
|
clearRequest.callback = () => {
|
|
if (clearRequest.resultCode == Cr.NS_OK) {
|
|
resolve();
|
|
} else {
|
|
reject({ message: "Clear indexedDB failed" });
|
|
}
|
|
};
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
};
|
|
|
|
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.
|
|
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.lsm.nextGenLocalStorageEnabled) {
|
|
// 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. So custom logic is used here to wipe only the QM
|
|
// localStorage client (when in use).
|
|
|
|
let promises = [];
|
|
|
|
await new Promise((resolve, reject) => {
|
|
quotaManagerService.getUsage(request => {
|
|
if (request.resultCode != Cr.NS_OK) {
|
|
reject({ message: "Clear localStorage failed" });
|
|
return;
|
|
}
|
|
|
|
for (let item of request.result) {
|
|
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
|
item.origin
|
|
);
|
|
let host = principal.URI.hostPort;
|
|
if (!options.hostnames || options.hostnames.includes(host)) {
|
|
promises.push(
|
|
new Promise((resolve, reject) => {
|
|
let clearRequest = quotaManagerService.clearStoragesForPrincipal(
|
|
principal,
|
|
"default",
|
|
"ls"
|
|
);
|
|
clearRequest.callback = () => {
|
|
if (clearRequest.resultCode == Cr.NS_OK) {
|
|
resolve();
|
|
} else {
|
|
reject({ message: "Clear localStorage failed" });
|
|
}
|
|
};
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
};
|
|
|
|
const clearPasswords = async function(options) {
|
|
let loginManager = Services.logins;
|
|
let yieldCounter = 0;
|
|
|
|
if (options.since) {
|
|
// Iterate through the logins and delete any updated after our cutoff.
|
|
let logins = loginManager.getAllLogins();
|
|
for (let login of logins) {
|
|
login.QueryInterface(Ci.nsILoginMetaInfo);
|
|
if (login.timePasswordChanged >= options.since) {
|
|
loginManager.removeLogin(login);
|
|
if (++yieldCounter % YIELD_PERIOD == 0) {
|
|
await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Remove everything.
|
|
loginManager.removeAllLogins();
|
|
}
|
|
};
|
|
|
|
const clearPluginData = options => {
|
|
return Sanitizer.items.pluginData.clear(makeRange(options));
|
|
};
|
|
|
|
const doRemoval = (options, dataToRemove, extension) => {
|
|
if (
|
|
options.originTypes &&
|
|
(options.originTypes.protectedWeb || options.originTypes.extension)
|
|
) {
|
|
return Promise.reject({
|
|
message:
|
|
"Firefox does not support protectedWeb or extension as originTypes.",
|
|
});
|
|
}
|
|
|
|
let removalPromises = [];
|
|
let invalidDataTypes = [];
|
|
for (let dataType in dataToRemove) {
|
|
if (dataToRemove[dataType]) {
|
|
switch (dataType) {
|
|
case "cache":
|
|
removalPromises.push(clearCache());
|
|
break;
|
|
case "cookies":
|
|
removalPromises.push(clearCookies(options));
|
|
break;
|
|
case "downloads":
|
|
removalPromises.push(clearDownloads(options));
|
|
break;
|
|
case "formData":
|
|
removalPromises.push(clearFormData(options));
|
|
break;
|
|
case "history":
|
|
removalPromises.push(clearHistory(options));
|
|
break;
|
|
case "indexedDB":
|
|
removalPromises.push(clearIndexedDB(options));
|
|
break;
|
|
case "localStorage":
|
|
removalPromises.push(clearLocalStorage(options));
|
|
break;
|
|
case "passwords":
|
|
removalPromises.push(clearPasswords(options));
|
|
break;
|
|
case "pluginData":
|
|
removalPromises.push(clearPluginData(options));
|
|
break;
|
|
case "serviceWorkers":
|
|
removalPromises.push(ServiceWorkerCleanUp.removeAll());
|
|
break;
|
|
default:
|
|
invalidDataTypes.push(dataType);
|
|
}
|
|
}
|
|
}
|
|
if (extension && invalidDataTypes.length) {
|
|
extension.logger.warn(
|
|
`Firefox does not support dataTypes: ${invalidDataTypes.toString()}.`
|
|
);
|
|
}
|
|
return Promise.all(removalPromises);
|
|
};
|
|
|
|
this.browsingData = class extends ExtensionAPI {
|
|
getAPI(context) {
|
|
let { extension } = context;
|
|
return {
|
|
browsingData: {
|
|
settings() {
|
|
const PREF_DOMAIN = "privacy.cpd.";
|
|
// The following prefs are the only ones in Firefox that match corresponding
|
|
// values used by Chrome when rerturning settings.
|
|
const PREF_LIST = [
|
|
"cache",
|
|
"cookies",
|
|
"history",
|
|
"formdata",
|
|
"downloads",
|
|
];
|
|
|
|
// since will be the start of what is returned by Sanitizer.getClearRange
|
|
// divided by 1000 to convert to ms.
|
|
// If Sanitizer.getClearRange returns undefined that means the range is
|
|
// currently "Everything", so we should set since to 0.
|
|
let clearRange = Sanitizer.getClearRange();
|
|
let since = clearRange ? clearRange[0] / 1000 : 0;
|
|
let options = { since };
|
|
|
|
let dataToRemove = {};
|
|
let dataRemovalPermitted = {};
|
|
|
|
for (let item of PREF_LIST) {
|
|
// The property formData needs a different case than the
|
|
// formdata preference.
|
|
const name = item === "formdata" ? "formData" : item;
|
|
dataToRemove[name] = Preferences.get(`${PREF_DOMAIN}${item}`);
|
|
// Firefox doesn't have the same concept of dataRemovalPermitted
|
|
// as Chrome, so it will always be true.
|
|
dataRemovalPermitted[name] = true;
|
|
}
|
|
|
|
return Promise.resolve({
|
|
options,
|
|
dataToRemove,
|
|
dataRemovalPermitted,
|
|
});
|
|
},
|
|
remove(options, dataToRemove) {
|
|
return doRemoval(options, dataToRemove, extension);
|
|
},
|
|
removeCache(options) {
|
|
return doRemoval(options, { cache: true });
|
|
},
|
|
removeCookies(options) {
|
|
return doRemoval(options, { cookies: true });
|
|
},
|
|
removeDownloads(options) {
|
|
return doRemoval(options, { downloads: true });
|
|
},
|
|
removeFormData(options) {
|
|
return doRemoval(options, { formData: true });
|
|
},
|
|
removeHistory(options) {
|
|
return doRemoval(options, { history: true });
|
|
},
|
|
removeIndexedDB(options) {
|
|
return doRemoval(options, { indexedDB: true });
|
|
},
|
|
removeLocalStorage(options) {
|
|
return doRemoval(options, { localStorage: true });
|
|
},
|
|
removePasswords(options) {
|
|
return doRemoval(options, { passwords: true });
|
|
},
|
|
removePluginData(options) {
|
|
return doRemoval(options, { pluginData: true });
|
|
},
|
|
},
|
|
};
|
|
}
|
|
};
|