Bug 1411700 - Show that an Extension is controlling the default notifications permission. r=flod,johannh
authorprathiksha <prathikshaprasadsuman@gmail.com>
Fri, 19 Oct 2018 04:31:33 +0000
changeset 490426 225546ee5bd2c9190cce2bf673000146bdd77952
parent 490425 8594386886d1e8b9b0a6216ec32c1ab86f32662b
child 490427 bad2ba4312cc3f385cd018527649c2c8065bcd40
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersflod, johannh
bugs1411700
milestone64.0a1
Bug 1411700 - Show that an Extension is controlling the default notifications permission. r=flod,johannh Show that an Extension is controlling the default notifications permission Differential Revision: https://phabricator.services.mozilla.com/D5042
browser/components/preferences/in-content/extensionControlled.js
browser/components/preferences/in-content/tests/browser_extension_controlled.js
browser/components/preferences/sitePermissions.css
browser/components/preferences/sitePermissions.js
browser/components/preferences/sitePermissions.xul
browser/locales/en-US/browser/preferences/preferences.ftl
--- a/browser/components/preferences/in-content/extensionControlled.js
+++ b/browser/components/preferences/in-content/extensionControlled.js
@@ -39,16 +39,17 @@ const API_PROXY_PREFS = [
   "network.proxy.autoconfig_url",
   "signon.autologin.proxy",
 ];
 
 let extensionControlledContentIds = {
   "privacy.containers": "browserContainersExtensionContent",
   "homepage_override": "browserHomePageExtensionContent",
   "newTabURL": "browserNewTabExtensionContent",
+  "webNotificationsDisabled": "browserNotificationsPermissionExtensionContent",
   "defaultSearch": "browserDefaultSearchExtensionContent",
   "proxy.settings": "proxyExtensionContent",
   get "websites.trackingProtectionMode"() {
     return {
       button: contentBlockingUiEnabled ?
         "contentBlockingDisableTrackingProtectionExtension" :
         "trackingProtectionExtensionContentButton",
       section: contentBlockingUiEnabled ?
@@ -56,16 +57,17 @@ let extensionControlledContentIds = {
         "trackingProtectionExtensionContentLabel",
     };
   },
 };
 
 const extensionControlledL10nKeys = {
   "homepage_override": "homepage-override",
   "newTabURL": "new-tab-url",
+  "webNotificationsDisabled": "web-notifications",
   "defaultSearch": "default-search",
   "privacy.containers": "privacy-containers",
   "websites.trackingProtectionMode": contentBlockingUiEnabled ?
                                        "websites-content-blocking-all-trackers" :
                                        "websites-tracking-protection-mode",
   "proxy.settings": "proxy-config",
 };
 
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -6,16 +6,18 @@ ChromeUtils.defineModuleGetter(this, "Ex
                                "resource://gre/modules/ExtensionSettingsStore.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 XPCOMUtils.defineLazyPreferenceGetter(this, "proxyType", PROXY_PREF);
 
 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
 const CHROME_URL_ROOT = TEST_DIR + "/";
+const PERMISSIONS_URL = "chrome://browser/content/preferences/sitePermissions.xul";
+let sitePermissionsDialog;
 
 function getSupportsFile(path) {
   let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
     .getService(Ci.nsIChromeRegistry);
   let uri = Services.io.newURI(CHROME_URL_ROOT + path);
   let fileurl = cr.convertChromeURL(uri);
   return fileurl.QueryInterface(Ci.nsIFileURL);
 }
@@ -63,16 +65,29 @@ function waitForEnableMessage(messageId,
 
 function waitForMessageContent(messageId, l10nId, doc) {
   return waitForMessageChange(
     getElement(messageId, doc),
     target => doc.l10n.getAttributes(target).id === l10nId,
     { childList: true });
 }
 
+async function openNotificationsPermissionDialog() {
+  let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    let doc = content.document;
+    let settingsButton = doc.getElementById("notificationSettingsButton");
+    settingsButton.click();
+  });
+
+  sitePermissionsDialog = await dialogOpened;
+  await sitePermissionsDialog.document.mozSubdialogReady;
+}
+
 add_task(async function testExtensionControlledHomepage() {
   await openPreferencesViaOpenPreferencesAPI("paneHome", {leaveOpen: true});
   // eslint-disable-next-line mozilla/no-cpows-in-tests
   let doc = gBrowser.contentDocument;
   is(gBrowser.currentURI.spec, "about:preferences#home",
      "#home should be in the URI for about:preferences");
   let homepagePref = () => Services.prefs.getCharPref("browser.startup.homepage");
   let originalHomepagePref = homepagePref();
@@ -334,16 +349,85 @@ add_task(async function testExtensionCon
   is(controlledContent.hidden, true, "The extension controlled row is shown");
 
   // Cleanup the tab and add-on.
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
   let addon = await AddonManager.getAddonByID("@set_newtab");
   await addon.uninstall();
 });
 
+add_task(async function testExtensionControlledWebNotificationsPermission() {
+  let manifest = {
+    manifest_version: 2,
+    name: "TestExtension",
+    version: "1.0",
+    description: "Testing WebNotificationsDisable",
+    applications: {gecko: {id: "@web_notifications_disable"}},
+    permissions: [
+      "browserSettings",
+    ],
+    browser_action: {
+      default_title: "Testing",
+    },
+  };
+
+  await openPreferencesViaOpenPreferencesAPI("privacy", {leaveOpen: true});
+  await openNotificationsPermissionDialog();
+
+  let doc = sitePermissionsDialog.document;
+  let extensionControlledContent = doc.getElementById("browserNotificationsPermissionExtensionContent");
+
+  // Test that extension content is initially hidden.
+  ok(extensionControlledContent.hidden, "Extension content is initially hidden");
+
+  // Install an extension that will disable web notifications permission.
+  let messageShown = waitForMessageShown("browserNotificationsPermissionExtensionContent", doc);
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest,
+    useAddonManager: "permanent",
+    background() {
+      browser.browserSettings.webNotificationsDisabled.set({value: true});
+      browser.test.sendMessage("load-extension");
+    },
+  });
+  await extension.startup();
+  await extension.awaitMessage("load-extension");
+  await messageShown;
+
+  let controlledDesc = extensionControlledContent.querySelector("description");
+  Assert.deepEqual(doc.l10n.getAttributes(controlledDesc), {
+    id: "extension-controlled-web-notifications",
+    args: {
+      name: "TestExtension",
+    },
+  }, "The user is notified that an extension is controlling the web notifications permission");
+  is(extensionControlledContent.hidden, false, "The extension controlled row is not hidden");
+
+  // Disable the extension.
+  doc.getElementById("disableNotificationsPermissionExtension").click();
+
+  // Verify the user is notified how to enable the extension.
+  await waitForEnableMessage(extensionControlledContent.id, doc);
+  is(doc.l10n.getAttributes(controlledDesc.querySelector("label")).id,
+    "extension-controlled-enable",
+    "The user is notified of how to enable the extension again");
+
+  // Verify the enable message can be dismissed.
+  let hidden = waitForMessageHidden(extensionControlledContent.id, doc);
+  let dismissButton = controlledDesc.querySelector("image:last-of-type");
+  dismissButton.click();
+  await hidden;
+
+  // Verify that the extension controlled content in hidden again.
+  is(extensionControlledContent.hidden, true, "The extension controlled row is now hidden");
+
+  await extension.unload();
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
 add_task(async function testExtensionControlledDefaultSearch() {
   await openPreferencesViaOpenPreferencesAPI("paneSearch", {leaveOpen: true});
   let doc = gBrowser.contentDocument;
   let extensionId = "@set_default_search";
   let manifest = {
     manifest_version: 2,
     name: "set_default_search",
     applications: {gecko: {id: extensionId}},
--- a/browser/components/preferences/sitePermissions.css
+++ b/browser/components/preferences/sitePermissions.css
@@ -18,18 +18,22 @@
   min-height: 35px;
 }
 
 .website-status {
   margin: 1px;
   margin-inline-end: 5px;
 }
 
+#browserNotificationsPermissionExtensionContent,
 #permissionsDisableDescription {
   margin-inline-start: 32px;
+}
+
+#permissionsDisableDescription {
   color: #737373;
   line-height: 110%;
 }
 
 #permissionsDisableCheckbox {
   margin-inline-start: 4px;
   padding-top: 10px;
 }
--- a/browser/components/preferences/sitePermissions.js
+++ b/browser/components/preferences/sitePermissions.js
@@ -1,12 +1,14 @@
 /* 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/. */
 
+/* import-globals-from in-content/extensionControlled.js */
+
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource:///modules/SitePermissions.jsm");
 
 const sitePermissionsL10n = {
   "desktop-notification": {
     window: "permissions-site-notification-window",
     description: "permissions-site-notification-desc",
@@ -37,16 +39,18 @@ function Permission(principal, type, cap
   this.principal = principal;
   this.origin = principal.origin;
   this.type = type;
   this.capability = capability;
   this.l10nId = l10nId;
 }
 
 const PERMISSION_STATES = [SitePermissions.ALLOW, SitePermissions.BLOCK, SitePermissions.PROMPT];
+const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled";
+const NOTIFICATIONS_PERMISSION_PREF = "permissions.default.desktop-notification";
 
 var gSitePermissionsManager = {
   _type: "",
   _isObserving: false,
   _permissions: new Map(),
   _permissionsToChange: new Map(),
   _permissionsToDelete: new Map(),
   _list: null,
@@ -69,52 +73,38 @@ var gSitePermissionsManager = {
     }
 
     this._type = params.permissionType;
     this._list = document.getElementById("permissionsBox");
     this._removeButton = document.getElementById("removePermission");
     this._removeAllButton = document.getElementById("removeAllPermissions");
     this._searchBox = document.getElementById("searchBox");
     this._checkbox = document.getElementById("permissionsDisableCheckbox");
+    this._disableExtensionButton = document.getElementById("disableNotificationsPermissionExtension");
+    this._permissionsDisableDescription = document.getElementById("permissionsDisableDescription");
 
-    let permissionsDisableDescription = document.getElementById("permissionsDisableDescription");
     let permissionsText = document.getElementById("permissionsText");
 
     let l10n = sitePermissionsL10n[this._type];
     document.l10n.setAttributes(permissionsText, l10n.description);
     document.l10n.setAttributes(this._checkbox, l10n.disableLabel);
-    document.l10n.setAttributes(permissionsDisableDescription, l10n.disableDescription);
+    document.l10n.setAttributes(this._permissionsDisableDescription, l10n.disableDescription);
     document.l10n.setAttributes(document.documentElement, l10n.window);
 
     await document.l10n.translateElements([
       permissionsText,
       this._checkbox,
-      permissionsDisableDescription,
+      this._permissionsDisableDescription,
       document.documentElement,
     ]);
 
-    // Initialize the checkbox state.
+    // Initialize the checkbox state and handle showing notification permission UI
+    // when it is disabled by an extension.
     this._defaultPermissionStatePrefName = "permissions.default." + this._type;
-    let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName);
-    if (pref != Services.prefs.PREF_INVALID) {
-      this._currentDefaultPermissionsState = Services.prefs.getIntPref(this._defaultPermissionStatePrefName);
-    }
-
-    if (this._currentDefaultPermissionsState === null) {
-      this._checkbox.setAttribute("hidden", true);
-      permissionsDisableDescription.setAttribute("hidden", true);
-    } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
-      this._checkbox.checked = true;
-    } else {
-      this._checkbox.checked = false;
-    }
-
-    if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) {
-      this._checkbox.disabled = true;
-    }
+    this._watchPermissionPrefChange();
 
     this._loadPermissions();
     this.buildPermissionsList();
 
     this._searchBox.focus();
   },
 
   uninit() {
@@ -150,16 +140,78 @@ var gSitePermissionsManager = {
 
   _handleCapabilityChange(perm) {
     let permissionlistitem = document.getElementsByAttribute("origin", perm.origin)[0];
     let menulist = permissionlistitem.getElementsByTagName("menulist")[0];
     menulist.selectedItem =
       menulist.getElementsByAttribute("value", perm.capability)[0];
   },
 
+  _handleCheckboxUIUpdates() {
+    let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName);
+    if (pref != Services.prefs.PREF_INVALID) {
+      this._currentDefaultPermissionsState = Services.prefs.getIntPref(this._defaultPermissionStatePrefName);
+    }
+
+    if (this._currentDefaultPermissionsState === null) {
+      this._checkbox.setAttribute("hidden", true);
+      this._permissionsDisableDescription.setAttribute("hidden", true);
+    } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
+      this._checkbox.checked = true;
+    } else {
+      this._checkbox.checked = false;
+    }
+
+    if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) {
+      this._checkbox.disabled = true;
+    }
+  },
+
+  /**
+  * Listen for changes to the permissions.default.* pref and make
+  * necessary changes to the UI.
+  */
+  _watchPermissionPrefChange() {
+    this._handleCheckboxUIUpdates();
+
+    if (this._type == "desktop-notification") {
+      this._handleWebNotificationsDisable();
+
+      this._disableExtensionButton.addEventListener(
+        "command",
+        makeDisableControllingExtension(PREF_SETTING_TYPE, NOTIFICATIONS_PERMISSION_OVERRIDE_KEY)
+      );
+    }
+
+    let observer = () => {
+      this._handleCheckboxUIUpdates();
+      if (this._type == "desktop-notification") {
+        this._handleWebNotificationsDisable();
+      }
+    };
+    Services.prefs.addObserver(this._defaultPermissionStatePrefName, observer);
+    window.addEventListener("unload", () => {
+      Services.prefs.removeObserver(this._defaultPermissionStatePrefName, observer);
+    });
+  },
+
+  /**
+  * Handles the UI update for web notifications disable by extensions.
+  */
+  async _handleWebNotificationsDisable() {
+    let prefLocked = Services.prefs.prefIsLocked(NOTIFICATIONS_PERMISSION_PREF);
+    if (prefLocked) {
+      // An extension can't control these settings if they're locked.
+      hideControllingExtension(NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
+    } else {
+      let isControlled = await handleControllingExtension(PREF_SETTING_TYPE, NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
+      this._checkbox.disabled = isControlled;
+    }
+  },
+
   _getCapabilityString(capability) {
     let stringKey = null;
     switch (capability) {
     case Services.perms.ALLOW_ACTION:
       stringKey = "permissions-capabilities-allow";
       break;
     case Services.perms.DENY_ACTION:
       stringKey = "permissions-capabilities-block";
--- a/browser/components/preferences/sitePermissions.xul
+++ b/browser/components/preferences/sitePermissions.xul
@@ -14,20 +14,22 @@
         data-l10n-id="permissions-window"
         data-l10n-attrs="title, style"
         onload="gSitePermissionsManager.onLoad();"
         onunload="gSitePermissionsManager.uninit();"
         persist="screenX screenY width height"
         onkeypress="gSitePermissionsManager.onWindowKeyPress(event);">
 
   <linkset>
+    <link rel="localization" href="browser/preferences/preferences.ftl"/>
     <link rel="localization" href="browser/preferences/permissions.ftl"/>
   </linkset>
 
   <script src="chrome://browser/content/preferences/sitePermissions.js"/>
+  <script type="application/javascript" src="chrome://browser/content/preferences/in-content/extensionControlled.js"/>
 
   <keyset>
     <key data-l10n-id="permissions-close-key" modifiers="accel" oncommand="window.close();"/>
   </keyset>
 
   <vbox class="contentPane largeDialogContainer" flex="1">
     <description id="permissionsText" control="url"/>
     <separator class="thin"/>
@@ -58,16 +60,23 @@
               data-l10n-id="permissions-remove-all"
               icon="clear"
               oncommand="gSitePermissionsManager.onAllPermissionsDelete();"/>
     </hbox>
     <spacer flex="1"/>
     <checkbox id="permissionsDisableCheckbox"/>
     <description id="permissionsDisableDescription"/>
     <spacer flex="1"/>
+    <hbox id="browserNotificationsPermissionExtensionContent" 
+          class="extension-controlled" align="center" hidden="true">
+      <description control="disableNotificationsPermissionExtension" flex="1"/>
+      <button id="disableNotificationsPermissionExtension"
+              class="extension-controlled-button accessory-button"
+              data-l10n-id="disable-extension"/>
+    </hbox>
     <hbox class="actionButtons" align="right" flex="1">
       <button oncommand="close();" icon="close" id="cancel"
               data-l10n-id="permissions-button-cancel" />
       <button id="btnApplyChanges" oncommand="gSitePermissionsManager.onApplyChanges();" icon="save"
               data-l10n-id="permissions-button-ok" />
     </hbox>
   </vbox>
 </window>
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -92,16 +92,20 @@ restart-later = Restart Later
 # This string is shown to notify the user that their home page
 # is being controlled by an extension.
 extension-controlled-homepage-override = An extension, <img data-l10n-name="icon"/> { $name }, is controlling your home page.
 
 # This string is shown to notify the user that their new tab page
 # is being controlled by an extension.
 extension-controlled-new-tab-url = An extension, <img data-l10n-name="icon"/> { $name }, is controlling your New Tab page.
 
+# This string is shown to notify the user that their notifications permission
+# is being controlled by an extension.
+extension-controlled-web-notifications= An extension, <img data-l10n-name="icon"/> { $name }, is controlling this setting.
+
 # This string is shown to notify the user that the default search engine
 # is being controlled by an extension.
 extension-controlled-default-search = An extension, <img data-l10n-name="icon"/> { $name }, has set your default search engine.
 
 # This string is shown to notify the user that Container Tabs
 # are being enabled by an extension.
 extension-controlled-privacy-containers = An extension, <img data-l10n-name="icon"/> { $name }, requires Container Tabs.