--- a/browser/components/preferences/SiteDataManager.jsm
+++ b/browser/components/preferences/SiteDataManager.jsm
@@ -75,16 +75,17 @@ this.SiteDataManager = {
_getOrInsertSite(host) {
let site = this._sites.get(host);
if (!site) {
site = {
baseDomain: this._getBaseDomainFromHost(host),
cookies: [],
persisted: false,
quotaUsage: 0,
+ lastAccessed: 0,
principals: [],
appCacheList: [],
};
this._sites.set(host, site);
}
return site;
},
@@ -148,16 +149,19 @@ this.SiteDataManager = {
// - Site A (not persisted): https://www.foo.com
// - Site B (not persisted): https://www.foo.com^userContextId=2
// - Site C (persisted): https://www.foo.com:1234
// Although only C is persisted, grouping by host, as a result,
// we still mark as persisted here under this host group.
if (item.persisted) {
site.persisted = true;
}
+ if (site.lastAccessed < item.lastAccessed) {
+ site.lastAccessed = item.lastAccessed;
+ }
site.principals.push(principal);
site.quotaUsage += item.usage;
}
}
}
resolve();
};
// XXX: The work of integrating localStorage into Quota Manager is in progress.
@@ -169,16 +173,19 @@ this.SiteDataManager = {
},
_getAllCookies() {
let cookiesEnum = Services.cookies.enumerator;
while (cookiesEnum.hasMoreElements()) {
let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
let site = this._getOrInsertSite(cookie.rawHost);
site.cookies.push(cookie);
+ if (site.lastAccessed < cookie.lastAccessed) {
+ site.lastAccessed = cookie.lastAccessed;
+ }
}
},
_cancelGetQuotaUsage() {
if (this._quotaUsageRequest) {
this._quotaUsageRequest.cancel();
this._quotaUsageRequest = null;
}
@@ -223,17 +230,18 @@ this.SiteDataManager = {
for (let cache of site.appCacheList) {
usage += cache.usage;
}
list.push({
baseDomain: site.baseDomain,
cookies: site.cookies,
host,
usage,
- persisted: site.persisted
+ persisted: site.persisted,
+ lastAccessed: new Date(site.lastAccessed / 1000),
});
}
return list;
});
},
_removePermission(site) {
let removals = new Set();
--- a/browser/components/preferences/in-content/tests/browser_siteData.js
+++ b/browser/components/preferences/in-content/tests/browser_siteData.js
@@ -59,16 +59,18 @@ add_task(async function() {
// Always remember to clean up
OfflineAppCacheHelper.clear();
await new Promise(resolve => {
let principal = Services.scriptSecurityManager
.createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
request.callback = resolve;
});
+
+ await SiteDataManager.removeAll();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
}).skip(); // Bug 1414751
// Test buttons are disabled and loading message shown while updating sites
add_task(async function() {
await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
let updatedPromise = promiseSiteDataManagerSitesUpdated();
await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
@@ -139,87 +141,105 @@ add_task(async function() {
saveBtn.doCommand();
} else {
ok(false, `Should have one site of ${host}`);
}
});
await acceptRemovePromise;
await updatePromise;
await promiseServiceWorkersCleared();
+ await SiteDataManager.removeAll();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// Test showing and removing sites with cookies.
add_task(async function() {
- SiteDataManager.removeAll();
-
// Add some test cookies.
let uri = Services.io.newURI("https://example.com");
let uri2 = Services.io.newURI("https://example.org");
Services.cookies.add(uri.host, uri.pathQueryRef, "test1", "1",
false, false, false, Date.now() + 1000 * 60 * 60);
Services.cookies.add(uri.host, uri.pathQueryRef, "test2", "2",
false, false, false, Date.now() + 1000 * 60 * 60);
Services.cookies.add(uri2.host, uri2.pathQueryRef, "test1", "1",
false, false, false, Date.now() + 1000 * 60 * 60);
// Ensure that private browsing cookies are ignored.
Services.cookies.add(uri.host, uri.pathQueryRef, "test3", "3",
false, false, false, Date.now() + 1000 * 60 * 60, { privateBrowsingId: 1 });
+ // Get the exact creation date from the cookies (to avoid intermittents
+ // from minimal time differences, since we round up to minutes).
+ let cookiesEnum1 = Services.cookies.getCookiesFromHost(uri.host);
+ // We made two valid cookies for example.com.
+ cookiesEnum1.getNext();
+ let cookiesEnum2 = Services.cookies.getCookiesFromHost(uri2.host);
+ let cookie1 = cookiesEnum1.getNext().QueryInterface(Ci.nsICookie2);
+ let cookie2 = cookiesEnum2.getNext().QueryInterface(Ci.nsICookie2);
+
+ let formatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short", timeStyle: "short",
+ });
+
+ let creationDate1 = formatter.format(new Date(cookie1.lastAccessed / 1000));
+ let creationDate2 = formatter.format(new Date(cookie2.lastAccessed / 1000));
+
await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
// Open the site data manager and remove one site.
await openSiteDataSettingsDialog();
let removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
- ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+ await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1, creationDate2}, function(args) {
let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
let siteItems = frameDoc.getElementsByTagName("richlistitem");
is(siteItems.length, 2, "Should list two sites with cookies");
let sitesList = frameDoc.getElementById("sitesList");
let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
let columns = site1.querySelectorAll(".item-box > label");
is(columns[0].value, "example.com", "Should show the correct host.");
is(columns[2].value, "2", "Should show the correct number of cookies.");
is(columns[3].value, "", "Should show no site data.");
+ is(columns[4].value, args.creationDate1, "Should show the correct date.");
columns = site2.querySelectorAll(".item-box > label");
is(columns[0].value, "example.org", "Should show the correct host.");
is(columns[2].value, "1", "Should show the correct number of cookies.");
is(columns[3].value, "", "Should show no site data.");
+ is(columns[4].value, args.creationDate2, "Should show the correct date.");
let removeBtn = frameDoc.getElementById("removeSelected");
let saveBtn = frameDoc.getElementById("save");
site2.click();
removeBtn.doCommand();
saveBtn.doCommand();
});
await removeDialogOpenPromise;
await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri2.host) == 0, "Cookies from the first host should be cleared");
is(Services.cookies.countCookiesFromHost(uri.host), 2, "Cookies from the second host should not be cleared");
// Open the site data manager and remove another site.
await openSiteDataSettingsDialog();
let acceptRemovePromise = promiseAlertDialogOpen("accept");
- ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+ await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1}, function(args) {
let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
let siteItems = frameDoc.getElementsByTagName("richlistitem");
is(siteItems.length, 1, "Should list one site with cookies");
let sitesList = frameDoc.getElementById("sitesList");
let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
let columns = site1.querySelectorAll(".item-box > label");
is(columns[0].value, "example.com", "Should show the correct host.");
is(columns[2].value, "2", "Should show the correct number of cookies.");
is(columns[3].value, "", "Should show no site data.");
+ is(columns[4].value, args.creationDate1, "Should show the correct date.");
let removeBtn = frameDoc.getElementById("removeSelected");
let saveBtn = frameDoc.getElementById("save");
site1.click();
removeBtn.doCommand();
saveBtn.doCommand();
});
await acceptRemovePromise;
--- a/browser/components/preferences/in-content/tests/browser_siteData2.js
+++ b/browser/components/preferences/in-content/tests/browser_siteData2.js
@@ -107,17 +107,17 @@ add_task(async function() {
assertAllSitesNotListed(win);
saveBtn.doCommand();
await acceptPromise;
await settingsDialogClosePromise;
await updatePromise;
await openSiteDataSettingsDialog();
assertAllSitesNotListed(win);
- mockSiteDataManager.unregister();
+ await mockSiteDataManager.unregister();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
function removeAllSitesOneByOne() {
frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
let removeBtn = frameDoc.getElementById("removeSelected");
let sitesList = frameDoc.getElementById("sitesList");
let sites = sitesList.getElementsByTagName("richlistitem");
for (let i = sites.length - 1; i >= 0; --i) {
@@ -219,17 +219,17 @@ add_task(async function() {
removeSelectedSite(fakeHosts.slice(0, 2));
assertSitesListed(doc, fakeHosts.slice(2));
saveBtn.doCommand();
await removeDialogOpenPromise;
await settingsDialogClosePromise;
await openSiteDataSettingsDialog();
assertSitesListed(doc, fakeHosts.slice(2));
- mockSiteDataManager.unregister();
+ await mockSiteDataManager.unregister();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
function removeSelectedSite(hosts) {
frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
let removeBtn = frameDoc.getElementById("removeSelected");
let sitesList = frameDoc.getElementById("sitesList");
hosts.forEach(host => {
let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
@@ -295,17 +295,17 @@ add_task(async function() {
removeAllBtn.doCommand();
saveBtn.doCommand();
await acceptRemovePromise;
await settingsDialogClosePromise;
await updatePromise;
await openSiteDataSettingsDialog();
assertSitesListed(doc, fakeHosts.filter(host => !host.includes("xyz")));
- mockSiteDataManager.unregister();
+ await mockSiteDataManager.unregister();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// Test dynamically clearing all site data
add_task(async function() {
await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
mockSiteDataManager.register(SiteDataManager, [
{
@@ -353,11 +353,11 @@ add_task(async function() {
removeAllBtn.doCommand();
saveBtn.doCommand();
await acceptRemovePromise;
await settingsDialogClosePromise;
await updatePromise;
await openSiteDataSettingsDialog();
assertAllSitesNotListed(win);
- mockSiteDataManager.unregister();
+ await mockSiteDataManager.unregister();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
--- a/browser/components/preferences/in-content/tests/browser_siteData3.js
+++ b/browser/components/preferences/in-content/tests/browser_siteData3.js
@@ -37,17 +37,17 @@ add_task(async function() {
let updatePromise = promiseSiteDataManagerSitesUpdated();
let doc = gBrowser.selectedBrowser.contentDocument;
await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
await updatePromise;
await openSiteDataSettingsDialog();
assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
- mockSiteDataManager.unregister();
+ await mockSiteDataManager.unregister();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// Test grouping and listing sites across scheme, port and origin attributes by host
add_task(async function() {
await SpecialPowers.pushPrefEnv({ set: [["browser.storageManager.enabled", true]] });
const quotaUsage = 1024;
mockSiteDataManager.register(SiteDataManager, [
@@ -99,41 +99,41 @@ add_task(async function() {
is(columns[1].value, expected, "Should mark persisted status across scheme, port and origin attributes");
is(columns[2].value, "5", "Should group cookies across scheme, port and origin attributes");
expected = prefStrBundle.getFormattedString("siteUsage",
DownloadUtils.convertByteUnits(quotaUsage * mockSiteDataManager.fakeSites.length));
is(columns[3].value, expected, "Should sum up usages across scheme, port and origin attributes");
- mockSiteDataManager.unregister();
+ await mockSiteDataManager.unregister();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// Test sorting
add_task(async function() {
await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
mockSiteDataManager.register(SiteDataManager, [
{
usage: 1024,
origin: "https://account.xyz.com",
cookies: 6,
- persisted: true
+ persisted: true,
},
{
usage: 1024 * 2,
origin: "https://books.foo.com",
cookies: 0,
- persisted: false
+ persisted: false,
},
{
usage: 1024 * 3,
origin: "http://cinema.bar.com",
cookies: 3,
- persisted: true
+ persisted: true,
},
]);
let updatePromise = promiseSiteDataManagerSitesUpdated();
await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
await updatePromise;
await openSiteDataSettingsDialog();
@@ -169,17 +169,17 @@ add_task(async function() {
assertSortByCookies("descending");
// Test sorting on the cookies column
statusCol.click();
assertSortByStatus("ascending");
statusCol.click();
assertSortByStatus("descending");
- mockSiteDataManager.unregister();
+ await mockSiteDataManager.unregister();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
function assertSortByBaseDomain(order) {
let siteItems = sitesList.getElementsByTagName("richlistitem");
for (let i = 0; i < siteItems.length - 1; ++i) {
let aHost = siteItems[i].getAttribute("host");
let bHost = siteItems[i + 1].getAttribute("host");
let a = findSiteByHost(aHost);
@@ -245,8 +245,74 @@ add_task(async function() {
}
}
}
function findSiteByHost(host) {
return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
}
});
+
+// Test sorting based on access date (separate from cookies for simplicity,
+// since cookies access date affects this as well, but we don't mock our cookies)
+add_task(async function() {
+ mockSiteDataManager.register(SiteDataManager, [
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ lastAccessed: (Date.now() - 120 * 1000) * 1000,
+ },
+ {
+ usage: 1024 * 2,
+ origin: "https://books.foo.com",
+ persisted: false,
+ lastAccessed: (Date.now() - 240 * 1000) * 1000,
+ },
+ {
+ usage: 1024 * 3,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ lastAccessed: Date.now() * 1000,
+ },
+ ]);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ let dialog = content.gSubDialog._topDialog;
+ let dialogFrame = dialog._frame;
+ let frameDoc = dialogFrame.contentDocument;
+ let lastAccessedCol = frameDoc.getElementById("lastAccessedCol");
+ let sitesList = frameDoc.getElementById("sitesList");
+
+ // Test sorting on the date column
+ lastAccessedCol.click();
+ assertSortByDate("ascending");
+ lastAccessedCol.click();
+ assertSortByDate("descending");
+
+ await mockSiteDataManager.unregister();
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ function assertSortByDate(order) {
+ let siteItems = sitesList.getElementsByTagName("richlistitem");
+ for (let i = 0; i < siteItems.length - 1; ++i) {
+ let aHost = siteItems[i].getAttribute("host");
+ let bHost = siteItems[i + 1].getAttribute("host");
+ let a = findSiteByHost(aHost);
+ let b = findSiteByHost(bHost);
+ let result = a.lastAccessed - b.lastAccessed;
+ if (order == "ascending") {
+ Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by date");
+ } else {
+ Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order date");
+ }
+ }
+ }
+
+ function findSiteByHost(host) {
+ return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+ }
+});
--- a/browser/components/preferences/in-content/tests/head.js
+++ b/browser/components/preferences/in-content/tests/head.js
@@ -224,17 +224,18 @@ const mockSiteDataManager = {
_SiteDataManager: null,
_originalQMS: null,
_originalRemoveQuotaUsage: null,
getUsage(onUsageResult) {
let result = this.fakeSites.map(site => ({
origin: site.principal.origin,
usage: site.usage,
- persisted: site.persisted
+ persisted: site.persisted,
+ lastAccessed: site.lastAccessed,
}));
onUsageResult({ result, resultCode: Components.results.NS_OK });
},
_removeQuotaUsage(site) {
var target = site.principals[0].URI.host;
this.fakeSites = this.fakeSites.filter(fakeSite => {
return fakeSite.principal.URI.host != target;
@@ -265,19 +266,19 @@ const mockSiteDataManager = {
// Add some cookies if needed.
for (let i = 0; i < (site.cookies || 0); i++) {
Services.cookies.add(uri.host, uri.pathQueryRef, Cu.now(), i,
false, false, false, Date.now() + 1000 * 60 * 60);
}
}
},
- unregister() {
+ async unregister() {
+ await this._SiteDataManager.removeAll();
this.fakeSites = null;
- this._SiteDataManager.removeAll();
this._SiteDataManager._qms = this._originalQMS;
this._SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
}
};
function getQuotaUsage(origin) {
return new Promise(resolve => {
let uri = NetUtil.newURI(origin);
--- a/browser/components/preferences/siteDataSettings.js
+++ b/browser/components/preferences/siteDataSettings.js
@@ -62,26 +62,34 @@ let gSiteDataSettings = {
let size = DownloadUtils.convertByteUnits(site.usage);
let str = this._prefStrBundle.getFormattedString("siteUsage", size);
addColumnItem(str, "1");
} else {
// Pass null to avoid showing "0KB" when there is no site data stored.
addColumnItem(null, "1");
}
+ // Add "Last Used" column.
+ addColumnItem(site.lastAccessed > 0 ?
+ this._formatter.format(site.lastAccessed) : null, "2");
+
item.appendChild(container);
return item;
},
init() {
function setEventListener(id, eventType, callback) {
document.getElementById(id)
.addEventListener(eventType, callback.bind(gSiteDataSettings));
}
+ this._formatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short", timeStyle: "short",
+ });
+
this._list = document.getElementById("sitesList");
this._searchBox = document.getElementById("searchBox");
this._prefStrBundle = document.getElementById("bundlePreferences");
SiteDataManager.getSites().then(sites => {
this._sites = sites;
let sortCol = document.querySelector("treecol[data-isCurrentSortCol=true]");
this._sortSites(this._sites, sortCol);
this._buildSitesList(this._sites);
@@ -90,16 +98,17 @@ let gSiteDataSettings = {
let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
let settingsDescription = document.getElementById("settingsDescription");
settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings2.description", [brandShortName]);
setEventListener("sitesList", "select", this.onSelect);
setEventListener("hostCol", "click", this.onClickTreeCol);
setEventListener("usageCol", "click", this.onClickTreeCol);
+ setEventListener("lastAccessedCol", "click", this.onClickTreeCol);
setEventListener("cookiesCol", "click", this.onClickTreeCol);
setEventListener("statusCol", "click", this.onClickTreeCol);
setEventListener("cancel", "command", this.close);
setEventListener("save", "command", this.saveChanges);
setEventListener("searchBox", "command", this.onCommandSearch);
setEventListener("removeAll", "command", this.onClickRemoveAll);
setEventListener("removeSelected", "command", this.onClickRemoveSelected);
},
@@ -156,16 +165,20 @@ let gSiteDataSettings = {
case "cookiesCol":
sortFunc = (a, b) => a.cookies.length - b.cookies.length;
break;
case "usageCol":
sortFunc = (a, b) => a.usage - b.usage;
break;
+
+ case "lastAccessedCol":
+ sortFunc = (a, b) => a.lastAccessed - b.lastAccessed;
+ break;
}
if (sortDirection === "descending") {
sites.sort((a, b) => sortFunc(b, a));
} else {
sites.sort(sortFunc);
}
let cols = this._list.querySelectorAll("treecol");
--- a/browser/components/preferences/siteDataSettings.xul
+++ b/browser/components/preferences/siteDataSettings.xul
@@ -37,16 +37,17 @@
<richlistbox id="sitesList" orient="vertical" flex="1">
<listheader>
<treecol flex="4" width="50" label="&hostCol.label;" id="hostCol"/>
<treecol flex="2" width="50" label="&statusCol.label;" id="statusCol"/>
<treecol flex="1" width="50" label="&cookiesCol.label;" id="cookiesCol"/>
<!-- Sorted by usage so the user can quickly see which sites use the most data. -->
<treecol flex="1" width="50" label="&usageCol.label;" id="usageCol" data-isCurrentSortCol="true"/>
+ <treecol flex="2" width="50" label="&lastAccessedCol.label;" id="lastAccessedCol" />
</listheader>
</richlistbox>
</vbox>
<hbox align="start">
<button id="removeSelected" label="&removeSelected.label;" accesskey="&removeSelected.accesskey;"/>
<button id="removeAll"/>
</hbox>
--- a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
@@ -2,16 +2,17 @@
- 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/. -->
<!ENTITY window1.title "Settings - Cookies and Site Data">
<!ENTITY hostCol.label "Site">
<!ENTITY statusCol.label "Status">
<!ENTITY cookiesCol.label "Cookies">
<!ENTITY usageCol.label "Storage">
+<!ENTITY lastAccessedCol.label "Last Used">
<!ENTITY searchTextboxPlaceHolder "Search websites">
<!ENTITY searchTextboxPlaceHolder.accesskey "S">
<!ENTITY removeSelected.label "Remove Selected">
<!ENTITY removeSelected.accesskey "r">
<!ENTITY save.label "Save Changes">
<!ENTITY save.accesskey "a">
<!ENTITY cancel.label "Cancel">
<!ENTITY cancel.accesskey "C">