Bug 1151509 - Implement the front-end side of the warning about add-ons detected as no longer signed during the periodic check. r=dtownsend
authorDão Gottwald <dao@mozilla.com>
Fri, 08 May 2015 19:27:25 +0200
changeset 274507 ac36a281c5c13c27d724cc5b04739df300dfd913
parent 274506 c4b635e71477f85c23c6392be1142555dfadaf2b
child 274508 dbec5281f6255715621a258a15bd2b6baee25df8
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdtownsend
bugs1151509
milestone40.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 1151509 - Implement the front-end side of the warning about add-ons detected as no longer signed during the periodic check. r=dtownsend
browser/app/profile/firefox.js
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.properties
toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xul
toolkit/mozapps/extensions/test/browser/browser_list.js
toolkit/themes/shared/extensions/extensions.inc.css
toolkit/themes/shared/in-content/common.inc.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -43,16 +43,17 @@ pref("xpinstall.customConfirmationUI", t
 pref("extensions.getAddons.cache.enabled", true);
 pref("extensions.getAddons.maxResults", 15);
 pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%");
 pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
 pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
 pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%?src=firefox");
 pref("extensions.webservice.discoverURL", "https://services.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%");
 pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/%APP%/api/%API_VERSION%/list/recommended/all/%MAX_RESULTS%/%OS%/%VERSION%?src=firefox");
+pref("extensions.getAddons.link.url", "https://addons.mozilla.org/%LOCALE%/firefox/");
 
 // Blocklist preferences
 pref("extensions.blocklist.enabled", true);
 pref("extensions.blocklist.interval", 86400);
 // Controls what level the blocklist switches from warning about items to forcibly
 // blocking them.
 pref("extensions.blocklist.level", 2);
 pref("extensions.blocklist.url", "https://blocklist.addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
@@ -66,16 +67,17 @@ pref("extensions.hotfix.cert.checkAttrib
 pref("extensions.hotfix.certs.1.sha1Fingerprint", "91:53:98:0C:C1:86:DF:47:8F:35:22:9E:11:C9:A7:31:04:49:A1:AA");
 
 // Disable add-ons that are not installed by the user in all scopes by default.
 // See the SCOPE constants in AddonManager.jsm for values to use here.
 pref("extensions.autoDisableScopes", 15);
 
 // Don't require signed add-ons by default
 pref("xpinstall.signatures.required", false);
+pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
 
 // Dictionary download preference
 pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
 
 // At startup, should we check to see if the installation
 // date is older than some threshold
 pref("app.update.checkInstallTime", true);
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -500,16 +500,20 @@ BrowserGlue.prototype = {
             Services.prefs.clearUserPref("privacy.trackingprotection.ui.enabled");
           }
         }
         break;
 #endif
       case "flash-plugin-hang":
         this._handleFlashHang();
         break;
+      case "xpi-signature-changed":
+        if (JSON.parse(data).disabled.length)
+          this._notifyUnsignedAddonsDisabled();
+        break;
     }
   },
 
   _syncSearchEngines: function () {
     // Only do this if the search service is already initialized. This function
     // gets called in finalUIStartup and from a browser-search-service observer,
     // to catch both cases (search service initialization occurring before and
     // after final-ui-startup)
@@ -548,16 +552,17 @@ BrowserGlue.prototype = {
     os.addObserver(this, "profile-before-change", false);
 #ifdef MOZ_SERVICES_HEALTHREPORT
     os.addObserver(this, "keyword-search", false);
 #endif
     os.addObserver(this, "browser-search-engine-modified", false);
     os.addObserver(this, "browser-search-service", false);
     os.addObserver(this, "restart-in-safe-mode", false);
     os.addObserver(this, "flash-plugin-hang", false);
+    os.addObserver(this, "xpi-signature-changed", false);
 
     this._flashHangCount = 0;
   },
 
   // cleanup (called on application shutdown)
   _dispose: function BG__dispose() {
     let os = Services.obs;
     os.removeObserver(this, "prefservice:after-app-defaults");
@@ -595,16 +600,17 @@ BrowserGlue.prototype = {
     try {
       os.removeObserver(this, "browser-search-service");
       // may have already been removed by the observer
     } catch (ex) {}
 #ifdef NIGHTLY_BUILD
     Services.prefs.removeObserver(POLARIS_ENABLED, this);
 #endif
     os.removeObserver(this, "flash-plugin-hang");
+    os.removeObserver(this, "xpi-signature-changed");
   },
 
   _onAppDefaults: function BG__onAppDefaults() {
     // apply distribution customizations (prefs)
     // other customizations are applied in _finalUIStartup()
     this._distributionCustomizer.applyPrefDefaults();
   },
 
@@ -892,16 +898,37 @@ BrowserGlue.prototype = {
     ];
 
     let nb = win.document.getElementById("global-notificationbox");
     nb.appendNotification(message, "reset-unused-profile",
                           "chrome://global/skin/icons/question-16.png",
                           nb.PRIORITY_INFO_LOW, buttons);
   },
 
+  _notifyUnsignedAddonsDisabled: function () {
+    let win = this.getMostRecentBrowserWindow();
+    if (!win)
+      return;
+
+    let message = win.gNavigatorBundle.getString("unsignedAddonsDisabled.message");
+    let buttons = [
+      {
+        label:     win.gNavigatorBundle.getString("unsignedAddonsDisabled.learnMore.label"),
+        accessKey: win.gNavigatorBundle.getString("unsignedAddonsDisabled.learnMore.accesskey"),
+        callback: function () {
+          win.BrowserOpenAddonsMgr("addons://list/extension?unsigned=true");
+        }
+      },
+    ];
+
+    let nb = win.document.getElementById("high-priority-global-notificationbox");
+    nb.appendNotification(message, "unsigned-addons-disabled", "",
+                          nb.PRIORITY_WARNING_MEDIUM, buttons);
+  },
+
   _firstWindowTelemetry: function(aWindow) {
 #ifdef XP_WIN
     let SCALING_PROBE_NAME = "DISPLAY_SCALING_MSWIN";
 #elifdef XP_MACOSX
     let SCALING_PROBE_NAME = "DISPLAY_SCALING_OSX";
 #elifdef XP_LINUX
     let SCALING_PROBE_NAME = "DISPLAY_SCALING_LINUX";
 #else
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -91,16 +91,20 @@ addonError-5=#3 has prevented this site 
 addonLocalError-1=This add-on could not be installed because of a filesystem error.
 addonLocalError-2=This add-on could not be installed because it does not match the add-on #3 expected.
 addonLocalError-3=This add-on could not be installed because it appears to be corrupt.
 addonLocalError-4=#1 could not be installed because #3 cannot modify the needed file.
 addonLocalError-5=This add-on could not be installed because it has not been verified.
 addonErrorIncompatible=#1 could not be installed because it is not compatible with #3 #4.
 addonErrorBlocklisted=#1 could not be installed because it has a high risk of causing stability or security problems.
 
+unsignedAddonsDisabled.message=One or more installed add-ons cannot be verified and have been disabled.
+unsignedAddonsDisabled.learnMore.label=Learn More
+unsignedAddonsDisabled.learnMore.accesskey=L
+
 # LOCALIZATION NOTE (deveditionTheme.name): This should be nearly the brand name for aurora.
 # See browser/branding/aurora/locales/*/brand.properties
 deveditionTheme.name=Developer Edition
 
 # LOCALIZATION NOTE (lwthemeInstallRequest.message): %S will be replaced with
 # the host name of the site.
 lwthemeInstallRequest.message=This site (%S) attempted to install a theme.
 lwthemeInstallRequest.allowButton=Allow
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
@@ -18,16 +18,19 @@
 <!ENTITY listEmpty.button.label               "Learn more about add-ons">
 <!ENTITY installAddonFromFile.label           "Install Add-on From File…">
 <!ENTITY installAddonFromFile.accesskey       "I">
 <!ENTITY toolsMenu.tooltip                    "Tools for all add-ons">
 
 <!ENTITY cmd.back.tooltip                     "Go back one page">
 <!ENTITY cmd.forward.tooltip                  "Go forward one page">
 
+<!ENTITY showUnsignedExtensions.button.label  "Some extensions could not be verified">
+<!ENTITY showAllExtensions.button.label       "Show all extensions">
+
 <!-- global warnings -->
 <!ENTITY warning.safemode.label                    "All add-ons have been disabled by safe mode.">
 <!ENTITY warning.checkcompatibility.label          "Add-on compatibility checking is disabled. You may have incompatible add-ons.">
 <!ENTITY warning.checkcompatibility.enable.label   "Enable">
 <!ENTITY warning.checkcompatibility.enable.tooltip "Enable add-on compatibility checking">
 <!ENTITY warning.updatesecurity.label              "Add-on update security checking is disabled. You may be compromised by updates.">
 <!ENTITY warning.updatesecurity.enable.label       "Enable">
 <!ENTITY warning.updatesecurity.enable.tooltip     "Enable add-on update security checking">
@@ -227,8 +230,23 @@
      to cause this tab to appear. -->
 <!ENTITY experiment.info.label "What's this? Telemetry may install and run experiments from time to time.">
 <!ENTITY experiment.info.learnmore "Learn More">
 <!ENTITY experiment.info.learnmore.accesskey "L">
 <!ENTITY experiment.info.changetelemetry "Telemetry Settings">
 <!ENTITY experiment.info.changetelemetry.accesskey "T">
 
 <!ENTITY setting.learnmore "Learn More…">
+
+<!ENTITY disabledUnsigned.heading "Some add-ons have been disabled">
+<!-- LOCALIZATION NOTE (disabledUnsigned.description.start, disabledUnsigned.description.findAddonsLink, disabledUnsigned.description.end):
+     These entities form a sentence, with
+     disabledUnsigned.description.findAddonsLink being a link to an external site. -->
+<!ENTITY disabledUnsigned.description.start "The following add-ons have not been verified for use in &brandShortName;. You can ">
+<!ENTITY disabledUnsigned.description.findAddonsLink "find replacements">
+<!ENTITY disabledUnsigned.description.end " or ask the developer to get them verified.">
+<!ENTITY disabledUnsigned.learnMore "Learn more about our efforts to help keep you safe online.">
+<!-- LOCALIZATION NOTE (disabledUnsigned.devInfo.start, disabledUnsigned.devInfo.linkToManual, disabledUnsigned.devInfo.end):
+     These entities form a sentence, with disabledUnsigned.devInfo.linkToManual
+     being a link to an external site. -->
+<!ENTITY disabledUnsigned.devInfo.start "Developers interested in getting their add-ons verified can continue by reading our ">
+<!ENTITY disabledUnsigned.devInfo.linkToManual "manual">
+<!ENTITY disabledUnsigned.devInfo.end ".">
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1328,16 +1328,34 @@ var gViewController = {
       isEnabled: function cmd_experimentsOpenTelemetryPreferences_isEnabled() {
         return !!getMainWindowWithPreferencesPane();
       },
       doCommand: function cmd_experimentsOpenTelemetryPreferences_doCommand() {
         let mainWindow = getMainWindowWithPreferencesPane();
         mainWindow.openAdvancedPreferences("dataChoicesTab");
       },
     },
+
+    cmd_showUnsignedExtensions: {
+      isEnabled: function cmd_showUnsignedExtensions_isEnabled() {
+        return true;
+      },
+      doCommand: function cmd_showUnsignedExtensions_doCommand() {
+        gViewController.loadView("addons://list/extension?unsigned=true");
+      },
+    },
+
+    cmd_showAllExtensions: {
+      isEnabled: function cmd_showUnsignedExtensions_isEnabled() {
+        return true;
+      },
+      doCommand: function cmd_showUnsignedExtensions_doCommand() {
+        gViewController.loadView("addons://list/extension");
+      },
+    },
   },
 
   supportsCommand: function gVC_supportsCommand(aCommand) {
     return (aCommand in this.commands);
   },
 
   isCommandEnabled: function gVC_isCommandEnabled(aCommand) {
     if (!this.supportsCommand(aCommand))
@@ -1829,16 +1847,17 @@ var gCategories = {
   },
 
   select: function gCategories_select(aId, aPreviousView) {
     var view = gViewController.parseViewId(aId);
     if (view.type == "detail" && aPreviousView) {
       aId = aPreviousView;
       view = gViewController.parseViewId(aPreviousView);
     }
+    aId = aId.replace(/\?.*/, "");
 
     Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, aId);
 
     if (this.node.selectedItem &&
         this.node.selectedItem.value == aId) {
       this.node.selectedItem.hidden = false;
       this.node.selectedItem.disabled = false;
       return;
@@ -2584,64 +2603,108 @@ var gListView = {
     var self = this;
     this._listBox.addEventListener("keydown", function listbox_onKeydown(aEvent) {
       if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
         var item = self._listBox.selectedItem;
         if (item)
           item.showInDetailView();
       }
     }, false);
+
+    document.getElementById("signing-learn-more").setAttribute("href",
+      Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons");
+
+    let findSignedAddonsLink = document.getElementById("find-alternative-addons");
+    try {
+      findSignedAddonsLink.setAttribute("href",
+        Services.urlFormatter.formatURLPref("extensions.getAddons.link.url"));
+    } catch (e) {
+      findSignedAddonsLink.classList.remove("text-link");
+    }
+
+    try {
+      document.getElementById("signing-dev-manual-link").setAttribute("href",
+        Services.prefs.getCharPref("xpinstall.signatures.devInfoURL"));
+    } catch (e) {
+      document.getElementById("signing-dev-info").hidden = true;
+    }
   },
 
   show: function gListView_show(aType, aRequest) {
+    let showOnlyDisabledUnsigned = false;
+    if (aType.endsWith("?unsigned=true")) {
+      aType = aType.replace(/\?.*/, "");
+      showOnlyDisabledUnsigned = true;
+    }
+
     if (!(aType in AddonManager.addonTypes))
       throw Components.Exception("Attempting to show unknown type " + aType, Cr.NS_ERROR_INVALID_ARG);
 
     this._type = aType;
     this.node.setAttribute("type", aType);
     this.showEmptyNotice(false);
 
     while (this._listBox.itemCount > 0)
       this._listBox.removeItemAt(0);
 
     if (aType == "plugin") {
       navigator.plugins.refresh(false);
     }
 
-    var self = this;
-    getAddonsAndInstalls(aType, function show_getAddonsAndInstalls(aAddonsList, aInstallsList) {
+    getAddonsAndInstalls(aType, (aAddonsList, aInstallsList) => {
       if (gViewController && aRequest != gViewController.currentViewRequest)
         return;
 
       var elements = [];
 
       for (let addonItem of aAddonsList)
         elements.push(createItem(addonItem));
 
       for (let installItem of aInstallsList)
         elements.push(createItem(installItem, true));
 
-      self.showEmptyNotice(elements.length == 0);
+      this.showEmptyNotice(elements.length == 0);
       if (elements.length > 0) {
         sortElements(elements, ["uiState", "name"], true);
         for (let element of elements)
-          self._listBox.appendChild(element);
+          this._listBox.appendChild(element);
       }
 
-      gEventManager.registerInstallListener(self);
+      this.filterDisabledUnsigned(showOnlyDisabledUnsigned);
+
+      gEventManager.registerInstallListener(this);
       gViewController.updateCommands();
       gViewController.notifyViewChanged();
     });
   },
 
   hide: function gListView_hide() {
     gEventManager.unregisterInstallListener(this);
     doPendingUninstalls(this._listBox);
   },
 
+  filterDisabledUnsigned: function gListView_filterDisabledUnsigned(aFilter = true) {
+    let foundDisabledUnsigned = false;
+
+    for (let item of this._listBox.childNodes) {
+      let isDisabledUnsigned = item.mAddon.appDisabled &&
+                               item.mAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING;
+      if (isDisabledUnsigned)
+        foundDisabledUnsigned = true;
+      else
+        item.hidden = aFilter;
+    }
+
+    document.getElementById("show-disabled-unsigned-extensions").hidden =
+      aFilter || !foundDisabledUnsigned;
+
+    document.getElementById("show-all-extensions").hidden = !aFilter;
+    document.getElementById("disabled-unsigned-addons-info").hidden = !aFilter;
+  },
+
   showEmptyNotice: function gListView_showEmptyNotice(aShow) {
     this._emptyNotice.hidden = !aShow;
     this._listBox.hidden = aShow;
   },
 
   onSortChanged: function gListView_onSortChanged(aSortBy, aAscending) {
     sortList(this._listBox, aSortBy, aAscending);
   },
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -87,16 +87,18 @@
     <command id="cmd_forward"/>
     <command id="cmd_enableCheckCompatibility"/>
     <command id="cmd_pluginCheck"/>
     <command id="cmd_enableUpdateSecurity"/>
     <command id="cmd_toggleAutoUpdateDefault"/>
     <command id="cmd_resetAddonAutoUpdate"/>
     <command id="cmd_experimentsLearnMore"/>
     <command id="cmd_experimentsOpenTelemetryPreferences"/>
+    <command id="cmd_showUnsignedExtensions"/>
+    <command id="cmd_showAllExtensions"/>
   </commandset>
 
   <!-- view commands - these act on the selected addon -->
   <commandset id="viewCommandSet"
               events="richlistbox-select" commandupdater="true">
     <command id="cmd_showItemDetails"/>
     <command id="cmd_findItemUpdates"/>
     <command id="cmd_showItemPreferences"/>
@@ -154,19 +156,22 @@
                       tooltiptext="&view.availableUpdates.label;"
                       disabled="true"/>
         <richlistitem id="category-recentUpdates" value="addons://updates/recent"
                       class="category"
                       name="&view.recentUpdates.label;" priority="101000"
                       tooltiptext="&view.recentUpdates.label;" disabled="true"/>
       </richlistbox>
     </vbox>
-    <vbox flex="1">
+    <vbox class="main-content" flex="1">
       <!-- main header -->
       <hbox id="header" align="center">
+        <button id="show-all-extensions" hidden="true"
+                label="&showAllExtensions.button.label;"
+                command="cmd_showAllExtensions"/>
         <spacer flex="1"/>
         <hbox id="updates-container" align="center">
           <image class="spinner"/>
           <label id="updates-noneFound" hidden="true"
                  value="&updates.noneFound.label;"/>
           <button id="updates-manualUpdatesFound-btn" class="button-link"
                   hidden="true" label="&updates.manualUpdatesFound.label;"
                   command="cmd_goToAvailableUpdates"/>
@@ -175,16 +180,19 @@
           <label id="updates-installed" hidden="true"
                  value="&updates.installed.label;"/>
           <label id="updates-downloaded" hidden="true"
                  value="&updates.downloaded.label;"/>
           <button id="updates-restart-btn" class="button-link" hidden="true"
                   label="&updates.restart.label;"
                   command="cmd_restartApp"/>
         </hbox>
+        <button id="show-disabled-unsigned-extensions" hidden="true"
+                label="&showUnsignedExtensions.button.label;"
+                command="cmd_showUnsignedExtensions"/>
         <toolbarbutton id="header-utils-btn" class="header-button" type="menu"
                 tooltiptext="&toolsMenu.tooltip;">
           <menupopup id="utils-menu">
             <menuitem id="utils-updateNow"
                       label="&updates.checkForUpdates.label;"
                       accesskey="&updates.checkForUpdates.accesskey;"
                       command="cmd_findAllUpdates"/>
             <menuitem id="utils-viewUpdates"
@@ -211,17 +219,16 @@
                       accesskey="&updates.resetUpdatesToManual.accesskey;"
                       command="cmd_resetAddonAutoUpdate"/>
           </menupopup>
         </toolbarbutton>
         <textbox id="header-search" type="search" searchbutton="true"
                  searchbuttonlabel="&search.buttonlabel;"
                  placeholder="&search.placeholder;"/>
       </hbox>
-      <box id="view-port-container" class="main-content" flex="1">
         <!-- view port -->
         <deck id="view-port" flex="1" selectedIndex="0">
 
           <!-- discover view -->
           <deck id="discover-view" flex="1" class="view-pane" selectedIndex="0" tabindex="0">
             <vbox id="discover-loading" align="center" pack="stretch" flex="1" class="alert-container">
               <spacer class="alert-spacer-before"/>
               <hbox class="alert loading" align="center">
@@ -324,16 +331,27 @@
               <hbox pack="center">
                 <label id="search-allresults-link" class="text-link"/>
               </hbox>
             </richlistbox>
           </vbox>
 
           <!-- list view -->
           <vbox id="list-view" flex="1" class="view-pane" align="stretch" tabindex="0">
+            <!-- info UI for add-ons that have been disabled for being unsigned -->
+            <vbox id="disabled-unsigned-addons-info" hidden="true">
+              <label id="disabled-unsigned-addons-heading" value="&disabledUnsigned.heading;"/>
+              <description>
+                &disabledUnsigned.description.start;<label class="text-link plain" id="find-alternative-addons">&disabledUnsigned.description.findAddonsLink;</label>&disabledUnsigned.description.end;
+              </description>
+              <hbox pack="start"><label class="text-link" id="signing-learn-more">&disabledUnsigned.learnMore;</label></hbox>
+              <description id="signing-dev-info">
+                &disabledUnsigned.devInfo.start;<label class="text-link plain" id="signing-dev-manual-link">&disabledUnsigned.devInfo.linkToManual;</label>&disabledUnsigned.devInfo.end;
+              </description>
+            </vbox>
             <hbox class="view-header global-warning-container">
               <!-- global warnings -->
               <hbox class="global-warning" flex="1">
                 <hbox class="global-warning-safemode" flex="1" align="center"
                       tooltiptext="&warning.safemode.label;">
                   <image class="warning-icon"/>
                   <label class="global-warning-text" flex="1" crop="end"
                          value="&warning.safemode.label;"/>
@@ -685,12 +703,11 @@
                     </hbox>
                   </vbox>
                 </hbox>
               </vbox>
               <spacer flex="1"/>
             </hbox>
           </scrollbox>
         </deck>
-      </box>
     </vbox>
   </hbox>
 </page>
--- a/toolkit/mozapps/extensions/test/browser/browser_list.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_list.js
@@ -123,17 +123,18 @@ function end_test() {
 
 function get_test_items() {
   var tests = "@tests.mozilla.org";
 
   var items = {};
   var item = gManagerWindow.document.getElementById("addon-list").firstChild;
 
   while (item) {
-    if (item.mAddon.id.substring(item.mAddon.id.length - tests.length) == tests)
+    if (item.mAddon.id.substring(item.mAddon.id.length - tests.length) == tests &&
+        !is_hidden(item))
       items[item.mAddon.name] = item;
     item = item.nextSibling;
   }
 
   return items;
 }
 
 function get_node(parent, anonid) {
@@ -430,17 +431,45 @@ add_test(function() {
     is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
     is_element_visible(get_node(addon, "error"), "Error message should be visible");
     is(get_node(addon, "error").textContent, "Test add-on 11 could not be verified for use in " + gApp + " and has been disabled.", "Error message should be correct");
     is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
     is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
     is(get_node(addon, "error-link").href, Services.prefs.getCharPref("xpinstall.signatures.infoURL"), "Error link should be correct");
     is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    run_next_test();
+    info("Filter for disabled unsigned extensions");
+    let filterButton = gManagerWindow.document.getElementById("show-disabled-unsigned-extensions");
+    let showAllButton = gManagerWindow.document.getElementById("show-all-extensions");
+    let signingInfoUI = gManagerWindow.document.getElementById("disabled-unsigned-addons-info");
+    is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible");
+    is_element_hidden(showAllButton, "Button for showing all extensions should be hidden");
+    is_element_hidden(signingInfoUI, "Signing info UI should be hidden");
+
+    filterButton.click();
+    wait_for_view_load(gManagerWindow, () => {
+      is_element_hidden(filterButton, "Button for showing disabled unsigned extensions should be hidden");
+      is_element_visible(showAllButton, "Button for showing all extensions should be visible");
+      is_element_visible(signingInfoUI, "Signing info UI should be visible");
+
+      items = get_test_items();
+      is(Object.keys(items).length, 1, "Only one add-on should be shown");
+      is(Object.keys(items)[0], "Test add-on 11", "The disabled unsigned extension should be shown");
+
+      showAllButton.click();
+      wait_for_view_load(gManagerWindow, () => {
+        items = get_test_items();
+        is(Object.keys(items).length, 11, "All add-ons should be shown again");
+        is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible again");
+        is_element_hidden(showAllButton, "Button for showing all extensions should be hidden again");
+        is_element_hidden(signingInfoUI, "Signing info UI should be hidden again");
+
+        run_next_test();
+      });
+    });
   });
 });
 
 // Check the add-ons are now in the right state
 add_test(function() {
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
--- a/toolkit/themes/shared/extensions/extensions.inc.css
+++ b/toolkit/themes/shared/extensions/extensions.inc.css
@@ -1012,8 +1012,28 @@ button.button-link:not([disabled="true"]
 #detail-experiment-bullet {
   fill: rgb(158, 158, 158);
 }
 
 .addon[active="true"] .experiment-bullet,
 #detail-view[active="true"] #detail-experiment-bullet {
   fill: rgb(106, 201, 20);
 }
+
+/*** info UI for add-ons that have been disabled for being unsigned ***/
+
+#show-disabled-unsigned-extensions:not(:hover) {
+  background-color: #fcf8ed;
+}
+
+#disabled-unsigned-addons-info {
+  margin-bottom: 2em;
+}
+
+#disabled-unsigned-addons-heading {
+  font-size: 1.4em;
+  font-weight: bold;
+  margin-bottom: .5em;
+}
+
+#signing-dev-info {
+  font-style: italic;
+}
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -43,20 +43,16 @@ xul|caption {
 xul|caption > xul|checkbox,
 xul|caption > xul|label {
   font-size: 1.3rem;
   font-weight: bold;
   line-height: 22px;
   margin: 0 !important;
 }
 
-xul|description {
-  -moz-margin-start: 0;
-}
-
 *|*.main-content {
   padding-top: 40px;
   -moz-padding-end: 44px; /* compensate the 4px margin of child elements */
   padding-bottom: 48px;
   -moz-padding-start: 48px;
   overflow: auto;
 }
 
@@ -70,17 +66,18 @@ xul|groupbox {
   -moz-appearance: none;
   border: none;
   margin: 15px 0 0;
   -moz-padding-start: 0;
   -moz-padding-end: 0;
   font-size: 1.25rem;
 }
 
-xul|groupbox xul|label {
+xul|groupbox xul|label,
+xul|groupbox xul|description {
   /* !important needed to override toolkit !important rule */
   -moz-margin-start: 0 !important;
   -moz-margin-end: 0 !important;
 }
 
 /* tabpanels and tabs */
 
 xul|tabpanels {