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 let displayURI = options.displayURI;
669 options.displayURI = undefined;
671 options.eventCallback = topic => {
672 if (topic !== "showing") {
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();
682 if (isSitePermissionAddon) {
683 message.textContent = gNavigatorBundle.getString(
684 "sitePermissionInstallFirstPrompt.message"
686 } else if (hasHost) {
687 let text = gNavigatorBundle.getString(
688 "xpinstallPromptMessage.message"
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);
695 message.textContent = gNavigatorBundle.getString(
696 "xpinstallPromptMessage.message.unknown"
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"
707 learnMore.setAttribute(
709 Services.urlFormatter.formatURLPref("app.support.baseURL") + article
713 let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
715 label: gNavigatorBundle.getString("xpinstallPromptMessage.install"),
716 accessKey: gNavigatorBundle.getString(
717 "xpinstallPromptMessage.install.accesskey"
721 Ci.nsISecurityUITelemetry
722 .WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
724 installInfo.install();
727 let dontAllowAction = {
728 label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
729 accessKey: gNavigatorBundle.getString(
730 "xpinstallPromptMessage.dontAllow.accesskey"
733 for (let install of installInfo.installs) {
734 if (install.state != AddonManager.STATE_CANCELLED) {
738 if (installInfo.cancel) {
739 installInfo.cancel();
744 let neverAllowCallback = () => {
745 SitePermissions.setForPrincipal(
746 browser.contentPrincipal,
748 SitePermissions.BLOCK
750 for (let install of installInfo.installs) {
751 if (install.state != AddonManager.STATE_CANCELLED) {
755 if (installInfo.cancel) {
756 installInfo.cancel();
760 let neverAllowAction = {
761 label: gNavigatorBundle.getString(
762 "xpinstallPromptMessage.neverAllow"
764 accessKey: gNavigatorBundle.getString(
765 "xpinstallPromptMessage.neverAllow.accesskey"
767 callback: neverAllowCallback,
770 let neverAllowAndReportAction = {
771 label: gNavigatorBundle.getString(
772 "xpinstallPromptMessage.neverAllowAndReport"
774 accessKey: gNavigatorBundle.getString(
775 "xpinstallPromptMessage.neverAllowAndReport.accesskey"
778 AMTelemetry.recordEvent({
779 method: "reportSuspiciousSite",
780 object: "suspiciousSite",
781 value: displayURI?.displayHost ?? "(unknown)",
784 neverAllowCallback();
789 Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
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);
798 let popup = PopupNotifications.show(
802 gUnifiedExtensions.getPopupAnchorID(browser, window),
807 removeNotificationOnEnd(popup, installInfo.installs);
810 case "addon-install-started": {
811 let needsDownload = function needsDownload(aInstall) {
812 return aInstall.state != AddonManager.STATE_DOWNLOADED;
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)) {
819 notificationID = "addon-progress";
820 messageString = gNavigatorBundle.getString(
821 "addonDownloadingAndVerifying"
823 messageString = PluralForm.get(
824 installInfo.installs.length,
827 messageString = messageString.replace(
829 installInfo.installs.length
831 options.installs = installInfo.installs;
832 options.contentWindow = browser.contentWindow;
833 options.sourceURI = browser.currentURI;
834 options.eventCallback = function(aEvent) {
837 options.contentWindow = null;
838 options.sourceURI = null;
843 label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
844 accessKey: gNavigatorBundle.getString(
845 "addonInstall.acceptButton2.accesskey"
850 let secondaryAction = {
851 label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
852 accessKey: gNavigatorBundle.getString(
853 "addonInstall.cancelButton.accesskey"
856 for (let install of installInfo.installs) {
857 if (install.state != AddonManager.STATE_CANCELLED) {
863 let notification = PopupNotifications.show(
867 gUnifiedExtensions.getPopupAnchorID(browser, window),
872 notification._startTime = Date.now();
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) {
884 host = options.displayURI.host;
886 // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
891 install.sourceURI instanceof Ci.nsIStandardURL &&
892 install.sourceURI.host;
895 // Construct the l10n ID for the error, e.g. "addonInstallError-3"
897 host || install.error == 0
898 ? "addonInstallError"
899 : "addonLocalInstallError";
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];
910 install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED
912 error += "Blocklisted";
913 args = [install.name];
915 error += "Incompatible";
916 args = [brandShortName, Services.appinfo.version, install.name];
921 !Services.policies.mayInstallAddon(install.addon)
923 error = "addonInstallBlockedByPolicy";
924 let extensionSettings = Services.policies.getExtensionSettings(
930 "blocked_install_message" in extensionSettings
932 message = " " + extensionSettings.blocked_install_message;
934 args = [install.name, install.addon.id, message];
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") +
944 messageString = gNavigatorBundle.getFormattedString(error, args);
946 PopupNotifications.show(
950 gUnifiedExtensions.getPopupAnchorID(browser, window),
956 // Can't have multiple notifications with the same ID, so stop here.
959 this._removeProgressNotification(browser);
962 case "addon-install-confirmation": {
963 let showNotification = () => {
964 let height = undefined;
966 if (PopupNotifications.isPanelOpen) {
968 .getElementById("addon-progress-notification")
969 .getBoundingClientRect();
970 height = rect.height;
973 this._removeProgressNotification(browser);
974 this.showInstallConfirmation(browser, installInfo, height);
977 let progressNotification = PopupNotifications.getNotification(
981 if (progressNotification) {
982 let downloadDuration = Date.now() - progressNotification._startTime;
984 Services.prefs.getIntPref("security.dialog_enable_delay") -
986 if (securityDelay > 0) {
988 // The download may have been cancelled during the security delay
990 PopupNotifications.getNotification("addon-progress", browser)
1001 case "addon-install-complete": {
1002 let secondaryActions = null;
1003 let numAddons = installInfo.installs.length;
1005 if (numAddons == 1) {
1006 messageString = gNavigatorBundle.getFormattedString(
1008 [installInfo.installs[0].name]
1011 messageString = gNavigatorBundle.getString("addonsGenericInstalled");
1012 messageString = PluralForm.get(numAddons, messageString);
1013 messageString = messageString.replace("#1", numAddons);
1017 options.removeOnDismissal = true;
1018 options.persistent = false;
1020 PopupNotifications.show(
1024 gUnifiedExtensions.getPopupAnchorID(browser, window),
1033 _removeProgressNotification(aBrowser) {
1034 let notification = PopupNotifications.getNotification(
1039 notification.remove();
1044 var gExtensionsNotifications = {
1047 this.updateAlerts();
1048 this.boundUpdate = this.updateAlerts.bind(this);
1049 ExtensionsUI.on("change", this.boundUpdate);
1050 this.initialized = true;
1054 // uninit() can race ahead of init() in some cases, if that happens,
1055 // we have no handler to remove.
1056 if (!this.initialized) {
1059 ExtensionsUI.off("change", this.boundUpdate);
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);
1077 let sideloaded = ExtensionsUI.sideloaded;
1078 let updates = ExtensionsUI.updates;
1080 let container = PanelUI.addonNotificationContainer;
1082 while (container.firstChild) {
1083 container.firstChild.remove();
1087 for (let update of updates) {
1091 let text = gNavigatorBundle.getFormattedString(
1092 "webextPerms.updateMenuItem",
1095 this._createAddonButton(text, update.addon.iconURL, evt => {
1096 ExtensionsUI.showUpdate(gBrowser, update);
1101 for (let addon of sideloaded) {
1106 let brandBundle = document.getElementById("bundle_brand");
1107 appName = brandBundle.getString("brandShortName");
1110 let text = gNavigatorBundle.getFormattedString(
1111 "webextPerms.sideloadMenuItem",
1112 [addon.name, appName]
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.
1119 ExtensionsUI.showSideloaded(gBrowser, addon);
1125 var BrowserAddonUI = {
1126 async promptRemoveExtension(addon) {
1127 let { name } = addon;
1128 let title = await document.l10n.formatValue("addon-removal-title", {
1131 let { getFormattedString, getString } = gNavigatorBundle;
1132 let btnTitle = getString("webext.remove.confirmation.button");
1134 BUTTON_TITLE_IS_STRING: titleString,
1135 BUTTON_TITLE_CANCEL: titleCancel,
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.
1148 gAddonAbuseReportEnabled &&
1149 ["extension", "theme"].includes(addon.type)
1151 checkboxMessage = await document.l10n.formatValue(
1152 "addon-removal-abuse-report-checkbox"
1158 if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
1159 message = getFormattedString("webext.remove.confirmation.message", [
1161 document.getElementById("bundle_brand").getString("brandShorterName"),
1165 let result = confirmEx(
1177 return { remove: result === 0, report: checkboxState.value };
1180 async reportAddon(addonId, reportEntryPoint) {
1181 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1186 const win = await BrowserOpenAddonsMgr("addons://list/extension");
1188 win.openAbuseReport({ addonId, reportEntryPoint });
1191 async removeAddon(addonId, eventObject) {
1192 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1193 if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
1197 let { remove, report } = await this.promptRemoveExtension(addon);
1199 AMTelemetry.recordActionEvent({
1200 object: eventObject,
1201 action: "uninstall",
1202 value: remove ? "accepted" : "cancelled",
1207 // Leave the extension in pending uninstall if we are also reporting the
1209 await addon.uninstall(report);
1212 await this.reportAddon(addon.id, "uninstall");
1217 async manageAddon(addonId, eventObject) {
1218 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1223 BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
1224 AMTelemetry.recordActionEvent({
1225 object: eventObject,
1227 extra: { addonId: addon.id },
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,
1248 if (this._initialized) {
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;
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);
1270 this._initialized = true;
1274 if (this.permListener) {
1275 lazy.ExtensionPermissions.removeListener(this.permListener);
1276 this.permListener = null;
1278 gNavToolbox.removeEventListener("customizationstarting", this);
1285 onLocationChange(browser, webProgress, _request, _uri, flags) {
1286 // Only update on top-level cross-document navigations in the selected tab.
1288 webProgress.isTopLevel &&
1289 browser === gBrowser.selectedBrowser &&
1290 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
1292 this.updateAttention();
1296 // Update the attention indicator for the whole unified extensions button.
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)) {
1310 this.button.toggleAttribute("attention", attention);
1311 this.button.ownerDocument.l10n.setAttributes(
1314 ? "unified-extensions-button-permissions-needed"
1315 : "unified-extensions-button"
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(
1330 // Anchor on the toolbar icon to position the popup right below the
1332 ).firstElementChild;
1338 return "addons-notification-icon";
1342 return this._button;
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.
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.
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") {
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)) {
1371 return all || !extension.hasBrowserActionUI;
1374 policies.sort((a, b) => a.name.localeCompare(b.name));
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).
1383 * @returns {boolean} Whether there are extensions listed in the panel.
1385 hasExtensionsInPanel() {
1386 const policies = this.getActivePolicies();
1389 .map(policy => this.browserActionFor(policy)?.widget)
1393 widget?.areaType !== CustomizableUI.TYPE_TOOLBAR ||
1394 widget?.forWindow(window).overflowed
1399 handleEvent(event) {
1400 switch (event.type) {
1402 this.onPanelViewShowing(event.target);
1406 this.onPanelViewHiding(event.target);
1409 case "customizationstarting":
1410 this.panel.hidePopup();
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);
1429 onPanelViewHiding(panelview) {
1430 const list = panelview.querySelector(".unified-extensions-list");
1431 while (list.lastChild) {
1432 list.lastChild.remove();
1438 // Lazy load the unified-extensions-panel panel the first time we need to
1441 let template = document.getElementById(
1442 "unified-extensions-panel-template"
1444 template.replaceWith(template.content);
1445 this._panel = document.getElementById("unified-extensions-panel");
1446 let customizationArea = this._panel.querySelector(
1447 "#unified-extensions-area"
1449 CustomizableUI.registerPanelNode(
1451 CustomizableUI.AREA_ADDONS
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.
1458 .getElementById("unified-extensions-context-menu")
1459 .querySelectorAll("[data-lazy-l10n-id]")
1461 el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
1462 el.removeAttribute("data-lazy-l10n-id");
1468 async togglePanel(aEvent) {
1469 if (!CustomizationHandler.isCustomizing()) {
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
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)
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/");
1493 let panel = this.panel;
1495 if (!this._listView) {
1496 this._listView = PanelMultiView.getViewNode(
1498 "unified-extensions-view"
1500 this._listView.addEventListener("ViewShowing", this);
1501 this._listView.addEventListener("ViewHiding", this);
1504 if (this._button.open) {
1505 PanelMultiView.hidePopup(panel);
1506 this._button.open = false;
1508 panel.hidden = false;
1509 PanelMultiView.openPopup(panel, this._button, {
1510 triggerEvent: aEvent,
1515 // We always dispatch an event (useful for testing purposes).
1516 window.dispatchEvent(new CustomEvent("UnifiedExtensionsTogglePanel"));
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
1523 if (event.target.id !== "unified-extensions-context-menu") {
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"
1535 const removeButton = menu.querySelector(
1536 ".unified-extensions-context-menu-remove-extension"
1538 const reportButton = menu.querySelector(
1539 ".unified-extensions-context-menu-report-extension"
1541 const menuSeparator = menu.querySelector(
1542 ".unified-extensions-context-menu-management-separator"
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);
1554 reportButton.hidden = !gAddonAbuseReportEnabled;
1555 removeButton.disabled = !(
1556 addon.permissions & AddonManager.PERM_CAN_UNINSTALL
1559 ExtensionsUI.originControlsMenu(menu, id);
1561 const browserAction = this.browserActionFor(WebExtensionPolicy.getByID(id));
1562 if (browserAction) {
1563 browserAction.updateContextMenu(menu);
1567 // This is registered on the top-level unified extensions context menu.
1568 onContextMenuCommand(menu, event) {
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);
1580 async manageExtension(menu) {
1581 const id = this._getExtensionId(menu);
1583 await BrowserAddonUI.manageAddon(id, "unifiedExtensions");
1586 async removeExtension(menu) {
1587 const id = this._getExtensionId(menu);
1589 await BrowserAddonUI.removeAddon(id, "unifiedExtensions");
1592 async reportExtension(menu) {
1593 const id = this._getExtensionId(menu);
1595 await BrowserAddonUI.reportAddon(id, "unified_context_menu");
1598 _getExtensionId(menu) {
1599 const { triggerNode } = menu;
1600 return triggerNode.dataset.extensionid;
1603 _getWidgetId(menu) {
1604 const { triggerNode } = menu;
1605 return triggerNode.closest(".unified-extensions-item")?.id;
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);
1623 this.pinToToolbar(widgetId, shouldPinToToolbar);
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();