Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / modules / ExtensionsUI.sys.mjs
blobd79fcdd597fdf16152a1089107b4cfe42b9158b3
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 const lazy = {};
10 ChromeUtils.defineESModuleGetters(lazy, {
11   AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
12   AMTelemetry: "resource://gre/modules/AddonManager.sys.mjs",
13   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
14   AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
15   AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
16   ExtensionData: "resource://gre/modules/Extension.sys.mjs",
17   ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
18   OriginControls: "resource://gre/modules/ExtensionPermissions.sys.mjs",
19   QuarantinedDomains: "resource://gre/modules/ExtensionPermissions.sys.mjs",
20 });
22 ChromeUtils.defineLazyGetter(
23   lazy,
24   "l10n",
25   () =>
26     new Localization(["browser/extensionsUI.ftl", "branding/brand.ftl"], true)
29 ChromeUtils.defineLazyGetter(lazy, "logConsole", () =>
30   console.createInstance({
31     prefix: "ExtensionsUI",
32     maxLogLevelPref: "extensions.webextensions.log.level",
33   })
36 XPCOMUtils.defineLazyPreferenceGetter(
37   lazy,
38   "POSTINSTALL_PRIVATEBROWSING_CHECKBOX",
39   "extensions.ui.postInstallPrivateBrowsingCheckbox",
40   false
43 XPCOMUtils.defineLazyPreferenceGetter(
44   lazy,
45   "SHOW_FULL_DOMAINS_LIST",
46   "extensions.ui.installDialogFullDomains",
47   true
50 const DEFAULT_EXTENSION_ICON =
51   "chrome://mozapps/skin/extensions/extensionGeneric.svg";
53 function getTabBrowser(browser) {
54   while (browser.ownerGlobal.docShell.itemType !== Ci.nsIDocShell.typeChrome) {
55     browser = browser.ownerGlobal.docShell.chromeEventHandler;
56   }
57   let window = browser.ownerGlobal;
58   let viewType = browser.getAttribute("webextension-view-type");
59   if (viewType == "sidebar") {
60     window = window.browsingContext.topChromeWindow;
61   }
62   if (viewType == "popup" || viewType == "sidebar") {
63     browser = window.gBrowser.selectedBrowser;
64   }
65   return { browser, window };
68 export var ExtensionsUI = {
69   sideloaded: new Set(),
70   updates: new Set(),
71   sideloadListener: null,
73   pendingNotifications: new WeakMap(),
75   get SHOW_FULL_DOMAINS_LIST() {
76     return lazy.SHOW_FULL_DOMAINS_LIST;
77   },
79   get POSTINSTALL_PRIVATEBROWSING_CHECKBOX() {
80     return lazy.POSTINSTALL_PRIVATEBROWSING_CHECKBOX;
81   },
83   async init() {
84     Services.obs.addObserver(this, "webextension-permission-prompt");
85     Services.obs.addObserver(this, "webextension-update-permissions");
86     Services.obs.addObserver(this, "webextension-install-notify");
87     Services.obs.addObserver(this, "webextension-optional-permission-prompt");
88     Services.obs.addObserver(this, "webextension-defaultsearch-prompt");
89     Services.obs.addObserver(this, "webextension-imported-addons-cancelled");
90     Services.obs.addObserver(this, "webextension-imported-addons-complete");
91     Services.obs.addObserver(this, "webextension-imported-addons-pending");
93     await Services.wm.getMostRecentWindow("navigator:browser")
94       .delayedStartupPromise;
96     this._checkForSideloaded();
97   },
99   async _checkForSideloaded() {
100     let sideloaded = await lazy.AddonManagerPrivate.getNewSideloads();
102     if (!sideloaded.length) {
103       // No new side-loads. We're done.
104       return;
105     }
107     // The ordering shouldn't matter, but tests depend on notifications
108     // happening in a specific order.
109     sideloaded.sort((a, b) => a.id.localeCompare(b.id));
111     if (!this.sideloadListener) {
112       this.sideloadListener = {
113         onEnabled: addon => {
114           if (!this.sideloaded.has(addon)) {
115             return;
116           }
118           this.sideloaded.delete(addon);
119           this._updateNotifications();
121           if (this.sideloaded.size == 0) {
122             lazy.AddonManager.removeAddonListener(this.sideloadListener);
123             this.sideloadListener = null;
124           }
125         },
126       };
127       lazy.AddonManager.addAddonListener(this.sideloadListener);
128     }
130     for (let addon of sideloaded) {
131       this.sideloaded.add(addon);
132     }
133     this._updateNotifications();
134   },
136   _updateNotifications() {
137     const { sideloaded, updates } = this;
138     const { importedAddonIDs } = lazy.AMBrowserExtensionsImport;
140     if (importedAddonIDs.length + sideloaded.size + updates.size == 0) {
141       lazy.AppMenuNotifications.removeNotification("addon-alert");
142     } else {
143       lazy.AppMenuNotifications.showBadgeOnlyNotification("addon-alert");
144     }
145     this.emit("change");
146   },
148   showAddonsManager(
149     tabbrowser,
150     strings,
151     icon,
152     addon = undefined,
153     shouldShowIncognitoCheckbox = false
154   ) {
155     let global = tabbrowser.selectedBrowser.ownerGlobal;
156     return global.BrowserAddonUI.openAddonsMgr("addons://list/extension").then(
157       aomWin => {
158         let aomBrowser = aomWin.docShell.chromeEventHandler;
159         return this.showPermissionsPrompt(
160           aomBrowser,
161           strings,
162           icon,
163           addon,
164           shouldShowIncognitoCheckbox
165         );
166       }
167     );
168   },
170   showSideloaded(tabbrowser, addon) {
171     addon.markAsSeen();
172     this.sideloaded.delete(addon);
173     this._updateNotifications();
175     let strings = this._buildStrings({
176       addon,
177       permissions: addon.installPermissions,
178       type: "sideload",
179     });
181     lazy.AMTelemetry.recordManageEvent(addon, "sideload_prompt", {
182       num_strings: strings.msgs.length,
183     });
185     this.showAddonsManager(
186       tabbrowser,
187       strings,
188       addon.iconURL,
189       addon,
190       true /* shouldShowIncognitoCheckbox */
191     ).then(async answer => {
192       if (answer) {
193         await addon.enable();
195         this._updateNotifications();
197         // The user has just enabled a sideloaded extension, if the permission
198         // can be changed for the extension, show the post-install panel to
199         // give the user that opportunity.
200         if (
201           ExtensionsUI.POSTINSTALL_PRIVATEBROWSING_CHECKBOX &&
202           addon.permissions &
203             lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
204         ) {
205           this.showInstallNotification(tabbrowser.selectedBrowser, addon);
206         }
207       }
208       this.emit("sideload-response");
209     });
210   },
212   showUpdate(browser, info) {
213     lazy.AMTelemetry.recordInstallEvent(info.install, {
214       step: "permissions_prompt",
215       num_strings: info.strings.msgs.length,
216     });
218     this.showAddonsManager(browser, info.strings, info.addon.iconURL).then(
219       answer => {
220         if (answer) {
221           info.resolve();
222         } else {
223           info.reject();
224         }
225         // At the moment, this prompt will re-appear next time we do an update
226         // check.  See bug 1332360 for proposal to avoid this.
227         this.updates.delete(info);
228         this._updateNotifications();
229       }
230     );
231   },
233   observe(subject, topic) {
234     if (topic == "webextension-permission-prompt") {
235       let { target, info } = subject.wrappedJSObject;
237       let { browser, window } = getTabBrowser(target);
239       // Dismiss the progress notification.  Note that this is bad if
240       // there are multiple simultaneous installs happening, see
241       // bug 1329884 for a longer explanation.
242       let progressNotification = window.PopupNotifications.getNotification(
243         "addon-progress",
244         browser
245       );
246       if (progressNotification) {
247         progressNotification.remove();
248       }
250       info.unsigned =
251         info.addon.signedState <= lazy.AddonManager.SIGNEDSTATE_MISSING;
252       if (
253         info.unsigned &&
254         Cu.isInAutomation &&
255         Services.prefs.getBoolPref(
256           "extensions.ui.showAddonIconForUnsigned",
257           false
258         )
259       ) {
260         info.unsigned = false;
261       }
263       let strings = this._buildStrings(info);
265       // If this is an update with no promptable permissions, just apply it
266       if (info.type == "update" && !strings.msgs.length) {
267         info.resolve();
268         return;
269       }
271       let icon = info.unsigned
272         ? "chrome://global/skin/icons/warning.svg"
273         : info.icon;
275       if (info.type == "sideload") {
276         lazy.AMTelemetry.recordManageEvent(info.addon, "sideload_prompt", {
277           num_strings: strings.msgs.length,
278         });
279       } else {
280         lazy.AMTelemetry.recordInstallEvent(info.install, {
281           step: "permissions_prompt",
282           num_strings: strings.msgs.length,
283         });
284       }
286       this.showPermissionsPrompt(
287         browser,
288         strings,
289         icon,
290         info.addon,
291         true /* shouldShowIncognitoCheckbox */
292       ).then(answer => {
293         if (answer) {
294           info.resolve();
295         } else {
296           info.reject();
297         }
298       });
299     } else if (topic == "webextension-update-permissions") {
300       let info = subject.wrappedJSObject;
301       info.type = "update";
302       let strings = this._buildStrings(info);
304       // If we don't prompt for any new permissions, just apply it
305       if (!strings.msgs.length) {
306         info.resolve();
307         return;
308       }
310       let update = {
311         strings,
312         permissions: info.permissions,
313         install: info.install,
314         addon: info.addon,
315         resolve: info.resolve,
316         reject: info.reject,
317       };
319       this.updates.add(update);
320       this._updateNotifications();
321     } else if (topic == "webextension-install-notify") {
322       let { target, addon, callback } = subject.wrappedJSObject;
323       this.showInstallNotification(target, addon).then(() => {
324         if (callback) {
325           callback();
326         }
327       });
328     } else if (topic == "webextension-optional-permission-prompt") {
329       let { browser, name, icon, permissions, resolve } =
330         subject.wrappedJSObject;
331       let strings = this._buildStrings({
332         type: "optional",
333         addon: { name },
334         permissions,
335       });
337       // If we don't have any promptable permissions, just proceed
338       if (!strings.msgs.length) {
339         resolve(true);
340         return;
341       }
342       // "userScripts" is an OptionalOnlyPermission, which means that it can
343       // only be requested through the permissions.request() API, without other
344       // permissions in the same request.
345       let isUserScriptsRequest =
346         permissions.permissions.length === 1 &&
347         permissions.permissions[0] === "userScripts";
348       resolve(
349         this.showPermissionsPrompt(
350           browser,
351           strings,
352           icon,
353           /* addon */ undefined,
354           /* shouldShowIncognitoCheckbox */ false,
355           isUserScriptsRequest
356         )
357       );
358     } else if (topic == "webextension-defaultsearch-prompt") {
359       let { browser, name, icon, respond, currentEngine, newEngine } =
360         subject.wrappedJSObject;
362       const [searchDesc, searchYes, searchNo] = lazy.l10n.formatMessagesSync([
363         {
364           id: "webext-default-search-description",
365           args: { addonName: "<>", currentEngine, newEngine },
366         },
367         "webext-default-search-yes",
368         "webext-default-search-no",
369       ]);
371       const strings = { addonName: name, text: searchDesc.value };
372       for (let attr of searchYes.attributes) {
373         if (attr.name === "label") {
374           strings.acceptText = attr.value;
375         } else if (attr.name === "accesskey") {
376           strings.acceptKey = attr.value;
377         }
378       }
379       for (let attr of searchNo.attributes) {
380         if (attr.name === "label") {
381           strings.cancelText = attr.value;
382         } else if (attr.name === "accesskey") {
383           strings.cancelKey = attr.value;
384         }
385       }
387       this.showDefaultSearchPrompt(browser, strings, icon).then(respond);
388     } else if (
389       [
390         "webextension-imported-addons-cancelled",
391         "webextension-imported-addons-complete",
392         "webextension-imported-addons-pending",
393       ].includes(topic)
394     ) {
395       this._updateNotifications();
396     }
397   },
399   // Create a set of formatted strings for a permission prompt
400   _buildStrings(info) {
401     const strings = lazy.ExtensionData.formatPermissionStrings(
402       info,
403       this.SHOW_FULL_DOMAINS_LIST
404         ? { fullDomainsList: true }
405         : { collapseOrigins: true }
406     );
407     strings.addonName = info.addon.name;
408     return strings;
409   },
411   async showPermissionsPrompt(
412     target,
413     strings,
414     icon,
415     addon = undefined,
416     shouldShowIncognitoCheckbox = false,
417     isUserScriptsRequest = false
418   ) {
419     let { browser, window } = getTabBrowser(target);
421     let showIncognitoCheckbox =
422       shouldShowIncognitoCheckbox && !lazy.POSTINSTALL_PRIVATEBROWSING_CHECKBOX;
424     if (showIncognitoCheckbox) {
425       showIncognitoCheckbox = !!(
426         addon.permissions &
427         lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
428       );
429     }
431     const incognitoPermissionName = "internal:privateBrowsingAllowed";
432     let grantPrivateBrowsingAllowed = false;
433     if (showIncognitoCheckbox) {
434       const { permissions } = await lazy.ExtensionPermissions.get(addon.id);
435       grantPrivateBrowsingAllowed = permissions.includes(
436         incognitoPermissionName
437       );
438     }
440     // Wait for any pending prompts to complete before showing the next one.
441     let pending;
442     while ((pending = this.pendingNotifications.get(browser))) {
443       await pending;
444     }
446     let promise = new Promise(resolve => {
447       function eventCallback(topic) {
448         if (topic == "swapping") {
449           return true;
450         }
451         if (topic == "removed") {
452           Services.tm.dispatchToMainThread(() => {
453             resolve(false);
454           });
455         }
456         return false;
457       }
459       // Show the SUMO link already part of the popupnotification by
460       // setting learnMoreURL option if there are permissions to be
461       // granted to the addon being installed (or if the private
462       // browsing checkbox is shown).
463       const learnMoreURL =
464         strings.msgs.length || showIncognitoCheckbox
465           ? Services.urlFormatter.formatURLPref("app.support.baseURL") +
466             "extension-permissions"
467           : undefined;
469       let options = {
470         hideClose: true,
471         popupIconURL: icon || DEFAULT_EXTENSION_ICON,
472         popupIconClass: icon ? "" : "addon-warning-icon",
473         learnMoreURL,
474         persistent: true,
475         eventCallback,
476         removeOnDismissal: true,
477         popupOptions: {
478           position: "bottomright topright",
479         },
480         // Pass additional options used internally by the
481         // addon-webext-permissions-notification custom element
482         // (defined and registered by browser-addons.js).
483         customElementOptions: {
484           strings,
485           showIncognitoCheckbox,
486           grantPrivateBrowsingAllowed,
487           onPrivateBrowsingAllowedChanged(value) {
488             grantPrivateBrowsingAllowed = value;
489           },
490           isUserScriptsRequest,
491         },
492       };
493       // The prompt/notification machinery has a special affordance wherein
494       // certain subsets of the header string can be designated "names", and
495       // referenced symbolically as "<>" and "{}" to receive special formatting.
496       // That code assumes that the existence of |name| and |secondName| in the
497       // options object imply the presence of "<>" and "{}" (respectively) in
498       // in the string.
499       //
500       // At present, WebExtensions use this affordance while SitePermission
501       // add-ons don't, so we need to conditionally set the |name| field.
502       //
503       // NB: This could potentially be cleaned up, see bug 1799710.
504       if (strings.header.includes("<>")) {
505         options.name = strings.addonName;
506       }
508       let action = {
509         label: strings.acceptText,
510         accessKey: strings.acceptKey,
511         callback: () => {
512           resolve(true);
513         },
514       };
515       let secondaryActions = [
516         {
517           label: strings.cancelText,
518           accessKey: strings.cancelKey,
519           callback: () => {
520             resolve(false);
521           },
522         },
523       ];
525       window.PopupNotifications.show(
526         browser,
527         "addon-webext-permissions",
528         strings.header,
529         browser.ownerGlobal.gUnifiedExtensions.getPopupAnchorID(
530           browser,
531           window
532         ),
533         action,
534         secondaryActions,
535         options
536       );
537     });
539     this.pendingNotifications.set(browser, promise);
540     promise.finally(() => this.pendingNotifications.delete(browser));
541     // NOTE: this method is also called from showQuarantineConfirmation and some of its
542     // related test cases (from browser_ext_originControls.js) seem to be hitting a race
543     // if the promise returned requires an additional tick to be resolved.
544     // Look more into the failure and determine a better option to avoid those failures.
545     if (!showIncognitoCheckbox) {
546       return promise;
547     }
548     return promise.then(continueInstall => {
549       if (!continueInstall) {
550         return continueInstall;
551       }
552       const incognitoPermission = {
553         permissions: [incognitoPermissionName],
554         origins: [],
555       };
556       let permUpdatePromise;
557       if (grantPrivateBrowsingAllowed) {
558         permUpdatePromise = lazy.ExtensionPermissions.add(
559           addon.id,
560           incognitoPermission
561         ).catch(err =>
562           lazy.logConsole.warn(
563             `Error on adding "${incognitoPermissionName}" permission to addon id "${addon.id}`,
564             err
565           )
566         );
567       } else {
568         permUpdatePromise = lazy.ExtensionPermissions.remove(
569           addon.id,
570           incognitoPermission
571         ).catch(err =>
572           lazy.logConsole.warn(
573             `Error on removing "${incognitoPermissionName}" permission to addon id "${addon.id}`,
574             err
575           )
576         );
577       }
578       return permUpdatePromise.then(() => continueInstall);
579     });
580   },
582   showDefaultSearchPrompt(target, strings, icon) {
583     return new Promise(resolve => {
584       let options = {
585         hideClose: true,
586         popupIconURL: icon || DEFAULT_EXTENSION_ICON,
587         persistent: true,
588         removeOnDismissal: true,
589         eventCallback(topic) {
590           if (topic == "removed") {
591             resolve(false);
592           }
593         },
594         name: strings.addonName,
595       };
597       let action = {
598         label: strings.acceptText,
599         accessKey: strings.acceptKey,
600         callback: () => {
601           resolve(true);
602         },
603       };
604       let secondaryActions = [
605         {
606           label: strings.cancelText,
607           accessKey: strings.cancelKey,
608           callback: () => {
609             resolve(false);
610           },
611         },
612       ];
614       let { browser, window } = getTabBrowser(target);
616       window.PopupNotifications.show(
617         browser,
618         "addon-webext-defaultsearch",
619         strings.text,
620         "addons-notification-icon",
621         action,
622         secondaryActions,
623         options
624       );
625     });
626   },
628   async showInstallNotification(target, addon) {
629     let { window } = getTabBrowser(target);
631     const message = await lazy.l10n.formatValue("addon-post-install-message", {
632       addonName: "<>",
633     });
635     const hideIncognitoCheckbox = !lazy.POSTINSTALL_PRIVATEBROWSING_CHECKBOX;
636     const permissionName = "internal:privateBrowsingAllowed";
637     const { permissions } = await lazy.ExtensionPermissions.get(addon.id);
638     const hasIncognito = permissions.includes(permissionName);
640     return new Promise(resolve => {
641       let icon = addon.isWebExtension
642         ? lazy.AddonManager.getPreferredIconURL(addon, 32, window) ||
643           DEFAULT_EXTENSION_ICON
644         : "chrome://browser/skin/addons/addon-install-installed.svg";
646       if (addon.type == "theme") {
647         const { previousActiveThemeID } = addon;
649         async function themeActionUndo() {
650           try {
651             // Undoing a theme install means re-enabling the previous active theme
652             // ID, and uninstalling the theme that was just installed
653             const theme = await lazy.AddonManager.getAddonByID(
654               previousActiveThemeID
655             );
657             if (theme) {
658               await theme.enable();
659             }
661             // `addon` is the theme that was just installed
662             await addon.uninstall();
663           } finally {
664             resolve();
665           }
666         }
668         let themePrimaryAction = { callback: resolve };
670         // Show the undo button if previousActiveThemeID is set.
671         let themeSecondaryAction = previousActiveThemeID
672           ? { callback: themeActionUndo }
673           : null;
675         let options = {
676           name: addon.name,
677           message,
678           popupIconURL: icon,
679           onDismissed: () => {
680             lazy.AppMenuNotifications.removeNotification("theme-installed");
681             resolve();
682           },
683         };
684         lazy.AppMenuNotifications.showNotification(
685           "theme-installed",
686           themePrimaryAction,
687           themeSecondaryAction,
688           options
689         );
690       } else {
691         // Show or hide private permission ui based on the pref.
692         function setCheckbox(win) {
693           let checkbox = win.document.getElementById(
694             "addon-incognito-checkbox"
695           );
696           checkbox.checked = hasIncognito;
697           checkbox.hidden =
698             hideIncognitoCheckbox ||
699             !(
700               addon.permissions &
701               lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
702             );
703         }
705         async function actionResolve(win) {
706           let checkbox = win.document.getElementById(
707             "addon-incognito-checkbox"
708           );
710           if (hideIncognitoCheckbox || checkbox.checked == hasIncognito) {
711             resolve();
712             return;
713           }
715           let incognitoPermission = {
716             permissions: [permissionName],
717             origins: [],
718           };
720           // The checkbox has been changed at this point, otherwise we would
721           // have exited early above.
722           if (checkbox.checked) {
723             await lazy.ExtensionPermissions.add(addon.id, incognitoPermission);
724           } else if (hasIncognito) {
725             await lazy.ExtensionPermissions.remove(
726               addon.id,
727               incognitoPermission
728             );
729           }
730           // Reload the extension if it is already enabled.  This ensures any change
731           // on the private browsing permission is properly handled.
732           if (addon.isActive) {
733             await addon.reload();
734           }
736           resolve();
737         }
739         let action = {
740           callback: actionResolve,
741         };
743         let options = {
744           name: addon.name,
745           message,
746           popupIconURL: icon,
747           onRefresh: setCheckbox,
748           onDismissed: win => {
749             lazy.AppMenuNotifications.removeNotification("addon-installed");
750             actionResolve(win);
751           },
752         };
753         lazy.AppMenuNotifications.showNotification(
754           "addon-installed",
755           action,
756           null,
757           options
758         );
759       }
760     });
761   },
763   async showQuarantineConfirmation(browser, policy) {
764     let [title, line1, line2, allow, deny] = await lazy.l10n.formatMessages([
765       {
766         id: "webext-quarantine-confirmation-title",
767         args: { addonName: "<>" },
768       },
769       "webext-quarantine-confirmation-line-1",
770       "webext-quarantine-confirmation-line-2",
771       "webext-quarantine-confirmation-allow",
772       "webext-quarantine-confirmation-deny",
773     ]);
775     let attr = (msg, name) => msg.attributes.find(a => a.name === name)?.value;
777     let strings = {
778       addonName: policy.name,
779       header: title.value,
780       text: line1.value + "\n\n" + line2.value,
781       msgs: [],
782       acceptText: attr(allow, "label"),
783       acceptKey: attr(allow, "accesskey"),
784       cancelText: attr(deny, "label"),
785       cancelKey: attr(deny, "accesskey"),
786     };
788     let icon = policy.extension?.getPreferredIcon(32);
790     if (await ExtensionsUI.showPermissionsPrompt(browser, strings, icon)) {
791       lazy.QuarantinedDomains.setUserAllowedAddonIdPref(policy.id, true);
792     }
793   },
795   // Populate extension toolbar popup menu with origin controls.
796   originControlsMenu(popup, extensionId) {
797     let policy = WebExtensionPolicy.getByID(extensionId);
799     let win = popup.ownerGlobal;
800     let doc = popup.ownerDocument;
801     let tab = win.gBrowser.selectedTab;
802     let uri = tab.linkedBrowser?.currentURI;
803     let state = lazy.OriginControls.getState(policy, tab);
805     let headerItem = doc.createXULElement("menuitem");
806     headerItem.setAttribute("disabled", true);
807     let items = [headerItem];
809     // MV2 normally don't have controls, but we show the quarantined state.
810     if (!policy?.extension.originControls && !state.quarantined) {
811       return;
812     }
814     if (state.noAccess) {
815       doc.l10n.setAttributes(headerItem, "origin-controls-no-access");
816     } else {
817       doc.l10n.setAttributes(headerItem, "origin-controls-options");
818     }
820     if (state.quarantined) {
821       doc.l10n.setAttributes(headerItem, "origin-controls-quarantined-status");
823       let allowQuarantined = doc.createXULElement("menuitem");
824       doc.l10n.setAttributes(
825         allowQuarantined,
826         "origin-controls-quarantined-allow"
827       );
828       allowQuarantined.addEventListener("command", () => {
829         this.showQuarantineConfirmation(tab.linkedBrowser, policy);
830       });
831       items.push(allowQuarantined);
832     }
834     if (state.allDomains) {
835       let allDomains = doc.createXULElement("menuitem");
836       allDomains.setAttribute("type", "radio");
837       allDomains.setAttribute("checked", state.hasAccess);
838       doc.l10n.setAttributes(allDomains, "origin-controls-option-all-domains");
839       items.push(allDomains);
840     }
842     if (state.whenClicked) {
843       let whenClicked = doc.createXULElement("menuitem");
844       whenClicked.setAttribute("type", "radio");
845       whenClicked.setAttribute("checked", !state.hasAccess);
846       doc.l10n.setAttributes(
847         whenClicked,
848         "origin-controls-option-when-clicked"
849       );
850       whenClicked.addEventListener("command", async () => {
851         await lazy.OriginControls.setWhenClicked(policy, uri);
852         win.gUnifiedExtensions.updateAttention();
853       });
854       items.push(whenClicked);
855     }
857     if (state.alwaysOn) {
858       let alwaysOn = doc.createXULElement("menuitem");
859       alwaysOn.setAttribute("type", "radio");
860       alwaysOn.setAttribute("checked", state.hasAccess);
861       doc.l10n.setAttributes(alwaysOn, "origin-controls-option-always-on", {
862         domain: uri.host,
863       });
864       alwaysOn.addEventListener("command", async () => {
865         await lazy.OriginControls.setAlwaysOn(policy, uri);
866         win.gUnifiedExtensions.updateAttention();
867       });
868       items.push(alwaysOn);
869     }
871     items.push(doc.createXULElement("menuseparator"));
873     // Insert all items before Pin to toolbar OR Manage Extension, but after
874     // any extension's menu items.
875     let manageItem =
876       popup.querySelector(".customize-context-manageExtension") ||
877       popup.querySelector(".unified-extensions-context-menu-pin-to-toolbar");
878     items.forEach(item => item && popup.insertBefore(item, manageItem));
880     let cleanup = e => {
881       if (e.target === popup) {
882         items.forEach(item => item?.remove());
883         popup.removeEventListener("popuphidden", cleanup);
884       }
885     };
886     popup.addEventListener("popuphidden", cleanup);
887   },
890 EventEmitter.decorate(ExtensionsUI);