Merge mozilla-central to autoland. a=merge CLOSED TREE
[gecko.git] / browser / base / content / browser-siteIdentity.js
blob86a1198d21be4bef0d201883ce0771d98824c2f8
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint-env mozilla/browser-window */
7 /**
8  * Utility object to handle manipulations of the identity indicators in the UI
9  */
10 var gIdentityHandler = {
11   /**
12    * nsIURI for which the identity UI is displayed. This has been already
13    * processed by createExposableURI.
14    */
15   _uri: null,
17   /**
18    * We only know the connection type if this._uri has a defined "host" part.
19    *
20    * These URIs, like "about:", "file:" and "data:" URIs, will usually be treated as a
21    * an unknown connection.
22    */
23   _uriHasHost: false,
25   /**
26    * If this tab belongs to a WebExtension, contains its WebExtensionPolicy.
27    */
28   _pageExtensionPolicy: null,
30   /**
31    * Whether this._uri refers to an internally implemented browser page.
32    *
33    * Note that this is set for some "about:" pages, but general "chrome:" URIs
34    * are not included in this category by default.
35    */
36   _isSecureInternalUI: false,
38   /**
39    * Whether the content window is considered a "secure context". This
40    * includes "potentially trustworthy" origins such as file:// URLs or localhost.
41    * https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
42    */
43   _isSecureContext: false,
45   /**
46    * nsITransportSecurityInfo metadata provided by gBrowser.securityUI the last
47    * time the identity UI was updated, or null if the connection is not secure.
48    */
49   _secInfo: null,
51   /**
52    * Bitmask provided by nsIWebProgressListener.onSecurityChange.
53    */
54   _state: 0,
56   /**
57    * Whether the established HTTPS connection is considered "broken".
58    * This could have several reasons, such as mixed content or weak
59    * cryptography. If this is true, _isSecureConnection is false.
60    */
61   get _isBrokenConnection() {
62     return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
63   },
65   /**
66    * Whether the connection to the current site was done via secure
67    * transport. Note that this attribute is not true in all cases that
68    * the site was accessed via HTTPS, i.e. _isSecureConnection will
69    * be false when _isBrokenConnection is true, even though the page
70    * was loaded over HTTPS.
71    */
72   get _isSecureConnection() {
73     // If a <browser> is included within a chrome document, then this._state
74     // will refer to the security state for the <browser> and not the top level
75     // document. In this case, don't upgrade the security state in the UI
76     // with the secure state of the embedded <browser>.
77     return (
78       !this._isURILoadedFromFile &&
79       this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE
80     );
81   },
83   get _isEV() {
84     // If a <browser> is included within a chrome document, then this._state
85     // will refer to the security state for the <browser> and not the top level
86     // document. In this case, don't upgrade the security state in the UI
87     // with the EV state of the embedded <browser>.
88     return (
89       !this._isURILoadedFromFile &&
90       this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL
91     );
92   },
94   get _isAssociatedIdentity() {
95     return this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_ASSOCIATED;
96   },
98   get _isMixedActiveContentLoaded() {
99     return (
100       this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT
101     );
102   },
104   get _isMixedActiveContentBlocked() {
105     return (
106       this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT
107     );
108   },
110   get _isMixedPassiveContentLoaded() {
111     return (
112       this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT
113     );
114   },
116   get _isContentHttpsOnlyModeUpgraded() {
117     return (
118       this._state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED
119     );
120   },
122   get _isContentHttpsOnlyModeUpgradeFailed() {
123     return (
124       this._state &
125       Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED
126     );
127   },
129   get _isContentHttpsFirstModeUpgraded() {
130     return (
131       this._state &
132       Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED_FIRST
133     );
134   },
136   get _isCertUserOverridden() {
137     return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN;
138   },
140   get _isCertErrorPage() {
141     let { documentURI } = gBrowser.selectedBrowser;
142     if (documentURI?.scheme != "about") {
143       return false;
144     }
146     return (
147       documentURI.filePath == "certerror" ||
148       (documentURI.filePath == "neterror" &&
149         new URLSearchParams(documentURI.query).get("e") == "nssFailure2")
150     );
151   },
153   get _isAboutNetErrorPage() {
154     let { documentURI } = gBrowser.selectedBrowser;
155     return documentURI?.scheme == "about" && documentURI.filePath == "neterror";
156   },
158   get _isAboutHttpsOnlyErrorPage() {
159     let { documentURI } = gBrowser.selectedBrowser;
160     return (
161       documentURI?.scheme == "about" && documentURI.filePath == "httpsonlyerror"
162     );
163   },
165   get _isPotentiallyTrustworthy() {
166     return (
167       !this._isBrokenConnection &&
168       (this._isSecureContext ||
169         gBrowser.selectedBrowser.documentURI?.scheme == "chrome")
170     );
171   },
173   get _isAboutBlockedPage() {
174     let { documentURI } = gBrowser.selectedBrowser;
175     return documentURI?.scheme == "about" && documentURI.filePath == "blocked";
176   },
178   _popupInitialized: false,
179   _initializePopup() {
180     window.ensureCustomElements("moz-support-link");
181     if (!this._popupInitialized) {
182       let wrapper = document.getElementById("template-identity-popup");
183       wrapper.replaceWith(wrapper.content);
184       this._popupInitialized = true;
185     }
186   },
188   hidePopup() {
189     if (this._popupInitialized) {
190       PanelMultiView.hidePopup(this._identityPopup);
191     }
192   },
194   // smart getters
195   get _identityPopup() {
196     if (!this._popupInitialized) {
197       return null;
198     }
199     delete this._identityPopup;
200     return (this._identityPopup = document.getElementById("identity-popup"));
201   },
202   get _identityBox() {
203     delete this._identityBox;
204     return (this._identityBox = document.getElementById("identity-box"));
205   },
206   get _identityIconBox() {
207     delete this._identityIconBox;
208     return (this._identityIconBox =
209       document.getElementById("identity-icon-box"));
210   },
211   get _identityPopupMultiView() {
212     delete this._identityPopupMultiView;
213     return (this._identityPopupMultiView = document.getElementById(
214       "identity-popup-multiView"
215     ));
216   },
217   get _identityPopupMainView() {
218     delete this._identityPopupMainView;
219     return (this._identityPopupMainView = document.getElementById(
220       "identity-popup-mainView"
221     ));
222   },
223   get _identityPopupMainViewHeaderLabel() {
224     delete this._identityPopupMainViewHeaderLabel;
225     return (this._identityPopupMainViewHeaderLabel = document.getElementById(
226       "identity-popup-mainView-panel-header-span"
227     ));
228   },
229   get _identityPopupSecurityView() {
230     delete this._identityPopupSecurityView;
231     return (this._identityPopupSecurityView = document.getElementById(
232       "identity-popup-securityView"
233     ));
234   },
235   get _identityPopupHttpsOnlyMode() {
236     delete this._identityPopupHttpsOnlyMode;
237     return (this._identityPopupHttpsOnlyMode = document.getElementById(
238       "identity-popup-security-httpsonlymode"
239     ));
240   },
241   get _identityPopupHttpsOnlyModeMenuList() {
242     delete this._identityPopupHttpsOnlyModeMenuList;
243     return (this._identityPopupHttpsOnlyModeMenuList = document.getElementById(
244       "identity-popup-security-httpsonlymode-menulist"
245     ));
246   },
247   get _identityPopupHttpsOnlyModeMenuListOffItem() {
248     delete this._identityPopupHttpsOnlyModeMenuListOffItem;
249     return (this._identityPopupHttpsOnlyModeMenuListOffItem =
250       document.getElementById("identity-popup-security-menulist-off-item"));
251   },
252   get _identityPopupSecurityEVContentOwner() {
253     delete this._identityPopupSecurityEVContentOwner;
254     return (this._identityPopupSecurityEVContentOwner = document.getElementById(
255       "identity-popup-security-ev-content-owner"
256     ));
257   },
258   get _identityPopupContentOwner() {
259     delete this._identityPopupContentOwner;
260     return (this._identityPopupContentOwner = document.getElementById(
261       "identity-popup-content-owner"
262     ));
263   },
264   get _identityPopupContentSupp() {
265     delete this._identityPopupContentSupp;
266     return (this._identityPopupContentSupp = document.getElementById(
267       "identity-popup-content-supplemental"
268     ));
269   },
270   get _identityPopupContentVerif() {
271     delete this._identityPopupContentVerif;
272     return (this._identityPopupContentVerif = document.getElementById(
273       "identity-popup-content-verifier"
274     ));
275   },
276   get _identityPopupCustomRootLearnMore() {
277     delete this._identityPopupCustomRootLearnMore;
278     return (this._identityPopupCustomRootLearnMore = document.getElementById(
279       "identity-popup-custom-root-learn-more"
280     ));
281   },
282   get _identityPopupMixedContentLearnMore() {
283     delete this._identityPopupMixedContentLearnMore;
284     return (this._identityPopupMixedContentLearnMore = [
285       ...document.querySelectorAll(".identity-popup-mcb-learn-more"),
286     ]);
287   },
289   get _identityIconLabel() {
290     delete this._identityIconLabel;
291     return (this._identityIconLabel = document.getElementById(
292       "identity-icon-label"
293     ));
294   },
295   get _overrideService() {
296     delete this._overrideService;
297     return (this._overrideService = Cc[
298       "@mozilla.org/security/certoverride;1"
299     ].getService(Ci.nsICertOverrideService));
300   },
301   get _identityIcon() {
302     delete this._identityIcon;
303     return (this._identityIcon = document.getElementById("identity-icon"));
304   },
305   get _clearSiteDataFooter() {
306     delete this._clearSiteDataFooter;
307     return (this._clearSiteDataFooter = document.getElementById(
308       "identity-popup-clear-sitedata-footer"
309     ));
310   },
311   get _insecureConnectionTextEnabled() {
312     delete this._insecureConnectionTextEnabled;
313     XPCOMUtils.defineLazyPreferenceGetter(
314       this,
315       "_insecureConnectionTextEnabled",
316       "security.insecure_connection_text.enabled"
317     );
318     return this._insecureConnectionTextEnabled;
319   },
320   get _insecureConnectionTextPBModeEnabled() {
321     delete this._insecureConnectionTextPBModeEnabled;
322     XPCOMUtils.defineLazyPreferenceGetter(
323       this,
324       "_insecureConnectionTextPBModeEnabled",
325       "security.insecure_connection_text.pbmode.enabled"
326     );
327     return this._insecureConnectionTextPBModeEnabled;
328   },
329   get _httpsOnlyModeEnabled() {
330     delete this._httpsOnlyModeEnabled;
331     XPCOMUtils.defineLazyPreferenceGetter(
332       this,
333       "_httpsOnlyModeEnabled",
334       "dom.security.https_only_mode"
335     );
336     return this._httpsOnlyModeEnabled;
337   },
338   get _httpsOnlyModeEnabledPBM() {
339     delete this._httpsOnlyModeEnabledPBM;
340     XPCOMUtils.defineLazyPreferenceGetter(
341       this,
342       "_httpsOnlyModeEnabledPBM",
343       "dom.security.https_only_mode_pbm"
344     );
345     return this._httpsOnlyModeEnabledPBM;
346   },
347   get _httpsFirstModeEnabled() {
348     delete this._httpsFirstModeEnabled;
349     XPCOMUtils.defineLazyPreferenceGetter(
350       this,
351       "_httpsFirstModeEnabled",
352       "dom.security.https_first"
353     );
354     return this._httpsFirstModeEnabled;
355   },
356   get _httpsFirstModeEnabledPBM() {
357     delete this._httpsFirstModeEnabledPBM;
358     XPCOMUtils.defineLazyPreferenceGetter(
359       this,
360       "_httpsFirstModeEnabledPBM",
361       "dom.security.https_first_pbm"
362     );
363     return this._httpsFirstModeEnabledPBM;
364   },
365   get _schemelessHttpsFirstModeEnabled() {
366     delete this._schemelessHttpsFirstModeEnabled;
367     XPCOMUtils.defineLazyPreferenceGetter(
368       this,
369       "_schemelessHttpsFirstModeEnabled",
370       "dom.security.https_first_schemeless"
371     );
372     return this._schemelessHttpsFirstModeEnabled;
373   },
375   _isHttpsOnlyModeActive(isWindowPrivate) {
376     return (
377       this._httpsOnlyModeEnabled ||
378       (isWindowPrivate && this._httpsOnlyModeEnabledPBM)
379     );
380   },
381   _isHttpsFirstModeActive(isWindowPrivate) {
382     return (
383       !this._isHttpsOnlyModeActive(isWindowPrivate) &&
384       (this._httpsFirstModeEnabled ||
385         (isWindowPrivate && this._httpsFirstModeEnabledPBM))
386     );
387   },
388   _isSchemelessHttpsFirstModeActive(isWindowPrivate) {
389     return (
390       !this._isHttpsOnlyModeActive(isWindowPrivate) &&
391       !this._isHttpsFirstModeActive(isWindowPrivate) &&
392       this._schemelessHttpsFirstModeEnabled
393     );
394   },
396   /**
397    * Handles clicks on the "Clear Cookies and Site Data" button.
398    */
399   async clearSiteData(event) {
400     if (!this._uriHasHost) {
401       return;
402     }
404     // Hide the popup before showing the removal prompt, to
405     // avoid a pretty ugly transition. Also hide it even
406     // if the update resulted in no site data, to keep the
407     // illusion that clicking the button had an effect.
408     let hidden = new Promise(c => {
409       this._identityPopup.addEventListener("popuphidden", c, { once: true });
410     });
411     PanelMultiView.hidePopup(this._identityPopup);
412     await hidden;
414     let baseDomain = SiteDataManager.getBaseDomainFromHost(this._uri.host);
415     if (SiteDataManager.promptSiteDataRemoval(window, [baseDomain])) {
416       SiteDataManager.remove(baseDomain);
417     }
419     event.stopPropagation();
420   },
422   /**
423    * Handler for mouseclicks on the "More Information" button in the
424    * "identity-popup" panel.
425    */
426   handleMoreInfoClick(event) {
427     displaySecurityInfo();
428     event.stopPropagation();
429     PanelMultiView.hidePopup(this._identityPopup);
430   },
432   showSecuritySubView() {
433     this._identityPopupMultiView.showSubView(
434       "identity-popup-securityView",
435       document.getElementById("identity-popup-security-button")
436     );
438     // Elements of hidden views have -moz-user-focus:ignore but setting that
439     // per CSS selector doesn't blur a focused element in those hidden views.
440     Services.focus.clearFocus(window);
441   },
443   disableMixedContentProtection() {
444     // Use telemetry to measure how often unblocking happens
445     const kMIXED_CONTENT_UNBLOCK_EVENT = 2;
446     let histogram = Services.telemetry.getHistogramById(
447       "MIXED_CONTENT_UNBLOCK_COUNTER"
448     );
449     histogram.add(kMIXED_CONTENT_UNBLOCK_EVENT);
451     SitePermissions.setForPrincipal(
452       gBrowser.contentPrincipal,
453       "mixed-content",
454       SitePermissions.ALLOW,
455       SitePermissions.SCOPE_SESSION
456     );
458     // Reload the page with the content unblocked
459     BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
460     if (this._popupInitialized) {
461       PanelMultiView.hidePopup(this._identityPopup);
462     }
463   },
465   // This is needed for some tests which need the permission reset, but which
466   // then reuse the browser and would race between the reload and the next
467   // load.
468   enableMixedContentProtectionNoReload() {
469     this.enableMixedContentProtection(false);
470   },
472   enableMixedContentProtection(reload = true) {
473     SitePermissions.removeFromPrincipal(
474       gBrowser.contentPrincipal,
475       "mixed-content"
476     );
477     if (reload) {
478       BrowserReload();
479     }
480     if (this._popupInitialized) {
481       PanelMultiView.hidePopup(this._identityPopup);
482     }
483   },
485   removeCertException() {
486     if (!this._uriHasHost) {
487       console.error(
488         "Trying to revoke a cert exception on a URI without a host?"
489       );
490       return;
491     }
492     let host = this._uri.host;
493     let port = this._uri.port > 0 ? this._uri.port : 443;
494     this._overrideService.clearValidityOverride(
495       host,
496       port,
497       gBrowser.contentPrincipal.originAttributes
498     );
499     BrowserReloadSkipCache();
500     if (this._popupInitialized) {
501       PanelMultiView.hidePopup(this._identityPopup);
502     }
503   },
505   /**
506    * Gets the current HTTPS-Only mode permission for the current page.
507    * Values are the same as in #identity-popup-security-httpsonlymode-menulist,
508    * -1 indicates a incompatible scheme on the current URI.
509    */
510   _getHttpsOnlyPermission() {
511     let uri = gBrowser.currentURI;
512     if (uri instanceof Ci.nsINestedURI) {
513       uri = uri.QueryInterface(Ci.nsINestedURI).innermostURI;
514     }
515     if (!uri.schemeIs("http") && !uri.schemeIs("https")) {
516       return -1;
517     }
518     uri = uri.mutate().setScheme("http").finalize();
519     const principal = Services.scriptSecurityManager.createContentPrincipal(
520       uri,
521       gBrowser.contentPrincipal.originAttributes
522     );
523     const { state } = SitePermissions.getForPrincipal(
524       principal,
525       "https-only-load-insecure"
526     );
527     switch (state) {
528       case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION:
529         return 2; // Off temporarily
530       case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW:
531         return 1; // Off
532       default:
533         return 0; // On
534     }
535   },
537   /**
538    * Sets/removes HTTPS-Only Mode exception and possibly reloads the page.
539    */
540   changeHttpsOnlyPermission() {
541     // Get the new value from the menulist and the current value
542     // Note: value and permission association is laid out
543     //       in _getHttpsOnlyPermission
544     const oldValue = this._getHttpsOnlyPermission();
545     if (oldValue < 0) {
546       console.error(
547         "Did not update HTTPS-Only permission since scheme is incompatible"
548       );
549       return;
550     }
552     let newValue = parseInt(
553       this._identityPopupHttpsOnlyModeMenuList.selectedItem.value,
554       10
555     );
557     // If nothing changed, just return here
558     if (newValue === oldValue) {
559       return;
560     }
562     // We always want to set the exception for the HTTP version of the current URI,
563     // since when we check wether we should upgrade a request, we are checking permissons
564     // for the HTTP principal (Bug 1757297).
565     let newURI = gBrowser.currentURI;
566     if (newURI instanceof Ci.nsINestedURI) {
567       newURI = newURI.QueryInterface(Ci.nsINestedURI).innermostURI;
568     }
569     newURI = newURI.mutate().setScheme("http").finalize();
570     const principal = Services.scriptSecurityManager.createContentPrincipal(
571       newURI,
572       gBrowser.contentPrincipal.originAttributes
573     );
575     // Set or remove the permission
576     if (newValue === 0) {
577       SitePermissions.removeFromPrincipal(
578         principal,
579         "https-only-load-insecure"
580       );
581     } else if (newValue === 1) {
582       SitePermissions.setForPrincipal(
583         principal,
584         "https-only-load-insecure",
585         Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW,
586         SitePermissions.SCOPE_PERSISTENT
587       );
588     } else {
589       SitePermissions.setForPrincipal(
590         principal,
591         "https-only-load-insecure",
592         Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
593         SitePermissions.SCOPE_SESSION
594       );
595     }
597     // If we're on the error-page, we have to redirect the user
598     // from HTTPS to HTTP. Otherwise we can just reload the page.
599     if (this._isAboutHttpsOnlyErrorPage) {
600       gBrowser.loadURI(newURI, {
601         triggeringPrincipal:
602           Services.scriptSecurityManager.getSystemPrincipal(),
603         loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
604       });
605       if (this._popupInitialized) {
606         PanelMultiView.hidePopup(this._identityPopup);
607       }
608       return;
609     }
610     // The page only needs to reload if we switch between allow and block
611     // Because "off" is 1 and "off temporarily" is 2, we can just check if the
612     // sum of newValue and oldValue is 3.
613     if (newValue + oldValue !== 3) {
614       BrowserReloadSkipCache();
615       if (this._popupInitialized) {
616         PanelMultiView.hidePopup(this._identityPopup);
617       }
618       return;
619     }
620     // Otherwise we just refresh the interface
621     this.refreshIdentityPopup();
622   },
624   /**
625    * Helper to parse out the important parts of _secInfo (of the SSL cert in
626    * particular) for use in constructing identity UI strings
627    */
628   getIdentityData() {
629     var result = {};
630     var cert = this._secInfo.serverCert;
632     // Human readable name of Subject
633     result.subjectOrg = cert.organization;
635     // SubjectName fields, broken up for individual access
636     if (cert.subjectName) {
637       result.subjectNameFields = {};
638       cert.subjectName.split(",").forEach(function (v) {
639         var field = v.split("=");
640         this[field[0]] = field[1];
641       }, result.subjectNameFields);
643       // Call out city, state, and country specifically
644       result.city = result.subjectNameFields.L;
645       result.state = result.subjectNameFields.ST;
646       result.country = result.subjectNameFields.C;
647     }
649     // Human readable name of Certificate Authority
650     result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
651     result.cert = cert;
653     return result;
654   },
656   _getIsSecureContext() {
657     if (gBrowser.contentPrincipal?.originNoSuffix != "resource://pdf.js") {
658       return gBrowser.securityUI.isSecureContext;
659     }
661     // For PDF viewer pages (pdf.js) we can't rely on the isSecureContext field.
662     // The backend will return isSecureContext = true, because the content
663     // principal has a resource:// URI. Instead use the URI of the selected
664     // browser to perform the isPotentiallyTrustWorthy check.
666     let principal;
667     try {
668       principal = Services.scriptSecurityManager.createContentPrincipal(
669         gBrowser.selectedBrowser.documentURI,
670         {}
671       );
672       return principal.isOriginPotentiallyTrustworthy;
673     } catch (error) {
674       console.error(
675         "Error while computing isPotentiallyTrustWorthy for pdf viewer page: ",
676         error
677       );
678       return false;
679     }
680   },
682   /**
683    * Update the identity user interface for the page currently being displayed.
684    *
685    * This examines the SSL certificate metadata, if available, as well as the
686    * connection type and other security-related state information for the page.
687    *
688    * @param state
689    *        Bitmask provided by nsIWebProgressListener.onSecurityChange.
690    * @param uri
691    *        nsIURI for which the identity UI should be displayed, already
692    *        processed by createExposableURI.
693    */
694   updateIdentity(state, uri) {
695     let shouldHidePopup = this._uri && this._uri.spec != uri.spec;
696     this._state = state;
698     // Firstly, populate the state properties required to display the UI. See
699     // the documentation of the individual properties for details.
700     this.setURI(uri);
701     this._secInfo = gBrowser.securityUI.secInfo;
702     this._isSecureContext = this._getIsSecureContext();
704     // Then, update the user interface with the available data.
705     this.refreshIdentityBlock();
706     // Handle a location change while the Control Center is focused
707     // by closing the popup (bug 1207542)
708     if (shouldHidePopup) {
709       this.hidePopup();
710       gPermissionPanel.hidePopup();
711     }
713     // NOTE: We do NOT update the identity popup (the control center) when
714     // we receive a new security state on the existing page (i.e. from a
715     // subframe). If the user opened the popup and looks at the provided
716     // information we don't want to suddenly change the panel contents.
717   },
719   /**
720    * Attempt to provide proper IDN treatment for host names
721    */
722   getEffectiveHost() {
723     if (!this._IDNService) {
724       this._IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(
725         Ci.nsIIDNService
726       );
727     }
728     try {
729       return this._IDNService.convertToDisplayIDN(this._uri.host, {});
730     } catch (e) {
731       // If something goes wrong (e.g. host is an IP address) just fail back
732       // to the full domain.
733       return this._uri.host;
734     }
735   },
737   getHostForDisplay() {
738     let host = "";
740     try {
741       host = this.getEffectiveHost();
742     } catch (e) {
743       // Some URIs might have no hosts.
744     }
746     if (this._uri.schemeIs("about")) {
747       // For example in about:certificate the original URL is
748       // about:certificate?cert=<large base64 encoded data>&cert=<large base64 encoded data>&cert=...
749       // So, instead of showing that large string in the identity panel header, we are just showing
750       // about:certificate now. For the other about pages we are just showing about:<page>
751       host = "about:" + this._uri.filePath;
752     }
754     if (this._uri.schemeIs("chrome")) {
755       host = this._uri.spec;
756     }
758     let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay(
759       this._uri.displaySpec
760     );
761     if (readerStrippedURI) {
762       host = readerStrippedURI.host;
763     }
765     if (this._pageExtensionPolicy) {
766       host = this._pageExtensionPolicy.name;
767     }
769     // Fallback for special protocols.
770     if (!host) {
771       host = this._uri.specIgnoringRef;
772     }
774     return host;
775   },
777   /**
778    * Return the CSS class name to set on the "fullscreen-warning" element to
779    * display information about connection security in the notification shown
780    * when a site enters the fullscreen mode.
781    */
782   get pointerlockFsWarningClassName() {
783     // Note that the fullscreen warning does not handle _isSecureInternalUI.
784     if (this._uriHasHost && this._isSecureConnection) {
785       return "verifiedDomain";
786     }
787     return "unknownIdentity";
788   },
790   /**
791    * Returns whether the issuer of the current certificate chain is
792    * built-in (returns false) or imported (returns true).
793    */
794   _hasCustomRoot() {
795     return !this._secInfo.isBuiltCertChainRootBuiltInRoot;
796   },
798   /**
799    * Returns whether the current URI results in an "invalid"
800    * URL bar state, which effectively means hidden security
801    * indicators.
802    */
803   _hasInvalidPageProxyState() {
804     return (
805       !this._uriHasHost &&
806       this._uri &&
807       isBlankPageURL(this._uri.spec) &&
808       !this._uri.schemeIs("moz-extension")
809     );
810   },
812   /**
813    * Updates the security identity in the identity block.
814    */
815   _refreshIdentityIcons() {
816     let icon_label = "";
817     let tooltip = "";
819     let warnTextOnInsecure =
820       this._insecureConnectionTextEnabled ||
821       (this._insecureConnectionTextPBModeEnabled &&
822         PrivateBrowsingUtils.isWindowPrivate(window));
824     if (this._isSecureInternalUI) {
825       // This is a secure internal Firefox page.
826       this._identityBox.className = "chromeUI";
827       let brandBundle = document.getElementById("bundle_brand");
828       icon_label = brandBundle.getString("brandShorterName");
829     } else if (this._pageExtensionPolicy) {
830       // This is a WebExtension page.
831       this._identityBox.className = "extensionPage";
832       let extensionName = this._pageExtensionPolicy.name;
833       icon_label = gNavigatorBundle.getFormattedString(
834         "identity.extension.label",
835         [extensionName]
836       );
837     } else if (this._uriHasHost && this._isSecureConnection) {
838       // This is a secure connection.
839       this._identityBox.className = "verifiedDomain";
840       if (this._isMixedActiveContentBlocked) {
841         this._identityBox.classList.add("mixedActiveBlocked");
842       }
843       if (!this._isCertUserOverridden) {
844         // It's a normal cert, verifier is the CA Org.
845         tooltip = gNavigatorBundle.getFormattedString(
846           "identity.identified.verifier",
847           [this.getIdentityData().caOrg]
848         );
849       }
850     } else if (this._isBrokenConnection) {
851       // This is a secure connection, but something is wrong.
852       this._identityBox.className = "unknownIdentity";
854       if (this._isMixedActiveContentLoaded) {
855         this._identityBox.classList.add("mixedActiveContent");
856         if (UrlbarPrefs.get("trimHttps") && warnTextOnInsecure) {
857           icon_label = gNavigatorBundle.getString("identity.notSecure.label");
858           this._identityBox.classList.add("notSecureText");
859         }
860       } else if (this._isMixedActiveContentBlocked) {
861         this._identityBox.classList.add(
862           "mixedDisplayContentLoadedActiveBlocked"
863         );
864       } else if (this._isMixedPassiveContentLoaded) {
865         this._identityBox.classList.add("mixedDisplayContent");
866       } else {
867         this._identityBox.classList.add("weakCipher");
868       }
869     } else if (this._isCertErrorPage) {
870       // We show a warning lock icon for certificate errors, and
871       // show the "Not Secure" text.
872       this._identityBox.className = "certErrorPage notSecureText";
873       icon_label = gNavigatorBundle.getString("identity.notSecure.label");
874       tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip");
875     } else if (this._isAboutHttpsOnlyErrorPage) {
876       // We show a not secure lock icon for 'about:httpsonlyerror' page.
877       this._identityBox.className = "httpsOnlyErrorPage";
878     } else if (
879       this._isAboutNetErrorPage ||
880       this._isAboutBlockedPage ||
881       this._isAssociatedIdentity
882     ) {
883       // Network errors, blocked pages, and pages associated
884       // with another page get a more neutral icon
885       this._identityBox.className = "unknownIdentity";
886     } else if (this._isPotentiallyTrustworthy) {
887       // This is a local resource (and shouldn't be marked insecure).
888       this._identityBox.className = "localResource";
889     } else {
890       // This is an insecure connection.
891       let className = "notSecure";
892       this._identityBox.className = className;
893       tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip");
894       if (warnTextOnInsecure) {
895         icon_label = gNavigatorBundle.getString("identity.notSecure.label");
896         this._identityBox.classList.add("notSecureText");
897       }
898     }
900     if (this._isCertUserOverridden) {
901       this._identityBox.classList.add("certUserOverridden");
902       // Cert is trusted because of a security exception, verifier is a special string.
903       tooltip = gNavigatorBundle.getString(
904         "identity.identified.verified_by_you"
905       );
906     }
908     // Push the appropriate strings out to the UI
909     this._identityIcon.setAttribute("tooltiptext", tooltip);
911     if (this._pageExtensionPolicy) {
912       let extensionName = this._pageExtensionPolicy.name;
913       this._identityIcon.setAttribute(
914         "tooltiptext",
915         gNavigatorBundle.getFormattedString("identity.extension.tooltip", [
916           extensionName,
917         ])
918       );
919     }
921     this._identityIconLabel.setAttribute("tooltiptext", tooltip);
922     this._identityIconLabel.setAttribute("value", icon_label);
923     this._identityIconLabel.collapsed = !icon_label;
924   },
926   /**
927    * Updates the identity block user interface with the data from this object.
928    */
929   refreshIdentityBlock() {
930     if (!this._identityBox) {
931       return;
932     }
934     this._refreshIdentityIcons();
936     // If this condition is true, the URL bar will have an "invalid"
937     // pageproxystate, so we should hide the permission icons.
938     if (this._hasInvalidPageProxyState()) {
939       gPermissionPanel.hidePermissionIcons();
940     } else {
941       gPermissionPanel.refreshPermissionIcons();
942     }
944     // Hide the shield icon if it is a chrome page.
945     gProtectionsHandler._trackingProtectionIconContainer.classList.toggle(
946       "chromeUI",
947       this._isSecureInternalUI
948     );
949   },
951   /**
952    * Set up the title and content messages for the identity message popup,
953    * based on the specified mode, and the details of the SSL cert, where
954    * applicable
955    */
956   refreshIdentityPopup() {
957     // Update cookies and site data information and show the
958     // "Clear Site Data" button if the site is storing local data, and
959     // if the page is not controlled by a WebExtension.
960     this._clearSiteDataFooter.hidden = true;
961     let identityPopupPanelView = document.getElementById(
962       "identity-popup-mainView"
963     );
964     identityPopupPanelView.removeAttribute("footerVisible");
965     if (this._uriHasHost && !this._pageExtensionPolicy) {
966       SiteDataManager.hasSiteData(this._uri.asciiHost).then(hasData => {
967         this._clearSiteDataFooter.hidden = !hasData;
968         identityPopupPanelView.setAttribute("footerVisible", hasData);
969       });
970     }
972     let customRoot = false;
974     // Determine connection security information.
975     let connection = "not-secure";
976     if (this._isSecureInternalUI) {
977       connection = "chrome";
978     } else if (this._pageExtensionPolicy) {
979       connection = "extension";
980     } else if (this._isURILoadedFromFile) {
981       connection = "file";
982     } else if (this._isEV) {
983       connection = "secure-ev";
984     } else if (this._isCertUserOverridden) {
985       connection = "secure-cert-user-overridden";
986     } else if (this._isSecureConnection) {
987       connection = "secure";
988       customRoot = this._hasCustomRoot();
989     } else if (this._isCertErrorPage) {
990       connection = "cert-error-page";
991     } else if (this._isAboutHttpsOnlyErrorPage) {
992       connection = "https-only-error-page";
993     } else if (this._isAboutBlockedPage) {
994       connection = "not-secure";
995     } else if (this._isAboutNetErrorPage) {
996       connection = "net-error-page";
997     } else if (this._isAssociatedIdentity) {
998       connection = "associated";
999     } else if (this._isPotentiallyTrustworthy) {
1000       connection = "file";
1001     }
1003     let securityButtonNode = document.getElementById(
1004       "identity-popup-security-button"
1005     );
1007     let disableSecurityButton = ![
1008       "not-secure",
1009       "secure",
1010       "secure-ev",
1011       "secure-cert-user-overridden",
1012       "cert-error-page",
1013       "net-error-page",
1014       "https-only-error-page",
1015     ].includes(connection);
1016     if (disableSecurityButton) {
1017       securityButtonNode.disabled = true;
1018       securityButtonNode.classList.remove("subviewbutton-nav");
1019     } else {
1020       securityButtonNode.disabled = false;
1021       securityButtonNode.classList.add("subviewbutton-nav");
1022     }
1024     // Determine the mixed content state.
1025     let mixedcontent = [];
1026     if (this._isMixedPassiveContentLoaded) {
1027       mixedcontent.push("passive-loaded");
1028     }
1029     if (this._isMixedActiveContentLoaded) {
1030       mixedcontent.push("active-loaded");
1031     } else if (this._isMixedActiveContentBlocked) {
1032       mixedcontent.push("active-blocked");
1033     }
1034     mixedcontent = mixedcontent.join(" ");
1036     // We have no specific flags for weak ciphers (yet). If a connection is
1037     // broken and we can't detect any mixed content loaded then it's a weak
1038     // cipher.
1039     let ciphers = "";
1040     if (
1041       this._isBrokenConnection &&
1042       !this._isMixedActiveContentLoaded &&
1043       !this._isMixedPassiveContentLoaded
1044     ) {
1045       ciphers = "weak";
1046     }
1048     // If HTTPS-Only Mode is enabled, check the permission status
1049     const privateBrowsingWindow = PrivateBrowsingUtils.isWindowPrivate(window);
1050     const isHttpsOnlyModeActive = this._isHttpsOnlyModeActive(
1051       privateBrowsingWindow
1052     );
1053     const isHttpsFirstModeActive = this._isHttpsFirstModeActive(
1054       privateBrowsingWindow
1055     );
1056     const isSchemelessHttpsFirstModeActive =
1057       this._isSchemelessHttpsFirstModeActive(privateBrowsingWindow);
1058     let httpsOnlyStatus = "";
1059     if (
1060       isHttpsFirstModeActive ||
1061       isHttpsOnlyModeActive ||
1062       isSchemelessHttpsFirstModeActive
1063     ) {
1064       // Note: value and permission association is laid out
1065       //       in _getHttpsOnlyPermission
1066       let value = this._getHttpsOnlyPermission();
1068       // We do not want to display the exception ui for schemeless
1069       // HTTPS-First, but we still want the "Upgraded to HTTPS" label.
1070       this._identityPopupHttpsOnlyMode.hidden =
1071         isSchemelessHttpsFirstModeActive;
1073       this._identityPopupHttpsOnlyModeMenuListOffItem.hidden =
1074         privateBrowsingWindow && value != 1;
1076       this._identityPopupHttpsOnlyModeMenuList.value = value;
1078       if (value > 0) {
1079         httpsOnlyStatus = "exception";
1080       } else if (
1081         this._isAboutHttpsOnlyErrorPage ||
1082         (isHttpsFirstModeActive && this._isContentHttpsOnlyModeUpgradeFailed)
1083       ) {
1084         httpsOnlyStatus = "failed-top";
1085       } else if (this._isContentHttpsOnlyModeUpgradeFailed) {
1086         httpsOnlyStatus = "failed-sub";
1087       } else if (
1088         this._isContentHttpsOnlyModeUpgraded ||
1089         this._isContentHttpsFirstModeUpgraded
1090       ) {
1091         httpsOnlyStatus = "upgraded";
1092       }
1093     }
1095     // Update all elements.
1096     let elementIDs = [
1097       "identity-popup",
1098       "identity-popup-securityView-extended-info",
1099     ];
1101     for (let id of elementIDs) {
1102       let element = document.getElementById(id);
1103       this._updateAttribute(element, "connection", connection);
1104       this._updateAttribute(element, "ciphers", ciphers);
1105       this._updateAttribute(element, "mixedcontent", mixedcontent);
1106       this._updateAttribute(element, "isbroken", this._isBrokenConnection);
1107       this._updateAttribute(element, "customroot", customRoot);
1108       this._updateAttribute(element, "httpsonlystatus", httpsOnlyStatus);
1109     }
1111     // Initialize the optional strings to empty values
1112     let supplemental = "";
1113     let verifier = "";
1114     let host = this.getHostForDisplay();
1115     let owner = "";
1117     // Fill in the CA name if we have a valid TLS certificate.
1118     if (this._isSecureConnection || this._isCertUserOverridden) {
1119       verifier = this._identityIconLabel.tooltipText;
1120     }
1122     // Fill in organization information if we have a valid EV certificate.
1123     if (this._isEV) {
1124       let iData = this.getIdentityData();
1125       owner = iData.subjectOrg;
1126       verifier = this._identityIconLabel.tooltipText;
1128       // Build an appropriate supplemental block out of whatever location data we have
1129       if (iData.city) {
1130         supplemental += iData.city + "\n";
1131       }
1132       if (iData.state && iData.country) {
1133         supplemental += gNavigatorBundle.getFormattedString(
1134           "identity.identified.state_and_country",
1135           [iData.state, iData.country]
1136         );
1137       } else if (iData.state) {
1138         // State only
1139         supplemental += iData.state;
1140       } else if (iData.country) {
1141         // Country only
1142         supplemental += iData.country;
1143       }
1144     }
1146     // Push the appropriate strings out to the UI.
1147     document.l10n.setAttributes(
1148       this._identityPopupMainViewHeaderLabel,
1149       "identity-site-information",
1150       {
1151         host,
1152       }
1153     );
1155     document.l10n.setAttributes(
1156       this._identityPopupSecurityView,
1157       "identity-header-security-with-host",
1158       {
1159         host,
1160       }
1161     );
1163     document.l10n.setAttributes(
1164       this._identityPopupMainViewHeaderLabel,
1165       "identity-site-information",
1166       {
1167         host,
1168       }
1169     );
1171     this._identityPopupSecurityEVContentOwner.textContent =
1172       gNavigatorBundle.getFormattedString("identity.ev.contentOwner2", [owner]);
1174     this._identityPopupContentOwner.textContent = owner;
1175     this._identityPopupContentSupp.textContent = supplemental;
1176     this._identityPopupContentVerif.textContent = verifier;
1177   },
1179   setURI(uri) {
1180     if (uri instanceof Ci.nsINestedURI) {
1181       uri = uri.QueryInterface(Ci.nsINestedURI).innermostURI;
1182     }
1183     this._uri = uri;
1185     try {
1186       // Account for file: urls and catch when "" is the value
1187       this._uriHasHost = !!this._uri.host;
1188     } catch (ex) {
1189       this._uriHasHost = false;
1190     }
1192     if (uri.schemeIs("about") || uri.schemeIs("moz-safe-about")) {
1193       let module = E10SUtils.getAboutModule(uri);
1194       if (module) {
1195         let flags = module.getURIFlags(uri);
1196         this._isSecureInternalUI = !!(
1197           flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI
1198         );
1199       }
1200     } else {
1201       this._isSecureInternalUI = false;
1202     }
1203     this._pageExtensionPolicy = WebExtensionPolicy.getByURI(uri);
1204     this._isURILoadedFromFile = uri.schemeIs("file");
1205   },
1207   /**
1208    * Click handler for the identity-box element in primary chrome.
1209    */
1210   handleIdentityButtonEvent(event) {
1211     event.stopPropagation();
1213     if (
1214       (event.type == "click" && event.button != 0) ||
1215       (event.type == "keypress" &&
1216         event.charCode != KeyEvent.DOM_VK_SPACE &&
1217         event.keyCode != KeyEvent.DOM_VK_RETURN)
1218     ) {
1219       return; // Left click, space or enter only
1220     }
1222     // Don't allow left click, space or enter if the location has been modified.
1223     if (gURLBar.getAttribute("pageproxystate") != "valid") {
1224       return;
1225     }
1227     this._openPopup(event);
1228   },
1230   _openPopup(event) {
1231     // Make the popup available.
1232     this._initializePopup();
1234     // Update the popup strings
1235     this.refreshIdentityPopup();
1237     // Check the panel state of other panels. Hide them if needed.
1238     let openPanels = Array.from(document.querySelectorAll("panel[openpanel]"));
1239     for (let panel of openPanels) {
1240       PanelMultiView.hidePopup(panel);
1241     }
1243     // Now open the popup, anchored off the primary chrome element
1244     PanelMultiView.openPopup(this._identityPopup, this._identityIconBox, {
1245       position: "bottomleft topleft",
1246       triggerEvent: event,
1247     }).catch(console.error);
1248   },
1250   onPopupShown(event) {
1251     if (event.target == this._identityPopup) {
1252       PopupNotifications.suppressWhileOpen(this._identityPopup);
1253       window.addEventListener("focus", this, true);
1254     }
1255   },
1257   onPopupHidden(event) {
1258     if (event.target == this._identityPopup) {
1259       window.removeEventListener("focus", this, true);
1260     }
1261   },
1263   handleEvent() {
1264     let elem = document.activeElement;
1265     let position = elem.compareDocumentPosition(this._identityPopup);
1267     if (
1268       !(
1269         position &
1270         (Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_CONTAINED_BY)
1271       ) &&
1272       !this._identityPopup.hasAttribute("noautohide")
1273     ) {
1274       // Hide the panel when focusing an element that is
1275       // neither an ancestor nor descendant unless the panel has
1276       // @noautohide (e.g. for a tour).
1277       PanelMultiView.hidePopup(this._identityPopup);
1278     }
1279   },
1281   observe(subject, topic) {
1282     switch (topic) {
1283       case "perm-changed": {
1284         // Exclude permissions which do not appear in the UI in order to avoid
1285         // doing extra work here.
1286         if (!subject) {
1287           return;
1288         }
1289         let { type } = subject.QueryInterface(Ci.nsIPermission);
1290         if (SitePermissions.isSitePermission(type)) {
1291           this.refreshIdentityBlock();
1292         }
1293         break;
1294       }
1295     }
1296   },
1298   onDragStart(event) {
1299     const TEXT_SIZE = 14;
1300     const IMAGE_SIZE = 16;
1301     const SPACING = 5;
1303     if (gURLBar.getAttribute("pageproxystate") != "valid") {
1304       return;
1305     }
1307     let value = gBrowser.currentURI.displaySpec;
1308     let urlString = value + "\n" + gBrowser.contentTitle;
1309     let htmlString = '<a href="' + value + '">' + value + "</a>";
1311     let scale = window.devicePixelRatio;
1312     let canvas = document.createElementNS(
1313       "http://www.w3.org/1999/xhtml",
1314       "canvas"
1315     );
1316     canvas.width = 550 * scale;
1317     let ctx = canvas.getContext("2d");
1318     ctx.font = `${TEXT_SIZE * scale}px sans-serif`;
1319     let tabIcon = gBrowser.selectedTab.iconImage;
1320     let image = new Image();
1321     image.src = tabIcon.src;
1322     let textWidth = ctx.measureText(value).width / scale;
1323     let textHeight = parseInt(ctx.font, 10) / scale;
1324     let imageHorizontalOffset, imageVerticalOffset;
1325     imageHorizontalOffset = imageVerticalOffset = SPACING;
1326     let textHorizontalOffset = image.width ? IMAGE_SIZE + SPACING * 2 : SPACING;
1327     let textVerticalOffset = textHeight + SPACING - 1;
1328     let backgroundColor = "white";
1329     let textColor = "black";
1330     let totalWidth = image.width
1331       ? textWidth + IMAGE_SIZE + 3 * SPACING
1332       : textWidth + 2 * SPACING;
1333     let totalHeight = image.width
1334       ? IMAGE_SIZE + 2 * SPACING
1335       : textHeight + 2 * SPACING;
1336     ctx.fillStyle = backgroundColor;
1337     ctx.fillRect(0, 0, totalWidth * scale, totalHeight * scale);
1338     ctx.fillStyle = textColor;
1339     ctx.fillText(
1340       `${value}`,
1341       textHorizontalOffset * scale,
1342       textVerticalOffset * scale
1343     );
1344     try {
1345       ctx.drawImage(
1346         image,
1347         imageHorizontalOffset * scale,
1348         imageVerticalOffset * scale,
1349         IMAGE_SIZE * scale,
1350         IMAGE_SIZE * scale
1351       );
1352     } catch (e) {
1353       // Sites might specify invalid data URIs favicons that
1354       // will result in errors when trying to draw, we can
1355       // just ignore this case and not paint any favicon.
1356     }
1358     let dt = event.dataTransfer;
1359     dt.setData("text/x-moz-url", urlString);
1360     dt.setData("text/uri-list", value);
1361     dt.setData("text/plain", value);
1362     dt.setData("text/html", htmlString);
1363     dt.setDragImage(canvas, 16, 16);
1365     // Don't cover potential drop targets on the toolbars or in content.
1366     gURLBar.view.close();
1367   },
1369   _updateAttribute(elem, attr, value) {
1370     if (value) {
1371       elem.setAttribute(attr, value);
1372     } else {
1373       elem.removeAttribute(attr);
1374     }
1375   },