/* 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/. */ /** * Utility object to handle manipulations of the identity indicators in the UI */ var gIdentityHandler = { /** * nsIURI for which the identity UI is displayed. This has been already * processed by nsIURIFixup.createExposableURI. */ _uri: null, /** * We only know the connection type if this._uri has a defined "host" part. * * These URIs, like "about:", "file:" and "data:" URIs, will usually be treated as a * an unknown connection. */ _uriHasHost: false, /** * If this tab belongs to a WebExtension, contains its WebExtensionPolicy. */ _pageExtensionPolicy: null, /** * Whether this._uri refers to an internally implemented browser page. * * Note that this is set for some "about:" pages, but general "chrome:" URIs * are not included in this category by default. */ _isSecureInternalUI: false, /** * nsISSLStatus metadata provided by gBrowser.securityUI the last time the * identity UI was updated, or null if the connection is not secure. */ _sslStatus: null, /** * Bitmask provided by nsIWebProgressListener.onSecurityChange. */ _state: 0, /** * RegExp used to decide if an about url should be shown as being part of * the browser UI. */ _secureInternalUIWhitelist: /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|license|permissions|preferences|rights|searchreset|sessionrestore|support|welcomeback)(?:[?#]|$)/i, get _isBroken() { return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; }, get _isSecure() { // If a is included within a chrome document, then this._state // will refer to the security state for the and not the top level // document. In this case, don't upgrade the security state in the UI // with the secure state of the embedded . return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE; }, get _isEV() { // If a is included within a chrome document, then this._state // will refer to the security state for the and not the top level // document. In this case, don't upgrade the security state in the UI // with the EV state of the embedded . return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL; }, get _isMixedActiveContentLoaded() { return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT; }, get _isMixedActiveContentBlocked() { return this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT; }, get _isMixedPassiveContentLoaded() { return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT; }, get _isCertUserOverridden() { return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN; }, get _isCertDistrustImminent() { return this._state & Ci.nsIWebProgressListener.STATE_CERT_DISTRUST_IMMINENT; }, get _hasInsecureLoginForms() { // checks if the page has been flagged for an insecure login. Also checks // if the pref to degrade the UI is set to true return LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser) && Services.prefs.getBoolPref("security.insecure_password.ui.enabled"); }, // smart getters get _identityPopup() { delete this._identityPopup; return this._identityPopup = document.getElementById("identity-popup"); }, get _identityBox() { delete this._identityBox; return this._identityBox = document.getElementById("identity-box"); }, get _identityPopupMultiView() { delete this._identityPopupMultiView; return this._identityPopupMultiView = document.getElementById("identity-popup-multiView"); }, get _identityPopupMainView() { delete this._identityPopupMainView; return this._identityPopupMainView = document.getElementById("identity-popup-mainView"); }, get _identityPopupContentHosts() { delete this._identityPopupContentHosts; return this._identityPopupContentHosts = [...document.querySelectorAll(".identity-popup-host")]; }, get _identityPopupContentHostless() { delete this._identityPopupContentHostless; return this._identityPopupContentHostless = [...document.querySelectorAll(".identity-popup-hostless")]; }, get _identityPopupContentOwner() { delete this._identityPopupContentOwner; return this._identityPopupContentOwner = document.getElementById("identity-popup-content-owner"); }, get _identityPopupContentSupp() { delete this._identityPopupContentSupp; return this._identityPopupContentSupp = document.getElementById("identity-popup-content-supplemental"); }, get _identityPopupContentVerif() { delete this._identityPopupContentVerif; return this._identityPopupContentVerif = document.getElementById("identity-popup-content-verifier"); }, get _identityPopupMixedContentLearnMore() { delete this._identityPopupMixedContentLearnMore; return this._identityPopupMixedContentLearnMore = document.getElementById("identity-popup-mcb-learn-more"); }, get _identityPopupInsecureLoginFormsLearnMore() { delete this._identityPopupInsecureLoginFormsLearnMore; return this._identityPopupInsecureLoginFormsLearnMore = document.getElementById("identity-popup-insecure-login-forms-learn-more"); }, get _identityIconLabels() { delete this._identityIconLabels; return this._identityIconLabels = document.getElementById("identity-icon-labels"); }, get _identityIconLabel() { delete this._identityIconLabel; return this._identityIconLabel = document.getElementById("identity-icon-label"); }, get _connectionIcon() { delete this._connectionIcon; return this._connectionIcon = document.getElementById("connection-icon"); }, get _extensionIcon() { delete this._extensionIcon; return this._extensionIcon = document.getElementById("extension-icon"); }, get _overrideService() { delete this._overrideService; return this._overrideService = Cc["@mozilla.org/security/certoverride;1"] .getService(Ci.nsICertOverrideService); }, get _identityIconCountryLabel() { delete this._identityIconCountryLabel; return this._identityIconCountryLabel = document.getElementById("identity-icon-country-label"); }, get _identityIcon() { delete this._identityIcon; return this._identityIcon = document.getElementById("identity-icon"); }, get _permissionList() { delete this._permissionList; return this._permissionList = document.getElementById("identity-popup-permission-list"); }, get _permissionEmptyHint() { delete this._permissionEmptyHint; return this._permissionEmptyHint = document.getElementById("identity-popup-permission-empty-hint"); }, get _permissionReloadHint() { delete this._permissionReloadHint; return this._permissionReloadHint = document.getElementById("identity-popup-permission-reload-hint"); }, get _popupExpander() { delete this._popupExpander; return this._popupExpander = document.getElementById("identity-popup-security-expander"); }, get _clearSiteDataFooter() { delete this._clearSiteDataFooter; return this._clearSiteDataFooter = document.getElementById("identity-popup-clear-sitedata-footer"); }, get _permissionAnchors() { delete this._permissionAnchors; let permissionAnchors = {}; for (let anchor of document.getElementById("blocked-permissions-container").children) { permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor; } return this._permissionAnchors = permissionAnchors; }, /** * Handles clicks on the "Clear Cookies and Site Data" button. */ async clearSiteData(event) { if (!this._uriHasHost) { return; } let host = this._uri.host; // Site data could have changed while the identity popup was open, // reload again to be sure. await SiteDataManager.updateSites(); let baseDomain = SiteDataManager.getBaseDomainFromHost(host); let siteData = await SiteDataManager.getSites(baseDomain); // Hide the popup before showing the removal prompt, to // avoid a pretty ugly transition. Also hide it even // if the update resulted in no site data, to keep the // illusion that clicking the button had an effect. PanelMultiView.hidePopup(this._identityPopup); if (siteData && siteData.length) { let hosts = siteData.map(site => site.host); if (SiteDataManager.promptSiteDataRemoval(window, hosts)) { SiteDataManager.remove(hosts); } } event.stopPropagation(); }, openPermissionPreferences() { openPreferences("privacy-permissions", { origin: "identityPopup-permissions-PreferencesButton" }); }, /** * Handler for mouseclicks on the "More Information" button in the * "identity-popup" panel. */ handleMoreInfoClick(event) { displaySecurityInfo(); event.stopPropagation(); PanelMultiView.hidePopup(this._identityPopup); }, showSecuritySubView() { this._identityPopupMultiView.showSubView("identity-popup-securityView", this._popupExpander); // Elements of hidden views have -moz-user-focus:ignore but setting that // per CSS selector doesn't blur a focused element in those hidden views. Services.focus.clearFocus(window); }, 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); PanelMultiView.hidePopup(this._identityPopup); }, enableMixedContentProtection() { gBrowser.selectedBrowser.messageManager.sendAsyncMessage( "MixedContent:ReenableProtection", {}); BrowserReload(); PanelMultiView.hidePopup(this._identityPopup); }, removeCertException() { if (!this._uriHasHost) { Cu.reportError("Trying to revoke a cert exception on a URI without a host?"); return; } let host = this._uri.host; let port = this._uri.port > 0 ? this._uri.port : 443; this._overrideService.clearValidityOverride(host, port); BrowserReloadSkipCache(); PanelMultiView.hidePopup(this._identityPopup); }, /** * Helper to parse out the important parts of _sslStatus (of the SSL cert in * particular) for use in constructing identity UI strings */ getIdentityData() { var result = {}; var cert = this._sslStatus.serverCert; // Human readable name of Subject result.subjectOrg = cert.organization; // SubjectName fields, broken up for individual access if (cert.subjectName) { result.subjectNameFields = {}; cert.subjectName.split(",").forEach(function(v) { var field = v.split("="); this[field[0]] = field[1]; }, result.subjectNameFields); // Call out city, state, and country specifically result.city = result.subjectNameFields.L; result.state = result.subjectNameFields.ST; result.country = result.subjectNameFields.C; } // Human readable name of Certificate Authority result.caOrg = cert.issuerOrganization || cert.issuerCommonName; result.cert = cert; return result; }, /** * Update the identity user interface for the page currently being displayed. * * This examines the SSL certificate metadata, if available, as well as the * connection type and other security-related state information for the page. * * @param state * Bitmask provided by nsIWebProgressListener.onSecurityChange. * @param uri * nsIURI for which the identity UI should be displayed, already * processed by nsIURIFixup.createExposableURI. */ updateIdentity(state, uri) { let shouldHidePopup = this._uri && (this._uri.spec != uri.spec); this._state = state; // Firstly, populate the state properties required to display the UI. See // the documentation of the individual properties for details. this.setURI(uri); this._sslStatus = gBrowser.securityUI.secInfo && gBrowser.securityUI.secInfo.SSLStatus; // Then, update the user interface with the available data. this.refreshIdentityBlock(); // Handle a location change while the Control Center is focused // by closing the popup (bug 1207542) if (shouldHidePopup) { PanelMultiView.hidePopup(this._identityPopup); } // NOTE: We do NOT update the identity popup (the control center) when // we receive a new security state on the existing page (i.e. from a // subframe). If the user opened the popup and looks at the provided // information we don't want to suddenly change the panel contents. // Finally, if there are warnings to issue, issue them if (this._isCertDistrustImminent) { let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError); let windowId = gBrowser.selectedBrowser.innerWindowID; let message = gBrowserBundle.GetStringFromName("certImminentDistrust.message"); // Use uri.prePath instead of initWithSourceURI() so that these can be // de-duplicated on the scheme+host+port combination. consoleMsg.initWithWindowID(message, uri.prePath, null, 0, 0, Ci.nsIScriptError.warningFlag, "SSL", windowId); Services.console.logMessage(consoleMsg); } }, /** * This is called asynchronously when requested by the Logins module, after * the insecure login forms state for the page has been updated. */ refreshForInsecureLoginForms() { // Check this._uri because we don't want to refresh the user interface if // this is called before the first page load in the window for any reason. if (!this._uri) { return; } this.refreshIdentityBlock(); }, updateSharingIndicator() { let tab = gBrowser.selectedTab; this._sharingState = tab._sharingState; this._identityBox.removeAttribute("paused"); this._identityBox.removeAttribute("sharing"); if (this._sharingState && this._sharingState.sharing) { this._identityBox.setAttribute("sharing", this._sharingState.sharing); if (this._sharingState.paused) { this._identityBox.setAttribute("paused", "true"); } } if (this._identityPopup.state == "open") { this.updateSitePermissions(); PanelView.forNode(this._identityPopupMainView) .descriptionHeightWorkaround(); } }, /** * Attempt to provide proper IDN treatment for host names */ getEffectiveHost() { if (!this._IDNService) this._IDNService = Cc["@mozilla.org/network/idn-service;1"] .getService(Ci.nsIIDNService); try { return this._IDNService.convertToDisplayIDN(this._uri.host, {}); } catch (e) { // If something goes wrong (e.g. host is an IP address) just fail back // to the full domain. return this._uri.host; } }, /** * Return the CSS class name to set on the "fullscreen-warning" element to * display information about connection security in the notification shown * when a site enters the fullscreen mode. */ get pointerlockFsWarningClassName() { // Note that the fullscreen warning does not handle _isSecureInternalUI. if (this._uriHasHost && this._isEV) { return "verifiedIdentity"; } if (this._uriHasHost && this._isSecure) { return "verifiedDomain"; } return "unknownIdentity"; }, /** * Updates the identity block user interface with the data from this object. */ refreshIdentityBlock() { if (!this._identityBox) { return; } let icon_label = ""; let tooltip = ""; let icon_country_label = ""; let icon_labels_dir = "ltr"; if (this._isSecureInternalUI) { this._identityBox.className = "chromeUI"; let brandBundle = document.getElementById("bundle_brand"); icon_label = brandBundle.getString("brandShorterName"); } else if (this._uriHasHost && this._isEV) { this._identityBox.className = "verifiedIdentity"; if (this._isMixedActiveContentBlocked) { this._identityBox.classList.add("mixedActiveBlocked"); } if (!this._isCertUserOverridden) { // If it's identified, then we can populate the dialog with credentials let iData = this.getIdentityData(); tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", [iData.caOrg]); icon_label = iData.subjectOrg; if (iData.country) icon_country_label = "(" + iData.country + ")"; // If the organization name starts with an RTL character, then // swap the positions of the organization and country code labels. // The Unicode ranges reflect the definition of the UTF16_CODE_UNIT_IS_BIDI // macro in intl/unicharutil/util/nsBidiUtils.h. When bug 218823 gets // fixed, this test should be replaced by one adhering to the // Unicode Bidirectional Algorithm proper (at the paragraph level). icon_labels_dir = /^[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc\ud802\ud803\ud83a\ud83b]/.test(icon_label) ? "rtl" : "ltr"; } } else if (this._pageExtensionPolicy) { this._identityBox.className = "extensionPage"; let extensionName = this._pageExtensionPolicy.name; icon_label = gNavigatorBundle.getFormattedString( "identity.extension.label", [extensionName]); } else if (this._uriHasHost && this._isSecure) { this._identityBox.className = "verifiedDomain"; if (this._isMixedActiveContentBlocked) { this._identityBox.classList.add("mixedActiveBlocked"); } if (!this._isCertUserOverridden) { // It's a normal cert, verifier is the CA Org. tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", [this.getIdentityData().caOrg]); } } else if (!this._uriHasHost) { this._identityBox.className = "unknownIdentity"; } else if (gBrowser.selectedBrowser.documentURI && (gBrowser.selectedBrowser.documentURI.scheme == "about" || gBrowser.selectedBrowser.documentURI.scheme == "chrome")) { // For net errors we should not show notSecure as it's likely confusing this._identityBox.className = "unknownIdentity"; } else { if (this._isBroken) { this._identityBox.className = "unknownIdentity"; if (this._isMixedActiveContentLoaded) { this._identityBox.classList.add("mixedActiveContent"); } else if (this._isMixedActiveContentBlocked) { this._identityBox.classList.add("mixedDisplayContentLoadedActiveBlocked"); } else if (this._isMixedPassiveContentLoaded) { this._identityBox.classList.add("mixedDisplayContent"); } else { this._identityBox.classList.add("weakCipher"); } } else { let warnOnInsecure = Services.prefs.getBoolPref("security.insecure_connection_icon.enabled") || (Services.prefs.getBoolPref("security.insecure_connection_icon.pbmode.enabled") && PrivateBrowsingUtils.isWindowPrivate(window)); let className = warnOnInsecure ? "notSecure" : "unknownIdentity"; this._identityBox.className = className; let warnTextOnInsecure = Services.prefs.getBoolPref("security.insecure_connection_text.enabled") || (Services.prefs.getBoolPref("security.insecure_connection_text.pbmode.enabled") && PrivateBrowsingUtils.isWindowPrivate(window)); if (warnTextOnInsecure) { icon_label = gNavigatorBundle.getString("identity.notSecure.label"); this._identityBox.classList.add("notSecureText"); } } if (this._hasInsecureLoginForms) { // Insecure login forms can only be present on "unknown identity" // pages, either already insecure or with mixed active content loaded. this._identityBox.classList.add("insecureLoginForms"); } } if (this._isCertUserOverridden) { this._identityBox.classList.add("certUserOverridden"); // Cert is trusted because of a security exception, verifier is a special string. tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you"); } let permissionAnchors = this._permissionAnchors; // hide all permission icons for (let icon of Object.values(permissionAnchors)) { icon.removeAttribute("showing"); } // keeps track if we should show an indicator that there are active permissions let hasGrantedPermissions = false; // show permission icons let permissions = SitePermissions.getAllForBrowser(gBrowser.selectedBrowser); for (let permission of permissions) { if (permission.state == SitePermissions.BLOCK) { let icon = permissionAnchors[permission.id]; if (icon) { icon.setAttribute("showing", "true"); } } else if (permission.state != SitePermissions.UNKNOWN) { hasGrantedPermissions = true; } } if (hasGrantedPermissions) { this._identityBox.classList.add("grantedPermissions"); } // Show blocked popup icon in the identity-box if popups are blocked // irrespective of popup permission capability value. if (gBrowser.selectedBrowser.blockedPopups && gBrowser.selectedBrowser.blockedPopups.length) { let icon = permissionAnchors.popup; icon.setAttribute("showing", "true"); } // Push the appropriate strings out to the UI this._connectionIcon.setAttribute("tooltiptext", tooltip); if (this._pageExtensionPolicy) { let extensionName = this._pageExtensionPolicy.name; this._extensionIcon.setAttribute("tooltiptext", gNavigatorBundle.getFormattedString("identity.extension.tooltip", [extensionName])); } this._identityIconLabels.setAttribute("tooltiptext", tooltip); this._identityIcon.setAttribute("tooltiptext", gNavigatorBundle.getString("identity.icon.tooltip")); this._identityIconLabel.setAttribute("value", icon_label); this._identityIconCountryLabel.setAttribute("value", icon_country_label); // Set cropping and direction this._identityIconLabel.setAttribute("crop", icon_country_label ? "end" : "center"); this._identityIconLabel.parentNode.style.direction = icon_labels_dir; // Hide completely if the organization label is empty this._identityIconLabel.parentNode.collapsed = !icon_label; }, /** * Set up the title and content messages for the identity message popup, * based on the specified mode, and the details of the SSL cert, where * applicable */ refreshIdentityPopup() { // Update cookies and site data information and show the // "Clear Site Data" button if the site is storing local data. this._clearSiteDataFooter.hidden = true; if (this._uriHasHost) { let host = this._uri.host; SiteDataManager.updateSites().then(async () => { let baseDomain = SiteDataManager.getBaseDomainFromHost(host); let siteData = await SiteDataManager.getSites(baseDomain); if (siteData && siteData.length) { this._clearSiteDataFooter.hidden = false; } }); } // Update "Learn More" for Mixed Content Blocking and Insecure Login Forms. let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); this._identityPopupMixedContentLearnMore .setAttribute("href", baseURL + "mixed-content"); this._identityPopupInsecureLoginFormsLearnMore .setAttribute("href", baseURL + "insecure-password"); // This is in the properties file because the expander used to switch its tooltip. this._popupExpander.tooltipText = gNavigatorBundle.getString("identity.showDetails.tooltip"); // Determine connection security information. let connection = "not-secure"; if (this._isSecureInternalUI) { connection = "chrome"; } else if (this._pageExtensionPolicy) { connection = "extension"; } else if (this._isURILoadedFromFile) { connection = "file"; } else if (this._isEV) { connection = "secure-ev"; } else if (this._isCertUserOverridden) { connection = "secure-cert-user-overridden"; } else if (this._isSecure) { connection = "secure"; } // Determine if there are insecure login forms. let loginforms = "secure"; if (this._hasInsecureLoginForms) { loginforms = "insecure"; } // Determine the mixed content state. let mixedcontent = []; if (this._isMixedPassiveContentLoaded) { mixedcontent.push("passive-loaded"); } if (this._isMixedActiveContentLoaded) { mixedcontent.push("active-loaded"); } else if (this._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 content loaded then it's a weak // cipher. let ciphers = ""; if (this._isBroken && !this._isMixedActiveContentLoaded && !this._isMixedPassiveContentLoaded) { 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, "loginforms", loginforms); updateAttribute(element, "ciphers", ciphers); updateAttribute(element, "mixedcontent", mixedcontent); updateAttribute(element, "isbroken", this._isBroken); } // Initialize the optional strings to empty values let supplemental = ""; let verifier = ""; let host = ""; let owner = ""; let hostless = false; try { host = this.getEffectiveHost(); } catch (e) { // Some URIs might have no hosts. } // Fallback for special protocols. if (!host) { host = this._uri.specIgnoringRef; // Special URIs without a host (eg, about:) should crop the end so // the protocol can be seen. hostless = true; } if (this._pageExtensionPolicy) { host = this._pageExtensionPolicy.name; } // Fill in the CA name if we have a valid TLS certificate. if (this._isSecure || this._isCertUserOverridden) { verifier = this._identityIconLabels.tooltipText; } // Fill in organization information if we have a valid EV certificate. if (this._isEV) { let iData = this.getIdentityData(); host = owner = iData.subjectOrg; verifier = this._identityIconLabels.tooltipText; // Build an appropriate supplemental block out of whatever location data we have if (iData.city) supplemental += iData.city + "\n"; if (iData.state && iData.country) supplemental += gNavigatorBundle.getFormattedString("identity.identified.state_and_country", [iData.state, iData.country]); else if (iData.state) // State only supplemental += iData.state; else if (iData.country) // Country only supplemental += iData.country; } // Push the appropriate strings out to the UI. this._identityPopupContentHosts.forEach((el) => { el.textContent = host; el.hidden = hostless; }); this._identityPopupContentHostless.forEach((el) => { el.setAttribute("value", host); el.hidden = !hostless; }); this._identityPopupContentOwner.textContent = owner; this._identityPopupContentSupp.textContent = supplemental; this._identityPopupContentVerif.textContent = verifier; // Update per-site permissions section. this.updateSitePermissions(); }, setURI(uri) { this._uri = uri; try { // Account for file: urls and catch when "" is the value this._uriHasHost = !!this._uri.host; } catch (ex) { this._uriHasHost = false; } this._isSecureInternalUI = uri.schemeIs("about") && this._secureInternalUIWhitelist.test(uri.pathQueryRef); this._pageExtensionPolicy = WebExtensionPolicy.getByURI(uri); // 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. this._isURILoadedFromFile = false; let chanOptions = {uri: this._uri, loadUsingSystemPrincipal: true}; let resolvedURI; try { resolvedURI = NetUtil.newChannel(chanOptions).URI; if (resolvedURI.schemeIs("jar")) { // Given a URI "jar:!/" // create a new URI using !/ resolvedURI = NetUtil.newURI(resolvedURI.pathQueryRef); } // Check the URI again after resolving. this._isURILoadedFromFile = resolvedURI.schemeIs("file"); } catch (ex) { // NetUtil's methods will throw for malformed URIs and the like } }, /** * Click handler for the identity-box element in primary chrome. */ handleIdentityButtonEvent(event) { event.stopPropagation(); if ((event.type == "click" && event.button != 0) || (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE && event.keyCode != KeyEvent.DOM_VK_RETURN)) { return; // Left click, space or enter only } // Don't allow left click, space or enter if the location has been modified, // so long as we're not sharing any devices. // If we are sharing a device, the identity block is prevented by CSS from // being focused (and therefore, interacted with) by the user. However, we // want to allow opening the identity popup from the device control menu, // which calls click() on the identity button, so we don't return early. if (!this._sharingState && gURLBar.getAttribute("pageproxystate") != "valid") { return; } // Move focus to the next available element in the identity popup. // This is required by role=alertdialog and fixes an issue where // an already open panel would steal focus from the identity popup. if (event.type == "keypress") { let panelView = PanelView.forNode(this._identityPopupMainView); this._identityPopupMainView.addEventListener("ViewShown", () => panelView.focusFirstNavigableElement(), {once: true}); } // Make sure that the display:none style we set in xul is removed now that // the popup is actually needed this._identityPopup.hidden = false; // Remove the reload hint that we show after a user has cleared a permission. this._permissionReloadHint.setAttribute("hidden", "true"); // Update the popup strings this.refreshIdentityPopup(); // Add the "open" attribute to the identity box for styling this._identityBox.setAttribute("open", "true"); // Now open the popup, anchored off the primary chrome element PanelMultiView.openPopup(this._identityPopup, this._identityIcon, "bottomcenter topleft").catch(Cu.reportError); }, onPopupShown(event) { if (event.target == this._identityPopup) { window.addEventListener("focus", this, true); } }, onPopupHidden(event) { if (event.target == this._identityPopup) { window.removeEventListener("focus", this, true); this._identityBox.removeAttribute("open"); } }, handleEvent(event) { let elem = document.activeElement; let position = elem.compareDocumentPosition(this._identityPopup); if (!(position & (Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_CONTAINED_BY)) && !this._identityPopup.hasAttribute("noautohide")) { // Hide the panel when focusing an element that is // neither an ancestor nor descendant unless the panel has // @noautohide (e.g. for a tour). PanelMultiView.hidePopup(this._identityPopup); } }, observe(subject, topic, data) { if (topic == "perm-changed") { this.refreshIdentityBlock(); } }, onDragStart(event) { if (gURLBar.getAttribute("pageproxystate") != "valid") return; let value = gBrowser.currentURI.displaySpec; let urlString = value + "\n" + gBrowser.contentTitle; let htmlString = "" + value + ""; let dt = event.dataTransfer; dt.setData("text/x-moz-url", urlString); dt.setData("text/uri-list", value); dt.setData("text/plain", value); dt.setData("text/html", htmlString); dt.setDragImage(this._identityIcon, 16, 16); }, onLocationChange() { this._permissionReloadHint.setAttribute("hidden", "true"); if (!this._permissionList.hasChildNodes()) { this._permissionEmptyHint.removeAttribute("hidden"); } }, updateSitePermissions() { while (this._permissionList.hasChildNodes()) this._permissionList.removeChild(this._permissionList.lastChild); let permissions = SitePermissions.getAllPermissionDetailsForBrowser(gBrowser.selectedBrowser); if (this._sharingState) { // If WebRTC device or screen permissions are in use, we need to find // the associated permission item to set the sharingState field. for (let id of ["camera", "microphone", "screen"]) { if (this._sharingState[id]) { let found = false; for (let permission of permissions) { if (permission.id != id) continue; found = true; permission.sharingState = this._sharingState[id]; break; } if (!found) { // If the permission item we were looking for doesn't exist, // the user has temporarily allowed sharing and we need to add // an item in the permissions array to reflect this. permissions.push({ id, state: SitePermissions.ALLOW, scope: SitePermissions.SCOPE_REQUEST, sharingState: this._sharingState[id], }); } } } } let hasBlockedPopupIndicator = false; for (let permission of permissions) { let item = this._createPermissionItem(permission); this._permissionList.appendChild(item); if (permission.id == "popup" && gBrowser.selectedBrowser.blockedPopups && gBrowser.selectedBrowser.blockedPopups.length) { this._createBlockedPopupIndicator(); hasBlockedPopupIndicator = true; } } if (gBrowser.selectedBrowser.blockedPopups && gBrowser.selectedBrowser.blockedPopups.length && !hasBlockedPopupIndicator) { let permission = { id: "popup", state: SitePermissions.getDefault("popup"), scope: SitePermissions.SCOPE_PERSISTENT, }; let item = this._createPermissionItem(permission); this._permissionList.appendChild(item); this._createBlockedPopupIndicator(); } // Show a placeholder text if there's no permission and no reload hint. if (!this._permissionList.hasChildNodes() && this._permissionReloadHint.hasAttribute("hidden")) { this._permissionEmptyHint.removeAttribute("hidden"); } else { this._permissionEmptyHint.setAttribute("hidden", "true"); } }, _createPermissionItem(aPermission) { let container = document.createElement("hbox"); container.setAttribute("class", "identity-popup-permission-item"); container.setAttribute("align", "center"); container.setAttribute("role", "group"); let img = document.createElement("image"); img.classList.add("identity-popup-permission-icon"); if (aPermission.id == "plugin:flash") { img.classList.add("plugin-icon"); } else { img.classList.add(aPermission.id + "-icon"); } if (aPermission.state == SitePermissions.BLOCK) img.classList.add("blocked-permission-icon"); if (aPermission.sharingState == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED || (aPermission.id == "screen" && aPermission.sharingState && !aPermission.sharingState.includes("Paused"))) { img.classList.add("in-use"); // Synchronize control center and identity block blinking animations. window.promiseDocumentFlushed(() => { let sharingIconBlink = document.getElementById("sharing-icon").getAnimations()[0]; let imgBlink = img.getAnimations()[0]; return [sharingIconBlink, imgBlink]; }).then(([sharingIconBlink, imgBlink]) => { if (sharingIconBlink && imgBlink) { imgBlink.startTime = sharingIconBlink.startTime; } }); } let nameLabel = document.createElement("label"); nameLabel.setAttribute("flex", "1"); nameLabel.setAttribute("class", "identity-popup-permission-label"); nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id); let nameLabelId = "identity-popup-permission-label-" + aPermission.id; nameLabel.setAttribute("id", nameLabelId); let isPolicyPermission = aPermission.scope == SitePermissions.SCOPE_POLICY; if (aPermission.id == "popup" && !isPolicyPermission) { let menulist = document.createElement("menulist"); let menupopup = document.createElement("menupopup"); let block = document.createElement("vbox"); block.setAttribute("id", "identity-popup-popup-container"); menulist.setAttribute("sizetopopup", "none"); menulist.setAttribute("class", "identity-popup-popup-menulist subviewkeynav"); menulist.setAttribute("id", "identity-popup-popup-menulist"); for (let state of SitePermissions.getAvailableStates(aPermission.id)) { let menuitem = document.createElement("menuitem"); // We need to correctly display the default/unknown state, which has its // own integer value (0) but represents one of the other states. if (state == SitePermissions.getDefault(aPermission.id)) { menuitem.setAttribute("value", "0"); } else { menuitem.setAttribute("value", state); } menuitem.setAttribute("label", SitePermissions.getMultichoiceStateLabel(state)); menupopup.appendChild(menuitem); } menulist.appendChild(menupopup); if (aPermission.state == SitePermissions.getDefault(aPermission.id)) { menulist.value = "0"; } else { menulist.value = aPermission.state; } // Avoiding listening to the "select" event on purpose. See Bug 1404262. menulist.addEventListener("command", () => { SitePermissions.set(gBrowser.currentURI, aPermission.id, menulist.selectedItem.value); }); container.appendChild(img); container.appendChild(nameLabel); container.appendChild(menulist); container.setAttribute("aria-labelledby", nameLabelId); block.appendChild(container); return block; } let stateLabel = document.createElement("label"); stateLabel.setAttribute("flex", "1"); stateLabel.setAttribute("class", "identity-popup-permission-state-label"); let stateLabelId = "identity-popup-permission-state-label-" + aPermission.id; stateLabel.setAttribute("id", stateLabelId); let {state, scope} = aPermission; // If the user did not permanently allow this device but it is currently // used, set the variables to display a "temporarily allowed" info. if (state != SitePermissions.ALLOW && aPermission.sharingState) { state = SitePermissions.ALLOW; scope = SitePermissions.SCOPE_REQUEST; } stateLabel.textContent = SitePermissions.getCurrentStateLabel(state, aPermission.id, scope); container.appendChild(img); container.appendChild(nameLabel); container.appendChild(stateLabel); container.setAttribute("aria-labelledby", nameLabelId + " " + stateLabelId); /* We return the permission item here without a remove button if the permission is a SCOPE_POLICY permission. Policy permissions cannot be removed/changed for the duration of the browser session. */ if (isPolicyPermission) { return container; } let button = document.createElement("button"); button.setAttribute("class", "identity-popup-permission-remove-button subviewkeynav"); let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip"); button.setAttribute("tooltiptext", tooltiptext); button.addEventListener("command", () => { let browser = gBrowser.selectedBrowser; this._permissionList.removeChild(container); if (aPermission.sharingState && ["camera", "microphone", "screen"].includes(aPermission.id)) { let windowId = this._sharingState.windowId; if (aPermission.id == "screen") { windowId = "screen:" + windowId; } else { // If we set persistent permissions or the sharing has // started due to existing persistent permissions, we need // to handle removing these even for frames with different hostnames. let uris = browser._devicePermissionURIs || []; for (let uri of uris) { // It's not possible to stop sharing one of camera/microphone // without the other. for (let id of ["camera", "microphone"]) { if (this._sharingState[id]) { let perm = SitePermissions.get(uri, id); if (perm.state == SitePermissions.ALLOW && perm.scope == SitePermissions.SCOPE_PERSISTENT) { SitePermissions.remove(uri, id); } } } } } browser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId); webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser); } SitePermissions.remove(gBrowser.currentURI, aPermission.id, browser); this._permissionReloadHint.removeAttribute("hidden"); PanelView.forNode(this._identityPopupMainView) .descriptionHeightWorkaround(); }); container.appendChild(button); return container; }, _createBlockedPopupIndicator() { let indicator = document.createElement("hbox"); indicator.setAttribute("class", "identity-popup-permission-item"); indicator.setAttribute("align", "center"); indicator.setAttribute("id", "blocked-popup-indicator-item"); let icon = document.createElement("image"); icon.setAttribute("class", "popup-subitem identity-popup-permission-icon"); let text = document.createElement("label"); text.setAttribute("flex", "1"); text.setAttribute("class", "identity-popup-permission-label text-link subviewkeynav"); let popupCount = gBrowser.selectedBrowser.blockedPopups.length; let messageBase = gNavigatorBundle.getString("popupShowBlockedPopupsIndicatorText"); let message = PluralForm.get(popupCount, messageBase) .replace("#1", popupCount); text.textContent = message; text.addEventListener("click", () => { gPopupBlockerObserver.showAllBlockedPopups(gBrowser.selectedBrowser); }); indicator.appendChild(icon); indicator.appendChild(text); document.getElementById("identity-popup-popup-container").appendChild(indicator); }, };