Bug 1431029 - Show a "last accessed" column in the site data manager. r=Gijs draft
authorJohann Hofmann <jhofmann@mozilla.com>
Tue, 20 Feb 2018 14:54:43 +0100
changeset 757888 d26f75b949112ca19c689986fe3f142bb3007d16
parent 757887 4a17e0b6b38073fea7b4b3d3b4483d4c495b3950
push id99865
push userjhofmann@mozilla.com
push dateWed, 21 Feb 2018 12:58:36 +0000
reviewersGijs
bugs1431029
milestone60.0a1
Bug 1431029 - Show a "last accessed" column in the site data manager. r=Gijs MozReview-Commit-ID: LidkPQ6kLfX
browser/components/preferences/SiteDataManager.jsm
browser/components/preferences/in-content/tests/browser_siteData.js
browser/components/preferences/in-content/tests/browser_siteData2.js
browser/components/preferences/in-content/tests/browser_siteData3.js
browser/components/preferences/in-content/tests/head.js
browser/components/preferences/siteDataSettings.js
browser/components/preferences/siteDataSettings.xul
browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
--- 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">