Bug 1799009 - Remove unified extensions pref and non-unified extensions variants...
[gecko.git] / browser / base / content / browser-addons.js
blobda67c847172fb029d0e5b10911ebe87e1a662651
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         let displayURI = options.displayURI;
669         options.displayURI = undefined;
671         options.eventCallback = topic => {
672           if (topic !== "showing") {
673             return;
674           }
675           let doc = browser.ownerDocument;
676           let message = doc.getElementById("addon-install-blocked-message");
677           // We must remove any prior use of this panel message in this window.
678           while (message.firstChild) {
679             message.firstChild.remove();
680           }
682           if (isSitePermissionAddon) {
683             message.textContent = gNavigatorBundle.getString(
684               "sitePermissionInstallFirstPrompt.message"
685             );
686           } else if (hasHost) {
687             let text = gNavigatorBundle.getString(
688               "xpinstallPromptMessage.message"
689             );
690             let b = doc.createElementNS("http://www.w3.org/1999/xhtml", "b");
691             b.textContent = options.name;
692             let fragment = BrowserUIUtils.getLocalizedFragment(doc, text, b);
693             message.appendChild(fragment);
694           } else {
695             message.textContent = gNavigatorBundle.getString(
696               "xpinstallPromptMessage.message.unknown"
697             );
698           }
700           let article = isSitePermissionAddon
701             ? "site-permission-addons"
702             : "unlisted-extensions-risks";
703           let learnMore = doc.getElementById("addon-install-blocked-info");
704           learnMore.textContent = gNavigatorBundle.getString(
705             "xpinstallPromptMessage.learnMore"
706           );
707           learnMore.setAttribute(
708             "href",
709             Services.urlFormatter.formatURLPref("app.support.baseURL") + article
710           );
711         };
713         let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
714         action = {
715           label: gNavigatorBundle.getString("xpinstallPromptMessage.install"),
716           accessKey: gNavigatorBundle.getString(
717             "xpinstallPromptMessage.install.accesskey"
718           ),
719           callback() {
720             secHistogram.add(
721               Ci.nsISecurityUITelemetry
722                 .WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
723             );
724             installInfo.install();
725           },
726         };
727         let dontAllowAction = {
728           label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
729           accessKey: gNavigatorBundle.getString(
730             "xpinstallPromptMessage.dontAllow.accesskey"
731           ),
732           callback: () => {
733             for (let install of installInfo.installs) {
734               if (install.state != AddonManager.STATE_CANCELLED) {
735                 install.cancel();
736               }
737             }
738             if (installInfo.cancel) {
739               installInfo.cancel();
740             }
741           },
742         };
744         let neverAllowCallback = () => {
745           SitePermissions.setForPrincipal(
746             browser.contentPrincipal,
747             "install",
748             SitePermissions.BLOCK
749           );
750           for (let install of installInfo.installs) {
751             if (install.state != AddonManager.STATE_CANCELLED) {
752               install.cancel();
753             }
754           }
755           if (installInfo.cancel) {
756             installInfo.cancel();
757           }
758         };
760         let neverAllowAction = {
761           label: gNavigatorBundle.getString(
762             "xpinstallPromptMessage.neverAllow"
763           ),
764           accessKey: gNavigatorBundle.getString(
765             "xpinstallPromptMessage.neverAllow.accesskey"
766           ),
767           callback: neverAllowCallback,
768         };
770         let neverAllowAndReportAction = {
771           label: gNavigatorBundle.getString(
772             "xpinstallPromptMessage.neverAllowAndReport"
773           ),
774           accessKey: gNavigatorBundle.getString(
775             "xpinstallPromptMessage.neverAllowAndReport.accesskey"
776           ),
777           callback: () => {
778             AMTelemetry.recordEvent({
779               method: "reportSuspiciousSite",
780               object: "suspiciousSite",
781               value: displayURI?.displayHost ?? "(unknown)",
782               extra: {},
783             });
784             neverAllowCallback();
785           },
786         };
788         secHistogram.add(
789           Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
790         );
792         let declineActions = [dontAllowAction, neverAllowAction];
793         if (isSitePermissionAddon) {
794           // Restrict this to site permission add-ons for now pending a decision
795           // from product about how to approach this for extensions.
796           declineActions.push(neverAllowAndReportAction);
797         }
798         let popup = PopupNotifications.show(
799           browser,
800           notificationID,
801           messageString,
802           gUnifiedExtensions.getPopupAnchorID(browser, window),
803           action,
804           declineActions,
805           options
806         );
807         removeNotificationOnEnd(popup, installInfo.installs);
808         break;
809       }
810       case "addon-install-started": {
811         let needsDownload = function needsDownload(aInstall) {
812           return aInstall.state != AddonManager.STATE_DOWNLOADED;
813         };
814         // If all installs have already been downloaded then there is no need to
815         // show the download progress
816         if (!installInfo.installs.some(needsDownload)) {
817           return;
818         }
819         notificationID = "addon-progress";
820         messageString = gNavigatorBundle.getString(
821           "addonDownloadingAndVerifying"
822         );
823         messageString = PluralForm.get(
824           installInfo.installs.length,
825           messageString
826         );
827         messageString = messageString.replace(
828           "#1",
829           installInfo.installs.length
830         );
831         options.installs = installInfo.installs;
832         options.contentWindow = browser.contentWindow;
833         options.sourceURI = browser.currentURI;
834         options.eventCallback = function(aEvent) {
835           switch (aEvent) {
836             case "removed":
837               options.contentWindow = null;
838               options.sourceURI = null;
839               break;
840           }
841         };
842         action = {
843           label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
844           accessKey: gNavigatorBundle.getString(
845             "addonInstall.acceptButton2.accesskey"
846           ),
847           disabled: true,
848           callback: () => {},
849         };
850         let secondaryAction = {
851           label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
852           accessKey: gNavigatorBundle.getString(
853             "addonInstall.cancelButton.accesskey"
854           ),
855           callback: () => {
856             for (let install of installInfo.installs) {
857               if (install.state != AddonManager.STATE_CANCELLED) {
858                 install.cancel();
859               }
860             }
861           },
862         };
863         let notification = PopupNotifications.show(
864           browser,
865           notificationID,
866           messageString,
867           gUnifiedExtensions.getPopupAnchorID(browser, window),
868           action,
869           [secondaryAction],
870           options
871         );
872         notification._startTime = Date.now();
874         break;
875       }
876       case "addon-install-failed": {
877         options.removeOnDismissal = true;
878         options.persistent = false;
880         // TODO This isn't terribly ideal for the multiple failure case
881         for (let install of installInfo.installs) {
882           let host;
883           try {
884             host = options.displayURI.host;
885           } catch (e) {
886             // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
887           }
889           if (!host) {
890             host =
891               install.sourceURI instanceof Ci.nsIStandardURL &&
892               install.sourceURI.host;
893           }
895           // Construct the l10n ID for the error, e.g. "addonInstallError-3"
896           let error =
897             host || install.error == 0
898               ? "addonInstallError"
899               : "addonLocalInstallError";
900           let args;
901           if (install.error < 0) {
902             // Append the error code for the installation failure to get the
903             // matching translation of the error. The error code is defined in
904             // AddonManager's _errors Map. Not all error codes listed there are
905             // translated, since errors that are only triggered during updates
906             // will never reach this code.
907             error += install.error;
908             args = [brandShortName, install.name];
909           } else if (
910             install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED
911           ) {
912             error += "Blocklisted";
913             args = [install.name];
914           } else {
915             error += "Incompatible";
916             args = [brandShortName, Services.appinfo.version, install.name];
917           }
919           if (
920             install.addon &&
921             !Services.policies.mayInstallAddon(install.addon)
922           ) {
923             error = "addonInstallBlockedByPolicy";
924             let extensionSettings = Services.policies.getExtensionSettings(
925               install.addon.id
926             );
927             let message = "";
928             if (
929               extensionSettings &&
930               "blocked_install_message" in extensionSettings
931             ) {
932               message = " " + extensionSettings.blocked_install_message;
933             }
934             args = [install.name, install.addon.id, message];
935           }
937           // Add Learn More link when refusing to install an unsigned add-on
938           if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
939             options.learnMoreURL =
940               Services.urlFormatter.formatURLPref("app.support.baseURL") +
941               "unsigned-addons";
942           }
944           messageString = gNavigatorBundle.getFormattedString(error, args);
946           PopupNotifications.show(
947             browser,
948             notificationID,
949             messageString,
950             gUnifiedExtensions.getPopupAnchorID(browser, window),
951             action,
952             null,
953             options
954           );
956           // Can't have multiple notifications with the same ID, so stop here.
957           break;
958         }
959         this._removeProgressNotification(browser);
960         break;
961       }
962       case "addon-install-confirmation": {
963         let showNotification = () => {
964           let height = undefined;
966           if (PopupNotifications.isPanelOpen) {
967             let rect = document
968               .getElementById("addon-progress-notification")
969               .getBoundingClientRect();
970             height = rect.height;
971           }
973           this._removeProgressNotification(browser);
974           this.showInstallConfirmation(browser, installInfo, height);
975         };
977         let progressNotification = PopupNotifications.getNotification(
978           "addon-progress",
979           browser
980         );
981         if (progressNotification) {
982           let downloadDuration = Date.now() - progressNotification._startTime;
983           let securityDelay =
984             Services.prefs.getIntPref("security.dialog_enable_delay") -
985             downloadDuration;
986           if (securityDelay > 0) {
987             setTimeout(() => {
988               // The download may have been cancelled during the security delay
989               if (
990                 PopupNotifications.getNotification("addon-progress", browser)
991               ) {
992                 showNotification();
993               }
994             }, securityDelay);
995             break;
996           }
997         }
998         showNotification();
999         break;
1000       }
1001       case "addon-install-complete": {
1002         let secondaryActions = null;
1003         let numAddons = installInfo.installs.length;
1005         if (numAddons == 1) {
1006           messageString = gNavigatorBundle.getFormattedString(
1007             "addonInstalled",
1008             [installInfo.installs[0].name]
1009           );
1010         } else {
1011           messageString = gNavigatorBundle.getString("addonsGenericInstalled");
1012           messageString = PluralForm.get(numAddons, messageString);
1013           messageString = messageString.replace("#1", numAddons);
1014         }
1015         action = null;
1017         options.removeOnDismissal = true;
1018         options.persistent = false;
1020         PopupNotifications.show(
1021           browser,
1022           notificationID,
1023           messageString,
1024           gUnifiedExtensions.getPopupAnchorID(browser, window),
1025           action,
1026           secondaryActions,
1027           options
1028         );
1029         break;
1030       }
1031     }
1032   },
1033   _removeProgressNotification(aBrowser) {
1034     let notification = PopupNotifications.getNotification(
1035       "addon-progress",
1036       aBrowser
1037     );
1038     if (notification) {
1039       notification.remove();
1040     }
1041   },
1044 var gExtensionsNotifications = {
1045   initialized: false,
1046   init() {
1047     this.updateAlerts();
1048     this.boundUpdate = this.updateAlerts.bind(this);
1049     ExtensionsUI.on("change", this.boundUpdate);
1050     this.initialized = true;
1051   },
1053   uninit() {
1054     // uninit() can race ahead of init() in some cases, if that happens,
1055     // we have no handler to remove.
1056     if (!this.initialized) {
1057       return;
1058     }
1059     ExtensionsUI.off("change", this.boundUpdate);
1060   },
1062   _createAddonButton(text, icon, callback) {
1063     let button = document.createXULElement("toolbarbutton");
1064     button.setAttribute("wrap", "true");
1065     button.setAttribute("label", text);
1066     button.setAttribute("tooltiptext", text);
1067     const DEFAULT_EXTENSION_ICON =
1068       "chrome://mozapps/skin/extensions/extensionGeneric.svg";
1069     button.setAttribute("image", icon || DEFAULT_EXTENSION_ICON);
1070     button.className = "addon-banner-item subviewbutton";
1072     button.addEventListener("command", callback);
1073     PanelUI.addonNotificationContainer.appendChild(button);
1074   },
1076   updateAlerts() {
1077     let sideloaded = ExtensionsUI.sideloaded;
1078     let updates = ExtensionsUI.updates;
1080     let container = PanelUI.addonNotificationContainer;
1082     while (container.firstChild) {
1083       container.firstChild.remove();
1084     }
1086     let items = 0;
1087     for (let update of updates) {
1088       if (++items > 4) {
1089         break;
1090       }
1091       let text = gNavigatorBundle.getFormattedString(
1092         "webextPerms.updateMenuItem",
1093         [update.addon.name]
1094       );
1095       this._createAddonButton(text, update.addon.iconURL, evt => {
1096         ExtensionsUI.showUpdate(gBrowser, update);
1097       });
1098     }
1100     let appName;
1101     for (let addon of sideloaded) {
1102       if (++items > 4) {
1103         break;
1104       }
1105       if (!appName) {
1106         let brandBundle = document.getElementById("bundle_brand");
1107         appName = brandBundle.getString("brandShortName");
1108       }
1110       let text = gNavigatorBundle.getFormattedString(
1111         "webextPerms.sideloadMenuItem",
1112         [addon.name, appName]
1113       );
1114       this._createAddonButton(text, addon.iconURL, evt => {
1115         // We need to hide the main menu manually because the toolbarbutton is
1116         // removed immediately while processing this event, and PanelUI is
1117         // unable to identify which panel should be closed automatically.
1118         PanelUI.hide();
1119         ExtensionsUI.showSideloaded(gBrowser, addon);
1120       });
1121     }
1122   },
1125 var BrowserAddonUI = {
1126   async promptRemoveExtension(addon) {
1127     let { name } = addon;
1128     let title = await document.l10n.formatValue("addon-removal-title", {
1129       name,
1130     });
1131     let { getFormattedString, getString } = gNavigatorBundle;
1132     let btnTitle = getString("webext.remove.confirmation.button");
1133     let {
1134       BUTTON_TITLE_IS_STRING: titleString,
1135       BUTTON_TITLE_CANCEL: titleCancel,
1136       BUTTON_POS_0,
1137       BUTTON_POS_1,
1138       confirmEx,
1139     } = Services.prompt;
1140     let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
1141     let checkboxState = { value: false };
1142     let checkboxMessage = null;
1144     // Enable abuse report checkbox in the remove extension dialog,
1145     // if enabled by the about:config prefs and the addon type
1146     // is currently supported.
1147     if (
1148       gAddonAbuseReportEnabled &&
1149       ["extension", "theme"].includes(addon.type)
1150     ) {
1151       checkboxMessage = await document.l10n.formatValue(
1152         "addon-removal-abuse-report-checkbox"
1153       );
1154     }
1156     let message = null;
1158     if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
1159       message = getFormattedString("webext.remove.confirmation.message", [
1160         name,
1161         document.getElementById("bundle_brand").getString("brandShorterName"),
1162       ]);
1163     }
1165     let result = confirmEx(
1166       window,
1167       title,
1168       message,
1169       btnFlags,
1170       btnTitle,
1171       /* button1 */ null,
1172       /* button2 */ null,
1173       checkboxMessage,
1174       checkboxState
1175     );
1177     return { remove: result === 0, report: checkboxState.value };
1178   },
1180   async reportAddon(addonId, reportEntryPoint) {
1181     let addon = addonId && (await AddonManager.getAddonByID(addonId));
1182     if (!addon) {
1183       return;
1184     }
1186     const win = await BrowserOpenAddonsMgr("addons://list/extension");
1188     win.openAbuseReport({ addonId, reportEntryPoint });
1189   },
1191   async removeAddon(addonId, eventObject) {
1192     let addon = addonId && (await AddonManager.getAddonByID(addonId));
1193     if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
1194       return;
1195     }
1197     let { remove, report } = await this.promptRemoveExtension(addon);
1199     AMTelemetry.recordActionEvent({
1200       object: eventObject,
1201       action: "uninstall",
1202       value: remove ? "accepted" : "cancelled",
1203       extra: { addonId },
1204     });
1206     if (remove) {
1207       // Leave the extension in pending uninstall if we are also reporting the
1208       // add-on.
1209       await addon.uninstall(report);
1211       if (report) {
1212         await this.reportAddon(addon.id, "uninstall");
1213       }
1214     }
1215   },
1217   async manageAddon(addonId, eventObject) {
1218     let addon = addonId && (await AddonManager.getAddonByID(addonId));
1219     if (!addon) {
1220       return;
1221     }
1223     BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
1224     AMTelemetry.recordActionEvent({
1225       object: eventObject,
1226       action: "manage",
1227       extra: { addonId: addon.id },
1228     });
1229   },
1232 // We must declare `gUnifiedExtensions` using `var` below to avoid a
1233 // "redeclaration" syntax error.
1234 var gUnifiedExtensions = {
1235   _initialized: false,
1237   // We use a `<deck>` in the extension items to show/hide messages below each
1238   // extension name. We have a default message for origin controls, and
1239   // optionally a second message shown on hover, which describes the action
1240   // (when clicking on the action button). We have another message shown when
1241   // the menu button is hovered/focused. The constants below define the indexes
1242   // of each message in the `<deck>`.
1243   MESSAGE_DECK_INDEX_DEFAULT: 0,
1244   MESSAGE_DECK_INDEX_HOVER: 1,
1245   MESSAGE_DECK_INDEX_MENU_HOVER: 2,
1247   init() {
1248     if (this._initialized) {
1249       return;
1250     }
1252     if (this.isEnabled) {
1253       this._button = document.getElementById("unified-extensions-button");
1254       // TODO: Bug 1778684 - Auto-hide button when there is no active extension.
1255       this._button.hidden = false;
1257       document
1258         .getElementById("nav-bar")
1259         .setAttribute("unifiedextensionsbuttonshown", true);
1261       gBrowser.addTabsProgressListener(this);
1262       window.addEventListener("TabSelect", () => this.updateAttention());
1264       this.permListener = () => this.updateAttention();
1265       lazy.ExtensionPermissions.addListener(this.permListener);
1267       gNavToolbox.addEventListener("customizationstarting", this);
1268     }
1270     this._initialized = true;
1271   },
1273   uninit() {
1274     if (this.permListener) {
1275       lazy.ExtensionPermissions.removeListener(this.permListener);
1276       this.permListener = null;
1277     }
1278     gNavToolbox.removeEventListener("customizationstarting", this);
1279   },
1281   get isEnabled() {
1282     return true;
1283   },
1285   onLocationChange(browser, webProgress, _request, _uri, flags) {
1286     // Only update on top-level cross-document navigations in the selected tab.
1287     if (
1288       webProgress.isTopLevel &&
1289       browser === gBrowser.selectedBrowser &&
1290       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
1291     ) {
1292       this.updateAttention();
1293     }
1294   },
1296   // Update the attention indicator for the whole unified extensions button.
1297   updateAttention() {
1298     let attention = false;
1299     for (let policy of this.getActivePolicies()) {
1300       let widget = this.browserActionFor(policy)?.widget;
1302       // Only show for extensions which are not already visible in the toolbar.
1303       if (!widget || widget.areaType !== CustomizableUI.TYPE_TOOLBAR) {
1304         if (lazy.OriginControls.getAttention(policy, window)) {
1305           attention = true;
1306           break;
1307         }
1308       }
1309     }
1310     this.button.toggleAttribute("attention", attention);
1311     this.button.ownerDocument.l10n.setAttributes(
1312       this.button,
1313       attention
1314         ? "unified-extensions-button-permissions-needed"
1315         : "unified-extensions-button"
1316     );
1317   },
1319   getPopupAnchorID(aBrowser, aWindow) {
1320     if (this.isEnabled) {
1321       const anchorID = "unified-extensions-button";
1322       const attr = anchorID + "popupnotificationanchor";
1324       if (!aBrowser[attr]) {
1325         // A hacky way of setting the popup anchor outside the usual url bar
1326         // icon box, similar to how it was done for CFR.
1327         // See: https://searchfox.org/mozilla-central/rev/c5c002f81f08a73e04868e0c2bf0eb113f200b03/toolkit/modules/PopupNotifications.sys.mjs#40
1328         aBrowser[attr] = aWindow.document.getElementById(
1329           anchorID
1330           // Anchor on the toolbar icon to position the popup right below the
1331           // button.
1332         ).firstElementChild;
1333       }
1335       return anchorID;
1336     }
1338     return "addons-notification-icon";
1339   },
1341   get button() {
1342     return this._button;
1343   },
1345   /**
1346    * Gets a list of active WebExtensionPolicy instances of type "extension",
1347    * sorted alphabetically based on add-on's names. Optionally, filter out
1348    * extensions with browser action.
1349    *
1350    * @param {bool} all When set to true (the default), return the list of all
1351    *                   active policies, including the ones that have a
1352    *                   browser action. Otherwise, extensions with browser
1353    *                   action are filtered out.
1354    * @returns {Array<WebExtensionPolicy>} An array of active policies.
1355    */
1356   getActivePolicies(all = true) {
1357     let policies = WebExtensionPolicy.getActiveExtensions();
1358     policies = policies.filter(policy => {
1359       let { extension } = policy;
1360       if (!policy.active || extension?.type !== "extension") {
1361         return false;
1362       }
1364       // Ignore hidden and extensions that cannot access the current window
1365       // (because of PB mode when we are in a private window), since users
1366       // cannot do anything with those extensions anyway.
1367       if (extension.isHidden || !policy.canAccessWindow(window)) {
1368         return false;
1369       }
1371       return all || !extension.hasBrowserActionUI;
1372     });
1374     policies.sort((a, b) => a.name.localeCompare(b.name));
1375     return policies;
1376   },
1378   /**
1379    * Returns true when there are active extensions listed/shown in the unified
1380    * extensions panel, and false otherwise (e.g. when extensions are pinned in
1381    * the toolbar OR there are 0 active extensions).
1382    *
1383    * @returns {boolean} Whether there are extensions listed in the panel.
1384    */
1385   hasExtensionsInPanel() {
1386     const policies = this.getActivePolicies();
1388     return !!policies
1389       .map(policy => this.browserActionFor(policy)?.widget)
1390       .filter(widget => {
1391         return (
1392           !widget ||
1393           widget?.areaType !== CustomizableUI.TYPE_TOOLBAR ||
1394           widget?.forWindow(window).overflowed
1395         );
1396       }).length;
1397   },
1399   handleEvent(event) {
1400     switch (event.type) {
1401       case "ViewShowing":
1402         this.onPanelViewShowing(event.target);
1403         break;
1405       case "ViewHiding":
1406         this.onPanelViewHiding(event.target);
1407         break;
1409       case "customizationstarting":
1410         this.panel.hidePopup();
1411         break;
1412     }
1413   },
1415   onPanelViewShowing(panelview) {
1416     const list = panelview.querySelector(".unified-extensions-list");
1417     // Only add extensions that do not have a browser action in this list since
1418     // the extensions with browser action have CUI widgets and will appear in
1419     // the panel (or toolbar) via the CUI mechanism.
1420     const policies = this.getActivePolicies(/* all */ false);
1422     for (const policy of policies) {
1423       const item = document.createElement("unified-extensions-item");
1424       item.setExtension(policy.extension);
1425       list.appendChild(item);
1426     }
1427   },
1429   onPanelViewHiding(panelview) {
1430     const list = panelview.querySelector(".unified-extensions-list");
1431     while (list.lastChild) {
1432       list.lastChild.remove();
1433     }
1434   },
1436   _panel: null,
1437   get panel() {
1438     // Lazy load the unified-extensions-panel panel the first time we need to
1439     // display it.
1440     if (!this._panel) {
1441       let template = document.getElementById(
1442         "unified-extensions-panel-template"
1443       );
1444       template.replaceWith(template.content);
1445       this._panel = document.getElementById("unified-extensions-panel");
1446       let customizationArea = this._panel.querySelector(
1447         "#unified-extensions-area"
1448       );
1449       CustomizableUI.registerPanelNode(
1450         customizationArea,
1451         CustomizableUI.AREA_ADDONS
1452       );
1453       CustomizableUI.addPanelCloseListeners(this._panel);
1455       // Lazy-load the l10n strings. Those strings are used for the CUI and
1456       // non-CUI extensions in the unified extensions panel.
1457       document
1458         .getElementById("unified-extensions-context-menu")
1459         .querySelectorAll("[data-lazy-l10n-id]")
1460         .forEach(el => {
1461           el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
1462           el.removeAttribute("data-lazy-l10n-id");
1463         });
1464     }
1465     return this._panel;
1466   },
1468   async togglePanel(aEvent) {
1469     if (!CustomizationHandler.isCustomizing()) {
1470       if (aEvent) {
1471         if (
1472           // On MacOS, ctrl-click will send a context menu event from the
1473           // widget, so we don't want to bring up the panel when ctrl key is
1474           // pressed.
1475           (aEvent.type == "mousedown" &&
1476             (aEvent.button !== 0 ||
1477               (AppConstants.platform === "macosx" && aEvent.ctrlKey))) ||
1478           (aEvent.type === "keypress" &&
1479             aEvent.charCode !== KeyEvent.DOM_VK_SPACE &&
1480             aEvent.keyCode !== KeyEvent.DOM_VK_RETURN)
1481         ) {
1482           return;
1483         }
1485         // The button should directly open `about:addons` when the user does not
1486         // have any active extensions listed in the unified extensions panel.
1487         if (!this.hasExtensionsInPanel()) {
1488           await BrowserOpenAddonsMgr("addons://discover/");
1489           return;
1490         }
1491       }
1493       let panel = this.panel;
1495       if (!this._listView) {
1496         this._listView = PanelMultiView.getViewNode(
1497           document,
1498           "unified-extensions-view"
1499         );
1500         this._listView.addEventListener("ViewShowing", this);
1501         this._listView.addEventListener("ViewHiding", this);
1502       }
1504       if (this._button.open) {
1505         PanelMultiView.hidePopup(panel);
1506         this._button.open = false;
1507       } else {
1508         panel.hidden = false;
1509         PanelMultiView.openPopup(panel, this._button, {
1510           triggerEvent: aEvent,
1511         });
1512       }
1513     }
1515     // We always dispatch an event (useful for testing purposes).
1516     window.dispatchEvent(new CustomEvent("UnifiedExtensionsTogglePanel"));
1517   },
1519   async updateContextMenu(menu, event) {
1520     // When the context menu is open, `onpopupshowing` is called when menu
1521     // items open sub-menus. We don't want to update the context menu in this
1522     // case.
1523     if (event.target.id !== "unified-extensions-context-menu") {
1524       return;
1525     }
1527     const id = this._getExtensionId(menu);
1528     const addon = await AddonManager.getAddonByID(id);
1529     const widgetId = this._getWidgetId(menu);
1530     const forBrowserAction = !!widgetId;
1532     const pinButton = menu.querySelector(
1533       ".unified-extensions-context-menu-pin-to-toolbar"
1534     );
1535     const removeButton = menu.querySelector(
1536       ".unified-extensions-context-menu-remove-extension"
1537     );
1538     const reportButton = menu.querySelector(
1539       ".unified-extensions-context-menu-report-extension"
1540     );
1541     const menuSeparator = menu.querySelector(
1542       ".unified-extensions-context-menu-management-separator"
1543     );
1545     menuSeparator.hidden = !forBrowserAction;
1546     pinButton.hidden = !forBrowserAction;
1548     if (forBrowserAction) {
1549       let area = CustomizableUI.getPlacementOfWidget(widgetId).area;
1550       let inToolbar = area != CustomizableUI.AREA_ADDONS;
1551       pinButton.setAttribute("checked", inToolbar);
1552     }
1554     reportButton.hidden = !gAddonAbuseReportEnabled;
1555     removeButton.disabled = !(
1556       addon.permissions & AddonManager.PERM_CAN_UNINSTALL
1557     );
1559     ExtensionsUI.originControlsMenu(menu, id);
1561     const browserAction = this.browserActionFor(WebExtensionPolicy.getByID(id));
1562     if (browserAction) {
1563       browserAction.updateContextMenu(menu);
1564     }
1565   },
1567   // This is registered on the top-level unified extensions context menu.
1568   onContextMenuCommand(menu, event) {
1569     this.togglePanel();
1570   },
1572   browserActionFor(policy) {
1573     // Ideally, we wouldn't do that because `browserActionFor()` will only be
1574     // defined in `global` when at least one extension has required loading the
1575     // `ext-browserAction` code.
1576     let method = lazy.ExtensionParent.apiManager.global.browserActionFor;
1577     return method?.(policy?.extension);
1578   },
1580   async manageExtension(menu) {
1581     const id = this._getExtensionId(menu);
1583     await BrowserAddonUI.manageAddon(id, "unifiedExtensions");
1584   },
1586   async removeExtension(menu) {
1587     const id = this._getExtensionId(menu);
1589     await BrowserAddonUI.removeAddon(id, "unifiedExtensions");
1590   },
1592   async reportExtension(menu) {
1593     const id = this._getExtensionId(menu);
1595     await BrowserAddonUI.reportAddon(id, "unified_context_menu");
1596   },
1598   _getExtensionId(menu) {
1599     const { triggerNode } = menu;
1600     return triggerNode.dataset.extensionid;
1601   },
1603   _getWidgetId(menu) {
1604     const { triggerNode } = menu;
1605     return triggerNode.closest(".unified-extensions-item")?.id;
1606   },
1608   async onPinToToolbarChange(menu, event) {
1609     let shouldPinToToolbar = event.target.getAttribute("checked") == "true";
1610     // Revert the checkbox back to its original state. This is because the
1611     // addon context menu handlers are asynchronous, and there seems to be
1612     // a race where the checkbox state won't get set in time to show the
1613     // right state. So we err on the side of caution, and presume that future
1614     // attempts to open this context menu on an extension button will show
1615     // the same checked state that we started in.
1616     event.target.setAttribute("checked", !shouldPinToToolbar);
1618     let widgetId = this._getWidgetId(menu);
1619     if (!widgetId) {
1620       return;
1621     }
1623     this.pinToToolbar(widgetId, shouldPinToToolbar);
1624   },
1626   pinToToolbar(widgetId, shouldPinToToolbar) {
1627     let newArea = shouldPinToToolbar
1628       ? CustomizableUI.AREA_NAVBAR
1629       : CustomizableUI.AREA_ADDONS;
1630     let newPosition = shouldPinToToolbar ? undefined : 0;
1632     CustomizableUI.addWidgetToArea(widgetId, newArea, newPosition);
1634     this.updateAttention();
1635   },