Bug 1462469 - Add a "Clear Cookies and Site Data" footer button to the identity popup. r=nhnt11 draft
authorJohann Hofmann <jhofmann@mozilla.com>
Fri, 18 May 2018 11:36:15 +0200
changeset 798357 a36e0e7049c5892464d91ad42c3bf523dd5013f9
parent 795657 3c9d69736f4a421218e5eb01b6571d535d38318a
push id110732
push userjhofmann@mozilla.com
push dateTue, 22 May 2018 18:33:27 +0000
reviewersnhnt11
bugs1462469, 1460768
milestone62.0a1
Bug 1462469 - Add a "Clear Cookies and Site Data" footer button to the identity popup. r=nhnt11 This implements a new button in the identity popup that allows users to quickly remove cookies and site data from the sites they're visiting. This uses the SiteDataManager behind the scenes and is similar to the changes we made for PageInfo already. There's a major drawback to this approach in that SiteDataManager needs to refresh its entire data set everytime we want information about a single site or want to remove anything (it's not trivial to get rid of that limitation while dealing with all the quirks of our storage APIs). I will work around this by implementing a way for SiteDataManager to incrementally update itself in bug 1460768. MozReview-Commit-ID: Iy7ia0KllFq
browser/base/content/browser-siteIdentity.js
browser/base/content/browser.js
browser/base/content/test/siteIdentity/browser.ini
browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js
browser/components/controlcenter/content/panel.inc.xul
browser/locales/en-US/chrome/browser/browser.dtd
browser/themes/shared/controlcenter/panel.inc.css
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -196,26 +196,63 @@ var gIdentityHandler = {
   get _permissionReloadHint() {
     delete this._permissionReloadHint;
     return this._permissionReloadHint = document.getElementById("identity-popup-permission-reload-hint");
   },
   get _popupExpander() {
     delete this._popupExpander;
     return this._popupExpander = document.getElementById("identity-popup-security-expander");
   },
+  get _clearSiteDataFooter() {
+    delete this._clearSiteDataFooter;
+    return this._clearSiteDataFooter = document.getElementById("identity-popup-clear-sitedata-footer");
+  },
   get _permissionAnchors() {
     delete this._permissionAnchors;
     let permissionAnchors = {};
     for (let anchor of document.getElementById("blocked-permissions-container").children) {
       permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor;
     }
     return this._permissionAnchors = permissionAnchors;
   },
 
   /**
+   * Handles clicks on the "Clear Cookies and Site Data" button.
+   */
+  async clearSiteData(event) {
+    if (!this._uriHasHost) {
+      return;
+    }
+
+    let host = this._uri.host;
+
+    // Site data could have changed while the identity popup was open,
+    // reload again to be sure.
+    await SiteDataManager.updateSites();
+
+    let baseDomain = SiteDataManager.getBaseDomainFromHost(host);
+    let siteData = await SiteDataManager.getSites(baseDomain);
+
+    // Hide the popup before showing the removal prompt, to
+    // avoid a pretty ugly transition. Also hide it even
+    // if the update resulted in no site data, to keep the
+    // illusion that clicking the button had an effect.
+    PanelMultiView.hidePopup(this._identityPopup);
+
+    if (siteData && siteData.length) {
+      let hosts = siteData.map(site => site.host);
+      if (SiteDataManager.promptSiteDataRemoval(window, hosts)) {
+        SiteDataManager.remove(hosts);
+      }
+    }
+
+    event.stopPropagation();
+  },
+
+  /**
    * Handler for mouseclicks on the "More Information" button in the
    * "identity-popup" panel.
    */
   handleMoreInfoClick(event) {
     displaySecurityInfo();
     event.stopPropagation();
     PanelMultiView.hidePopup(this._identityPopup);
   },
@@ -573,16 +610,31 @@ var gIdentityHandler = {
   },
 
   /**
    * Set up the title and content messages for the identity message popup,
    * based on the specified mode, and the details of the SSL cert, where
    * applicable
    */
   refreshIdentityPopup() {
+    // Update cookies and site data information and show the
+    // "Clear Site Data" button if the site is storing local data.
+    this._clearSiteDataFooter.hidden = true;
+    if (this._uriHasHost) {
+      let host = this._uri.host;
+      SiteDataManager.updateSites().then(async () => {
+        let baseDomain = SiteDataManager.getBaseDomainFromHost(host);
+        let siteData = await SiteDataManager.getSites(baseDomain);
+
+        if (siteData && siteData.length) {
+          this._clearSiteDataFooter.hidden = false;
+        }
+      });
+    }
+
     // Update "Learn More" for Mixed Content Blocking and Insecure Login Forms.
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
     this._identityPopupMixedContentLearnMore
         .setAttribute("href", baseURL + "mixed-content");
     this._identityPopupInsecureLoginFormsLearnMore
         .setAttribute("href", baseURL + "insecure-password");
 
     // This is in the properties file because the expander used to switch its tooltip.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -50,16 +50,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ReaderMode: "resource://gre/modules/ReaderMode.jsm",
   ReaderParent: "resource:///modules/ReaderParent.jsm",
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   SchedulePressure: "resource:///modules/SchedulePressure.jsm",
   ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
   SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
+  SiteDataManager: "resource:///modules/SiteDataManager.jsm",
   SitePermissions: "resource:///modules/SitePermissions.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   Translation: "resource:///modules/translation/Translation.jsm",
   UITour: "resource:///modules/UITour.jsm",
   UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
   Weave: "resource://services-sync/main.js",
--- a/browser/base/content/test/siteIdentity/browser.ini
+++ b/browser/base/content/test/siteIdentity/browser.ini
@@ -41,16 +41,17 @@ support-files =
 [browser_csp_block_all_mixedcontent.js]
 tags = mcb
 support-files =
   file_csp_block_all_mixedcontent.html
   file_csp_block_all_mixedcontent.js
 [browser_identity_UI.js]
 [browser_identityBlock_focus.js]
 support-files = ../permissions/permissions.html
+[browser_identityPopup_clearSiteData.js]
 [browser_identityPopup_focus.js]
 [browser_insecureLoginForms.js]
 support-files =
   insecure_opener.html
   !/toolkit/components/passwordmgr/test/browser/form_basic.html
   !/toolkit/components/passwordmgr/test/browser/insecure_test.html
   !/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
 [browser_mcb_redirect.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_ORIGIN = "https://example.com";
+const TEST_SUB_ORIGIN = "https://test1.example.com";
+const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+
+ChromeUtils.defineModuleGetter(this, "SiteDataTestUtils",
+                               "resource://testing-common/SiteDataTestUtils.jsm");
+
+async function testClearing(testQuota, testCookies) {
+  // Add some test quota storage.
+  if (testQuota) {
+    await SiteDataTestUtils.addToIndexedDB(TEST_ORIGIN);
+    await SiteDataTestUtils.addToIndexedDB(TEST_SUB_ORIGIN);
+  }
+
+  // Add some test cookies.
+  if (testCookies) {
+    SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test1", "1");
+    SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test2", "2");
+    SiteDataTestUtils.addToCookies(TEST_SUB_ORIGIN, "test3", "1");
+  }
+
+  await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
+    // Verify we have added quota storage.
+    if (testQuota) {
+      let usage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
+      Assert.greater(usage, 0, "Should have data for the base origin.");
+
+      usage = await SiteDataTestUtils.getQuotaUsage(TEST_SUB_ORIGIN);
+      Assert.greater(usage, 0, "Should have data for the sub origin.");
+    }
+
+    // Open the identity popup.
+    let { gIdentityHandler } = gBrowser.ownerGlobal;
+    let promisePanelOpen = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
+    let siteDataUpdated = TestUtils.topicObserved("sitedatamanager:sites-updated");
+    gIdentityHandler._identityBox.click();
+    await promisePanelOpen;
+    await siteDataUpdated;
+
+    let clearFooter = document.getElementById("identity-popup-clear-sitedata-footer");
+    let clearButton = document.getElementById("identity-popup-clear-sitedata-button");
+    ok(!clearFooter.hidden, "The clear data footer is not hidden.");
+
+    let cookiesCleared;
+    if (testCookies) {
+      cookiesCleared = Promise.all([
+        TestUtils.topicObserved("cookie-changed", (subj, data) => data == "deleted" && subj.name == "test1"),
+        TestUtils.topicObserved("cookie-changed", (subj, data) => data == "deleted" && subj.name == "test2"),
+        TestUtils.topicObserved("cookie-changed", (subj, data) => data == "deleted" && subj.name == "test3"),
+      ]);
+    }
+
+    // Click the "Clear data" button.
+    siteDataUpdated = TestUtils.topicObserved("sitedatamanager:sites-updated");
+    let hideEvent = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
+    let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+    clearButton.click();
+    await hideEvent;
+    await removeDialogPromise;
+
+    await siteDataUpdated;
+
+    // Check that cookies were deleted.
+    if (testCookies) {
+      await cookiesCleared;
+      let uri = Services.io.newURI(TEST_ORIGIN);
+      is(Services.cookies.countCookiesFromHost(uri.host), 0, "Cookies from the base domain should be cleared");
+      uri = Services.io.newURI(TEST_SUB_ORIGIN);
+      is(Services.cookies.countCookiesFromHost(uri.host), 0, "Cookies from the sub domain should be cleared");
+    }
+
+    // Check that quota storage was deleted.
+    if (testQuota) {
+      await TestUtils.waitForCondition(async () => {
+        let usage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
+        return usage == 0;
+      }, "Should have no data for the base origin.");
+
+      let usage = await SiteDataTestUtils.getQuotaUsage(TEST_SUB_ORIGIN);
+      is(usage, 0, "Should have no data for the sub origin.");
+    }
+
+    // Open the site identity panel again to check that the button isn't shown anymore.
+    promisePanelOpen = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
+    siteDataUpdated = TestUtils.topicObserved("sitedatamanager:sites-updated");
+    gIdentityHandler._identityBox.click();
+    await promisePanelOpen;
+    await siteDataUpdated;
+
+    ok(clearFooter.hidden, "The clear data footer is hidden after clearing data.");
+  });
+}
+
+// Test removing quota managed storage.
+add_task(async function test_ClearSiteData() {
+  await testClearing(true, false);
+});
+
+// Test removing cookies.
+add_task(async function test_ClearCookies() {
+  await testClearing(false, true);
+});
+
+// Test removing both.
+add_task(async function test_ClearCookiesAndSiteData() {
+  await testClearing(true, true);
+});
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -90,16 +90,26 @@
           <label id="identity-popup-permissions-headline"
                  class="identity-popup-headline"
                  value="&identity.permissions;"/>
           <vbox id="identity-popup-permission-list"/>
           <description id="identity-popup-permission-reload-hint">&identity.permissionsReloadHint;</description>
           <description id="identity-popup-permission-empty-hint">&identity.permissionsEmpty;</description>
         </vbox>
       </hbox>
+
+      <!-- Clear Site Data Button -->
+      <vbox hidden="true"
+            id="identity-popup-clear-sitedata-footer"
+            class="identity-popup-footer">
+        <button class="subviewkeynav"
+                id="identity-popup-clear-sitedata-button"
+                label="&identity.clearSiteData;"
+                oncommand="gIdentityHandler.clearSiteData(event);"/>
+      </vbox>
     </panelview>
 
     <!-- Security SubView -->
     <panelview id="identity-popup-securityView"
                title="&identity.securityView.label;"
                descriptionheightworkaround="true">
       <vbox class="identity-popup-security-content">
         <label class="plain">
@@ -173,17 +183,17 @@
                 accesskey="&identity.disableMixedContentBlocking.accesskey;"
                 oncommand="gIdentityHandler.disableMixedContentProtection()"/>
         <button when-mixedcontent="active-loaded" class="subviewkeynav"
                 label="&identity.enableMixedContentBlocking.label;"
                 accesskey="&identity.enableMixedContentBlocking.accesskey;"
                 oncommand="gIdentityHandler.enableMixedContentProtection()"/>
       </vbox>
 
-      <vbox id="identity-popup-securityView-footer">
+      <vbox id="identity-popup-more-info-footer" class="identity-popup-footer">
         <!-- More Security Information -->
         <button id="identity-popup-more-info"  class="subviewkeynav"
                 label="&identity.moreInfoLinkText2;"
                 oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
       </vbox>
 
     </panelview>
   </panelmultiview>
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -784,16 +784,18 @@ you can use these alternative items. Oth
 <!ENTITY identity.disableMixedContentBlocking.accesskey "D">
 <!ENTITY identity.learnMore "Learn More">
 
 <!ENTITY identity.removeCertException.label "Remove Exception">
 <!ENTITY identity.removeCertException.accesskey "R">
 
 <!ENTITY identity.moreInfoLinkText2 "More Information">
 
+<!ENTITY identity.clearSiteData "Clear Cookies and Site Data">
+
 <!ENTITY identity.permissions "Permissions">
 <!ENTITY identity.permissionsEmpty "You have not granted this site any special permissions.">
 <!ENTITY identity.permissionsReloadHint "You may need to reload the page for changes to apply.">
 
 <!-- Name for the tabs toolbar as spoken by screen readers.
      The word "toolbar" is appended automatically and should not be contained below! -->
 <!ENTITY tabsToolbar.label "Browser tabs">
 
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -234,37 +234,40 @@
 
 #identity-popup-securityView-body {
   margin-inline-start: calc(2em + 24px);
   margin-inline-end: 1em;
   border-top: 1px solid var(--panel-separator-color);
   padding-inline-end: 1em;
 }
 
-#identity-popup-securityView-footer {
+#identity-popup-more-info-footer {
   margin-top: 1em;
+}
+
+.identity-popup-footer {
   background-color: var(--arrowpanel-dimmed);
 }
 
-#identity-popup-securityView-footer > button {
+.identity-popup-footer > button {
   -moz-appearance: none;
   margin: 0;
   border: none;
   border-top: 1px solid var(--panel-separator-color);
   padding: 8px 20px;
   color: inherit;
   background-color: transparent;
 }
 
-#identity-popup-securityView-footer > button:hover,
-#identity-popup-securityView-footer > button:focus {
+.identity-popup-footer > button:hover,
+.identity-popup-footer > button:focus {
   background-color: var(--arrowpanel-dimmed);
 }
 
-#identity-popup-securityView-footer > button:hover:active {
+.identity-popup-footer > button:hover:active {
   background-color: var(--arrowpanel-dimmed-further);
 }
 
 #identity-popup-content-verifier ~ description {
   margin-top: 1em;
   color: Graytext;
 }