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.js1326
1 files changed, 1326 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..fb243dcb8f
--- /dev/null
+++ b/browser/base/content/browser-siteIdentity.js
@@ -0,0 +1,1326 @@
+/* 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,
+
+ /**
+ * 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 _isCertErrorPage() {
+ let { documentURI } = gBrowser.selectedBrowser;
+ if (documentURI?.scheme != "about") {
+ return false;
+ }
+
+ return (
+ documentURI.filePath == "certerror" ||
+ (documentURI.filePath == "neterror" &&
+ new URLSearchParams(documentURI.query).get("e") == "nssFailure2")
+ );
+ },
+
+ get _isAboutNetErrorPage() {
+ let { documentURI } = gBrowser.selectedBrowser;
+ return documentURI?.scheme == "about" && documentURI.filePath == "neterror";
+ },
+
+ get _isAboutHttpsOnlyErrorPage() {
+ let { documentURI } = gBrowser.selectedBrowser;
+ return (
+ documentURI?.scheme == "about" && documentURI.filePath == "httpsonlyerror"
+ );
+ },
+
+ get _isPotentiallyTrustworthy() {
+ return (
+ !this._isBrokenConnection &&
+ (this._isSecureContext ||
+ gBrowser.selectedBrowser.documentURI?.scheme == "chrome")
+ );
+ },
+
+ get _isAboutBlockedPage() {
+ let { documentURI } = gBrowser.selectedBrowser;
+ return documentURI?.scheme == "about" && documentURI.filePath == "blocked";
+ },
+
+ _popupInitialized: false,
+ _initializePopup() {
+ window.ensureCustomElements("moz-support-link");
+ 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 _identityIconBox() {
+ delete this._identityIconBox;
+ return (this._identityIconBox =
+ document.getElementById("identity-icon-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 _clearSiteDataFooter() {
+ delete this._clearSiteDataFooter;
+ return (this._clearSiteDataFooter = document.getElementById(
+ "identity-popup-clear-sitedata-footer"
+ ));
+ },
+
+ 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;
+ },
+
+ /**
+ * Handles clicks on the "Clear Cookies and Site Data" button.
+ */
+ async clearSiteData(event) {
+ if (!this._uriHasHost) {
+ return;
+ }
+
+ // 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(this._uri.host);
+ if (SiteDataManager.promptSiteDataRemoval(window, [baseDomain])) {
+ SiteDataManager.remove(baseDomain);
+ }
+
+ event.stopPropagation();
+ },
+
+ /**
+ * 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",
+ document.getElementById("identity-popup-security-button")
+ );
+
+ // 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);
+
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ "mixed-content",
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_SESSION
+ );
+
+ // Reload the page with the content unblocked
+ BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ },
+
+ // This is needed for some tests which need the permission reset, but which
+ // then reuse the browser and would race between the reload and the next
+ // load.
+ enableMixedContentProtectionNoReload() {
+ this.enableMixedContentProtection(false);
+ },
+
+ enableMixedContentProtection(reload = true) {
+ SitePermissions.removeFromPrincipal(
+ gBrowser.contentPrincipal,
+ "mixed-content"
+ );
+ if (reload) {
+ BrowserReload();
+ }
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ },
+
+ removeCertException() {
+ if (!this._uriHasHost) {
+ console.error(
+ "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,
+ gBrowser.contentPrincipal.originAttributes
+ );
+ 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.sys.mjs)
+ 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, {
+ 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;
+ },
+
+ _getIsSecureContext() {
+ if (gBrowser.contentPrincipal?.originNoSuffix != "resource://pdf.js") {
+ return gBrowser.securityUI.isSecureContext;
+ }
+
+ // For PDF viewer pages (pdf.js) we can't rely on the isSecureContext field.
+ // The backend will return isSecureContext = true, because the content
+ // principal has a resource:// URI. Instead use the URI of the selected
+ // browser to perform the isPotentiallyTrustWorthy check.
+
+ let principal;
+ try {
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ gBrowser.selectedBrowser.documentURI,
+ {}
+ );
+ return principal.isOriginPotentiallyTrustworthy;
+ } catch (error) {
+ console.error(
+ "Error while computing isPotentiallyTrustWorthy for pdf viewer page: ",
+ error
+ );
+ return false;
+ }
+ },
+
+ /**
+ * 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 = this._getIsSecureContext();
+
+ // 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.hidePopup();
+ gPermissionPanel.hidePopup();
+ }
+
+ // 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.
+ },
+
+ /**
+ * 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._isCertErrorPage) {
+ // We show a warning lock icon for certificate errors, and
+ // show the "Not Secure" text.
+ this._identityBox.className = "certErrorPage notSecureText";
+ icon_label = gNavigatorBundle.getString("identity.notSecure.label");
+ tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip");
+ } 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"
+ );
+ }
+
+ // 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 identity block user interface with the data from this object.
+ */
+ refreshIdentityBlock() {
+ if (!this._identityBox) {
+ return;
+ }
+
+ this._refreshIdentityIcons();
+
+ // If this condition is true, the URL bar will have an "invalid"
+ // pageproxystate, so we should hide the permission icons.
+ if (this._hasInvalidPageProxyState()) {
+ gPermissionPanel.hidePermissionIcons();
+ } else {
+ gPermissionPanel.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, and
+ // if the page is not controlled by a WebExtension.
+ this._clearSiteDataFooter.hidden = true;
+ let identityPopupPanelView = document.getElementById(
+ "identity-popup-mainView"
+ );
+ identityPopupPanelView.removeAttribute("footerVisible");
+ if (this._uriHasHost && !this._pageExtensionPolicy) {
+ SiteDataManager.hasSiteData(this._uri.asciiHost).then(hasData => {
+ this._clearSiteDataFooter.hidden = !hasData;
+ identityPopupPanelView.setAttribute("footerVisible", hasData);
+ });
+ }
+
+ 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._isCertErrorPage) {
+ connection = "cert-error-page";
+ } else if (this._isAboutHttpsOnlyErrorPage) {
+ connection = "https-only-error-page";
+ } else if (this._isAboutBlockedPage) {
+ connection = "not-secure";
+ } else if (this._isAboutNetErrorPage) {
+ connection = "net-error-page";
+ } else if (this._isPotentiallyTrustworthy) {
+ connection = "file";
+ }
+
+ let securityButtonNode = document.getElementById(
+ "identity-popup-security-button"
+ );
+
+ let disableSecurityButton = ![
+ "not-secure",
+ "secure",
+ "secure-ev",
+ "secure-cert-user-overridden",
+ "cert-error-page",
+ "net-error-page",
+ "https-only-error-page",
+ ].includes(connection);
+ if (disableSecurityButton) {
+ securityButtonNode.disabled = true;
+ securityButtonNode.classList.remove("subviewbutton-nav");
+ } else {
+ securityButtonNode.disabled = false;
+ securityButtonNode.classList.add("subviewbutton-nav");
+ }
+
+ // 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";
+ }
+
+ // 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-extended-info",
+ ];
+
+ 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.
+ document.l10n.setAttributes(
+ this._identityPopupMainViewHeaderLabel,
+ "identity-site-information",
+ {
+ host,
+ }
+ );
+
+ document.l10n.setAttributes(
+ this._identityPopupSecurityView,
+ "identity-header-security-with-host",
+ {
+ host,
+ }
+ );
+
+ this._identityPopupSecurityEVContentOwner.textContent =
+ gNavigatorBundle.getFormattedString("identity.ev.contentOwner2", [owner]);
+
+ this._identityPopupContentOwner.textContent = owner;
+ this._identityPopupContentSupp.textContent = supplemental;
+ this._identityPopupContentVerif.textContent = verifier;
+ },
+
+ 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;
+ }
+
+ if (uri.schemeIs("about")) {
+ let module = E10SUtils.getAboutModule(uri);
+ if (module) {
+ let flags = module.getURIFlags(uri);
+ this._isSecureInternalUI = !!(
+ flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI
+ );
+ }
+ } else {
+ this._isSecureInternalUI = false;
+ }
+ 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.
+ if (gURLBar.getAttribute("pageproxystate") != "valid") {
+ return;
+ }
+
+ this._openPopup(event);
+ },
+
+ _openPopup(event) {
+ // Make the popup available.
+ this._initializePopup();
+
+ // Update the popup strings
+ this.refreshIdentityPopup();
+
+ // 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._identityIconBox, {
+ position: "bottomleft topleft",
+ triggerEvent: event,
+ }).catch(console.error);
+ },
+
+ onPopupShown(event) {
+ if (event.target == this._identityPopup) {
+ PopupNotifications.suppressWhileOpen(this._identityPopup);
+ window.addEventListener("focus", this, true);
+ }
+ },
+
+ onPopupHidden(event) {
+ if (event.target == this._identityPopup) {
+ window.removeEventListener("focus", this, true);
+ }
+ },
+
+ 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;
+ }
+ }
+ },
+
+ 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 scale = window.devicePixelRatio;
+ 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();
+ },
+
+ _updateAttribute(elem, attr, value) {
+ if (value) {
+ elem.setAttribute(attr, value);
+ } else {
+ elem.removeAttribute(attr);
+ }
+ },
+};