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 */
11 ChromeUtils.defineModuleGetter(
14 "resource://gre/modules/ExtensionParent.jsm"
16 ChromeUtils.defineModuleGetter(
19 "resource://gre/modules/ExtensionPermissions.jsm"
21 ChromeUtils.defineModuleGetter(
23 "ExtensionPermissions",
24 "resource://gre/modules/ExtensionPermissions.jsm"
26 ChromeUtils.defineESModuleGetters(lazy, {
28 "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
31 customElements.define(
32 "addon-progress-notification",
33 class MozAddonProgressNotification extends customElements.get(
38 this.progressmeter = document.getElementById(
39 "addon-progress-notification-progressmeter"
42 this.progresstext = document.getElementById(
43 "addon-progress-notification-progresstext"
46 if (!this.notification) {
50 this.notification.options.installs.forEach(function(aInstall) {
51 aInstall.addListener(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
59 this.setProgress(0, -1);
60 this._updateProgressTimeout = setTimeout(
61 this.updateProgress.bind(this),
66 disconnectedCallback() {
71 if (!this.notification) {
74 this.notification.options.installs.forEach(function(aInstall) {
75 aInstall.removeListener(this);
78 clearTimeout(this._updateProgressTimeout);
81 setProgress(aProgress, aMaxProgress) {
82 if (aMaxProgress == -1) {
83 this.progressmeter.removeAttribute("value");
85 this.progressmeter.setAttribute(
87 (aProgress * 100) / aMaxProgress
93 if (!this.notification.lastUpdate) {
94 this.notification.lastUpdate = now;
95 this.notification.lastProgress = aProgress;
99 let delta = now - this.notification.lastUpdate;
100 if (delta < 400 && aProgress < aMaxProgress) {
104 // Set min. time delta to avoid division by zero in the upcoming speed calculation
105 delta = Math.max(delta, 400);
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;
114 this.notification.lastUpdate = now;
115 this.notification.lastProgress = aProgress;
116 this.notification.speed = speed;
119 [status, this.notification.last] = DownloadUtils.getDownloadStatus(
123 this.notification.last
125 this.progresstext.setAttribute("value", status);
126 this.progresstext.setAttribute("tooltiptext", status);
130 let installs = this.notification.options.installs;
131 installs.forEach(function(aInstall) {
135 // Cancel will throw if the download has already failed
139 PopupNotifications.remove(this.notification);
143 if (!this.notification) {
147 let downloadingCount = 0;
151 this.notification.options.installs.forEach(function(aInstall) {
152 if (aInstall.maxProgress == -1) {
155 progress += aInstall.progress;
156 if (maxProgress >= 0) {
157 maxProgress += aInstall.maxProgress;
159 if (aInstall.state < AddonManager.STATE_DOWNLOADED) {
164 if (downloadingCount == 0) {
166 this.progressmeter.removeAttribute("value");
167 let status = gNavigatorBundle.getString("addonDownloadVerifying");
168 this.progresstext.setAttribute("value", status);
169 this.progresstext.setAttribute("tooltiptext", status);
171 this.setProgress(progress, maxProgress);
175 onDownloadProgress() {
176 this.updateProgress();
180 this.updateProgress();
183 onDownloadCancelled() {
184 this.updateProgress();
188 this.updateProgress();
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);
202 // Check that the notification is still showing
203 let current = PopupNotifications.getNotification(
207 if (current === notification) {
208 notification.remove();
213 for (let install of installs) {
214 install.addListener({
215 onDownloadCancelled: maybeRemove,
216 onDownloadFailed: maybeRemove,
217 onInstallFailed: maybeRemove,
218 onInstallEnded: maybeRemove,
223 var gXPInstallObserver = {
224 _findChildShell(aDocShell, aSoughtShell) {
225 if (aDocShell == aSoughtShell) {
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) {
240 _getBrowser(aDocShell) {
241 for (let browser of gBrowser.browsers) {
242 if (this._findChildShell(browser.docShell, aDocShell)) {
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
255 PopupNotifications.getNotification("addon-install-confirmation", browser)
257 let pending = this.pendingInstalls.get(browser);
259 pending.push(installInfo);
261 this.pendingInstalls.set(browser, [installInfo]);
266 let showNextConfirmation = () => {
267 // Make sure the browser is still alive.
268 if (!gBrowser.browsers.includes(browser)) {
272 let pending = this.pendingInstalls.get(browser);
273 if (pending && pending.length) {
274 this.showInstallConfirmation(browser, pending.shift());
278 // If all installs have already been cancelled in some way then just show
279 // the next confirmation
281 installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)
283 showNextConfirmation();
287 // Make notifications persistent
289 displayURI: installInfo.originatingURI,
294 if (gUnifiedExtensions.isEnabled) {
295 options.popupOptions = {
296 position: "bottomright topright",
300 let acceptInstallation = () => {
301 for (let install of installInfo.installs) {
307 .getHistogramById("SECURITY_UI")
309 Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH
313 let cancelInstallation = () => {
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
319 if (install.state != AddonManager.STATE_CANCELLED) {
325 showNextConfirmation();
328 let unsigned = installInfo.installs.filter(
329 i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
332 !!unsigned.length && unsigned.length < installInfo.installs.length;
334 options.eventCallback = aEvent => {
337 cancelInstallation();
340 let addonList = document.getElementById(
341 "addon-install-confirmation-content"
343 while (addonList.firstChild) {
344 addonList.firstChild.remove();
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);
357 install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
359 let unsignedLabel = document.createXULElement("label");
360 unsignedLabel.setAttribute(
362 gNavigatorBundle.getString("addonInstall.unsigned")
364 unsignedLabel.setAttribute(
366 "addon-install-confirmation-unsigned"
368 container.appendChild(unsignedLabel);
371 addonList.appendChild(container);
377 options.learnMoreURL = Services.urlFormatter.formatURLPref(
378 "app.support.baseURL"
382 let notification = document.getElementById(
383 "addon-install-confirmation-notification"
385 if (unsigned.length == installInfo.installs.length) {
386 // None of the add-ons are verified
387 messageString = gNavigatorBundle.getString(
388 "addonConfirmInstallUnsigned.message"
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";
398 // Some of the add-ons are unverified, the list of names will indicate
400 messageString = gNavigatorBundle.getString(
401 "addonConfirmInstallSomeUnsigned.message"
403 notification.setAttribute("warning", "true");
404 options.learnMoreURL += "unsigned-addons";
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);
415 label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
416 accessKey: gNavigatorBundle.getString(
417 "addonInstall.acceptButton2.accesskey"
419 callback: acceptInstallation,
422 let secondaryAction = {
423 label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
424 accessKey: gNavigatorBundle.getString(
425 "addonInstall.cancelButton.accesskey"
431 notification.style.minHeight = height + "px";
434 let tab = gBrowser.getTabForBrowser(browser);
436 gBrowser.selectedTab = tab;
439 let popup = PopupNotifications.show(
441 "addon-install-confirmation",
443 gUnifiedExtensions.getPopupAnchorID(browser, window),
449 removeNotificationOnEnd(popup, installInfo.installs);
452 .getHistogramById("SECURITY_UI")
453 .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
456 // IDs of addon install related notifications
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",
466 "addon-webext-permissions",
467 "xpinstall-disabled",
471 * Remove all opened addon installation notifications
473 * @param {*} browser - Browser to remove notifications for
474 * @returns {boolean} - true if notifications have been removed.
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;
486 logWarningFullScreenInstallBlocked() {
487 // If notifications have been removed, log a warning to the website console
488 let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
491 let message = gBrowserBundle.GetStringFromName(
492 "addonInstallFullScreenBlocked"
494 consoleMsg.initWithWindowID(
496 gBrowser.currentURI.spec,
500 Ci.nsIScriptError.warningFlag,
502 gBrowser.selectedBrowser.innerWindowID
504 Services.console.logMessage(consoleMsg);
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)) {
517 var messageString, action;
518 var brandShortName = brandBundle.getString("brandShortName");
520 var notificationID = aTopic;
521 // Make notifications persistent
523 displayURI: installInfo.originatingURI,
526 timeout: Date.now() + 30000,
529 if (gUnifiedExtensions.isEnabled) {
530 options.popupOptions = {
531 position: "bottomright topright",
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"
545 messageString = gNavigatorBundle.getString(
546 "xpinstallDisabledMessage"
550 label: gNavigatorBundle.getString("xpinstallDisabledButton"),
551 accessKey: gNavigatorBundle.getString(
552 "xpinstallDisabledButton.accesskey"
554 callback: function editPrefs() {
555 Services.prefs.setBoolPref("xpinstall.enabled", true);
561 label: gNavigatorBundle.getString(
562 "addonInstall.cancelButton.label"
564 accessKey: gNavigatorBundle.getString(
565 "addonInstall.cancelButton.accesskey"
572 PopupNotifications.show(
576 gUnifiedExtensions.getPopupAnchorID(browser, window),
583 case "addon-install-fullscreen-blocked": {
584 // AddonManager denied installation because we are in DOM fullscreen
585 this.logWarningFullScreenInstallBlocked();
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"
596 messageString = gNavigatorBundle.getFormattedString(
597 "xpinstallPromptMessage",
602 if (Services.policies) {
603 let extensionSettings = Services.policies.getExtensionSettings("*");
606 "blocked_install_message" in extensionSettings
608 messageString += " " + extensionSettings.blocked_install_message;
612 options.removeOnDismissal = true;
613 options.persistent = false;
615 let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
617 Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
619 let popup = PopupNotifications.show(
623 gUnifiedExtensions.getPopupAnchorID(browser, window),
628 removeNotificationOnEnd(popup, installInfo.installs);
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(
639 if (progressNotification) {
640 progressNotification.remove();
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
649 let hasHost = !!options.displayURI;
651 if (isSitePermissionAddon) {
652 messageString = gNavigatorBundle.getString(
653 "sitePermissionInstallFirstPrompt.header"
655 } else if (hasHost) {
656 messageString = gNavigatorBundle.getFormattedString(
657 "xpinstallPromptMessage.header",
660 options.name = options.displayURI.displayHost;
662 messageString = gNavigatorBundle.getString(
663 "xpinstallPromptMessage.header.unknown"
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") {
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();
681 if (isSitePermissionAddon) {
682 message.textContent = gNavigatorBundle.getString(
683 "sitePermissionInstallFirstPrompt.message"
685 } else if (hasHost) {
686 let text = gNavigatorBundle.getString(
687 "xpinstallPromptMessage.message"
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);
694 message.textContent = gNavigatorBundle.getString(
695 "xpinstallPromptMessage.message.unknown"
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"
706 learnMore.setAttribute(
708 Services.urlFormatter.formatURLPref("app.support.baseURL") + article
712 let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
714 label: gNavigatorBundle.getString("xpinstallPromptMessage.install"),
715 accessKey: gNavigatorBundle.getString(
716 "xpinstallPromptMessage.install.accesskey"
720 Ci.nsISecurityUITelemetry
721 .WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
723 installInfo.install();
726 let dontAllowAction = {
727 label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
728 accessKey: gNavigatorBundle.getString(
729 "xpinstallPromptMessage.dontAllow.accesskey"
732 for (let install of installInfo.installs) {
733 if (install.state != AddonManager.STATE_CANCELLED) {
737 if (installInfo.cancel) {
738 installInfo.cancel();
742 let neverAllowAction = {
743 label: gNavigatorBundle.getString(
744 "xpinstallPromptMessage.neverAllow"
746 accessKey: gNavigatorBundle.getString(
747 "xpinstallPromptMessage.neverAllow.accesskey"
750 SitePermissions.setForPrincipal(
751 browser.contentPrincipal,
753 SitePermissions.BLOCK
755 for (let install of installInfo.installs) {
756 if (install.state != AddonManager.STATE_CANCELLED) {
760 if (installInfo.cancel) {
761 installInfo.cancel();
767 Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
769 let popup = PopupNotifications.show(
773 gUnifiedExtensions.getPopupAnchorID(browser, window),
775 [dontAllowAction, neverAllowAction],
778 removeNotificationOnEnd(popup, installInfo.installs);
781 case "addon-install-started": {
782 let needsDownload = function needsDownload(aInstall) {
783 return aInstall.state != AddonManager.STATE_DOWNLOADED;
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)) {
790 notificationID = "addon-progress";
791 messageString = gNavigatorBundle.getString(
792 "addonDownloadingAndVerifying"
794 messageString = PluralForm.get(
795 installInfo.installs.length,
798 messageString = messageString.replace(
800 installInfo.installs.length
802 options.installs = installInfo.installs;
803 options.contentWindow = browser.contentWindow;
804 options.sourceURI = browser.currentURI;
805 options.eventCallback = function(aEvent) {
808 options.contentWindow = null;
809 options.sourceURI = null;
814 label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
815 accessKey: gNavigatorBundle.getString(
816 "addonInstall.acceptButton2.accesskey"
821 let secondaryAction = {
822 label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
823 accessKey: gNavigatorBundle.getString(
824 "addonInstall.cancelButton.accesskey"
827 for (let install of installInfo.installs) {
828 if (install.state != AddonManager.STATE_CANCELLED) {
834 let notification = PopupNotifications.show(
838 gUnifiedExtensions.getPopupAnchorID(browser, window),
843 notification._startTime = Date.now();
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) {
855 host = options.displayURI.host;
857 // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
862 install.sourceURI instanceof Ci.nsIStandardURL &&
863 install.sourceURI.host;
866 // Construct the l10n ID for the error, e.g. "addonInstallError-3"
868 host || install.error == 0
869 ? "addonInstallError"
870 : "addonLocalInstallError";
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];
881 install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED
883 error += "Blocklisted";
884 args = [install.name];
886 error += "Incompatible";
887 args = [brandShortName, Services.appinfo.version, install.name];
892 !Services.policies.mayInstallAddon(install.addon)
894 error = "addonInstallBlockedByPolicy";
895 let extensionSettings = Services.policies.getExtensionSettings(
901 "blocked_install_message" in extensionSettings
903 message = " " + extensionSettings.blocked_install_message;
905 args = [install.name, install.addon.id, message];
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") +
915 messageString = gNavigatorBundle.getFormattedString(error, args);
917 PopupNotifications.show(
921 gUnifiedExtensions.getPopupAnchorID(browser, window),
927 // Can't have multiple notifications with the same ID, so stop here.
930 this._removeProgressNotification(browser);
933 case "addon-install-confirmation": {
934 let showNotification = () => {
935 let height = undefined;
937 if (PopupNotifications.isPanelOpen) {
939 .getElementById("addon-progress-notification")
940 .getBoundingClientRect();
941 height = rect.height;
944 this._removeProgressNotification(browser);
945 this.showInstallConfirmation(browser, installInfo, height);
948 let progressNotification = PopupNotifications.getNotification(
952 if (progressNotification) {
953 let downloadDuration = Date.now() - progressNotification._startTime;
955 Services.prefs.getIntPref("security.dialog_enable_delay") -
957 if (securityDelay > 0) {
959 // The download may have been cancelled during the security delay
961 PopupNotifications.getNotification("addon-progress", browser)
972 case "addon-install-complete": {
973 let secondaryActions = null;
974 let numAddons = installInfo.installs.length;
976 if (numAddons == 1) {
977 messageString = gNavigatorBundle.getFormattedString(
979 [installInfo.installs[0].name]
982 messageString = gNavigatorBundle.getString("addonsGenericInstalled");
983 messageString = PluralForm.get(numAddons, messageString);
984 messageString = messageString.replace("#1", numAddons);
988 options.removeOnDismissal = true;
989 options.persistent = false;
991 PopupNotifications.show(
995 gUnifiedExtensions.getPopupAnchorID(browser, window),
1004 _removeProgressNotification(aBrowser) {
1005 let notification = PopupNotifications.getNotification(
1010 notification.remove();
1015 var gExtensionsNotifications = {
1018 this.updateAlerts();
1019 this.boundUpdate = this.updateAlerts.bind(this);
1020 ExtensionsUI.on("change", this.boundUpdate);
1021 this.initialized = true;
1025 // uninit() can race ahead of init() in some cases, if that happens,
1026 // we have no handler to remove.
1027 if (!this.initialized) {
1030 ExtensionsUI.off("change", this.boundUpdate);
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);
1048 let sideloaded = ExtensionsUI.sideloaded;
1049 let updates = ExtensionsUI.updates;
1051 let container = PanelUI.addonNotificationContainer;
1053 while (container.firstChild) {
1054 container.firstChild.remove();
1058 for (let update of updates) {
1062 let text = gNavigatorBundle.getFormattedString(
1063 "webextPerms.updateMenuItem",
1066 this._createAddonButton(text, update.addon.iconURL, evt => {
1067 ExtensionsUI.showUpdate(gBrowser, update);
1072 for (let addon of sideloaded) {
1077 let brandBundle = document.getElementById("bundle_brand");
1078 appName = brandBundle.getString("brandShortName");
1081 let text = gNavigatorBundle.getFormattedString(
1082 "webextPerms.sideloadMenuItem",
1083 [addon.name, appName]
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.
1090 ExtensionsUI.showSideloaded(gBrowser, addon);
1096 var BrowserAddonUI = {
1097 async promptRemoveExtension(addon) {
1098 let { name } = addon;
1099 let title = await document.l10n.formatValue("addon-removal-title", {
1102 let { getFormattedString, getString } = gNavigatorBundle;
1103 let btnTitle = getString("webext.remove.confirmation.button");
1105 BUTTON_TITLE_IS_STRING: titleString,
1106 BUTTON_TITLE_CANCEL: titleCancel,
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.
1119 gAddonAbuseReportEnabled &&
1120 ["extension", "theme"].includes(addon.type)
1122 checkboxMessage = await document.l10n.formatValue(
1123 "addon-removal-abuse-report-checkbox"
1129 if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
1130 message = getFormattedString("webext.remove.confirmation.message", [
1132 document.getElementById("bundle_brand").getString("brandShorterName"),
1136 let result = confirmEx(
1148 return { remove: result === 0, report: checkboxState.value };
1151 async reportAddon(addonId, reportEntryPoint) {
1152 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1157 const win = await BrowserOpenAddonsMgr("addons://list/extension");
1159 win.openAbuseReport({ addonId, reportEntryPoint });
1162 async removeAddon(addonId, eventObject) {
1163 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1164 if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
1168 let { remove, report } = await this.promptRemoveExtension(addon);
1170 AMTelemetry.recordActionEvent({
1171 object: eventObject,
1172 action: "uninstall",
1173 value: remove ? "accepted" : "cancelled",
1178 // Leave the extension in pending uninstall if we are also reporting the
1180 await addon.uninstall(report);
1183 await this.reportAddon(addon.id, "uninstall");
1188 async manageAddon(addonId, eventObject) {
1189 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1194 BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
1195 AMTelemetry.recordActionEvent({
1196 object: eventObject,
1198 extra: { addonId: addon.id },
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()`:
1211 * let item = document.createElement("unified-extensions-item");
1212 * item.setAddon(addon);
1213 * document.body.appendChild(item);
1216 customElements.define(
1217 "unified-extensions-item",
1218 class extends HTMLElement {
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.
1223 * @param {AddonWrapper} addon The add-on to use.
1229 connectedCallback() {
1230 if (this._openMenuButton) {
1234 const template = document.getElementById(
1235 "unified-extensions-item-template"
1237 this.appendChild(template.content.cloneNode(true));
1239 this._actionButton = this.querySelector(
1240 ".unified-extensions-item-action"
1242 this._openMenuButton = this.querySelector(
1243 ".unified-extensions-item-open-menu"
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);
1256 handleEvent(event) {
1257 const { target } = event;
1259 switch (event.type) {
1261 if (target === this._openMenuButton) {
1262 const popup = target.ownerDocument.getElementById(
1263 "unified-extensions-context-menu"
1270 true /* isContextMenu */,
1271 false /* attributesOverride */,
1274 } else if (target === this._actionButton) {
1275 const extension = WebExtensionPolicy.getByID(this.addon.id)
1281 const win = event.target.ownerGlobal;
1282 const tab = win.gBrowser.selectedTab;
1284 extension.tabManager.addActiveTabPermission(tab);
1285 extension.tabManager.activateScripts(tab);
1291 if (target === this._openMenuButton) {
1292 this.removeAttribute("secondary-button-hovered");
1293 } else if (target === this._actionButton) {
1294 this._updateStateMessage();
1300 if (target === this._openMenuButton) {
1301 this.setAttribute("secondary-button-hovered", true);
1302 } else if (target === this._actionButton) {
1303 this._updateStateMessage({ hover: true });
1309 async _updateStateMessage({ hover = false } = {}) {
1310 const policy = WebExtensionPolicy.getByID(this.addon.id);
1312 const messages = lazy.OriginControls.getStateMessageIDs(
1314 this.ownerGlobal.gBrowser.currentURI
1320 const messageElement = this.querySelector(
1321 ".unified-extensions-item-message-default"
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;
1336 await document.l10n.translateElements([messageElement]);
1338 if (adjustMinHeight) {
1339 const contentsElement = this.querySelector(
1340 ".unified-extensions-item-contents"
1342 const { height } = getComputedStyle(contentsElement);
1343 contentsElement.style.minHeight = height;
1348 const policy = WebExtensionPolicy.getByID(this.addon.id);
1349 const state = lazy.OriginControls.getState(
1351 this.ownerGlobal.gBrowser.currentURI
1354 return state && state.whenClicked && !state.hasAccess;
1360 "unified-extensions-item requires an add-on, forgot to call setAddon()?"
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(
1373 lazy.OriginControls.getAttention(policy, this.ownerGlobal)
1377 ".unified-extensions-item-name"
1378 ).textContent = this.addon.name;
1380 const iconURL = AddonManager.getPreferredIconURL(this.addon, 32, window);
1382 this.querySelector(".unified-extensions-item-icon").setAttribute(
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(
1395 JSON.stringify({ extensionName: this.addon.name })
1398 this._updateStateMessage();
1403 // We must declare `gUnifiedExtensions` using `var` below to avoid a
1404 // "redeclaration" syntax error.
1405 var gUnifiedExtensions = {
1406 _initialized: false,
1409 if (this._initialized) {
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.
1421 .getElementById("unified-extensions-context-menu")
1422 .querySelectorAll("[data-lazy-l10n-id]")
1424 el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
1425 el.removeAttribute("data-lazy-l10n-id");
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);
1439 this._initialized = true;
1443 if (this.permListener) {
1444 lazy.ExtensionPermissions.removeListener(this.permListener);
1445 this.permListener = null;
1450 return Services.prefs.getBoolPref(
1451 "extensions.unifiedExtensions.enabled",
1456 onLocationChange(browser, webProgress, _request, _uri, flags) {
1457 // Only update on top-level cross-document navigations in the selected tab.
1459 webProgress.isTopLevel &&
1460 browser === gBrowser.selectedBrowser &&
1461 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
1463 this.updateAttention();
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);
1481 this.button.toggleAttribute("attention", false);
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(
1495 // Anchor on the toolbar icon to position the popup right below the
1497 ).firstElementChild;
1503 return "addons-notification-icon";
1507 return this._button;
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.
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.
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(
1533 !WebExtensionPolicy.getByID(addon.id).extension.hasBrowserActionUI)
1535 addons.sort((a1, a2) => a1.name.localeCompare(a2.name));
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).
1545 * @returns {boolean} Whether there are extensions listed in the panel.
1547 async hasExtensionsInPanel() {
1548 const extensions = await this.getActiveExtensions();
1552 const policy = WebExtensionPolicy.getByID(extension.id);
1553 return this.browserActionFor(policy)?.widget;
1558 widget?.areaType !== CustomizableUI.TYPE_TOOLBAR ||
1559 widget?.forWindow(window).overflowed
1564 handleEvent(event) {
1565 switch (event.type) {
1566 case "ViewShowing": {
1567 this.onPanelViewShowing(event.target);
1570 case "ViewHiding": {
1571 this.onPanelViewHiding(event.target);
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);
1590 onPanelViewHiding(panelview) {
1591 const list = panelview.querySelector(".unified-extensions-list");
1592 while (list.lastChild) {
1593 list.lastChild.remove();
1599 // Lazy load the unified-extensions-panel panel the first time we need to display it.
1601 let template = document.getElementById(
1602 "unified-extensions-panel-template"
1604 template.replaceWith(template.content);
1605 this._panel = document.getElementById("unified-extensions-panel");
1606 let customizationArea = this._panel.querySelector(
1607 "#unified-extensions-area"
1609 CustomizableUI.registerPanelNode(
1611 CustomizableUI.AREA_ADDONS
1613 CustomizableUI.addPanelCloseListeners(this._panel);
1618 async togglePanel(aEvent) {
1619 if (!CustomizationHandler.isCustomizing()) {
1620 if (aEvent && aEvent.button !== 0) {
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/");
1632 if (!this._listView) {
1633 this._listView = PanelMultiView.getViewNode(
1635 "unified-extensions-view"
1637 this._listView.addEventListener("ViewShowing", this);
1638 this._listView.addEventListener("ViewHiding", this);
1641 if (this._button.open) {
1642 PanelMultiView.hidePopup(panel);
1643 this._button.open = false;
1645 panel.hidden = false;
1646 PanelMultiView.openPopup(panel, this._button, {
1647 triggerEvent: aEvent,
1652 // We always dispatch an event (useful for testing purposes).
1653 window.dispatchEvent(new CustomEvent("UnifiedExtensionsTogglePanel"));
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
1660 if (event.target.id !== "unified-extensions-context-menu") {
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"
1672 const removeButton = menu.querySelector(
1673 ".unified-extensions-context-menu-remove-extension"
1675 const reportButton = menu.querySelector(
1676 ".unified-extensions-context-menu-report-extension"
1678 const menuSeparator = menu.querySelector(
1679 ".unified-extensions-context-menu-management-separator"
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);
1691 reportButton.hidden = !gAddonAbuseReportEnabled;
1692 removeButton.disabled = !(
1693 addon.permissions & AddonManager.PERM_CAN_UNINSTALL
1696 ExtensionsUI.originControlsMenu(menu, id);
1698 const browserAction = this.browserActionFor(WebExtensionPolicy.getByID(id));
1699 if (browserAction) {
1700 browserAction.updateContextMenu(menu);
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);
1712 async manageExtension(menu) {
1713 const id = this._getExtensionId(menu);
1715 await this.togglePanel();
1716 await BrowserAddonUI.manageAddon(id, "unifiedExtensions");
1719 async removeExtension(menu) {
1720 const id = this._getExtensionId(menu);
1722 await this.togglePanel();
1723 await BrowserAddonUI.removeAddon(id, "unifiedExtensions");
1726 async reportExtension(menu) {
1727 const id = this._getExtensionId(menu);
1729 await this.togglePanel();
1730 await BrowserAddonUI.reportAddon(id, "unified_context_menu");
1733 _getExtensionId(menu) {
1734 const { triggerNode } = menu;
1735 return triggerNode.dataset.extensionid;
1738 _getWidgetId(menu) {
1739 const { triggerNode } = menu;
1740 return triggerNode.closest(".unified-extensions-item")?.id;
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);
1757 this.pinToToolbar(widgetId, shouldPinToToolbar);
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);