summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-siteIdentity.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-siteIdentity.js')
-rw-r--r--browser/base/content/browser-siteIdentity.js2114
1 files changed, 2114 insertions, 0 deletions
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
new file mode 100644
index 0000000000..5850d7ceb4
--- /dev/null
+++ b/browser/base/content/browser-siteIdentity.js
@@ -0,0 +1,2114 @@
+/* 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 <browser> is included within a chrome document, then this._state
+ // will refer to the security state for the <browser> 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 <browser>.
+ return (
+ !this._isURILoadedFromFile &&
+ this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE
+ );
+ },
+
+ get _isEV() {
+ // If a <browser> is included within a chrome document, then this._state
+ // will refer to the security state for the <browser> 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 <browser>.
+ 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=<large base64 encoded data>&cert=<large base64 encoded data>&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:<page>
+ 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:<jar-file-uri>!/<jar-entry>"
+ // create a new URI using <jar-file-uri>!/<jar-entry>
+ 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 = '<a href="' + value + '">' + value + "</a>";
+
+ 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);
+ },
+};