Source code

Revision control

Copy as Markdown

Other Tools

ChromeUtils.defineESModuleGetters(this, {
});
const kMsecPerMin = 60 * 1000;
const kUsecPerMin = kMsecPerMin * 1000;
let today = Date.now() - new Date().setHours(0, 0, 0, 0);
let nowMSec = Date.now();
let nowUSec = nowMSec * 1000;
const TEST_TARGET_FILE_NAME = "test-download.txt";
const TEST_QUOTA_USAGE_HOST = "example.com";
const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
const TEST_QUOTA_USAGE_URL =
getRootDirectory(gTestPath).replace(
TEST_QUOTA_USAGE_ORIGIN
) + "site_data_test.html";
const SITE_ORIGINS = [
];
let fileURL;
function createIndexedDB(host, originAttributes) {
let uri = Services.io.newURI("https://" + host);
let principal = Services.scriptSecurityManager.createContentPrincipal(
uri,
originAttributes
);
return SiteDataTestUtils.addToIndexedDB(principal.origin);
}
function checkIndexedDB(host, originAttributes) {
return new Promise(resolve => {
let data = true;
let uri = Services.io.newURI("https://" + host);
let principal = Services.scriptSecurityManager.createContentPrincipal(
uri,
originAttributes
);
let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
request.onupgradeneeded = function () {
data = false;
};
request.onsuccess = function () {
resolve(data);
};
});
}
function createHostCookie(host, originAttributes) {
Services.cookies.add(
host,
"/test",
"foo",
"bar",
false,
false,
false,
Date.now() + 24000 * 60 * 60,
originAttributes,
Ci.nsICookie.SAMESITE_NONE,
Ci.nsICookie.SCHEME_HTTPS
);
}
function createDomainCookie(host, originAttributes) {
Services.cookies.add(
"." + host,
"/test",
"foo",
"bar",
false,
false,
false,
Date.now() + 24000 * 60 * 60,
originAttributes,
Ci.nsICookie.SAMESITE_NONE,
Ci.nsICookie.SCHEME_HTTPS
);
}
function checkCookie(host, originAttributes) {
for (let cookie of Services.cookies.cookies) {
if (
ChromeUtils.isOriginAttributesEqual(
originAttributes,
cookie.originAttributes
) &&
cookie.host.includes(host)
) {
return true;
}
}
return false;
}
async function deleteOnShutdown(opt) {
// Let's clean up all the data.
await new Promise(resolve => {
Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
});
await SpecialPowers.pushPrefEnv({
set: [
["privacy.sanitize.sanitizeOnShutdown", opt.sanitize],
["privacy.clearOnShutdown.cookies", opt.sanitize],
["privacy.clearOnShutdown.offlineApps", opt.sanitize],
["browser.sanitizer.loglevel", "All"],
],
});
// Custom permission without considering OriginAttributes
if (opt.cookiePermission !== undefined) {
let uri = Services.io.newURI("https://www.example.com");
PermissionTestUtils.add(uri, "cookie", opt.cookiePermission);
}
// Let's create a tab with some data.
await opt.createData(
(opt.fullHost ? "www." : "") + "example.org",
opt.originAttributes
);
ok(
await opt.checkData(
(opt.fullHost ? "www." : "") + "example.org",
opt.originAttributes
),
"We have data for www.example.org"
);
await opt.createData(
(opt.fullHost ? "www." : "") + "example.com",
opt.originAttributes
);
ok(
await opt.checkData(
(opt.fullHost ? "www." : "") + "example.com",
opt.originAttributes
),
"We have data for www.example.com"
);
// Cleaning up.
await Sanitizer.runSanitizeOnShutdown();
// All gone!
is(
!!(await opt.checkData(
(opt.fullHost ? "www." : "") + "example.org",
opt.originAttributes
)),
opt.expectedForOrg,
"Do we have data for www.example.org?"
);
is(
!!(await opt.checkData(
(opt.fullHost ? "www." : "") + "example.com",
opt.originAttributes
)),
opt.expectedForCom,
"Do we have data for www.example.com?"
);
// Clean up.
await Sanitizer.sanitize(["cookies", "offlineApps"]);
if (opt.cookiePermission !== undefined) {
let uri = Services.io.newURI("https://www.example.com");
PermissionTestUtils.remove(uri, "cookie");
}
}
function runAllCookiePermissionTests(originAttributes) {
let tests = [
{ name: "IDB", createData: createIndexedDB, checkData: checkIndexedDB },
{
name: "Host Cookie",
createData: createHostCookie,
checkData: checkCookie,
},
{
name: "Domain Cookie",
createData: createDomainCookie,
checkData: checkCookie,
},
];
// Delete all, no custom permission, data in example.com, cookie permission set
// for www.example.com
tests.forEach(methods => {
add_task(async function deleteStorageOnShutdown() {
info(
methods.name +
": Delete all, no custom permission, data in example.com, cookie permission set for www.example.com - OA: " +
originAttributes.name
);
await deleteOnShutdown({
sanitize: true,
createData: methods.createData,
checkData: methods.checkData,
originAttributes: originAttributes.oa,
cookiePermission: undefined,
expectedForOrg: false,
expectedForCom: false,
fullHost: false,
});
});
});
// Delete all, no custom permission, data in www.example.com, cookie permission
// set for www.example.com
tests.forEach(methods => {
add_task(async function deleteStorageOnShutdown() {
info(
methods.name +
": Delete all, no custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
originAttributes.name
);
await deleteOnShutdown({
sanitize: true,
createData: methods.createData,
checkData: methods.checkData,
originAttributes: originAttributes.oa,
cookiePermission: undefined,
expectedForOrg: false,
expectedForCom: false,
fullHost: true,
});
});
});
// All is session, but with ALLOW custom permission, data in example.com,
// cookie permission set for www.example.com
tests.forEach(methods => {
add_task(async function deleteStorageWithCustomPermission() {
info(
methods.name +
": All is session, but with ALLOW custom permission, data in example.com, cookie permission set for www.example.com - OA: " +
originAttributes.name
);
await deleteOnShutdown({
sanitize: true,
createData: methods.createData,
checkData: methods.checkData,
originAttributes: originAttributes.oa,
cookiePermission: Ci.nsICookiePermission.ACCESS_ALLOW,
expectedForOrg: false,
expectedForCom: true,
fullHost: false,
});
});
});
// All is session, but with ALLOW custom permission, data in www.example.com,
// cookie permission set for www.example.com
tests.forEach(methods => {
add_task(async function deleteStorageWithCustomPermission() {
info(
methods.name +
": All is session, but with ALLOW custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
originAttributes.name
);
await deleteOnShutdown({
sanitize: true,
createData: methods.createData,
checkData: methods.checkData,
originAttributes: originAttributes.oa,
cookiePermission: Ci.nsICookiePermission.ACCESS_ALLOW,
expectedForOrg: false,
expectedForCom: true,
fullHost: true,
});
});
});
// All is default, but with SESSION custom permission, data in example.com,
// cookie permission set for www.example.com
tests.forEach(methods => {
add_task(async function deleteStorageOnlyCustomPermission() {
info(
methods.name +
": All is default, but with SESSION custom permission, data in example.com, cookie permission set for www.example.com - OA: " +
originAttributes.name
);
await deleteOnShutdown({
sanitize: false,
createData: methods.createData,
checkData: methods.checkData,
originAttributes: originAttributes.oa,
cookiePermission: Ci.nsICookiePermission.ACCESS_SESSION,
expectedForOrg: true,
// expected data just for example.com when using indexedDB because
// QuotaManager deletes for principal.
expectedForCom: false,
fullHost: false,
});
});
});
// All is default, but with SESSION custom permission, data in www.example.com,
// cookie permission set for www.example.com
tests.forEach(methods => {
add_task(async function deleteStorageOnlyCustomPermission() {
info(
methods.name +
": All is default, but with SESSION custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
originAttributes.name
);
await deleteOnShutdown({
sanitize: false,
createData: methods.createData,
checkData: methods.checkData,
originAttributes: originAttributes.oa,
cookiePermission: Ci.nsICookiePermission.ACCESS_SESSION,
expectedForOrg: true,
expectedForCom: false,
fullHost: true,
});
});
});
// Session mode, but with unsupported custom permission, data in
// www.example.com, cookie permission set for www.example.com
tests.forEach(methods => {
add_task(async function deleteStorageOnlyCustomPermission() {
info(
methods.name +
": All is session only, but with unsupported custom custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
originAttributes.name
);
await deleteOnShutdown({
sanitize: true,
createData: methods.createData,
checkData: methods.checkData,
originAttributes: originAttributes.oa,
cookiePermission: 123, // invalid cookie permission
expectedForOrg: false,
expectedForCom: false,
fullHost: true,
});
});
});
}
function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) {
return new Promise(resolve => {
let finalPrefPaneLoaded = TestUtils.topicObserved(
"sync-pane-loaded",
() => true
);
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
openPreferences(aPane);
let newTabBrowser = gBrowser.selectedBrowser;
newTabBrowser.addEventListener(
"Initialized",
function () {
newTabBrowser.contentWindow.addEventListener(
"load",
async function () {
let win = gBrowser.contentWindow;
let selectedPane = win.history.state;
await finalPrefPaneLoaded;
if (!aOptions || !aOptions.leaveOpen) {
gBrowser.removeCurrentTab();
}
resolve({ selectedPane });
},
{ once: true }
);
},
{ capture: true, once: true }
);
});
}
async function createDummyDataForHost(host) {
let origin = "https://" + host;
let dummySWURL =
getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
"dummy.js";
await SiteDataTestUtils.addToIndexedDB(origin);
await SiteDataTestUtils.addServiceWorker(dummySWURL);
}
/**
* Helper function to create file URL to open
*
* @returns {Object} a file URL
*/
function createFileURL() {
if (!fileURL) {
let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
file.append("foo.txt");
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
fileURL = Services.io.newFileURI(file);
}
return fileURL;
}
/**
* Removes all history visits, downloads, and form entries.
*/
async function blankSlate() {
let publicList = await Downloads.getList(Downloads.PUBLIC);
let downloads = await publicList.getAll();
for (let download of downloads) {
await publicList.remove(download);
await download.finalize(true);
}
await FormHistory.update({ op: "remove" });
await PlacesUtils.history.clear();
}
/**
* Adds multiple downloads to the PUBLIC download list
*/
async function addToDownloadList() {
const url = createFileURL();
const downloadsList = await Downloads.getList(Downloads.PUBLIC);
let timeOptions = [1, 2, 4, 24, 128, 128];
let buffer = 100000;
for (let i = 0; i < timeOptions.length; i++) {
let timeDownloaded = 60 * kMsecPerMin * timeOptions[i];
if (timeOptions[i] === 24) {
timeDownloaded = today;
}
let download = await Downloads.createDownload({
source: { url: url.spec, isPrivate: false },
target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path },
startTime: {
getTime: _ => {
return nowMSec - timeDownloaded + buffer;
},
},
});
Assert.ok(!!download);
downloadsList.add(download);
}
let items = await downloadsList.getAll();
Assert.equal(items.length, 6, "Items were added to the list");
}
async function addToSiteUsage() {
// Fill indexedDB with test data.
// Don't wait for the page to load, to register the content event handler as quickly as possible.
// If this test goes intermittent, we might have to tell the page to wait longer before
// firing the event.
BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false);
await BrowserTestUtils.waitForContentEvent(
gBrowser.selectedBrowser,
"test-indexedDB-done",
false,
null,
true
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
let siteLastAccessed = [1, 2, 4, 24];
let staticUsage = 4096 * 6;
// Add a time buffer so the site access falls within the time range
const buffer = 10000;
// Change lastAccessed of sites
for (let index = 0; index < siteLastAccessed.length; index++) {
let lastAccessedTime = 60 * kMsecPerMin * siteLastAccessed[index];
if (siteLastAccessed[index] === 24) {
lastAccessedTime = today;
}
let site = SiteDataManager._testInsertSite(SITE_ORIGINS[index], {
quotaUsage: staticUsage,
lastAccessed: (nowMSec - lastAccessedTime + buffer) * 1000,
});
Assert.ok(site, "Site added successfully");
}
}
function promiseSanitizationComplete() {
return TestUtils.topicObserved("sanitizer-sanitization-complete");
}
/**
* This wraps the dialog and provides some convenience methods for interacting
* with it.
*
* @param {Window} browserWin (optional)
* The browser window that the dialog is expected to open in. If not
* supplied, the initial browser window of the test run is used.
* @param {Object} {mode, checkingDataSizes}
* mode: context to open the dialog in
* One of
* clear on shutdown settings context ("clearOnShutdown"),
* clear site data settings context ("clearSiteData"),
* clear history context ("clearHistory"),
* browser context ("browser")
* "browser" by default
* checkingDataSizes: boolean check if we should wait for the data sizes
* to load
*
*/
function ClearHistoryDialogHelper({
mode = "browser",
checkingDataSizes = false,
} = {}) {
this._browserWin = window;
this.win = null;
this._mode = mode;
this._checkingDataSizes = checkingDataSizes;
this.promiseClosed = new Promise(resolve => {
this._resolveClosed = resolve;
});
}
ClearHistoryDialogHelper.prototype = {
/**
* "Presses" the dialog's OK button.
*/
acceptDialog() {
let dialogEl = this.win.document.querySelector("dialog");
is(
dialogEl.getButton("accept").disabled,
false,
"Dialog's OK button should not be disabled"
);
dialogEl.acceptDialog();
},
/**
* "Presses" the dialog's Cancel button.
*/
cancelDialog() {
this.win.document.querySelector("dialog").cancelDialog();
},
/**
* (Un)checks a history scope checkbox (browser & download history,
* form history, etc.).
*
* @param aPrefName
* The final portion of the checkbox's privacy.cpd.* preference name
* @param aCheckState
* True if the checkbox should be checked, false otherwise
*/
checkPrefCheckbox(aPrefName, aCheckState) {
var cb = this.win.document.querySelectorAll(
"checkbox[id='" + aPrefName + "']"
);
is(cb.length, 1, "found checkbox for " + aPrefName + " id");
if (cb[0].checked != aCheckState) {
cb[0].click();
}
},
/**
* @param {String} aCheckboxId
* The checkbox id name
* @param {Boolean} aCheckState
* True if the checkbox should be checked, false otherwise
*/
validateCheckbox(aCheckboxId, aCheckState) {
let cb = this.win.document.querySelectorAll(
"checkbox[id='" + aCheckboxId + "']"
);
is(cb.length, 1, `found checkbox for id=${aCheckboxId}`);
is(
cb[0].checked,
aCheckState,
`checkbox for ${aCheckboxId} is ${aCheckState}`
);
},
/**
* Makes sure all the checkboxes are checked.
*/
_checkAllCheckboxesCustom(check) {
var cb = this.win.document.querySelectorAll(".clearingItemCheckbox");
ok(cb.length, "found checkboxes for ids");
for (var i = 0; i < cb.length; ++i) {
if (cb[i].checked != check) {
cb[i].click();
}
}
},
checkAllCheckboxes() {
this._checkAllCheckboxesCustom(true);
},
uncheckAllCheckboxes() {
this._checkAllCheckboxesCustom(false);
},
/**
* @return The dialog's duration dropdown
*/
getDurationDropdown() {
return this.win.document.getElementById("sanitizeDurationChoice");
},
/**
* @return The clear-everything warning box
*/
getWarningPanel() {
return this.win.document.getElementById("sanitizeEverythingWarningBox");
},
/**
* @return True if the "Everything" warning panel is visible (as opposed to
* the tree)
*/
isWarningPanelVisible() {
return !this.getWarningPanel().hidden;
},
/**
* Opens the clear recent history dialog. Before calling this, set
* this.onload to a function to execute onload. It should close the dialog
* when done so that the tests may continue. Set this.onunload to a function
* to execute onunload. this.onunload is optional. If it returns true, the
* caller is expected to call promiseAsyncUpdates at some point; if false is
* returned, promiseAsyncUpdates is called automatically.
*/
async open() {
let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
null,
{
isSubDialog: true,
}
);
// We want to simulate opening the dialog inside preferences for clear history
// and clear site data
if (this._mode != "browser") {
await openPreferencesViaOpenPreferencesAPI("privacy", {
leaveOpen: true,
});
let tabWindow = gBrowser.selectedBrowser.contentWindow;
let clearDialogOpenButtonId = this._mode + "Button";
// the id for clear on shutdown is of a different format
if (this._mode == "clearOnShutdown") {
// set always clear to true to enable the clear on shutdown dialog
let enableSettingsCheckbox =
tabWindow.document.getElementById("alwaysClear");
if (!enableSettingsCheckbox.checked) {
enableSettingsCheckbox.click();
}
clearDialogOpenButtonId = "clearDataSettings";
}
// open dialog
tabWindow.document.getElementById(clearDialogOpenButtonId).click();
}
// We open the dialog in the chrome context in other cases
else {
executeSoon(() => {
Sanitizer.showUI(this._browserWin, this._mode);
});
}
this.win = await dialogPromise;
this.win.addEventListener(
"load",
() => {
// Run onload on next tick so that gSanitizePromptDialog.init can run first.
executeSoon(async () => {
if (this._checkingDataSizes) {
// we wait for the data sizes to load to avoid async errors when validating sizes
await this.win.gSanitizePromptDialog
.dataSizesFinishedUpdatingPromise;
}
this.onload();
});
},
{ once: true }
);
this.win.addEventListener(
"unload",
() => {
// Some exceptions that reach here don't reach the test harness, but
// ok()/is() do...
(async () => {
if (this.onunload) {
await this.onunload();
}
if (this._mode != "browser") {
BrowserTestUtils.removeTab(gBrowser.selectedTab);
}
await PlacesTestUtils.promiseAsyncUpdates();
this._resolveClosed();
this.win = null;
})();
},
{ once: true }
);
},
/**
* Selects a duration in the duration dropdown.
*
* @param aDurVal
* One of the Sanitizer.TIMESPAN_* values
*/
selectDuration(aDurVal) {
this.getDurationDropdown().value = aDurVal;
if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) {
is(
this.isWarningPanelVisible(),
true,
"Warning panel should be visible for TIMESPAN_EVERYTHING"
);
} else {
is(
this.isWarningPanelVisible(),
false,
"Warning panel should not be visible for non-TIMESPAN_EVERYTHING"
);
}
},
};