Bug 1429593 - Part 2: Show that a WebExtension is managing the proxy config setting, r=jaws,mstriemer
☠☠ backed out by 814985e63c0e ☠ ☠
authorBob Silverberg <bsilverberg@mozilla.com>
Mon, 22 Jan 2018 11:49:42 -0500
changeset 402781 9ce1b89f874463196844012b121aada56d56f8a8
parent 402780 fa845d221e7a5847067714ac5ecea11ba893e057
child 402782 54134ad6f663dd42551c50f3cde5876f4941bee5
push id99659
push useraciure@mozilla.com
push dateWed, 07 Feb 2018 22:33:57 +0000
treeherdermozilla-inbound@5ceb1098fef3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws, mstriemer
bugs1429593
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1429593 - Part 2: Show that a WebExtension is managing the proxy config setting, r=jaws,mstriemer Update the general page of about:preferences, as well as the Connection Settings panel, to show when an extension is controlling proxy settings, and allow a user to disable the extension to regain control. MozReview-Commit-ID: HKYPkg78IOK
browser/components/preferences/connection.js
browser/components/preferences/connection.xul
browser/components/preferences/in-content/extensionControlled.js
browser/components/preferences/in-content/findInPage.js
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
browser/components/preferences/in-content/privacy.js
browser/components/preferences/in-content/tests/browser_extension_controlled.js
browser/locales/en-US/chrome/browser/preferences/advanced.dtd
browser/locales/en-US/chrome/browser/preferences/connection.dtd
browser/locales/en-US/chrome/browser/preferences/preferences.properties
--- a/browser/components/preferences/connection.js
+++ b/browser/components/preferences/connection.js
@@ -1,15 +1,16 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 /* 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 ../../base/content/utilityOverlay.js */
 /* import-globals-from ../../../toolkit/content/preferencesBindings.js */
+/* import-globals-from in-content/extensionControlled.js */
 
 Preferences.addAll([
   { id: "network.proxy.type", type: "int" },
   { id: "network.proxy.http", type: "string" },
   { id: "network.proxy.http_port", type: "int" },
   { id: "network.proxy.ftp", type: "string" },
   { id: "network.proxy.ftp_port", type: "int" },
   { id: "network.proxy.ssl", type: "string" },
@@ -31,16 +32,24 @@ Preferences.addAll([
   { id: "network.proxy.backup.socks_port", type: "int" },
 ]);
 
 window.addEventListener("DOMContentLoaded", () => {
   Preferences.get("network.proxy.type").on("change",
     gConnectionsDialog.proxyTypeChanged.bind(gConnectionsDialog));
   Preferences.get("network.proxy.socks_version").on("change",
     gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog));
+
+  document
+    .getElementById("disableProxyExtension")
+    .addEventListener(
+      "command", makeDisableControllingExtension(
+        PREF_SETTING_TYPE, PROXY_KEY).bind(gConnectionsDialog));
+  gConnectionsDialog.updateProxySettingsUI();
+  initializeProxyUI(gConnectionsDialog);
 }, { once: true, capture: true });
 
 var gConnectionsDialog = {
   beforeAccept() {
     var proxyTypePref = Preferences.get("network.proxy.type");
     if (proxyTypePref.value == 2) {
       this.doAutoconfigURLFixup();
       return true;
@@ -222,10 +231,45 @@ var gConnectionsDialog = {
     return undefined;
   },
 
   readHTTPProxyPort() {
     var shareProxiesPref = Preferences.get("network.proxy.share_proxy_settings");
     if (shareProxiesPref.value)
       this.updateProtocolPrefs();
     return undefined;
+  },
+
+  getProxyControls() {
+    let controlGroup = document.getElementById("networkProxyType");
+    return [
+      ...controlGroup.querySelectorAll(":scope > radio"),
+      ...controlGroup.querySelectorAll("label"),
+      ...controlGroup.querySelectorAll("textbox"),
+      ...controlGroup.querySelectorAll("checkbox"),
+      ...document.querySelectorAll("#networkProxySOCKSVersion > radio"),
+      ...document.querySelectorAll("#ConnectionsDialogPane > checkbox"),
+    ];
+  },
+
+  // Update the UI to show/hide the extension controlled message for
+  // proxy settings.
+  async updateProxySettingsUI() {
+    let isLocked = API_PROXY_PREFS.some(
+      pref => Services.prefs.prefIsLocked(pref));
+
+    function setInputsDisabledState(isControlled) {
+      let disabled = isLocked || isControlled;
+      for (let element of gConnectionsDialog.getProxyControls()) {
+        element.disabled = disabled;
+      }
+    }
+
+    if (isLocked) {
+      // An extension can't control this setting if any pref is locked.
+      hideControllingExtension(PROXY_KEY);
+      setInputsDisabledState(false);
+    } else {
+      handleControllingExtension(PREF_SETTING_TYPE, PROXY_KEY)
+        .then(setInputsDisabledState);
+    }
   }
 };
--- a/browser/components/preferences/connection.xul
+++ b/browser/components/preferences/connection.xul
@@ -4,16 +4,18 @@
    - 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/. -->
 
 <!DOCTYPE dialog [
   <!ENTITY % preferencesDTD SYSTEM "chrome://global/locale/preferences.dtd">
   %preferencesDTD;
   <!ENTITY % connectionDTD SYSTEM "chrome://browser/locale/preferences/connection.dtd">
   %connectionDTD;
+  <!ENTITY % mainDTD SYSTEM "chrome://browser/locale/preferences/main.dtd">
+  %mainDTD;
 ]>
 
 <?xml-stylesheet href="chrome://global/skin/"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
 
 <dialog id="ConnectionsDialog" type="child" class="prefwindow"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="&connectionsDialog.title;"
@@ -30,28 +32,37 @@
 #ifdef XP_MACOSX
         style="width: &window.macWidth2; !important;">
 #else
         style="width: &window.width2; !important;">
 #endif
 
   <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/>
+  <script type="application/javascript" src="chrome://browser/content/preferences/in-content/extensionControlled.js"/>
 
   <keyset>
     <key key="&windowClose.key;" modifiers="accel" oncommand="Preferences.close(event)"/>
   </keyset>
 
   <vbox id="ConnectionsDialogPane" class="prefpane largeDialogContainer">
 
-    <stringbundle id="preferencesBundle" src="chrome://browser/locale/preferences/preferences.properties"/>
+    <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/>
+    <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/>
     <script type="application/javascript" src="chrome://browser/content/preferences/connection.js"/>
 
+    <hbox id="proxyExtensionContent" align="top" hidden="true">
+      <description control="disableProxyExtension" flex="1" />
+      <button id="disableProxyExtension"
+              class="extension-controlled-button accessory-button"
+              label="&disableExtension.label;" />
+    </hbox>
+
     <groupbox>
-      <caption><label>&proxyTitle.label;</label></caption>
+      <caption><label>&proxyTitle.label2;</label></caption>
 
       <radiogroup id="networkProxyType" preference="network.proxy.type"
                   onsyncfrompreference="return gConnectionsDialog.readProxyType();">
         <radio value="0" label="&noProxyTypeRadio.label;" accesskey="&noProxyTypeRadio.accesskey;"/>
         <radio value="4" label="&WPADTypeRadio.label;" accesskey="&WPADTypeRadio.accesskey;"/>
         <radio value="5" label="&systemTypeRadio.label;" accesskey="&systemTypeRadio.accesskey;" id="systemPref" hidden="true"/>
         <radio value="1" label="&manualTypeRadio2.label;" accesskey="&manualTypeRadio2.accesskey;"/>
         <grid class="indent" flex="1">
--- a/browser/components/preferences/in-content/extensionControlled.js
+++ b/browser/components/preferences/in-content/extensionControlled.js
@@ -1,44 +1,76 @@
 /* - 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 preferences.js */
+
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "DeferredTask",
+                                  "resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
                                   "resource://gre/modules/ExtensionSettingsStore.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "trackingprotectionUiEnabled",
                                       "privacy.trackingprotection.ui.enabled");
 
 const PREF_SETTING_TYPE = "prefs";
+const PROXY_KEY = "proxyConfig";
+const API_PROXY_PREFS = [
+  "network.proxy.type",
+  "network.proxy.http",
+  "network.proxy.http_port",
+  "network.proxy.share_proxy_settings",
+  "network.proxy.ftp",
+  "network.proxy.ftp_port",
+  "network.proxy.ssl",
+  "network.proxy.ssl_port",
+  "network.proxy.socks",
+  "network.proxy.socks_port",
+  "network.proxy.socks_version",
+  "network.proxy.socks_remote_dns",
+  "network.proxy.no_proxies_on",
+  "network.proxy.autoconfig_url",
+  "signon.autologin.proxy",
+];
 
 let extensionControlledContentIds = {
   "privacy.containers": "browserContainersExtensionContent",
   "homepage_override": "browserHomePageExtensionContent",
   "newTabURL": "browserNewTabExtensionContent",
   "defaultSearch": "browserDefaultSearchExtensionContent",
+  "proxyConfig": "proxyExtensionContent",
   get "websites.trackingProtectionMode"() {
     return {
       button: "trackingProtectionExtensionContentButton",
       section:
         trackingprotectionUiEnabled ?
           "trackingProtectionExtensionContentLabel" :
           "trackingProtectionPBMExtensionContentLabel",
     };
   }
 };
 
+function getExtensionControlledArgs(settingName) {
+  switch (settingName) {
+    case "proxyConfig":
+      return [document.getElementById("bundleBrand").getString("brandShortName")];
+    default:
+      return [];
+  }
+}
+
 let extensionControlledIds = {};
 
 /**
   * Check if a pref is being managed by an extension.
   */
 async function getControllingExtensionInfo(type, settingName) {
   await ExtensionSettingsStore.initialize();
   return ExtensionSettingsStore.getSetting(type, settingName);
@@ -52,67 +84,77 @@ function getControllingExtensionEls(sett
     section.querySelector("button");
   return {
     section,
     button,
     description: section.querySelector("description"),
   };
 }
 
-async function handleControllingExtension(type, settingName) {
+async function getControllingExtension(type, settingName) {
   let info = await getControllingExtensionInfo(type, settingName);
   let addon = info && info.id
     && await AddonManager.getAddonByID(info.id);
+  return addon;
+}
+
+async function handleControllingExtension(type, settingName) {
+  let addon = await getControllingExtension(type, settingName);
 
   // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
   // an extension is controlling a setting but the extension has been uninstalled
   // outside of the regular lifecycle. If the extension isn't currently installed
   // then we should treat the setting as not being controlled.
   // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
   if (addon) {
-    extensionControlledIds[settingName] = info.id;
+    extensionControlledIds[settingName] = addon.id;
     showControllingExtension(settingName, addon);
   } else {
     let elements = getControllingExtensionEls(settingName);
     if (extensionControlledIds[settingName]
         && !document.hidden
         && elements.button) {
       showEnableExtensionMessage(settingName);
     } else {
       hideControllingExtension(settingName);
     }
     delete extensionControlledIds[settingName];
   }
 
   return !!addon;
 }
 
+function getControllingExtensionFragment(settingName, addon, ...extraArgs) {
+  let msg = document.getElementById("bundlePreferences")
+                    .getString(`extensionControlled.${settingName}`);
+  let image = document.createElement("image");
+  const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+  image.setAttribute("src", addon.iconURL || defaultIcon);
+  image.classList.add("extension-controlled-icon");
+  let addonBit = document.createDocumentFragment();
+  addonBit.appendChild(image);
+  addonBit.appendChild(document.createTextNode(" " + addon.name));
+  return BrowserUtils.getLocalizedFragment(document, msg, addonBit, ...extraArgs);
+}
+
 async function showControllingExtension(settingName, addon) {
   // Tell the user what extension is controlling the setting.
   let elements = getControllingExtensionEls(settingName);
+  let extraArgs = getExtensionControlledArgs(settingName);
 
   elements.section.classList.remove("extension-controlled-disabled");
   let description = elements.description;
 
   // Remove the old content from the description.
   while (description.firstChild) {
     description.firstChild.remove();
   }
 
-  // Populate the description.
-  let msg = document.getElementById("bundlePreferences")
-                    .getString(`extensionControlled.${settingName}`);
-  let image = document.createElement("image");
-  const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
-  image.setAttribute("src", addon.iconURL || defaultIcon);
-  image.classList.add("extension-controlled-icon");
-  let addonBit = document.createDocumentFragment();
-  addonBit.appendChild(image);
-  addonBit.appendChild(document.createTextNode(" " + addon.name));
-  let fragment = BrowserUtils.getLocalizedFragment(document, msg, addonBit);
+  let fragment = getControllingExtensionFragment(
+    settingName, addon, ...extraArgs);
   description.appendChild(fragment);
 
   if (elements.button) {
     elements.button.hidden = false;
   }
 
   // Show the controlling extension row and hide the old label.
   elements.section.hidden = false;
@@ -155,8 +197,25 @@ function showEnableExtensionMessage(sett
 
 function makeDisableControllingExtension(type, settingName) {
   return async function disableExtension() {
     let {id} = await getControllingExtensionInfo(type, settingName);
     let addon = await AddonManager.getAddonByID(id);
     addon.userDisabled = true;
   };
 }
+
+function initializeProxyUI(container) {
+  let deferredUpdate = new DeferredTask(() => {
+    container.updateProxySettingsUI();
+  }, 10);
+  let proxyObserver = {
+    observe: (subject, topic, data) => {
+      if (API_PROXY_PREFS.includes(data)) {
+        deferredUpdate.arm();
+      }
+    },
+  };
+  Services.prefs.addObserver("", proxyObserver);
+  window.addEventListener("unload", () => {
+    Services.prefs.removeObserver("", proxyObserver);
+  });
+}
--- a/browser/components/preferences/in-content/findInPage.js
+++ b/browser/components/preferences/in-content/findInPage.js
@@ -1,12 +1,13 @@
 /* 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 extensionControlled.js */
 /* import-globals-from preferences.js */
 
 var gSearchResultsPane = {
   listSearchTooltips: new Set(),
   listSearchMenuitemIndicators: new Set(),
   searchInput: null,
   inited: false,
 
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -363,16 +363,23 @@ var gMainPane = {
           handleControllingExtension(URL_OVERRIDES_TYPE, NEW_TAB_KEY);
       },
     };
     Services.obs.addObserver(newTabObserver, "newtab-url-changed");
     window.addEventListener("unload", () => {
       Services.obs.removeObserver(newTabObserver, "newtab-url-changed");
     });
 
+    let connectionSettingsLink = document.getElementById("connectionSettingsLearnMore");
+    let connectionSettingsUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") +
+                                "prefs-connection-settings";
+    connectionSettingsLink.setAttribute("href", connectionSettingsUrl);
+    this.updateProxySettingsUI();
+    initializeProxyUI(gMainPane);
+
     if (AppConstants.platform == "win") {
       // Functionality for "Show tabs in taskbar" on Windows 7 and up.
       try {
         let ver = parseFloat(Services.sysinfo.getProperty("version"));
         let showTabsInTaskbar = document.getElementById("showTabsInTaskbar");
         showTabsInTaskbar.hidden = ver < 6.1;
       } catch (ex) { }
     }
@@ -1086,17 +1093,38 @@ var gMainPane = {
     gSubDialog.open("chrome://browser/content/preferences/colors.xul", "resizable=no");
   },
 
   // NETWORK
   /**
    * Displays a dialog in which proxy settings may be changed.
    */
   showConnections() {
-    gSubDialog.open("chrome://browser/content/preferences/connection.xul");
+    gSubDialog.open("chrome://browser/content/preferences/connection.xul",
+                    null, null, this.updateProxySettingsUI.bind(this));
+  },
+
+  // Update the UI to show the proper description depending on whether an
+  // extension is in control or not.
+  async updateProxySettingsUI() {
+    let controllingExtension = await getControllingExtension(PREF_SETTING_TYPE, PROXY_KEY);
+    let fragment = controllingExtension ?
+      getControllingExtensionFragment(PROXY_KEY, controllingExtension, this._brandShortName) :
+      BrowserUtils.getLocalizedFragment(
+        document,
+        this._prefsBundle.getString("connectionDesc.label"),
+        this._brandShortName);
+    let description = document.getElementById("connectionSettingsDescription");
+
+    // Remove the old content from the description.
+    while (description.firstChild) {
+      description.firstChild.remove();
+    }
+
+    description.appendChild(fragment);
   },
 
   checkBrowserContainers(event) {
     let checkbox = document.getElementById("browserContainersCheckbox");
     if (checkbox.checked) {
       Services.prefs.setBoolPref("privacy.userContext.enabled", true);
       return;
     }
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -744,17 +744,25 @@
   <label class="header-name" flex="1">&networkProxy.label;</label>
 </hbox>
 
 <!-- Network Proxy-->
 <groupbox id="connectionGroup" data-category="paneGeneral" hidden="true">
   <caption class="search-header" hidden="true"><label>&networkProxy.label;</label></caption>
 
   <hbox align="center">
-    <description flex="1" control="connectionSettings">&connectionDesc.label;</description>
+    <hbox align="center" flex="1">
+      <description id="connectionSettingsDescription" control="connectionSettings"></description>
+      <spacer width="5"/>
+      <label id="connectionSettingsLearnMore" class="learnMore text-link">
+        &connectionSettingsLearnMore.label;
+      </label>
+      <separator orient="vertical"/>
+    </hbox>
+
     <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
     <hbox>
       <button id="connectionSettings"
               class="accessory-button"
               icon="network"
               label="&connectionSettings.label;"
               accesskey="&connectionSettings.accesskey;"
               searchkeywords="&connectionsDialog.title;
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -1,12 +1,13 @@
 /* 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 extensionControlled.js */
 /* import-globals-from preferences.js */
 
 /* FIXME: ESlint globals workaround should be removed once bug 1395426 gets fixed */
 /* globals DownloadUtils, LoadContextInfo */
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
 
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -1,15 +1,18 @@
 /* eslint-env webextensions */
 
+const PROXY_PREF = "network.proxy.type";
+
 ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
                                "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 + "/";
 
 function getSupportsFile(path) {
   let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
     .getService(Ci.nsIChromeRegistry);
   let uri = Services.io.newURI(CHROME_URL_ROOT + path);
@@ -43,36 +46,47 @@ function waitForMutation(target, opts, c
         observer.disconnect();
         resolve();
       }
     });
     observer.observe(target, opts);
   });
 }
 
-function waitForMessageChange(id, cb, opts = { attributes: true, attributeFilter: ["hidden"] }) {
-  // eslint-disable-next-line mozilla/no-cpows-in-tests
-  return waitForMutation(gBrowser.contentDocument.getElementById(id), opts, cb);
+function waitForMessageChange(element, cb, opts = { attributes: true, attributeFilter: ["hidden"] }) {
+  return waitForMutation(element, opts, cb);
 }
 
-function waitForMessageHidden(messageId) {
-  return waitForMessageChange(messageId, target => target.hidden);
+// eslint-disable-next-line mozilla/no-cpows-in-tests
+function getElement(id, doc = gBrowser.contentDocument) {
+  return doc.getElementById(id);
+}
+
+function waitForMessageHidden(messageId, doc) {
+  return waitForMessageChange(getElement(messageId, doc), target => target.hidden);
 }
 
-function waitForMessageShown(messageId) {
-  return waitForMessageChange(messageId, target => !target.hidden);
+function waitForMessageShown(messageId, doc) {
+  return waitForMessageChange(getElement(messageId, doc), target => !target.hidden);
 }
 
-function waitForEnableMessage(messageId) {
+function waitForEnableMessage(messageId, doc) {
   return waitForMessageChange(
-    messageId,
+    getElement(messageId, doc),
     target => target.classList.contains("extension-controlled-disabled"),
     { attributeFilter: ["class"], attributes: true });
 }
 
+function waitForMessageContent(messageId, content, doc) {
+  return waitForMessageChange(
+    getElement(messageId, doc),
+    target => target.textContent === content,
+    { childList: true });
+}
+
 add_task(async function testExtensionControlledHomepage() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
   // eslint-disable-next-line mozilla/no-cpows-in-tests
   let doc = gBrowser.contentDocument;
   is(gBrowser.currentURI.spec, "about:preferences#general",
      "#general should be in the URI for about:preferences");
   let homepagePref = () => Services.prefs.getCharPref("browser.startup.homepage");
   let originalHomepagePref = homepagePref();
@@ -599,8 +613,198 @@ add_task(async function testExtensionCon
   // ExtensionPreferencesManager to clean up properly.
   // TODO: BUG 1408226
   await reEnableExtension(addon);
 
   await extension.unload();
 
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
+
+add_task(async function testExtensionControlledProxyConfig() {
+  const proxySvc = Ci.nsIProtocolProxyService;
+  const PROXY_DEFAULT = proxySvc.PROXYCONFIG_SYSTEM;
+  const EXTENSION_ID = "@set_proxy";
+  const CONTROLLED_SECTION_ID = "proxyExtensionContent";
+  const CONTROLLED_BUTTON_ID = "disableProxyExtension";
+  const CONNECTION_SETTINGS_DESC_ID = "connectionSettingsDescription";
+  const PANEL_URL = "chrome://browser/content/preferences/connection.xul";
+
+  await SpecialPowers.pushPrefEnv({"set": [[PROXY_PREF, PROXY_DEFAULT]]});
+
+  function background() {
+    browser.browserSettings.proxyConfig.set({value: {proxyType: "none"}});
+  }
+
+  function expectedConnectionSettingsMessage(doc, isControlled) {
+    let brandShortName = doc.getElementById("bundleBrand").getString("brandShortName");
+    return isControlled ?
+      `An extension,  set_proxy, is controlling how ${brandShortName} connects to the internet.` :
+      `Configure how ${brandShortName} connects to the internet.`;
+  }
+
+  function connectionSettingsMessagePromise(doc, isControlled) {
+    return waitForMessageContent(
+      CONNECTION_SETTINGS_DESC_ID,
+      expectedConnectionSettingsMessage(doc, isControlled)
+    );
+  }
+
+  function verifyState(doc, isControlled) {
+    let isPanel = doc.getElementById(CONTROLLED_BUTTON_ID);
+    let brandShortName = doc.getElementById("bundleBrand").getString("brandShortName");
+    is(proxyType === proxySvc.PROXYCONFIG_DIRECT, isControlled,
+      "Proxy pref is set to the expected value.");
+
+    if (isPanel) {
+      let controlledSection = doc.getElementById(CONTROLLED_SECTION_ID);
+
+      is(controlledSection.hidden, !isControlled, "The extension controlled row's visibility is as expected.");
+      if (isPanel) {
+        is(doc.getElementById(CONTROLLED_BUTTON_ID).hidden, !isControlled,
+           "The disable extension button's visibility is as expected.");
+      }
+      if (isControlled) {
+        let controlledDesc = controlledSection.querySelector("description");
+        // There are two spaces before "set_proxy" because it's " <image /> set_proxy".
+        is(controlledDesc.textContent, `An extension,  set_proxy, is controlling how ${brandShortName} connects to the internet.`,
+          "The user is notified that an extension is controlling proxy settings.");
+      }
+      function getProxyControls() {
+        let controlGroup = doc.getElementById("networkProxyType");
+        return [
+          ...controlGroup.querySelectorAll(":scope > radio"),
+          ...controlGroup.querySelectorAll("label"),
+          ...controlGroup.querySelectorAll("textbox"),
+          ...controlGroup.querySelectorAll("checkbox"),
+          ...doc.querySelectorAll("#networkProxySOCKSVersion > radio"),
+          ...doc.querySelectorAll("#ConnectionsDialogPane > checkbox"),
+        ];
+      }
+      let controlState = isControlled ? "disabled" : "enabled";
+      for (let element of getProxyControls()) {
+        is(element.disabled, isControlled, `Proxy controls are ${controlState}.`);
+      }
+    } else {
+      is(doc.getElementById(CONNECTION_SETTINGS_DESC_ID).textContent,
+         expectedConnectionSettingsMessage(doc, isControlled),
+         "The connection settings description is as expected.");
+    }
+  }
+
+  async function disableViaClick() {
+    let sectionId = CONTROLLED_SECTION_ID;
+    let controlledSection = panelDoc.getElementById(sectionId);
+
+    let enableMessageShown = waitForEnableMessage(sectionId, panelDoc);
+    panelDoc.getElementById(CONTROLLED_BUTTON_ID).click();
+    await enableMessageShown;
+
+    // The user is notified how to enable the extension.
+    let controlledDesc = controlledSection.querySelector("description");
+    is(controlledDesc.textContent, "To enable the extension go to  Add-ons in the  menu.",
+       "The user is notified of how to enable the extension again");
+
+    // The user can dismiss the enable instructions.
+    let hidden = waitForMessageHidden(sectionId, panelDoc);
+    controlledSection.querySelector("image:last-of-type").click();
+    return hidden;
+  }
+
+  async function reEnableExtension(addon) {
+    let messageChanged = connectionSettingsMessagePromise(mainDoc, true);
+    addon.userDisabled = false;
+    await messageChanged;
+  }
+
+  async function openProxyPanel() {
+    let panel = await openAndLoadSubDialog(PANEL_URL);
+    let closingPromise = waitForEvent(panel.document.documentElement, "dialogclosing");
+    ok(panel, "Proxy panel opened.");
+    return {panel, closingPromise};
+  }
+
+  async function closeProxyPanel(panelObj) {
+    panelObj.panel.document.documentElement.cancelDialog();
+    let panelClosingEvent = await panelObj.closingPromise;
+    ok(panelClosingEvent, "Proxy panel closed.");
+  }
+
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  // eslint-disable-next-line mozilla/no-cpows-in-tests
+  let mainDoc = gBrowser.contentDocument;
+
+  is(gBrowser.currentURI.spec, "about:preferences#general",
+   "#general should be in the URI for about:preferences");
+
+  verifyState(mainDoc, false);
+
+  // Open the connections panel.
+  let panelObj = await openProxyPanel();
+  let panelDoc = panelObj.panel.document;
+
+  verifyState(panelDoc, false);
+
+  await closeProxyPanel(panelObj);
+
+  verifyState(mainDoc, false);
+
+  // Install an extension that sets Tracking Protection.
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "permanent",
+    manifest: {
+      name: "set_proxy",
+      applications: {gecko: {id: EXTENSION_ID}},
+      permissions: ["browserSettings"],
+    },
+    background,
+  });
+
+  let messageChanged = connectionSettingsMessagePromise(mainDoc, true);
+  await extension.startup();
+  await messageChanged;
+  let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+
+  verifyState(mainDoc, true);
+  messageChanged = connectionSettingsMessagePromise(mainDoc, false);
+
+  panelObj = await openProxyPanel();
+  panelDoc = panelObj.panel.document;
+
+  verifyState(panelDoc, true);
+
+  await disableViaClick();
+
+  verifyState(panelDoc, false);
+
+  await closeProxyPanel(panelObj);
+  await messageChanged;
+
+  verifyState(mainDoc, false);
+
+  await reEnableExtension(addon);
+
+  verifyState(mainDoc, true);
+  messageChanged = connectionSettingsMessagePromise(mainDoc, false);
+
+  panelObj = await openProxyPanel();
+  panelDoc = panelObj.panel.document;
+
+  verifyState(panelDoc, true);
+
+  await disableViaClick();
+
+  verifyState(panelDoc, false);
+
+  await closeProxyPanel(panelObj);
+  await messageChanged;
+
+  verifyState(mainDoc, false);
+
+  // Enable the extension so we get the UNINSTALL event, which is needed by
+  // ExtensionPreferencesManager to clean up properly.
+  // TODO: BUG 1408226
+  await reEnableExtension(addon);
+
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
@@ -40,17 +40,17 @@ available. -->
 <!ENTITY alwaysSubmitCrashReports1.label  "Allow &brandShortName; to send crash reports to Mozilla">
 <!ENTITY alwaysSubmitCrashReports1.accesskey "c">
 <!ENTITY crashReporterLearnMore.label    "Learn more">
 
 <!ENTITY networkTab.label                "Network">
 
 <!ENTITY networkProxy.label              "Network Proxy">
 
-<!ENTITY connectionDesc.label            "Configure how &brandShortName; connects to the Internet">
+<!ENTITY connectionSettingsLearnMore.label "Learn more">
 <!ENTITY connectionSettings.label        "Settingsā€¦">
 <!ENTITY connectionSettings.accesskey    "e">
 
 <!ENTITY httpCache.label                 "Cached Web Content">
 
 <!--  Site Data section manages sites using Storage API and is under Network -->
 <!ENTITY siteData.label                  "Site Data">
 <!ENTITY clearSiteData.label             "Clear All Data">
--- a/browser/locales/en-US/chrome/browser/preferences/connection.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/connection.dtd
@@ -2,17 +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  connectionsDialog.title       "Connection Settings">
 <!ENTITY  window.width2                 "49em">
 <!ENTITY  window.macWidth2              "44em">
 
-<!ENTITY  proxyTitle.label              "Configure Proxies to Access the Internet">
+<!ENTITY  proxyTitle.label2             "Configure Proxy Access to the Internet">
 <!ENTITY  noProxyTypeRadio.label        "No proxy">
 <!ENTITY  noProxyTypeRadio.accesskey    "y">
 <!ENTITY  systemTypeRadio.label         "Use system proxy settings">
 <!ENTITY  systemTypeRadio.accesskey     "u">
 <!ENTITY  WPADTypeRadio.label           "Auto-detect proxy settings for this network">
 <!ENTITY  WPADTypeRadio.accesskey       "w">
 <!ENTITY  manualTypeRadio2.label        "Manual proxy configuration">
 <!ENTITY  manualTypeRadio2.accesskey    "m">
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -290,13 +290,23 @@ extensionControlled.defaultSearch = An e
 # This string is shown to notify the user that Container Tabs are being enabled by an extension
 # %S is the container addon controlling it
 extensionControlled.privacy.containers = An extension, %S, requires Container Tabs.
 
 # LOCALIZATION NOTE (extensionControlled.websites.trackingProtectionMode):
 # This string is shown to notify the user that their tracking protection preferences are being controlled by an extension.
 extensionControlled.websites.trackingProtectionMode = An extension, %S, is controlling tracking protection.
 
+# LOCALIZATION NOTE (extensionControlled.proxyConfig):
+# This string is shown to notify the user that their proxy configuration preferences are being controlled by an extension.
+# %1$S is the icon and name of the extension.
+# %2$S is the brandShortName from brand.properties (for example "Nightly")
+extensionControlled.proxyConfig = An extension, %1$S, is controlling how %2$S connects to the internet.
+
 # LOCALIZATION NOTE (extensionControlled.enable):
 # %1$S is replaced with the icon for the add-ons menu.
 # %2$S is replaced with the icon for the toolbar menu.
 # This string is shown to notify the user how to enable an extension that they disabled.
 extensionControlled.enable = To enable the extension go to %1$S Add-ons in the %2$S menu.
+
+# LOCALIZATION NOTE (connectionDesc.label):
+# %S is the brandShortName from brand.properties (for example "Nightly")
+connectionDesc.label = Configure how %S connects to the internet.