Bug 1799846 - Filter out extensions that have a browser action in the non-CUI list...
[gecko.git] / browser / base / content / browser-addons.js
blob65e12c190d02d455dc1a92ee7a7e44907601a10f
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 // This file is loaded into the browser window scope.
7 /* eslint-env mozilla/browser-window */
9 const lazy = {};
11 ChromeUtils.defineModuleGetter(
12   lazy,
13   "ExtensionParent",
14   "resource://gre/modules/ExtensionParent.jsm"
16 ChromeUtils.defineModuleGetter(
17   lazy,
18   "OriginControls",
19   "resource://gre/modules/ExtensionPermissions.jsm"
21 ChromeUtils.defineModuleGetter(
22   lazy,
23   "ExtensionPermissions",
24   "resource://gre/modules/ExtensionPermissions.jsm"
26 ChromeUtils.defineESModuleGetters(lazy, {
27   SITEPERMS_ADDON_TYPE:
28     "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
29 });
31 customElements.define(
32   "addon-progress-notification",
33   class MozAddonProgressNotification extends customElements.get(
34     "popupnotification"
35   ) {
36     show() {
37       super.show();
38       this.progressmeter = document.getElementById(
39         "addon-progress-notification-progressmeter"
40       );
42       this.progresstext = document.getElementById(
43         "addon-progress-notification-progresstext"
44       );
46       if (!this.notification) {
47         return;
48       }
50       this.notification.options.installs.forEach(function(aInstall) {
51         aInstall.addListener(this);
52       }, this);
54       // Calling updateProgress can sometimes cause this notification to be
55       // removed in the middle of refreshing the notification panel which
56       // makes the panel get refreshed again. Just initialise to the
57       // undetermined state and then schedule a proper check at the next
58       // opportunity
59       this.setProgress(0, -1);
60       this._updateProgressTimeout = setTimeout(
61         this.updateProgress.bind(this),
62         0
63       );
64     }
66     disconnectedCallback() {
67       this.destroy();
68     }
70     destroy() {
71       if (!this.notification) {
72         return;
73       }
74       this.notification.options.installs.forEach(function(aInstall) {
75         aInstall.removeListener(this);
76       }, this);
78       clearTimeout(this._updateProgressTimeout);
79     }
81     setProgress(aProgress, aMaxProgress) {
82       if (aMaxProgress == -1) {
83         this.progressmeter.removeAttribute("value");
84       } else {
85         this.progressmeter.setAttribute(
86           "value",
87           (aProgress * 100) / aMaxProgress
88         );
89       }
91       let now = Date.now();
93       if (!this.notification.lastUpdate) {
94         this.notification.lastUpdate = now;
95         this.notification.lastProgress = aProgress;
96         return;
97       }
99       let delta = now - this.notification.lastUpdate;
100       if (delta < 400 && aProgress < aMaxProgress) {
101         return;
102       }
104       // Set min. time delta to avoid division by zero in the upcoming speed calculation
105       delta = Math.max(delta, 400);
106       delta /= 1000;
108       // This algorithm is the same used by the downloads code.
109       let speed = (aProgress - this.notification.lastProgress) / delta;
110       if (this.notification.speed) {
111         speed = speed * 0.9 + this.notification.speed * 0.1;
112       }
114       this.notification.lastUpdate = now;
115       this.notification.lastProgress = aProgress;
116       this.notification.speed = speed;
118       let status = null;
119       [status, this.notification.last] = DownloadUtils.getDownloadStatus(
120         aProgress,
121         aMaxProgress,
122         speed,
123         this.notification.last
124       );
125       this.progresstext.setAttribute("value", status);
126       this.progresstext.setAttribute("tooltiptext", status);
127     }
129     cancel() {
130       let installs = this.notification.options.installs;
131       installs.forEach(function(aInstall) {
132         try {
133           aInstall.cancel();
134         } catch (e) {
135           // Cancel will throw if the download has already failed
136         }
137       }, this);
139       PopupNotifications.remove(this.notification);
140     }
142     updateProgress() {
143       if (!this.notification) {
144         return;
145       }
147       let downloadingCount = 0;
148       let progress = 0;
149       let maxProgress = 0;
151       this.notification.options.installs.forEach(function(aInstall) {
152         if (aInstall.maxProgress == -1) {
153           maxProgress = -1;
154         }
155         progress += aInstall.progress;
156         if (maxProgress >= 0) {
157           maxProgress += aInstall.maxProgress;
158         }
159         if (aInstall.state < AddonManager.STATE_DOWNLOADED) {
160           downloadingCount++;
161         }
162       });
164       if (downloadingCount == 0) {
165         this.destroy();
166         this.progressmeter.removeAttribute("value");
167         let status = gNavigatorBundle.getString("addonDownloadVerifying");
168         this.progresstext.setAttribute("value", status);
169         this.progresstext.setAttribute("tooltiptext", status);
170       } else {
171         this.setProgress(progress, maxProgress);
172       }
173     }
175     onDownloadProgress() {
176       this.updateProgress();
177     }
179     onDownloadFailed() {
180       this.updateProgress();
181     }
183     onDownloadCancelled() {
184       this.updateProgress();
185     }
187     onDownloadEnded() {
188       this.updateProgress();
189     }
190   }
193 // Removes a doorhanger notification if all of the installs it was notifying
194 // about have ended in some way.
195 function removeNotificationOnEnd(notification, installs) {
196   let count = installs.length;
198   function maybeRemove(install) {
199     install.removeListener(this);
201     if (--count == 0) {
202       // Check that the notification is still showing
203       let current = PopupNotifications.getNotification(
204         notification.id,
205         notification.browser
206       );
207       if (current === notification) {
208         notification.remove();
209       }
210     }
211   }
213   for (let install of installs) {
214     install.addListener({
215       onDownloadCancelled: maybeRemove,
216       onDownloadFailed: maybeRemove,
217       onInstallFailed: maybeRemove,
218       onInstallEnded: maybeRemove,
219     });
220   }
223 var gXPInstallObserver = {
224   _findChildShell(aDocShell, aSoughtShell) {
225     if (aDocShell == aSoughtShell) {
226       return aDocShell;
227     }
229     var node = aDocShell.QueryInterface(Ci.nsIDocShellTreeItem);
230     for (var i = 0; i < node.childCount; ++i) {
231       var docShell = node.getChildAt(i);
232       docShell = this._findChildShell(docShell, aSoughtShell);
233       if (docShell == aSoughtShell) {
234         return docShell;
235       }
236     }
237     return null;
238   },
240   _getBrowser(aDocShell) {
241     for (let browser of gBrowser.browsers) {
242       if (this._findChildShell(browser.docShell, aDocShell)) {
243         return browser;
244       }
245     }
246     return null;
247   },
249   pendingInstalls: new WeakMap(),
251   showInstallConfirmation(browser, installInfo, height = undefined) {
252     // If the confirmation notification is already open cache the installInfo
253     // and the new confirmation will be shown later
254     if (
255       PopupNotifications.getNotification("addon-install-confirmation", browser)
256     ) {
257       let pending = this.pendingInstalls.get(browser);
258       if (pending) {
259         pending.push(installInfo);
260       } else {
261         this.pendingInstalls.set(browser, [installInfo]);
262       }
263       return;
264     }
266     let showNextConfirmation = () => {
267       // Make sure the browser is still alive.
268       if (!gBrowser.browsers.includes(browser)) {
269         return;
270       }
272       let pending = this.pendingInstalls.get(browser);
273       if (pending && pending.length) {
274         this.showInstallConfirmation(browser, pending.shift());
275       }
276     };
278     // If all installs have already been cancelled in some way then just show
279     // the next confirmation
280     if (
281       installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)
282     ) {
283       showNextConfirmation();
284       return;
285     }
287     // Make notifications persistent
288     var options = {
289       displayURI: installInfo.originatingURI,
290       persistent: true,
291       hideClose: true,
292     };
294     if (gUnifiedExtensions.isEnabled) {
295       options.popupOptions = {
296         position: "bottomright topright",
297       };
298     }
300     let acceptInstallation = () => {
301       for (let install of installInfo.installs) {
302         install.install();
303       }
304       installInfo = null;
306       Services.telemetry
307         .getHistogramById("SECURITY_UI")
308         .add(
309           Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH
310         );
311     };
313     let cancelInstallation = () => {
314       if (installInfo) {
315         for (let install of installInfo.installs) {
316           // The notification may have been closed because the add-ons got
317           // cancelled elsewhere, only try to cancel those that are still
318           // pending install.
319           if (install.state != AddonManager.STATE_CANCELLED) {
320             install.cancel();
321           }
322         }
323       }
325       showNextConfirmation();
326     };
328     let unsigned = installInfo.installs.filter(
329       i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
330     );
331     let someUnsigned =
332       !!unsigned.length && unsigned.length < installInfo.installs.length;
334     options.eventCallback = aEvent => {
335       switch (aEvent) {
336         case "removed":
337           cancelInstallation();
338           break;
339         case "shown":
340           let addonList = document.getElementById(
341             "addon-install-confirmation-content"
342           );
343           while (addonList.firstChild) {
344             addonList.firstChild.remove();
345           }
347           for (let install of installInfo.installs) {
348             let container = document.createXULElement("hbox");
350             let name = document.createXULElement("label");
351             name.setAttribute("value", install.addon.name);
352             name.setAttribute("class", "addon-install-confirmation-name");
353             container.appendChild(name);
355             if (
356               someUnsigned &&
357               install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
358             ) {
359               let unsignedLabel = document.createXULElement("label");
360               unsignedLabel.setAttribute(
361                 "value",
362                 gNavigatorBundle.getString("addonInstall.unsigned")
363               );
364               unsignedLabel.setAttribute(
365                 "class",
366                 "addon-install-confirmation-unsigned"
367               );
368               container.appendChild(unsignedLabel);
369             }
371             addonList.appendChild(container);
372           }
373           break;
374       }
375     };
377     options.learnMoreURL = Services.urlFormatter.formatURLPref(
378       "app.support.baseURL"
379     );
381     let messageString;
382     let notification = document.getElementById(
383       "addon-install-confirmation-notification"
384     );
385     if (unsigned.length == installInfo.installs.length) {
386       // None of the add-ons are verified
387       messageString = gNavigatorBundle.getString(
388         "addonConfirmInstallUnsigned.message"
389       );
390       notification.setAttribute("warning", "true");
391       options.learnMoreURL += "unsigned-addons";
392     } else if (!unsigned.length) {
393       // All add-ons are verified or don't need to be verified
394       messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
395       notification.removeAttribute("warning");
396       options.learnMoreURL += "find-and-install-add-ons";
397     } else {
398       // Some of the add-ons are unverified, the list of names will indicate
399       // which
400       messageString = gNavigatorBundle.getString(
401         "addonConfirmInstallSomeUnsigned.message"
402       );
403       notification.setAttribute("warning", "true");
404       options.learnMoreURL += "unsigned-addons";
405     }
407     let brandBundle = document.getElementById("bundle_brand");
408     let brandShortName = brandBundle.getString("brandShortName");
410     messageString = PluralForm.get(installInfo.installs.length, messageString);
411     messageString = messageString.replace("#1", brandShortName);
412     messageString = messageString.replace("#2", installInfo.installs.length);
414     let action = {
415       label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
416       accessKey: gNavigatorBundle.getString(
417         "addonInstall.acceptButton2.accesskey"
418       ),
419       callback: acceptInstallation,
420     };
422     let secondaryAction = {
423       label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
424       accessKey: gNavigatorBundle.getString(
425         "addonInstall.cancelButton.accesskey"
426       ),
427       callback: () => {},
428     };
430     if (height) {
431       notification.style.minHeight = height + "px";
432     }
434     let tab = gBrowser.getTabForBrowser(browser);
435     if (tab) {
436       gBrowser.selectedTab = tab;
437     }
439     let popup = PopupNotifications.show(
440       browser,
441       "addon-install-confirmation",
442       messageString,
443       gUnifiedExtensions.getPopupAnchorID(browser, window),
444       action,
445       [secondaryAction],
446       options
447     );
449     removeNotificationOnEnd(popup, installInfo.installs);
451     Services.telemetry
452       .getHistogramById("SECURITY_UI")
453       .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
454   },
456   // IDs of addon install related notifications
457   NOTIFICATION_IDS: [
458     "addon-install-blocked",
459     "addon-install-complete",
460     "addon-install-confirmation",
461     "addon-install-failed",
462     "addon-install-origin-blocked",
463     "addon-install-webapi-blocked",
464     "addon-install-policy-blocked",
465     "addon-progress",
466     "addon-webext-permissions",
467     "xpinstall-disabled",
468   ],
470   /**
471    * Remove all opened addon installation notifications
472    *
473    * @param {*} browser - Browser to remove notifications for
474    * @returns {boolean} - true if notifications have been removed.
475    */
476   removeAllNotifications(browser) {
477     let notifications = this.NOTIFICATION_IDS.map(id =>
478       PopupNotifications.getNotification(id, browser)
479     ).filter(notification => notification != null);
481     PopupNotifications.remove(notifications, true);
483     return !!notifications.length;
484   },
486   logWarningFullScreenInstallBlocked() {
487     // If notifications have been removed, log a warning to the website console
488     let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
489       Ci.nsIScriptError
490     );
491     let message = gBrowserBundle.GetStringFromName(
492       "addonInstallFullScreenBlocked"
493     );
494     consoleMsg.initWithWindowID(
495       message,
496       gBrowser.currentURI.spec,
497       null,
498       0,
499       0,
500       Ci.nsIScriptError.warningFlag,
501       "FullScreen",
502       gBrowser.selectedBrowser.innerWindowID
503     );
504     Services.console.logMessage(consoleMsg);
505   },
507   observe(aSubject, aTopic, aData) {
508     var brandBundle = document.getElementById("bundle_brand");
509     var installInfo = aSubject.wrappedJSObject;
510     var browser = installInfo.browser;
512     // Make sure the browser is still alive.
513     if (!browser || !gBrowser.browsers.includes(browser)) {
514       return;
515     }
517     var messageString, action;
518     var brandShortName = brandBundle.getString("brandShortName");
520     var notificationID = aTopic;
521     // Make notifications persistent
522     var options = {
523       displayURI: installInfo.originatingURI,
524       persistent: true,
525       hideClose: true,
526       timeout: Date.now() + 30000,
527     };
529     if (gUnifiedExtensions.isEnabled) {
530       options.popupOptions = {
531         position: "bottomright topright",
532       };
533     }
535     switch (aTopic) {
536       case "addon-install-disabled": {
537         notificationID = "xpinstall-disabled";
538         let secondaryActions = null;
540         if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
541           messageString = gNavigatorBundle.getString(
542             "xpinstallDisabledMessageLocked"
543           );
544         } else {
545           messageString = gNavigatorBundle.getString(
546             "xpinstallDisabledMessage"
547           );
549           action = {
550             label: gNavigatorBundle.getString("xpinstallDisabledButton"),
551             accessKey: gNavigatorBundle.getString(
552               "xpinstallDisabledButton.accesskey"
553             ),
554             callback: function editPrefs() {
555               Services.prefs.setBoolPref("xpinstall.enabled", true);
556             },
557           };
559           secondaryActions = [
560             {
561               label: gNavigatorBundle.getString(
562                 "addonInstall.cancelButton.label"
563               ),
564               accessKey: gNavigatorBundle.getString(
565                 "addonInstall.cancelButton.accesskey"
566               ),
567               callback: () => {},
568             },
569           ];
570         }
572         PopupNotifications.show(
573           browser,
574           notificationID,
575           messageString,
576           gUnifiedExtensions.getPopupAnchorID(browser, window),
577           action,
578           secondaryActions,
579           options
580         );
581         break;
582       }
583       case "addon-install-fullscreen-blocked": {
584         // AddonManager denied installation because we are in DOM fullscreen
585         this.logWarningFullScreenInstallBlocked();
586         break;
587       }
588       case "addon-install-webapi-blocked":
589       case "addon-install-policy-blocked":
590       case "addon-install-origin-blocked": {
591         if (aTopic == "addon-install-policy-blocked") {
592           messageString = gNavigatorBundle.getString(
593             "addonDomainBlockedByPolicy"
594           );
595         } else {
596           messageString = gNavigatorBundle.getFormattedString(
597             "xpinstallPromptMessage",
598             [brandShortName]
599           );
600         }
602         if (Services.policies) {
603           let extensionSettings = Services.policies.getExtensionSettings("*");
604           if (
605             extensionSettings &&
606             "blocked_install_message" in extensionSettings
607           ) {
608             messageString += " " + extensionSettings.blocked_install_message;
609           }
610         }
612         options.removeOnDismissal = true;
613         options.persistent = false;
615         let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
616         secHistogram.add(
617           Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
618         );
619         let popup = PopupNotifications.show(
620           browser,
621           notificationID,
622           messageString,
623           gUnifiedExtensions.getPopupAnchorID(browser, window),
624           null,
625           null,
626           options
627         );
628         removeNotificationOnEnd(popup, installInfo.installs);
629         break;
630       }
631       case "addon-install-blocked": {
632         // Dismiss the progress notification.  Note that this is bad if
633         // there are multiple simultaneous installs happening, see
634         // bug 1329884 for a longer explanation.
635         let progressNotification = PopupNotifications.getNotification(
636           "addon-progress",
637           browser
638         );
639         if (progressNotification) {
640           progressNotification.remove();
641         }
643         // The informational content differs somewhat for site permission
644         // add-ons. AOM no longer supports installing multiple addons,
645         // so the array handling here is vestigial.
646         let isSitePermissionAddon = installInfo.installs.every(
647           ({ addon }) => addon?.type === lazy.SITEPERMS_ADDON_TYPE
648         );
649         let hasHost = !!options.displayURI;
651         if (isSitePermissionAddon) {
652           messageString = gNavigatorBundle.getString(
653             "sitePermissionInstallFirstPrompt.header"
654           );
655         } else if (hasHost) {
656           messageString = gNavigatorBundle.getFormattedString(
657             "xpinstallPromptMessage.header",
658             ["<>"]
659           );
660           options.name = options.displayURI.displayHost;
661         } else {
662           messageString = gNavigatorBundle.getString(
663             "xpinstallPromptMessage.header.unknown"
664           );
665         }
666         // displayURI becomes it's own label, so we unset it for this panel. It will become part of the
667         // messageString above.
668         options.displayURI = undefined;
670         options.eventCallback = topic => {
671           if (topic !== "showing") {
672             return;
673           }
674           let doc = browser.ownerDocument;
675           let message = doc.getElementById("addon-install-blocked-message");
676           // We must remove any prior use of this panel message in this window.
677           while (message.firstChild) {
678             message.firstChild.remove();
679           }
681           if (isSitePermissionAddon) {
682             message.textContent = gNavigatorBundle.getString(
683               "sitePermissionInstallFirstPrompt.message"
684             );
685           } else if (hasHost) {
686             let text = gNavigatorBundle.getString(
687               "xpinstallPromptMessage.message"
688             );
689             let b = doc.createElementNS("http://www.w3.org/1999/xhtml", "b");
690             b.textContent = options.name;
691             let fragment = BrowserUIUtils.getLocalizedFragment(doc, text, b);
692             message.appendChild(fragment);
693           } else {
694             message.textContent = gNavigatorBundle.getString(
695               "xpinstallPromptMessage.message.unknown"
696             );
697           }
699           let article = isSitePermissionAddon
700             ? "site-permission-addons"
701             : "unlisted-extensions-risks";
702           let learnMore = doc.getElementById("addon-install-blocked-info");
703           learnMore.textContent = gNavigatorBundle.getString(
704             "xpinstallPromptMessage.learnMore"
705           );
706           learnMore.setAttribute(
707             "href",
708             Services.urlFormatter.formatURLPref("app.support.baseURL") + article
709           );
710         };
712         let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
713         action = {
714           label: gNavigatorBundle.getString("xpinstallPromptMessage.install"),
715           accessKey: gNavigatorBundle.getString(
716             "xpinstallPromptMessage.install.accesskey"
717           ),
718           callback() {
719             secHistogram.add(
720               Ci.nsISecurityUITelemetry
721                 .WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
722             );
723             installInfo.install();
724           },
725         };
726         let dontAllowAction = {
727           label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
728           accessKey: gNavigatorBundle.getString(
729             "xpinstallPromptMessage.dontAllow.accesskey"
730           ),
731           callback: () => {
732             for (let install of installInfo.installs) {
733               if (install.state != AddonManager.STATE_CANCELLED) {
734                 install.cancel();
735               }
736             }
737             if (installInfo.cancel) {
738               installInfo.cancel();
739             }
740           },
741         };
742         let neverAllowAction = {
743           label: gNavigatorBundle.getString(
744             "xpinstallPromptMessage.neverAllow"
745           ),
746           accessKey: gNavigatorBundle.getString(
747             "xpinstallPromptMessage.neverAllow.accesskey"
748           ),
749           callback: () => {
750             SitePermissions.setForPrincipal(
751               browser.contentPrincipal,
752               "install",
753               SitePermissions.BLOCK
754             );
755             for (let install of installInfo.installs) {
756               if (install.state != AddonManager.STATE_CANCELLED) {
757                 install.cancel();
758               }
759             }
760             if (installInfo.cancel) {
761               installInfo.cancel();
762             }
763           },
764         };
766         secHistogram.add(
767           Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
768         );
769         let popup = PopupNotifications.show(
770           browser,
771           notificationID,
772           messageString,
773           gUnifiedExtensions.getPopupAnchorID(browser, window),
774           action,
775           [dontAllowAction, neverAllowAction],
776           options
777         );
778         removeNotificationOnEnd(popup, installInfo.installs);
779         break;
780       }
781       case "addon-install-started": {
782         let needsDownload = function needsDownload(aInstall) {
783           return aInstall.state != AddonManager.STATE_DOWNLOADED;
784         };
785         // If all installs have already been downloaded then there is no need to
786         // show the download progress
787         if (!installInfo.installs.some(needsDownload)) {
788           return;
789         }
790         notificationID = "addon-progress";
791         messageString = gNavigatorBundle.getString(
792           "addonDownloadingAndVerifying"
793         );
794         messageString = PluralForm.get(
795           installInfo.installs.length,
796           messageString
797         );
798         messageString = messageString.replace(
799           "#1",
800           installInfo.installs.length
801         );
802         options.installs = installInfo.installs;
803         options.contentWindow = browser.contentWindow;
804         options.sourceURI = browser.currentURI;
805         options.eventCallback = function(aEvent) {
806           switch (aEvent) {
807             case "removed":
808               options.contentWindow = null;
809               options.sourceURI = null;
810               break;
811           }
812         };
813         action = {
814           label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
815           accessKey: gNavigatorBundle.getString(
816             "addonInstall.acceptButton2.accesskey"
817           ),
818           disabled: true,
819           callback: () => {},
820         };
821         let secondaryAction = {
822           label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
823           accessKey: gNavigatorBundle.getString(
824             "addonInstall.cancelButton.accesskey"
825           ),
826           callback: () => {
827             for (let install of installInfo.installs) {
828               if (install.state != AddonManager.STATE_CANCELLED) {
829                 install.cancel();
830               }
831             }
832           },
833         };
834         let notification = PopupNotifications.show(
835           browser,
836           notificationID,
837           messageString,
838           gUnifiedExtensions.getPopupAnchorID(browser, window),
839           action,
840           [secondaryAction],
841           options
842         );
843         notification._startTime = Date.now();
845         break;
846       }
847       case "addon-install-failed": {
848         options.removeOnDismissal = true;
849         options.persistent = false;
851         // TODO This isn't terribly ideal for the multiple failure case
852         for (let install of installInfo.installs) {
853           let host;
854           try {
855             host = options.displayURI.host;
856           } catch (e) {
857             // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
858           }
860           if (!host) {
861             host =
862               install.sourceURI instanceof Ci.nsIStandardURL &&
863               install.sourceURI.host;
864           }
866           // Construct the l10n ID for the error, e.g. "addonInstallError-3"
867           let error =
868             host || install.error == 0
869               ? "addonInstallError"
870               : "addonLocalInstallError";
871           let args;
872           if (install.error < 0) {
873             // Append the error code for the installation failure to get the
874             // matching translation of the error. The error code is defined in
875             // AddonManager's _errors Map. Not all error codes listed there are
876             // translated, since errors that are only triggered during updates
877             // will never reach this code.
878             error += install.error;
879             args = [brandShortName, install.name];
880           } else if (
881             install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED
882           ) {
883             error += "Blocklisted";
884             args = [install.name];
885           } else {
886             error += "Incompatible";
887             args = [brandShortName, Services.appinfo.version, install.name];
888           }
890           if (
891             install.addon &&
892             !Services.policies.mayInstallAddon(install.addon)
893           ) {
894             error = "addonInstallBlockedByPolicy";
895             let extensionSettings = Services.policies.getExtensionSettings(
896               install.addon.id
897             );
898             let message = "";
899             if (
900               extensionSettings &&
901               "blocked_install_message" in extensionSettings
902             ) {
903               message = " " + extensionSettings.blocked_install_message;
904             }
905             args = [install.name, install.addon.id, message];
906           }
908           // Add Learn More link when refusing to install an unsigned add-on
909           if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
910             options.learnMoreURL =
911               Services.urlFormatter.formatURLPref("app.support.baseURL") +
912               "unsigned-addons";
913           }
915           messageString = gNavigatorBundle.getFormattedString(error, args);
917           PopupNotifications.show(
918             browser,
919             notificationID,
920             messageString,
921             gUnifiedExtensions.getPopupAnchorID(browser, window),
922             action,
923             null,
924             options
925           );
927           // Can't have multiple notifications with the same ID, so stop here.
928           break;
929         }
930         this._removeProgressNotification(browser);
931         break;
932       }
933       case "addon-install-confirmation": {
934         let showNotification = () => {
935           let height = undefined;
937           if (PopupNotifications.isPanelOpen) {
938             let rect = document
939               .getElementById("addon-progress-notification")
940               .getBoundingClientRect();
941             height = rect.height;
942           }
944           this._removeProgressNotification(browser);
945           this.showInstallConfirmation(browser, installInfo, height);
946         };
948         let progressNotification = PopupNotifications.getNotification(
949           "addon-progress",
950           browser
951         );
952         if (progressNotification) {
953           let downloadDuration = Date.now() - progressNotification._startTime;
954           let securityDelay =
955             Services.prefs.getIntPref("security.dialog_enable_delay") -
956             downloadDuration;
957           if (securityDelay > 0) {
958             setTimeout(() => {
959               // The download may have been cancelled during the security delay
960               if (
961                 PopupNotifications.getNotification("addon-progress", browser)
962               ) {
963                 showNotification();
964               }
965             }, securityDelay);
966             break;
967           }
968         }
969         showNotification();
970         break;
971       }
972       case "addon-install-complete": {
973         let secondaryActions = null;
974         let numAddons = installInfo.installs.length;
976         if (numAddons == 1) {
977           messageString = gNavigatorBundle.getFormattedString(
978             "addonInstalled",
979             [installInfo.installs[0].name]
980           );
981         } else {
982           messageString = gNavigatorBundle.getString("addonsGenericInstalled");
983           messageString = PluralForm.get(numAddons, messageString);
984           messageString = messageString.replace("#1", numAddons);
985         }
986         action = null;
988         options.removeOnDismissal = true;
989         options.persistent = false;
991         PopupNotifications.show(
992           browser,
993           notificationID,
994           messageString,
995           gUnifiedExtensions.getPopupAnchorID(browser, window),
996           action,
997           secondaryActions,
998           options
999         );
1000         break;
1001       }
1002     }
1003   },
1004   _removeProgressNotification(aBrowser) {
1005     let notification = PopupNotifications.getNotification(
1006       "addon-progress",
1007       aBrowser
1008     );
1009     if (notification) {
1010       notification.remove();
1011     }
1012   },
1015 var gExtensionsNotifications = {
1016   initialized: false,
1017   init() {
1018     this.updateAlerts();
1019     this.boundUpdate = this.updateAlerts.bind(this);
1020     ExtensionsUI.on("change", this.boundUpdate);
1021     this.initialized = true;
1022   },
1024   uninit() {
1025     // uninit() can race ahead of init() in some cases, if that happens,
1026     // we have no handler to remove.
1027     if (!this.initialized) {
1028       return;
1029     }
1030     ExtensionsUI.off("change", this.boundUpdate);
1031   },
1033   _createAddonButton(text, icon, callback) {
1034     let button = document.createXULElement("toolbarbutton");
1035     button.setAttribute("wrap", "true");
1036     button.setAttribute("label", text);
1037     button.setAttribute("tooltiptext", text);
1038     const DEFAULT_EXTENSION_ICON =
1039       "chrome://mozapps/skin/extensions/extensionGeneric.svg";
1040     button.setAttribute("image", icon || DEFAULT_EXTENSION_ICON);
1041     button.className = "addon-banner-item subviewbutton";
1043     button.addEventListener("command", callback);
1044     PanelUI.addonNotificationContainer.appendChild(button);
1045   },
1047   updateAlerts() {
1048     let sideloaded = ExtensionsUI.sideloaded;
1049     let updates = ExtensionsUI.updates;
1051     let container = PanelUI.addonNotificationContainer;
1053     while (container.firstChild) {
1054       container.firstChild.remove();
1055     }
1057     let items = 0;
1058     for (let update of updates) {
1059       if (++items > 4) {
1060         break;
1061       }
1062       let text = gNavigatorBundle.getFormattedString(
1063         "webextPerms.updateMenuItem",
1064         [update.addon.name]
1065       );
1066       this._createAddonButton(text, update.addon.iconURL, evt => {
1067         ExtensionsUI.showUpdate(gBrowser, update);
1068       });
1069     }
1071     let appName;
1072     for (let addon of sideloaded) {
1073       if (++items > 4) {
1074         break;
1075       }
1076       if (!appName) {
1077         let brandBundle = document.getElementById("bundle_brand");
1078         appName = brandBundle.getString("brandShortName");
1079       }
1081       let text = gNavigatorBundle.getFormattedString(
1082         "webextPerms.sideloadMenuItem",
1083         [addon.name, appName]
1084       );
1085       this._createAddonButton(text, addon.iconURL, evt => {
1086         // We need to hide the main menu manually because the toolbarbutton is
1087         // removed immediately while processing this event, and PanelUI is
1088         // unable to identify which panel should be closed automatically.
1089         PanelUI.hide();
1090         ExtensionsUI.showSideloaded(gBrowser, addon);
1091       });
1092     }
1093   },
1096 var BrowserAddonUI = {
1097   async promptRemoveExtension(addon) {
1098     let { name } = addon;
1099     let title = await document.l10n.formatValue("addon-removal-title", {
1100       name,
1101     });
1102     let { getFormattedString, getString } = gNavigatorBundle;
1103     let btnTitle = getString("webext.remove.confirmation.button");
1104     let {
1105       BUTTON_TITLE_IS_STRING: titleString,
1106       BUTTON_TITLE_CANCEL: titleCancel,
1107       BUTTON_POS_0,
1108       BUTTON_POS_1,
1109       confirmEx,
1110     } = Services.prompt;
1111     let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
1112     let checkboxState = { value: false };
1113     let checkboxMessage = null;
1115     // Enable abuse report checkbox in the remove extension dialog,
1116     // if enabled by the about:config prefs and the addon type
1117     // is currently supported.
1118     if (
1119       gAddonAbuseReportEnabled &&
1120       ["extension", "theme"].includes(addon.type)
1121     ) {
1122       checkboxMessage = await document.l10n.formatValue(
1123         "addon-removal-abuse-report-checkbox"
1124       );
1125     }
1127     let message = null;
1129     if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
1130       message = getFormattedString("webext.remove.confirmation.message", [
1131         name,
1132         document.getElementById("bundle_brand").getString("brandShorterName"),
1133       ]);
1134     }
1136     let result = confirmEx(
1137       window,
1138       title,
1139       message,
1140       btnFlags,
1141       btnTitle,
1142       /* button1 */ null,
1143       /* button2 */ null,
1144       checkboxMessage,
1145       checkboxState
1146     );
1148     return { remove: result === 0, report: checkboxState.value };
1149   },
1151   async reportAddon(addonId, reportEntryPoint) {
1152     let addon = addonId && (await AddonManager.getAddonByID(addonId));
1153     if (!addon) {
1154       return;
1155     }
1157     const win = await BrowserOpenAddonsMgr("addons://list/extension");
1159     win.openAbuseReport({ addonId, reportEntryPoint });
1160   },
1162   async removeAddon(addonId, eventObject) {
1163     let addon = addonId && (await AddonManager.getAddonByID(addonId));
1164     if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
1165       return;
1166     }
1168     let { remove, report } = await this.promptRemoveExtension(addon);
1170     AMTelemetry.recordActionEvent({
1171       object: eventObject,
1172       action: "uninstall",
1173       value: remove ? "accepted" : "cancelled",
1174       extra: { addonId },
1175     });
1177     if (remove) {
1178       // Leave the extension in pending uninstall if we are also reporting the
1179       // add-on.
1180       await addon.uninstall(report);
1182       if (report) {
1183         await this.reportAddon(addon.id, "uninstall");
1184       }
1185     }
1186   },
1188   async manageAddon(addonId, eventObject) {
1189     let addon = addonId && (await AddonManager.getAddonByID(addonId));
1190     if (!addon) {
1191       return;
1192     }
1194     BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
1195     AMTelemetry.recordActionEvent({
1196       object: eventObject,
1197       action: "manage",
1198       extra: { addonId: addon.id },
1199     });
1200   },
1204  * The `unified-extensions-item` custom element is used to manage an extension
1205  * in the list of extensions, which is displayed when users click the unified
1206  * extensions (toolbar) button.
1208  * This custom element must be initialized with `setAddon()`:
1210  * ```
1211  * let item = document.createElement("unified-extensions-item");
1212  * item.setAddon(addon);
1213  * document.body.appendChild(item);
1214  * ```
1215  */
1216 customElements.define(
1217   "unified-extensions-item",
1218   class extends HTMLElement {
1219     /**
1220      * Set the add-on for this item. The item will be populated based on the
1221      * add-on when it is rendered into the DOM.
1222      *
1223      * @param {AddonWrapper} addon The add-on to use.
1224      */
1225     setAddon(addon) {
1226       this.addon = addon;
1227     }
1229     connectedCallback() {
1230       if (this._openMenuButton) {
1231         return;
1232       }
1234       const template = document.getElementById(
1235         "unified-extensions-item-template"
1236       );
1237       this.appendChild(template.content.cloneNode(true));
1239       this._actionButton = this.querySelector(
1240         ".unified-extensions-item-action"
1241       );
1242       this._openMenuButton = this.querySelector(
1243         ".unified-extensions-item-open-menu"
1244       );
1246       this._openMenuButton.addEventListener("blur", this);
1247       this._openMenuButton.addEventListener("focus", this);
1249       this.addEventListener("command", this);
1250       this.addEventListener("mouseout", this);
1251       this.addEventListener("mouseover", this);
1253       this.render();
1254     }
1256     handleEvent(event) {
1257       const { target } = event;
1259       switch (event.type) {
1260         case "command":
1261           if (target === this._openMenuButton) {
1262             const popup = target.ownerDocument.getElementById(
1263               "unified-extensions-context-menu"
1264             );
1265             popup.openPopup(
1266               target,
1267               "after_end",
1268               0,
1269               0,
1270               true /* isContextMenu */,
1271               false /* attributesOverride */,
1272               event
1273             );
1274           } else if (target === this._actionButton) {
1275             const extension = WebExtensionPolicy.getByID(this.addon.id)
1276               ?.extension;
1277             if (!extension) {
1278               return;
1279             }
1281             const win = event.target.ownerGlobal;
1282             const tab = win.gBrowser.selectedTab;
1284             extension.tabManager.addActiveTabPermission(tab);
1285             extension.tabManager.activateScripts(tab);
1286           }
1287           break;
1289         case "blur":
1290         case "mouseout":
1291           if (target === this._openMenuButton) {
1292             this.removeAttribute("secondary-button-hovered");
1293           } else if (target === this._actionButton) {
1294             this._updateStateMessage();
1295           }
1296           break;
1298         case "focus":
1299         case "mouseover":
1300           if (target === this._openMenuButton) {
1301             this.setAttribute("secondary-button-hovered", true);
1302           } else if (target === this._actionButton) {
1303             this._updateStateMessage({ hover: true });
1304           }
1305           break;
1306       }
1307     }
1309     async _updateStateMessage({ hover = false } = {}) {
1310       const policy = WebExtensionPolicy.getByID(this.addon.id);
1312       const messages = lazy.OriginControls.getStateMessageIDs(
1313         policy,
1314         this.ownerGlobal.gBrowser.currentURI
1315       );
1316       if (!messages) {
1317         return;
1318       }
1320       const messageElement = this.querySelector(
1321         ".unified-extensions-item-message-default"
1322       );
1324       // We only want to adjust the height of an item in the panel when we
1325       // first draw it, and not on hover (even if the hover message is longer,
1326       // which shouldn't happen in practice but even if it was, we don't want
1327       // to change the height on hover).
1328       let adjustMinHeight = false;
1329       if (hover && messages.onHover) {
1330         this.ownerDocument.l10n.setAttributes(messageElement, messages.onHover);
1331       } else if (messages.default) {
1332         this.ownerDocument.l10n.setAttributes(messageElement, messages.default);
1333         adjustMinHeight = true;
1334       }
1336       await document.l10n.translateElements([messageElement]);
1338       if (adjustMinHeight) {
1339         const contentsElement = this.querySelector(
1340           ".unified-extensions-item-contents"
1341         );
1342         const { height } = getComputedStyle(contentsElement);
1343         contentsElement.style.minHeight = height;
1344       }
1345     }
1347     _hasAction() {
1348       const policy = WebExtensionPolicy.getByID(this.addon.id);
1349       const state = lazy.OriginControls.getState(
1350         policy,
1351         this.ownerGlobal.gBrowser.currentURI
1352       );
1354       return state && state.whenClicked && !state.hasAccess;
1355     }
1357     render() {
1358       if (!this.addon) {
1359         throw new Error(
1360           "unified-extensions-item requires an add-on, forgot to call setAddon()?"
1361         );
1362       }
1364       this.setAttribute("extension-id", this.addon.id);
1366       // Note that the data-extensionid attribute is used by context menu handlers
1367       // to identify the extension being manipulated by the context menu.
1368       this._actionButton.dataset.extensionid = this.addon.id;
1370       let policy = WebExtensionPolicy.getByID(this.addon.id);
1371       this.toggleAttribute(
1372         "attention",
1373         lazy.OriginControls.getAttention(policy, this.ownerGlobal)
1374       );
1376       this.querySelector(
1377         ".unified-extensions-item-name"
1378       ).textContent = this.addon.name;
1380       const iconURL = AddonManager.getPreferredIconURL(this.addon, 32, window);
1381       if (iconURL) {
1382         this.querySelector(".unified-extensions-item-icon").setAttribute(
1383           "src",
1384           iconURL
1385         );
1386       }
1388       this._actionButton.disabled = !this._hasAction();
1390       // Note that the data-extensionid attribute is used by context menu handlers
1391       // to identify the extension being manipulated by the context menu.
1392       this._openMenuButton.dataset.extensionid = this.addon.id;
1393       this._openMenuButton.setAttribute(
1394         "data-l10n-args",
1395         JSON.stringify({ extensionName: this.addon.name })
1396       );
1398       this._updateStateMessage();
1399     }
1400   }
1403 // We must declare `gUnifiedExtensions` using `var` below to avoid a
1404 // "redeclaration" syntax error.
1405 var gUnifiedExtensions = {
1406   _initialized: false,
1408   init() {
1409     if (this._initialized) {
1410       return;
1411     }
1413     if (this.isEnabled) {
1414       this._button = document.getElementById("unified-extensions-button");
1415       // TODO: Bug 1778684 - Auto-hide button when there is no active extension.
1416       this._button.hidden = false;
1418       // Lazy-load the l10n strings. Those strings are used for the CUI and
1419       // non-CUI extensions in the unified extensions panel.
1420       document
1421         .getElementById("unified-extensions-context-menu")
1422         .querySelectorAll("[data-lazy-l10n-id]")
1423         .forEach(el => {
1424           el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
1425           el.removeAttribute("data-lazy-l10n-id");
1426         });
1428       document
1429         .getElementById("nav-bar")
1430         .setAttribute("unifiedextensionsbuttonshown", true);
1432       gBrowser.addTabsProgressListener(this);
1433       window.addEventListener("TabSelect", () => this.updateAttention());
1435       this.permListener = () => this.updateAttention();
1436       lazy.ExtensionPermissions.addListener(this.permListener);
1437     }
1439     this._initialized = true;
1440   },
1442   uninit() {
1443     if (this.permListener) {
1444       lazy.ExtensionPermissions.removeListener(this.permListener);
1445       this.permListener = null;
1446     }
1447   },
1449   get isEnabled() {
1450     return Services.prefs.getBoolPref(
1451       "extensions.unifiedExtensions.enabled",
1452       false
1453     );
1454   },
1456   onLocationChange(browser, webProgress, _request, _uri, flags) {
1457     // Only update on top-level cross-document navigations in the selected tab.
1458     if (
1459       webProgress.isTopLevel &&
1460       browser === gBrowser.selectedBrowser &&
1461       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
1462     ) {
1463       this.updateAttention();
1464     }
1465   },
1467   // Update the attention indicator for the whole unified extensions button.
1468   async updateAttention() {
1469     for (let addon of await this.getActiveExtensions()) {
1470       let policy = WebExtensionPolicy.getByID(addon.id);
1471       let widget = this.browserActionFor(policy)?.widget;
1473       // Only show for extensions which are not already visible in the toolbar.
1474       if (!widget || widget.areaType !== CustomizableUI.TYPE_TOOLBAR) {
1475         if (lazy.OriginControls.getAttention(policy, window)) {
1476           this.button.toggleAttribute("attention", true);
1477           return;
1478         }
1479       }
1480     }
1481     this.button.toggleAttribute("attention", false);
1482   },
1484   getPopupAnchorID(aBrowser, aWindow) {
1485     if (this.isEnabled) {
1486       const anchorID = "unified-extensions-button";
1487       const attr = anchorID + "popupnotificationanchor";
1489       if (!aBrowser[attr]) {
1490         // A hacky way of setting the popup anchor outside the usual url bar
1491         // icon box, similar to how it was done for CFR.
1492         // See: https://searchfox.org/mozilla-central/rev/c5c002f81f08a73e04868e0c2bf0eb113f200b03/toolkit/modules/PopupNotifications.sys.mjs#40
1493         aBrowser[attr] = aWindow.document.getElementById(
1494           anchorID
1495           // Anchor on the toolbar icon to position the popup right below the
1496           // button.
1497         ).firstElementChild;
1498       }
1500       return anchorID;
1501     }
1503     return "addons-notification-icon";
1504   },
1506   get button() {
1507     return this._button;
1508   },
1510   /**
1511    * Gets a list of active AddonWrapper instances of type "extension", sorted
1512    * alphabetically based on add-on's names. Optionally, filter out extensions
1513    * with browser action.
1514    *
1515    * @param {bool} all When set to true (the default), return the list of all
1516    *                   active extensions, including the ones that have a
1517    *                   browser action. Otherwise, extensions with browser
1518    *                   action are filtered out.
1519    * @returns {Array<AddonWrapper>} An array of active extensions.
1520    */
1521   async getActiveExtensions(all = true) {
1522     // TODO: Bug 1778682 - Use a new method on `AddonManager` so that we get
1523     // the same list of extensions as the one in `about:addons`.
1525     // We only want to display active and visible extensions that do not have a
1526     // browser action, and we want to list them alphabetically.
1527     let addons = await AddonManager.getAddonsByTypes(["extension"]);
1528     addons = addons.filter(
1529       addon =>
1530         !addon.hidden &&
1531         addon.isActive &&
1532         (all ||
1533           !WebExtensionPolicy.getByID(addon.id).extension.hasBrowserActionUI)
1534     );
1535     addons.sort((a1, a2) => a1.name.localeCompare(a2.name));
1537     return addons;
1538   },
1540   /**
1541    * Returns true when there are active extensions listed/shown in the unified
1542    * extensions panel, and false otherwise (e.g. when extensions are pinned in
1543    * the toolbar OR there are 0 active extensions).
1544    *
1545    * @returns {boolean} Whether there are extensions listed in the panel.
1546    */
1547   async hasExtensionsInPanel() {
1548     const extensions = await this.getActiveExtensions();
1550     return !!extensions
1551       .map(extension => {
1552         const policy = WebExtensionPolicy.getByID(extension.id);
1553         return this.browserActionFor(policy)?.widget;
1554       })
1555       .filter(widget => {
1556         return (
1557           !widget ||
1558           widget?.areaType !== CustomizableUI.TYPE_TOOLBAR ||
1559           widget?.forWindow(window).overflowed
1560         );
1561       }).length;
1562   },
1564   handleEvent(event) {
1565     switch (event.type) {
1566       case "ViewShowing": {
1567         this.onPanelViewShowing(event.target);
1568         break;
1569       }
1570       case "ViewHiding": {
1571         this.onPanelViewHiding(event.target);
1572       }
1573     }
1574   },
1576   async onPanelViewShowing(panelview) {
1577     const list = panelview.querySelector(".unified-extensions-list");
1578     // Only add extensions that do not have a browser action in this list since
1579     // the extensions with browser action have CUI widgets and will appear in
1580     // the panel (or toolbar) via the CUI mechanism.
1581     const extensions = await this.getActiveExtensions(/* all */ false);
1583     for (const extension of extensions) {
1584       const item = document.createElement("unified-extensions-item");
1585       item.setAddon(extension);
1586       list.appendChild(item);
1587     }
1588   },
1590   onPanelViewHiding(panelview) {
1591     const list = panelview.querySelector(".unified-extensions-list");
1592     while (list.lastChild) {
1593       list.lastChild.remove();
1594     }
1595   },
1597   _panel: null,
1598   get panel() {
1599     // Lazy load the unified-extensions-panel panel the first time we need to display it.
1600     if (!this._panel) {
1601       let template = document.getElementById(
1602         "unified-extensions-panel-template"
1603       );
1604       template.replaceWith(template.content);
1605       this._panel = document.getElementById("unified-extensions-panel");
1606       let customizationArea = this._panel.querySelector(
1607         "#unified-extensions-area"
1608       );
1609       CustomizableUI.registerPanelNode(
1610         customizationArea,
1611         CustomizableUI.AREA_ADDONS
1612       );
1613       CustomizableUI.addPanelCloseListeners(this._panel);
1614     }
1615     return this._panel;
1616   },
1618   async togglePanel(aEvent) {
1619     if (!CustomizationHandler.isCustomizing()) {
1620       if (aEvent && aEvent.button !== 0) {
1621         return;
1622       }
1624       let panel = this.panel;
1625       // The button should directly open `about:addons` when the user does not
1626       // have any active extensions listed in the unified extensions panel.
1627       if (!(await this.hasExtensionsInPanel())) {
1628         await BrowserOpenAddonsMgr("addons://discover/");
1629         return;
1630       }
1632       if (!this._listView) {
1633         this._listView = PanelMultiView.getViewNode(
1634           document,
1635           "unified-extensions-view"
1636         );
1637         this._listView.addEventListener("ViewShowing", this);
1638         this._listView.addEventListener("ViewHiding", this);
1639       }
1641       if (this._button.open) {
1642         PanelMultiView.hidePopup(panel);
1643         this._button.open = false;
1644       } else {
1645         panel.hidden = false;
1646         PanelMultiView.openPopup(panel, this._button, {
1647           triggerEvent: aEvent,
1648         });
1649       }
1650     }
1652     // We always dispatch an event (useful for testing purposes).
1653     window.dispatchEvent(new CustomEvent("UnifiedExtensionsTogglePanel"));
1654   },
1656   async updateContextMenu(menu, event) {
1657     // When the context menu is open, `onpopupshowing` is called when menu
1658     // items open sub-menus. We don't want to update the context menu in this
1659     // case.
1660     if (event.target.id !== "unified-extensions-context-menu") {
1661       return;
1662     }
1664     const id = this._getExtensionId(menu);
1665     const addon = await AddonManager.getAddonByID(id);
1666     const widgetId = this._getWidgetId(menu);
1667     const forBrowserAction = !!widgetId;
1669     const pinButton = menu.querySelector(
1670       ".unified-extensions-context-menu-pin-to-toolbar"
1671     );
1672     const removeButton = menu.querySelector(
1673       ".unified-extensions-context-menu-remove-extension"
1674     );
1675     const reportButton = menu.querySelector(
1676       ".unified-extensions-context-menu-report-extension"
1677     );
1678     const menuSeparator = menu.querySelector(
1679       ".unified-extensions-context-menu-management-separator"
1680     );
1682     menuSeparator.hidden = !forBrowserAction;
1683     pinButton.hidden = !forBrowserAction;
1685     if (forBrowserAction) {
1686       let area = CustomizableUI.getPlacementOfWidget(widgetId).area;
1687       let inToolbar = area != CustomizableUI.AREA_ADDONS;
1688       pinButton.setAttribute("checked", inToolbar);
1689     }
1691     reportButton.hidden = !gAddonAbuseReportEnabled;
1692     removeButton.disabled = !(
1693       addon.permissions & AddonManager.PERM_CAN_UNINSTALL
1694     );
1696     ExtensionsUI.originControlsMenu(menu, id);
1698     const browserAction = this.browserActionFor(WebExtensionPolicy.getByID(id));
1699     if (browserAction) {
1700       browserAction.updateContextMenu(menu);
1701     }
1702   },
1704   browserActionFor(policy) {
1705     // Ideally, we wouldn't do that because `browserActionFor()` will only be
1706     // defined in `global` when at least one extension has required loading the
1707     // `ext-browserAction` code.
1708     let method = lazy.ExtensionParent.apiManager.global.browserActionFor;
1709     return method?.(policy?.extension);
1710   },
1712   async manageExtension(menu) {
1713     const id = this._getExtensionId(menu);
1715     await this.togglePanel();
1716     await BrowserAddonUI.manageAddon(id, "unifiedExtensions");
1717   },
1719   async removeExtension(menu) {
1720     const id = this._getExtensionId(menu);
1722     await this.togglePanel();
1723     await BrowserAddonUI.removeAddon(id, "unifiedExtensions");
1724   },
1726   async reportExtension(menu) {
1727     const id = this._getExtensionId(menu);
1729     await this.togglePanel();
1730     await BrowserAddonUI.reportAddon(id, "unified_context_menu");
1731   },
1733   _getExtensionId(menu) {
1734     const { triggerNode } = menu;
1735     return triggerNode.dataset.extensionid;
1736   },
1738   _getWidgetId(menu) {
1739     const { triggerNode } = menu;
1740     return triggerNode.closest(".unified-extensions-item")?.id;
1741   },
1743   onPinToToolbarChange(menu, event) {
1744     let shouldPinToToolbar = event.target.getAttribute("checked") == "true";
1745     // Revert the checkbox back to its original state. This is because the
1746     // addon context menu handlers are asynchronous, and there seems to be
1747     // a race where the checkbox state won't get set in time to show the
1748     // right state. So we err on the side of caution, and presume that future
1749     // attempts to open this context menu on an extension button will show
1750     // the same checked state that we started in.
1751     event.target.setAttribute("checked", !shouldPinToToolbar);
1753     let widgetId = this._getWidgetId(menu);
1754     if (!widgetId) {
1755       return;
1756     }
1757     this.pinToToolbar(widgetId, shouldPinToToolbar);
1758   },
1760   pinToToolbar(widgetId, shouldPinToToolbar) {
1761     let newArea = shouldPinToToolbar
1762       ? CustomizableUI.AREA_NAVBAR
1763       : CustomizableUI.AREA_ADDONS;
1764     let newPosition = shouldPinToToolbar ? undefined : 0;
1765     CustomizableUI.addWidgetToArea(widgetId, newArea, newPosition);
1766   },