/* 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/. */ /* eslint-env mozilla/browser-window */ /** * 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 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, /** * Whether the content window is considered a "secure context". This * includes "potentially trustworthy" origins such as file:// URLs or localhost. * https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy */ _isSecureContext: false, /** * nsITransportSecurityInfo metadata provided by gBrowser.securityUI the last * time the identity UI was updated, or null if the connection is not secure. */ _secInfo: 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. */ _secureInternalPages: /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion)(?:[?#]|$)/i, /** * Whether the established HTTPS connection is considered "broken". * This could have several reasons, such as mixed content or weak * cryptography. If this is true, _isSecureConnection is false. */ get _isBrokenConnection() { return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; }, /** * Whether the connection to the current site was done via secure * transport. Note that this attribute is not true in all cases that * the site was accessed via HTTPS, i.e. _isSecureConnection will * be false when _isBrokenConnection is true, even though the page * was loaded over HTTPS. */ get _isSecureConnection() { // 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 _isContentHttpsOnlyModeUpgraded() { return ( this._state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED ); }, get _isContentHttpsOnlyModeUpgradeFailed() { return ( this._state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED ); }, get _isCertUserOverridden() { return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN; }, get _isCertDistrustImminent() { return this._state & Ci.nsIWebProgressListener.STATE_CERT_DISTRUST_IMMINENT; }, get _isAboutCertErrorPage() { return ( gBrowser.selectedBrowser.documentURI && gBrowser.selectedBrowser.documentURI.scheme == "about" && gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith("certerror") ); }, get _isAboutNetErrorPage() { return ( gBrowser.selectedBrowser.documentURI && gBrowser.selectedBrowser.documentURI.scheme == "about" && gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith("neterror") ); }, get _isAboutHttpsOnlyErrorPage() { return ( gBrowser.selectedBrowser.documentURI && gBrowser.selectedBrowser.documentURI.scheme == "about" && gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith( "httpsonlyerror" ) ); }, get _isPotentiallyTrustworthy() { return ( !this._isBrokenConnection && (this._isSecureContext || (gBrowser.selectedBrowser.documentURI && gBrowser.selectedBrowser.documentURI.scheme == "chrome")) ); }, get _isAboutBlockedPage() { return ( gBrowser.selectedBrowser.documentURI && gBrowser.selectedBrowser.documentURI.scheme == "about" && gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith("blocked") ); }, _popupInitialized: false, _initializePopup() { if (!this._popupInitialized) { let wrapper = document.getElementById("template-identity-popup"); wrapper.replaceWith(wrapper.content); this._popupInitialized = true; } }, hidePopup() { if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } }, // smart getters get _identityPopup() { if (!this._popupInitialized) { return null; } 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 _identityPopupMainViewHeaderLabel() { delete this._identityPopupMainViewHeaderLabel; return (this._identityPopupMainViewHeaderLabel = document.getElementById( "identity-popup-mainView-panel-header-span" )); }, get _identityPopupSecurityView() { delete this._identityPopupSecurityView; return (this._identityPopupSecurityView = document.getElementById( "identity-popup-securityView" )); }, get _identityPopupHttpsOnlyModeMenuList() { delete this._identityPopupHttpsOnlyModeMenuList; return (this._identityPopupHttpsOnlyModeMenuList = document.getElementById( "identity-popup-security-httpsonlymode-menulist" )); }, get _identityPopupHttpsOnlyModeMenuListTempItem() { delete this._identityPopupHttpsOnlyModeMenuListTempItem; return (this._identityPopupHttpsOnlyModeMenuListTempItem = document.getElementById( "identity-popup-security-menulist-tempitem" )); }, get _identityPopupSecurityEVContentOwner() { delete this._identityPopupSecurityEVContentOwner; return (this._identityPopupSecurityEVContentOwner = document.getElementById( "identity-popup-security-ev-content-owner" )); }, 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 _identityPopupCustomRootLearnMore() { delete this._identityPopupCustomRootLearnMore; return (this._identityPopupCustomRootLearnMore = document.getElementById( "identity-popup-custom-root-learn-more" )); }, get _identityPopupMixedContentLearnMore() { delete this._identityPopupMixedContentLearnMore; return (this._identityPopupMixedContentLearnMore = [ ...document.querySelectorAll(".identity-popup-mcb-learn-more"), ]); }, get _identityIconLabel() { delete this._identityIconLabel; return (this._identityIconLabel = document.getElementById( "identity-icon-label" )); }, get _overrideService() { delete this._overrideService; return (this._overrideService = Cc[ "@mozilla.org/security/certoverride;1" ].getService(Ci.nsICertOverrideService)); }, 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 _defaultPermissionAnchor() { delete this._defaultPermissionAnchor; return (this._defaultPermissionAnchor = document.getElementById( "identity-popup-permission-list-default-anchor" )); }, 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); }, get _geoSharingIcon() { delete this._geoSharingIcon; return (this._geoSharingIcon = document.getElementById("geo-sharing-icon")); }, get _xrSharingIcon() { delete this._xrSharingIcon; return (this._xrSharingIcon = document.getElementById("xr-sharing-icon")); }, get _webRTCSharingIcon() { delete this._webRTCSharingIcon; return (this._webRTCSharingIcon = document.getElementById( "webrtc-sharing-icon" )); }, get _insecureConnectionIconEnabled() { delete this._insecureConnectionIconEnabled; XPCOMUtils.defineLazyPreferenceGetter( this, "_insecureConnectionIconEnabled", "security.insecure_connection_icon.enabled" ); return this._insecureConnectionIconEnabled; }, get _insecureConnectionIconPBModeEnabled() { delete this._insecureConnectionIconPBModeEnabled; XPCOMUtils.defineLazyPreferenceGetter( this, "_insecureConnectionIconPBModeEnabled", "security.insecure_connection_icon.pbmode.enabled" ); return this._insecureConnectionIconPBModeEnabled; }, get _insecureConnectionTextEnabled() { delete this._insecureConnectionTextEnabled; XPCOMUtils.defineLazyPreferenceGetter( this, "_insecureConnectionTextEnabled", "security.insecure_connection_text.enabled" ); return this._insecureConnectionTextEnabled; }, get _insecureConnectionTextPBModeEnabled() { delete this._insecureConnectionTextPBModeEnabled; XPCOMUtils.defineLazyPreferenceGetter( this, "_insecureConnectionTextPBModeEnabled", "security.insecure_connection_text.pbmode.enabled" ); return this._insecureConnectionTextPBModeEnabled; }, get _protectionsPanelEnabled() { delete this._protectionsPanelEnabled; XPCOMUtils.defineLazyPreferenceGetter( this, "_protectionsPanelEnabled", "browser.protections_panel.enabled", false ); return this._protectionsPanelEnabled; }, get _httpsOnlyModeEnabled() { delete this._httpsOnlyModeEnabled; XPCOMUtils.defineLazyPreferenceGetter( this, "_httpsOnlyModeEnabled", "dom.security.https_only_mode" ); return this._httpsOnlyModeEnabled; }, get _httpsOnlyModeEnabledPBM() { delete this._httpsOnlyModeEnabledPBM; XPCOMUtils.defineLazyPreferenceGetter( this, "_httpsOnlyModeEnabledPBM", "dom.security.https_only_mode_pbm" ); return this._httpsOnlyModeEnabledPBM; }, get _useGrayLockIcon() { delete this._useGrayLockIcon; XPCOMUtils.defineLazyPreferenceGetter( this, "_useGrayLockIcon", "security.secure_connection_icon_color_gray", false ); return this._useGrayLockIcon; }, /** * Handles clicks on the "Clear Cookies and Site Data" button. */ async clearSiteData(event) { if (!this._uriHasHost) { return; } let host = this._uri.host; // 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. let hidden = new Promise(c => { this._identityPopup.addEventListener("popuphidden", c, { once: true }); }); PanelMultiView.hidePopup(this._identityPopup); await hidden; let baseDomain = SiteDataManager.getBaseDomainFromHost(host); if (SiteDataManager.promptSiteDataRemoval(window, null, baseDomain)) { let siteData = await SiteDataManager.getSites(baseDomain); if (siteData && siteData.length) { let hosts = siteData.map(site => site.host); SiteDataManager.remove(hosts); } } event.stopPropagation(); }, openPermissionPreferences() { openPreferences("privacy-permissions"); }, /** * 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); if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } }, enableMixedContentProtection() { gBrowser.selectedBrowser.sendMessageToActor( "MixedContent:ReenableProtection", {}, "BrowserTab" ); BrowserReload(); if (this._popupInitialized) { 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(); if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } }, /** * Gets the current HTTPS-Only mode permission for the current page. * Values are the same as in #identity-popup-security-httpsonlymode-menulist */ _getHttpsOnlyPermission() { const { state } = SitePermissions.getForPrincipal( gBrowser.contentPrincipal, "https-only-load-insecure" ); switch (state) { case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION: return 2; // Off temporarily case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW: return 1; // Off default: return 0; // On } }, /** * Sets/removes HTTPS-Only Mode exception and possibly reloads the page. */ changeHttpsOnlyPermission() { // Get the new value from the menulist and the current value // Note: value and permission association is laid out // in _getHttpsOnlyPermission const oldValue = this._getHttpsOnlyPermission(); let newValue = parseInt( this._identityPopupHttpsOnlyModeMenuList.selectedItem.value, 10 ); // If nothing changed, just return here if (newValue === oldValue) { return; } // Permissions set in PMB get deleted anyway, but to make sure, let's make // the permission session-only. if (newValue === 1 && PrivateBrowsingUtils.isWindowPrivate(window)) { newValue = 2; } // Usually we want to set the permission for the current site and therefore // the current principal... let principal = gBrowser.contentPrincipal; // ...but if we're on the HTTPS-Only error page, the content-principal is // for HTTPS but. We always want to set the exception for HTTP. (Code should // be almost identical to the one in AboutHttpsOnlyErrorParent.jsm) let newURI; if (this._isAboutHttpsOnlyErrorPage) { newURI = gBrowser.currentURI .mutate() .setScheme("http") .finalize(); principal = Services.scriptSecurityManager.createContentPrincipal( newURI, gBrowser.contentPrincipal.originAttributes ); } // Set or remove the permission if (newValue === 0) { SitePermissions.removeFromPrincipal( principal, "https-only-load-insecure" ); } else if (newValue === 1) { SitePermissions.setForPrincipal( principal, "https-only-load-insecure", Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW, SitePermissions.SCOPE_PERSISTENT ); } else { SitePermissions.setForPrincipal( principal, "https-only-load-insecure", Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, SitePermissions.SCOPE_SESSION ); } // If we're on the error-page, we have to redirect the user // from HTTPS to HTTP. Otherwise we can just reload the page. if (this._isAboutHttpsOnlyErrorPage) { gBrowser.loadURI(newURI.spec, { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, }); if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } return; } // The page only needs to reload if we switch between allow and block // Because "off" is 1 and "off temporarily" is 2, we can just check if the // sum of newValue and oldValue is 3. if (newValue + oldValue !== 3) { BrowserReloadSkipCache(); if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } return; } // Otherwise we just refresh the interface this.refreshIdentityPopup(); }, /** * Helper to parse out the important parts of _secInfo (of the SSL cert in * particular) for use in constructing identity UI strings */ getIdentityData() { var result = {}; var cert = this._secInfo.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 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._secInfo = gBrowser.securityUI.secInfo; this._isSecureContext = gBrowser.securityUI.isSecureContext; // 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 && this._popupInitialized) { 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); } }, updateSharingIndicator() { let tab = gBrowser.selectedTab; this._sharingState = tab._sharingState; this._webRTCSharingIcon.removeAttribute("paused"); this._webRTCSharingIcon.removeAttribute("sharing"); this._geoSharingIcon.removeAttribute("sharing"); this._xrSharingIcon.removeAttribute("sharing"); if (this._sharingState) { if ( this._sharingState && this._sharingState.webRTC && this._sharingState.webRTC.sharing ) { this._webRTCSharingIcon.setAttribute( "sharing", this._sharingState.webRTC.sharing ); if (this._sharingState.webRTC.paused) { this._webRTCSharingIcon.setAttribute("paused", "true"); } } if (this._sharingState.geo) { this._geoSharingIcon.setAttribute("sharing", this._sharingState.geo); } if (this._sharingState.xr) { this._xrSharingIcon.setAttribute("sharing", this._sharingState.xr); } } if (this._popupInitialized && this._identityPopup.state != "closed") { 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; } }, getHostForDisplay() { let host = ""; try { host = this.getEffectiveHost(); } catch (e) { // Some URIs might have no hosts. } if (this._uri.schemeIs("about")) { // For example in about:certificate the original URL is // about:certificate?cert=&cert=&cert=... // So, instead of showing that large string in the identity panel header, we are just showing // about:certificate now. For the other about pages we are just showing about: host = "about:" + this._uri.filePath; } if (this._uri.schemeIs("chrome")) { host = this._uri.spec; } let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay( this._uri.displaySpec ); if (readerStrippedURI) { host = readerStrippedURI.host; } if (this._pageExtensionPolicy) { host = this._pageExtensionPolicy.name; } // Fallback for special protocols. if (!host) { host = this._uri.specIgnoringRef; } return 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._isSecureConnection) { return "verifiedDomain"; } return "unknownIdentity"; }, /** * Returns whether the issuer of the current certificate chain is * built-in (returns false) or imported (returns true). */ _hasCustomRoot() { let issuerCert = null; issuerCert = this._secInfo.succeededCertChain[ this._secInfo.succeededCertChain.length - 1 ]; return !issuerCert.isBuiltInRoot; }, /** * Returns whether the current URI results in an "invalid" * URL bar state, which effectively means hidden security * indicators. */ _hasInvalidPageProxyState() { return ( !this._uriHasHost && this._uri && isBlankPageURL(this._uri.spec) && !this._uri.schemeIs("moz-extension") ); }, /** * Updates the security identity in the identity block. */ _refreshIdentityIcons() { let icon_label = ""; let tooltip = ""; if (this._isSecureInternalUI) { // This is a secure internal Firefox page. this._identityBox.className = "chromeUI"; let brandBundle = document.getElementById("bundle_brand"); icon_label = brandBundle.getString("brandShorterName"); } else if (this._pageExtensionPolicy) { // This is a WebExtension page. this._identityBox.className = "extensionPage"; let extensionName = this._pageExtensionPolicy.name; icon_label = gNavigatorBundle.getFormattedString( "identity.extension.label", [extensionName] ); } else if (this._uriHasHost && this._isSecureConnection) { // This is a secure connection. 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._isBrokenConnection) { // This is a secure connection, but something is wrong. 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 if (this._isAboutCertErrorPage) { // We show a warning lock icon for 'about:certerror' page. this._identityBox.className = "certErrorPage"; } else if (this._isAboutHttpsOnlyErrorPage) { // We show a not secure lock icon for 'about:httpsonlyerror' page. this._identityBox.className = "httpsOnlyErrorPage"; } else if (this._isAboutNetErrorPage || this._isAboutBlockedPage) { // Network errors and blocked pages get a more neutral icon this._identityBox.className = "unknownIdentity"; } else if (this._isPotentiallyTrustworthy) { // This is a local resource (and shouldn't be marked insecure). this._identityBox.className = "localResource"; } else { // This is an insecure connection. let warnOnInsecure = this._insecureConnectionIconEnabled || (this._insecureConnectionIconPBModeEnabled && PrivateBrowsingUtils.isWindowPrivate(window)); let className = warnOnInsecure ? "notSecure" : "unknownIdentity"; this._identityBox.className = className; tooltip = warnOnInsecure ? gNavigatorBundle.getString("identity.notSecure.tooltip") : ""; let warnTextOnInsecure = this._insecureConnectionTextEnabled || (this._insecureConnectionTextPBModeEnabled && PrivateBrowsingUtils.isWindowPrivate(window)); if (warnTextOnInsecure) { icon_label = gNavigatorBundle.getString("identity.notSecure.label"); this._identityBox.classList.add("notSecureText"); } } 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" ); } // Gray lock icon for secure connections if pref set this._updateAttribute( this._identityIcon, "lock-icon-gray", this._useGrayLockIcon ); // Push the appropriate strings out to the UI this._identityIcon.setAttribute("tooltiptext", tooltip); if (this._pageExtensionPolicy) { let extensionName = this._pageExtensionPolicy.name; this._identityIcon.setAttribute( "tooltiptext", gNavigatorBundle.getFormattedString("identity.extension.tooltip", [ extensionName, ]) ); } this._identityIconLabel.setAttribute("tooltiptext", tooltip); this._identityIconLabel.setAttribute("value", icon_label); this._identityIconLabel.collapsed = !icon_label; }, /** * Updates the permissions block in the identity block. */ _refreshPermissionIcons() { 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 || permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL ) { 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.popupBlocker.getBlockedPopupCount()) { let icon = permissionAnchors.popup; icon.setAttribute("showing", "true"); } }, /** * Updates the identity block user interface with the data from this object. */ refreshIdentityBlock() { if (!this._identityBox) { return; } // If this condition is true, the URL bar will have an "invalid" // pageproxystate, which will hide the security indicators. Thus, we can // safely avoid updating the security UI. // // This will also filter out intermediate about:blank loads to avoid // flickering the identity block and doing unnecessary work. if (this._hasInvalidPageProxyState()) { return; } this._refreshIdentityIcons(); this._refreshPermissionIcons(); // Hide the shield icon if it is a chrome page. gProtectionsHandler._trackingProtectionIconContainer.classList.toggle( "chromeUI", this._isSecureInternalUI ); }, /** * 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) { SiteDataManager.hasSiteData(this._uri.asciiHost).then(hasData => { this._clearSiteDataFooter.hidden = !hasData; }); } // Update "Learn More" for Mixed Content Blocking and Insecure Login Forms. let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); this._identityPopupMixedContentLearnMore.forEach(e => e.setAttribute("href", baseURL + "mixed-content") ); this._identityPopupCustomRootLearnMore.setAttribute( "href", baseURL + "enterprise-roots" ); // This is in the properties file because the expander used to switch its tooltip. this._popupExpander.tooltipText = gNavigatorBundle.getString( "identity.showDetails.tooltip" ); let customRoot = false; // 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._isSecureConnection) { connection = "secure"; customRoot = this._hasCustomRoot(); } else if (this._isAboutCertErrorPage) { connection = "cert-error-page"; } else if (this._isAboutHttpsOnlyErrorPage) { connection = "https-only-error-page"; } else if (this._isAboutNetErrorPage || this._isAboutBlockedPage) { connection = "not-secure"; } else if (this._isPotentiallyTrustworthy) { connection = "file"; } // 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._isBrokenConnection && !this._isMixedActiveContentLoaded && !this._isMixedPassiveContentLoaded ) { ciphers = "weak"; } // Gray lock icon for secure connections if pref set this._updateAttribute( this._identityPopup, "lock-icon-gray", this._useGrayLockIcon ); // If HTTPS-Only Mode is enabled, check the permission status const privateBrowsingWindow = PrivateBrowsingUtils.isWindowPrivate(window); let httpsOnlyStatus = ""; if ( this._httpsOnlyModeEnabled || (privateBrowsingWindow && this._httpsOnlyModeEnabledPBM) ) { // Note: value and permission association is laid out // in _getHttpsOnlyPermission let value = this._getHttpsOnlyPermission(); // Because everything in PBM is temporary anyway, we don't need to make the distinction if (privateBrowsingWindow) { if (value === 2) { value = 1; } // Hide "off temporarily" option this._identityPopupHttpsOnlyModeMenuListTempItem.style.display = "none"; } else { this._identityPopupHttpsOnlyModeMenuListTempItem.style.display = ""; } this._identityPopupHttpsOnlyModeMenuList.value = value; if (value > 0) { httpsOnlyStatus = "exception"; } else if (this._isAboutHttpsOnlyErrorPage) { httpsOnlyStatus = "failed-top"; } else if (this._isContentHttpsOnlyModeUpgradeFailed) { httpsOnlyStatus = "failed-sub"; } else if (this._isContentHttpsOnlyModeUpgraded) { httpsOnlyStatus = "upgraded"; } } // Update all elements. let elementIDs = ["identity-popup", "identity-popup-securityView-body"]; for (let id of elementIDs) { let element = document.getElementById(id); this._updateAttribute(element, "connection", connection); this._updateAttribute(element, "ciphers", ciphers); this._updateAttribute(element, "mixedcontent", mixedcontent); this._updateAttribute(element, "isbroken", this._isBrokenConnection); this._updateAttribute(element, "customroot", customRoot); this._updateAttribute(element, "httpsonlystatus", httpsOnlyStatus); } // Initialize the optional strings to empty values let supplemental = ""; let verifier = ""; let host = this.getHostForDisplay(); let owner = ""; // Fill in the CA name if we have a valid TLS certificate. if (this._isSecureConnection || this._isCertUserOverridden) { verifier = this._identityIconLabel.tooltipText; } // Fill in organization information if we have a valid EV certificate. if (this._isEV) { let iData = this.getIdentityData(); owner = iData.subjectOrg; verifier = this._identityIconLabel.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._identityPopupMainViewHeaderLabel.textContent = gNavigatorBundle.getFormattedString( "identity.headerMainWithHost", [host] ); this._identityPopupSecurityView.setAttribute( "title", gNavigatorBundle.getFormattedString("identity.headerSecurityWithHost", [ host, ]) ); this._identityPopupSecurityEVContentOwner.textContent = gNavigatorBundle.getFormattedString( "identity.ev.contentOwner2", [owner] ); this._identityPopupContentOwner.textContent = owner; this._identityPopupContentSupp.textContent = supplemental; this._identityPopupContentVerif.textContent = verifier; // Update per-site permissions section. this.updateSitePermissions(); }, setURI(uri) { if (uri.schemeIs("view-source")) { uri = Services.io.newURI(uri.spec.replace(/^view-source:/i, "")); } 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._secureInternalPages.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; } // If we are in DOM full-screen, exit it before showing the identity popup // (see bug 1557041) if (document.fullscreen) { // Open the identity popup after DOM full-screen exit // We need to wait for the exit event and after that wait for the fullscreen exit transition to complete // If we call _openPopup before the full-screen transition ends it can get cancelled // Only waiting for painted is not sufficient because we could still be in the full-screen enter transition. this._exitedEventReceived = false; this._event = event; Services.obs.addObserver(this, "fullscreen-painted"); window.addEventListener( "MozDOMFullscreen:Exited", () => { this._exitedEventReceived = true; }, { once: true } ); document.exitFullscreen(); return; } this._openPopup(event); }, _openPopup(event) { // Make the popup available. this._initializePopup(); // 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"); // Check the panel state of other panels. Hide them if needed. let openPanels = Array.from(document.querySelectorAll("panel[openpanel]")); for (let panel of openPanels) { PanelMultiView.hidePopup(panel); } // Now open the popup, anchored off the primary chrome element PanelMultiView.openPopup(this._identityPopup, this._identityIcon, { position: "bottomcenter topleft", triggerEvent: event, }).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) { switch (topic) { case "perm-changed": { // Exclude permissions which do not appear in the UI in order to avoid // doing extra work here. if (!subject) { return; } let { type } = subject.QueryInterface(Ci.nsIPermission); if (SitePermissions.isSitePermission(type)) { this.refreshIdentityBlock(); } break; } case "fullscreen-painted": { if (subject != window || !this._exitedEventReceived) { return; } Services.obs.removeObserver(this, "fullscreen-painted"); this._openPopup(this._event); delete this._event; break; } } }, onDragStart(event) { const TEXT_SIZE = 14; const IMAGE_SIZE = 16; const SPACING = 5; if (gURLBar.getAttribute("pageproxystate") != "valid") { return; } let value = gBrowser.currentURI.displaySpec; let urlString = value + "\n" + gBrowser.contentTitle; let htmlString = '' + value + ""; let windowUtils = window.windowUtils; let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom; let canvas = document.createElementNS( "http://www.w3.org/1999/xhtml", "canvas" ); canvas.width = 550 * scale; let ctx = canvas.getContext("2d"); ctx.font = `${TEXT_SIZE * scale}px sans-serif`; let tabIcon = gBrowser.selectedTab.iconImage; let image = new Image(); image.src = tabIcon.src; let textWidth = ctx.measureText(value).width / scale; let textHeight = parseInt(ctx.font, 10) / scale; let imageHorizontalOffset, imageVerticalOffset; imageHorizontalOffset = imageVerticalOffset = SPACING; let textHorizontalOffset = image.width ? IMAGE_SIZE + SPACING * 2 : SPACING; let textVerticalOffset = textHeight + SPACING - 1; let backgroundColor = "white"; let textColor = "black"; let totalWidth = image.width ? textWidth + IMAGE_SIZE + 3 * SPACING : textWidth + 2 * SPACING; let totalHeight = image.width ? IMAGE_SIZE + 2 * SPACING : textHeight + 2 * SPACING; ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, totalWidth * scale, totalHeight * scale); ctx.fillStyle = textColor; ctx.fillText( `${value}`, textHorizontalOffset * scale, textVerticalOffset * scale ); try { ctx.drawImage( image, imageHorizontalOffset * scale, imageVerticalOffset * scale, IMAGE_SIZE * scale, IMAGE_SIZE * scale ); } catch (e) { // Sites might specify invalid data URIs favicons that // will result in errors when trying to draw, we can // just ignore this case and not paint any favicon. } 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(canvas, 16, 16); // Don't cover potential drop targets on the toolbars or in content. gURLBar.view.close(); }, onLocationChange() { if (this._popupInitialized && this._identityPopup.state != "closed") { this._permissionReloadHint.setAttribute("hidden", "true"); if (this._isPermissionListEmpty()) { this._permissionEmptyHint.removeAttribute("hidden"); } } }, _updateAttribute(elem, attr, value) { if (value) { elem.setAttribute(attr, value); } else { elem.removeAttribute(attr); } }, _isPermissionListEmpty() { return !this._permissionList.querySelectorAll( ".identity-popup-permission-item" ).length; }, updateSitePermissions() { let permissionItemSelector = [ ".identity-popup-permission-item, .identity-popup-permission-item-container", ]; this._permissionList .querySelectorAll(permissionItemSelector) .forEach(e => e.remove()); let permissions = SitePermissions.getAllPermissionDetailsForBrowser( gBrowser.selectedBrowser ); if (this._sharingState && this._sharingState.geo) { let geoPermission = permissions.find(perm => perm.id === "geo"); if (geoPermission) { geoPermission.sharingState = true; } else { permissions.push({ id: "geo", state: SitePermissions.ALLOW, scope: SitePermissions.SCOPE_REQUEST, sharingState: true, }); } } if (this._sharingState && this._sharingState.xr) { let xrPermission = permissions.find(perm => perm.id === "xr"); if (xrPermission) { xrPermission.sharingState = true; } else { permissions.push({ id: "xr", state: SitePermissions.ALLOW, scope: SitePermissions.SCOPE_REQUEST, sharingState: true, }); } } if (this._sharingState && this._sharingState.webRTC) { let webrtcState = this._sharingState.webRTC; // 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 (webrtcState[id]) { let found = false; for (let permission of permissions) { if (permission.id != id) { continue; } found = true; permission.sharingState = webrtcState[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: webrtcState[id], }); } } } } let totalBlockedPopups = gBrowser.selectedBrowser.popupBlocker.getBlockedPopupCount(); let hasBlockedPopupIndicator = false; for (let permission of permissions) { let [id, key] = permission.id.split(SitePermissions.PERM_KEY_DELIMITER); if (id == "storage-access") { // Ignore storage access permissions here, they are made visible inside // the Content Blocking UI. continue; } let item; let anchor = this._permissionList.querySelector(`[anchorfor="${id}"]`) || this._defaultPermissionAnchor; if (id == "open-protocol-handler") { let permContainer = this._createProtocolHandlerPermissionItem( permission, key ); if (permContainer) { anchor.appendChild(permContainer); } } else { item = this._createPermissionItem({ permission, isContainer: id == "geo" || id == "xr", nowrapLabel: id == "3rdPartyStorage", }); if (!item) { continue; } anchor.appendChild(item); } if (id == "popup" && totalBlockedPopups) { this._createBlockedPopupIndicator(totalBlockedPopups); hasBlockedPopupIndicator = true; } else if (id == "geo" && permission.state === SitePermissions.ALLOW) { this._createGeoLocationLastAccessIndicator(); } } if (totalBlockedPopups && !hasBlockedPopupIndicator) { let permission = { id: "popup", state: SitePermissions.getDefault("popup"), scope: SitePermissions.SCOPE_PERSISTENT, }; let item = this._createPermissionItem({ permission }); this._defaultPermissionAnchor.appendChild(item); this._createBlockedPopupIndicator(totalBlockedPopups); } // Show a placeholder text if there's no permission and no reload hint. if ( this._isPermissionListEmpty() && this._permissionReloadHint.hasAttribute("hidden") ) { this._permissionEmptyHint.removeAttribute("hidden"); } else { this._permissionEmptyHint.setAttribute("hidden", "true"); } }, /** * Creates a permission item based on the supplied options and returns it. * It is up to the caller to actually insert the element somewhere. * * @param permission - An object containing information representing the * permission, typically obtained via SitePermissions.jsm * @param isContainer - If true, the permission item will be added to a vbox * and the vbox will be returned. * @param permClearButton - Whether to show an "x" button to clear the permission * @param showStateLabel - Whether to show a label indicating the current status * of the permission e.g. "Temporary Allowed" * @param idNoSuffix - Some permission types have additional information suffixed * to the ID - callers can pass the unsuffixed ID via this * parameter to indicate the permission type manually. * @param nowrapLabel - Whether to prevent the permission item's label from * wrapping its text content. This allows styling text-overflow * and is useful for e.g. 3rdPartyStorage permissions whose * labels are origins - which could be of any length. */ _createPermissionItem({ permission, isContainer = false, permClearButton = true, showStateLabel = true, idNoSuffix = permission.id, nowrapLabel = false, }) { let container = document.createXULElement("hbox"); container.setAttribute("class", "identity-popup-permission-item"); container.setAttribute("align", "center"); container.setAttribute("role", "group"); let img = document.createXULElement("image"); img.classList.add("identity-popup-permission-icon", idNoSuffix + "-icon"); if ( permission.state == SitePermissions.BLOCK || permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL ) { img.classList.add("blocked-permission-icon"); } if ( permission.sharingState == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED || (idNoSuffix == "screen" && permission.sharingState && !permission.sharingState.includes("Paused")) ) { img.classList.add("in-use"); // Synchronize control center and identity block blinking animations. window .promiseDocumentFlushed(() => { let sharingIconBlink = this._webRTCSharingIcon.getAnimations()[0]; let imgBlink = img.getAnimations()[0]; return [sharingIconBlink, imgBlink]; }) .then(([sharingIconBlink, imgBlink]) => { if (sharingIconBlink && imgBlink) { imgBlink.startTime = sharingIconBlink.startTime; } }); } let nameLabel = document.createXULElement("label"); nameLabel.setAttribute("flex", "1"); nameLabel.setAttribute("class", "identity-popup-permission-label"); let label = SitePermissions.getPermissionLabel(idNoSuffix); if (label === null) { return null; } if (nowrapLabel) { nameLabel.setAttribute("value", label); nameLabel.setAttribute("tooltiptext", label); nameLabel.setAttribute("crop", "end"); } else { nameLabel.textContent = label; } let nameLabelId = "identity-popup-permission-label-" + idNoSuffix; nameLabel.setAttribute("id", nameLabelId); let isPolicyPermission = [ SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL, ].includes(permission.scope); if ( (idNoSuffix == "popup" && !isPolicyPermission) || idNoSuffix == "autoplay-media" ) { let menulist = document.createXULElement("menulist"); let menupopup = document.createXULElement("menupopup"); let block = document.createXULElement("vbox"); block.setAttribute("id", "identity-popup-popup-container"); block.setAttribute("class", "identity-popup-permission-item-container"); menulist.setAttribute("sizetopopup", "none"); menulist.setAttribute("id", "identity-popup-popup-menulist"); for (let state of SitePermissions.getAvailableStates(idNoSuffix)) { let menuitem = document.createXULElement("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(idNoSuffix)) { menuitem.setAttribute("value", "0"); } else { menuitem.setAttribute("value", state); } menuitem.setAttribute( "label", SitePermissions.getMultichoiceStateLabel(idNoSuffix, state) ); menupopup.appendChild(menuitem); } menulist.appendChild(menupopup); if (permission.state == SitePermissions.getDefault(idNoSuffix)) { menulist.value = "0"; } else { menulist.value = permission.state; } // Avoiding listening to the "select" event on purpose. See Bug 1404262. menulist.addEventListener("command", () => { SitePermissions.setForPrincipal( gBrowser.contentPrincipal, idNoSuffix, menulist.selectedItem.value ); }); container.appendChild(img); container.appendChild(nameLabel); container.appendChild(menulist); container.setAttribute("aria-labelledby", nameLabelId); block.appendChild(container); return block; } container.appendChild(img); container.appendChild(nameLabel); let labelledBy = nameLabelId; if (showStateLabel) { let stateLabel = this._createStateLabel(permission, idNoSuffix); container.appendChild(stateLabel); labelledBy += " " + stateLabel.id; } container.setAttribute("aria-labelledby", labelledBy); /* We return the permission item here without a remove button if the permission is a SCOPE_POLICY or SCOPE_GLOBAL permission. Policy permissions cannot be removed/changed for the duration of the browser session. */ if (isPolicyPermission) { return container; } if (isContainer) { let block = document.createXULElement("vbox"); block.setAttribute("id", "identity-popup-" + idNoSuffix + "-container"); block.setAttribute("class", "identity-popup-permission-item-container"); if (permClearButton) { let button = this._createPermissionClearButton(permission, block); container.appendChild(button); } block.appendChild(container); return block; } if (permClearButton) { let button = this._createPermissionClearButton(permission, container); container.appendChild(button); } return container; }, _createStateLabel(aPermission, idNoSuffix) { let label = document.createXULElement("label"); label.setAttribute("flex", "1"); label.setAttribute("class", "identity-popup-permission-state-label"); let labelId = "identity-popup-permission-state-label-" + idNoSuffix; label.setAttribute("id", labelId); 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; } label.textContent = SitePermissions.getCurrentStateLabel( state, idNoSuffix, scope ); return label; }, _removePermPersistentAllow(principal, id) { let perm = SitePermissions.getForPrincipal(principal, id); if ( perm.state == SitePermissions.ALLOW && perm.scope == SitePermissions.SCOPE_PERSISTENT ) { SitePermissions.removeFromPrincipal(principal, id); } }, _createPermissionClearButton( aPermission, container, clearCallback = () => {} ) { let button = document.createXULElement("button"); button.setAttribute("class", "identity-popup-permission-remove-button"); let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip"); button.setAttribute("tooltiptext", tooltiptext); button.addEventListener("command", () => { let browser = gBrowser.selectedBrowser; container.remove(); if (aPermission.sharingState) { if (aPermission.id === "geo" || aPermission.id === "xr") { let origins = browser.getDevicePermissionOrigins(aPermission.id); for (let origin of origins) { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( origin ); this._removePermPersistentAllow(principal, aPermission.id); } origins.clear(); } else if ( ["camera", "microphone", "screen"].includes(aPermission.id) ) { let windowId = this._sharingState.webRTC.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 origins = browser.getDevicePermissionOrigins("webrtc"); for (let origin of origins) { // It's not possible to stop sharing one of camera/microphone // without the other. let principal; for (let id of ["camera", "microphone"]) { if (this._sharingState.webRTC[id]) { if (!principal) { principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( origin ); } this._removePermPersistentAllow(principal, id); } } } } let bc = this._sharingState.webRTC.browsingContext; bc.currentWindowGlobal .getActor("WebRTC") .sendAsyncMessage("webrtc:StopSharing", windowId); webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser); } } SitePermissions.removeFromPrincipal( gBrowser.contentPrincipal, aPermission.id, browser ); this._permissionReloadHint.removeAttribute("hidden"); PanelView.forNode( this._identityPopupMainView ).descriptionHeightWorkaround(); if (aPermission.id === "geo") { gBrowser.updateBrowserSharing(browser, { geo: false }); } else if (aPermission.id === "xr") { gBrowser.updateBrowserSharing(browser, { xr: false }); } clearCallback(); }); return button; }, _getGeoLocationLastAccess() { return new Promise(resolve => { let lastAccess = null; ContentPrefService2.getByDomainAndName( gBrowser.currentURI.spec, "permissions.geoLocation.lastAccess", gBrowser.selectedBrowser.loadContext, { handleResult(pref) { lastAccess = pref.value; }, handleCompletion() { resolve(lastAccess); }, } ); }); }, async _createGeoLocationLastAccessIndicator() { let lastAccessStr = await this._getGeoLocationLastAccess(); let geoContainer = document.getElementById("identity-popup-geo-container"); // Check whether geoContainer still exists. // We are async, the identity popup could have been closed already. // Also check if it is already populated with a time label. // This can happen if we update the permission panel multiple times in a // short timeframe. if ( lastAccessStr == null || !geoContainer || document.getElementById("geo-access-indicator-item") ) { return; } let lastAccess = new Date(lastAccessStr); if (isNaN(lastAccess)) { Cu.reportError("Invalid timestamp for last geolocation access"); return; } let icon = document.createXULElement("image"); icon.setAttribute("class", "popup-subitem"); let indicator = document.createXULElement("hbox"); indicator.setAttribute("class", "identity-popup-permission-item"); indicator.setAttribute("align", "center"); indicator.setAttribute("id", "geo-access-indicator-item"); let timeFormat = new Services.intl.RelativeTimeFormat(undefined, {}); let text = document.createXULElement("label"); text.setAttribute("flex", "1"); text.setAttribute("class", "identity-popup-permission-label"); text.textContent = gNavigatorBundle.getFormattedString( "geolocationLastAccessIndicatorText", [timeFormat.formatBestUnit(lastAccess)] ); indicator.appendChild(icon); indicator.appendChild(text); geoContainer.appendChild(indicator); }, _createProtocolHandlerPermissionItem(permission, key) { let container = document.getElementById( "identity-popup-open-protocol-handler-container" ); let initialCall; if (!container) { // First open-protocol-handler permission, create container. container = this._createPermissionItem({ permission, isContainer: true, permClearButton: false, showStateLabel: false, idNoSuffix: "open-protocol-handler", }); initialCall = true; } let icon = document.createXULElement("image"); icon.setAttribute("class", "popup-subitem-no-arrow"); let item = document.createXULElement("hbox"); item.setAttribute("class", "identity-popup-permission-item"); item.setAttribute("align", "center"); let text = document.createXULElement("label"); text.setAttribute("flex", "1"); text.setAttribute("class", "identity-popup-permission-label-subitem"); text.textContent = gNavigatorBundle.getFormattedString( "openProtocolHandlerPermissionEntryLabel", [key] ); let stateLabel = this._createStateLabel( permission, "open-protocol-handler" ); item.appendChild(text); item.appendChild(stateLabel); let button = this._createPermissionClearButton(permission, item, () => { // When we're clearing the last open-protocol-handler permission, clean up // the empty container. // (<= 1 because the heading item is also a child of the container) if (container.childElementCount <= 1) { container.remove(); } }); item.appendChild(button); container.appendChild(item); // If container already exists in permission list, don't return it again. return initialCall && container; }, _createBlockedPopupIndicator(aTotalBlockedPopups) { let indicator = document.createXULElement("hbox"); indicator.setAttribute("class", "identity-popup-permission-item"); indicator.setAttribute("align", "center"); indicator.setAttribute("id", "blocked-popup-indicator-item"); let icon = document.createXULElement("image"); icon.setAttribute("class", "popup-subitem"); let text = document.createXULElement("label", { is: "text-link" }); text.setAttribute("flex", "1"); text.setAttribute("class", "identity-popup-permission-label"); let messageBase = gNavigatorBundle.getString( "popupShowBlockedPopupsIndicatorText" ); let message = PluralForm.get(aTotalBlockedPopups, messageBase).replace( "#1", aTotalBlockedPopups ); text.textContent = message; text.addEventListener("click", () => { gBrowser.selectedBrowser.popupBlocker.unblockAllPopups(); }); indicator.appendChild(icon); indicator.appendChild(text); document .getElementById("identity-popup-popup-container") .appendChild(indicator); }, };