From 2d06faf7c4bb181dee647f5e5a43d2f0d48eb547 Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Tue, 4 Aug 2015 20:04:24 +0200 Subject: [PATCH] Bug 1175702 - Implement mixed content states in the control center r=paolo --- browser/base/content/browser.js | 294 ++++++++++-------- .../controlcenter/content/panel.inc.xul | 124 +++++--- .../locales/en-US/chrome/browser/browser.dtd | 23 ++ .../en-US/chrome/browser/browser.properties | 5 - browser/themes/osx/controlcenter/panel.css | 9 +- browser/themes/osx/jar.mn | 6 +- .../themes/shared/controlcenter/panel.inc.css | 135 ++++---- .../shared/controlcenter/warning-gray.svg | 9 + .../shared/controlcenter/warning-yellow.svg | 9 + 9 files changed, 383 insertions(+), 231 deletions(-) create mode 100644 browser/themes/shared/controlcenter/warning-gray.svg create mode 100644 browser/themes/shared/controlcenter/warning-yellow.svg diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 80037169ebec..65d17398dd11 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -6678,12 +6678,10 @@ var gIdentityHandler = { IDENTITY_MODE_MIXED_ACTIVE_BLOCKED : "verifiedDomain mixedContent mixedActiveBlocked", // SSL with unauthenticated active content blocked; no unauthenticated display content IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED : "verifiedIdentity mixedContent mixedActiveBlocked", // SSL with unauthenticated active content blocked; no unauthenticated display content IDENTITY_MODE_CHROMEUI : "chromeUI", // Part of the product's UI - IDENTITY_MODE_FILE_URI : "fileURI", // File path - // Cache the most recent SSLStatus and Location seen in checkIdentity - _lastStatus : null, - _lastUri : null, - _mode : "unknownIdentity", + _isChromeUI: false, + _sslStatus: null, + _uri: null, // smart getters get _identityPopup () { @@ -6714,20 +6712,10 @@ var gIdentityHandler = { return this._identityPopupContentVerif = document.getElementById("identity-popup-content-verifier"); }, - get _identityPopupSecurityContent () { - delete this._identityPopupSecurityContent; - return this._identityPopupSecurityContent = - document.getElementById("identity-popup-security-content"); - }, - get _identityPopupSecurityView () { - delete this._identityPopupSecurityView; - return this._identityPopupSecurityView = - document.getElementById("identity-popup-securityView"); - }, - get _identityPopupMainView () { - delete this._identityPopupMainView; - return this._identityPopupMainView = - document.getElementById("identity-popup-mainView"); + get _identityPopupMixedContentLearnMore () { + delete this._identityPopupMixedContentLearnMore; + return this._identityPopupMixedContentLearnMore = + document.getElementById("identity-popup-mcb-learn-more"); }, get _identityIconLabel () { delete this._identityIconLabel; @@ -6806,13 +6794,33 @@ var gIdentityHandler = { } }, + disableMixedContentProtection() { + // Use telemetry to measure how often unblocking happens + const kMIXED_CONTENT_UNBLOCK_EVENT = 2; + let histogram = + Services.telemetry.getHistogramById( + "MIXED_CONTENT_UNBLOCK_COUNTER"); + histogram.add(kMIXED_CONTENT_UNBLOCK_EVENT); + // Reload the page with the content unblocked + BrowserReloadWithFlags( + Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT); + this._identityPopup.hidePopup(); + }, + + enableMixedContentProtection() { + gBrowser.selectedBrowser.messageManager.sendAsyncMessage( + "MixedContent:ReenableProtection", {}); + BrowserReload(); + this._identityPopup.hidePopup(); + }, + /** - * Helper to parse out the important parts of _lastStatus (of the SSL cert in + * Helper to parse out the important parts of _sslStatus (of the SSL cert in * particular) for use in constructing identity UI strings */ getIdentityData : function() { var result = {}; - var status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus); + var status = this._sslStatus.QueryInterface(Ci.nsISSLStatus); var cert = status.serverCert; // Human readable name of Subject @@ -6848,68 +6856,58 @@ var gIdentityHandler = { * @param nsIURI uri The address for which the UI should be updated. */ checkIdentity : function(state, uri) { - var currentStatus = gBrowser.securityUI - .QueryInterface(Components.interfaces.nsISSLStatusProvider) - .SSLStatus; - this._lastStatus = currentStatus; - this._lastUri = uri; - let nsIWebProgressListener = Ci.nsIWebProgressListener; - // For some URIs like data: we can't get a host and so can't do - // anything useful here. - let unknown = false; - try { - uri.host; - } catch (e) { unknown = true; } - // Chrome URIs however get special treatment. Some chrome URIs are // whitelisted to provide a positive security signal to the user. let whitelist = /^about:(accounts|addons|app-manager|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)/i; let isChromeUI = uri.schemeIs("about") && whitelist.test(uri.spec); + let mode = this.IDENTITY_MODE_UNKNOWN; + if (isChromeUI) { - this.setMode(this.IDENTITY_MODE_CHROMEUI); - } else if (unknown) { - this.setMode(this.IDENTITY_MODE_UNKNOWN); + mode = this.IDENTITY_MODE_CHROMEUI; } else if (state & nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) { if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) { - this.setMode(this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED); + mode = this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED; } else { - this.setMode(this.IDENTITY_MODE_IDENTIFIED); + mode = this.IDENTITY_MODE_IDENTIFIED; } } else if (state & nsIWebProgressListener.STATE_IS_SECURE) { if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) { - this.setMode(this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED); + mode = this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED; } else { - this.setMode(this.IDENTITY_MODE_DOMAIN_VERIFIED); + mode = this.IDENTITY_MODE_DOMAIN_VERIFIED; } } else if (state & nsIWebProgressListener.STATE_IS_BROKEN) { if (state & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) { - this.setMode(this.IDENTITY_MODE_MIXED_ACTIVE_LOADED); + mode = this.IDENTITY_MODE_MIXED_ACTIVE_LOADED; } else if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) { - this.setMode(this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED); + mode = this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED; } else if (state & nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) { - this.setMode(this.IDENTITY_MODE_MIXED_DISPLAY_LOADED); + mode = this.IDENTITY_MODE_MIXED_DISPLAY_LOADED; } else { - this.setMode(this.IDENTITY_MODE_USES_WEAK_CIPHER); - } - } else { - // Create a channel for the sole purpose of getting the resolved URI - // of the request to determine if it's loaded from the file system. - let resolvedURI = NetUtil.newChannel({uri,loadUsingSystemPrincipal:true}).URI; - if (resolvedURI.schemeIs("jar")) { - // Given a URI "jar:!/" - // create a new URI using !/ - resolvedURI = NetUtil.newURI(resolvedURI.path); - } - - if (resolvedURI.schemeIs("file")) { - this.setMode(this.IDENTITY_MODE_FILE_URI); - } else { - this.setMode(this.IDENTITY_MODE_UNKNOWN); + mode = this.IDENTITY_MODE_USES_WEAK_CIPHER; } } + // We need those values later when populating the control center. + this._uri = uri; + this._state = state; + this._isChromeUI = isChromeUI; + this._sslStatus = + gBrowser.securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus; + + // Update the identity block. + if (this._identityBox) { + this._identityBox.className = mode; + this.refreshIdentityBlock(mode); + } + + // NOTE: We do NOT update the identity popup (the control center) when + // we receive a new security state. If the user opened the popup and looks + // at the provided information we don't want to suddenly change the panel + // contents. + // Show the doorhanger when: // - mixed active content is blocked // - mixed active content is loaded (detected but not blocked) @@ -6961,46 +6959,22 @@ var gIdentityHandler = { .getService(Ci.nsIIDNService); try { let baseDomain = - Services.eTLD.getBaseDomainFromHost(this._lastUri.host); + Services.eTLD.getBaseDomainFromHost(this._uri.host); return this._IDNService.convertToDisplayIDN(baseDomain, {}); } catch (e) { // If something goes wrong (e.g. host is an IP address) just fail back // to the full domain. - return this._lastUri.host; + return this._uri.host; } }, - /** - * Update the UI to reflect the specified mode, which should be one of the - * IDENTITY_MODE_* constants. - */ - setMode : function(newMode) { - if (!this._identityBox) { - // No identity box means the identity box is not visible, in which - // case there's nothing to do. - return; - } - - this._identityPopup.className = newMode; - this._identityBox.className = newMode; - this.setIdentityMessages(newMode); - - // Update the popup too, if it's open - if (this._identityPopup.state == "open") { - this.setPopupMessages(newMode); - this.updateSitePermissions(); - } - - this._mode = newMode; - }, - /** * Set up the messages for the primary identity UI based on the specified mode, * and the details of the SSL cert, where applicable * * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. */ - setIdentityMessages : function(newMode) { + refreshIdentityBlock(newMode) { let icon_label = ""; let tooltip = ""; let icon_country_label = ""; @@ -7017,11 +6991,11 @@ var gIdentityHandler = { [iData.caOrg]); // This can't throw, because URI's with a host that throw don't end up in this case. - let host = this._lastUri.host; + let host = this._uri.host; let port = 443; try { - if (this._lastUri.port > 0) - port = this._lastUri.port; + if (this._uri.port > 0) + port = this._uri.port; } catch (e) {} if (this._overrideService.hasMatchingOverride(host, port, iData.cert, {}, {})) @@ -7070,15 +7044,78 @@ 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 - * - * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. */ - setPopupMessages : function(newMode) { + refreshIdentityPopup() { + // Update the "Learn More" hrefs for Mixed Content Blocking. + let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + let learnMoreHref = `${baseURL}mixed-content`; + this._identityPopupMixedContentLearnMore.setAttribute("href", learnMoreHref); - this._identityPopup.className = newMode; - this._identityPopupMainView.className = newMode; - this._identityPopupSecurityView.className = newMode; - this._identityPopupSecurityContent.className = newMode; + // Basic connection properties. + let isBroken = this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; + let isSecure = this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE; + let isEV = this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL; + + // Determine connection security information. + let connection = "not-secure"; + if (this._isChromeUI) { + connection = "chrome"; + } else if (this._isURILoadedFromFile(this._uri)) { + connection = "file"; + } else if (isEV) { + connection = "secure-ev"; + } else if (isSecure) { + connection = "secure"; + } + + // Mixed content flags. + let isMixedActiveContentLoaded = + this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT; + let isMixedActiveContentBlocked = + this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT; + let isMixedPassiveContentLoaded = + this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT; + + // Determine the mixed content state. + let mixedcontent = []; + if (isMixedPassiveContentLoaded) { + mixedcontent.push("passive-loaded"); + } + if (isMixedActiveContentLoaded) { + mixedcontent.push("active-loaded"); + } else if (isMixedActiveContentBlocked) { + mixedcontent.push("active-blocked"); + } + mixedcontent = mixedcontent.join(" "); + + // We have no specific flags for weak ciphers (yet). If a connection is + // broken and we can't detect any mixed active content loaded then it's + // a weak cipher. + let ciphers = ""; + if (isBroken && !isMixedActiveContentLoaded) { + ciphers = "weak"; + } + + // Update all elements. + let elementIDs = [ + "identity-popup", + "identity-popup-securityView-body", + ]; + + function updateAttribute(elem, attr, value) { + if (value) { + elem.setAttribute(attr, value); + } else { + elem.removeAttribute(attr); + } + } + + for (let id of elementIDs) { + let element = document.getElementById(id); + updateAttribute(element, "connection", connection); + updateAttribute(element, "ciphers", ciphers); + updateAttribute(element, "mixedcontent", mixedcontent); + } // Initialize the optional strings to empty values let supplemental = ""; @@ -7092,19 +7129,18 @@ var gIdentityHandler = { // Some URIs might have no hosts. } + // Fallback for special protocols. if (!host) { - // Fallback for special protocols. - host = this._lastUri.specIgnoringRef; + host = this._uri.specIgnoringRef; } - switch (newMode) { - case this.IDENTITY_MODE_DOMAIN_VERIFIED: - case this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED: + // Fill in the CA name if we have a valid TLS certificate. + if (isSecure) { verifier = this._identityBox.tooltipText; - break; - case this.IDENTITY_MODE_IDENTIFIED: - case this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED: { - // If it's identified, then we can populate the dialog with credentials + } + + // Fill in organization information if we have a valid EV certificate. + if (isEV) { let iData = this.getIdentityData(); host = owner = iData.subjectOrg; verifier = this._identityBox.tooltipText; @@ -7119,21 +7155,6 @@ var gIdentityHandler = { supplemental += iData.state; else if (iData.country) // Country only supplemental += iData.country; - break; - } - case this.IDENTITY_MODE_UNKNOWN: - supplemental = gNavigatorBundle.getString("identity.not_secure"); - break; - case this.IDENTITY_MODE_USES_WEAK_CIPHER: - supplemental = gNavigatorBundle.getString("identity.uses_weak_cipher"); - break; - case this.IDENTITY_MODE_MIXED_DISPLAY_LOADED: - case this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED: - supplemental = gNavigatorBundle.getString("identity.mixed_display_loaded"); - break; - case this.IDENTITY_MODE_MIXED_ACTIVE_LOADED: - supplemental = gNavigatorBundle.getString("identity.mixed_active_loaded2"); - break; } // Push the appropriate strings out to the UI. Need to use |value| for the @@ -7144,8 +7165,31 @@ var gIdentityHandler = { this._identityPopupContentSupp.textContent = supplemental; this._identityPopupContentVerif.textContent = verifier; - // Hide subviews when updating panel information. - document.getElementById("identity-popup-multiView").showMainView(); + // Update per-site permissions section. + this.updateSitePermissions(); + }, + + _isURILoadedFromFile(uri) { + try { + uri.host; + // No internal/file URI if we have a host. + return false; + } catch (e) { + // All good, let's continue. + } + + // Create a channel for the sole purpose of getting the resolved URI + // of the request to determine if it's loaded from the file system. + let chanOptions = {uri, loadUsingSystemPrincipal: true}; + let resolvedURI = NetUtil.newChannel(chanOptions).URI; + if (resolvedURI.schemeIs("jar")) { + // Given a URI "jar:!/" + // create a new URI using !/ + resolvedURI = NetUtil.newURI(resolvedURI.path); + } + + // Check the URI again after resolving. + return resolvedURI.schemeIs("file"); }, /** @@ -7170,9 +7214,7 @@ var gIdentityHandler = { this._identityPopup.hidden = false; // Update the popup strings - this.setPopupMessages(this._identityBox.className); - - this.updateSitePermissions(); + this.refreshIdentityPopup(); // Add the "open" attribute to the identity box for styling this._identityBox.setAttribute("open", "true"); diff --git a/browser/components/controlcenter/content/panel.inc.xul b/browser/components/controlcenter/content/panel.inc.xul index c3e6b05f2029..cf386cad98f8 100644 --- a/browser/components/controlcenter/content/panel.inc.xul +++ b/browser/components/controlcenter/content/panel.inc.xul @@ -10,7 +10,8 @@ orient="vertical"> - + + - + - - -