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"
27 customElements.define(
28 "addon-progress-notification",
29 class MozAddonProgressNotification extends customElements.get(
34 this.progressmeter = document.getElementById(
35 "addon-progress-notification-progressmeter"
38 this.progresstext = document.getElementById(
39 "addon-progress-notification-progresstext"
42 if (!this.notification) {
46 this.notification.options.installs.forEach(function(aInstall) {
47 aInstall.addListener(this);
50 // Calling updateProgress can sometimes cause this notification to be
51 // removed in the middle of refreshing the notification panel which
52 // makes the panel get refreshed again. Just initialise to the
53 // undetermined state and then schedule a proper check at the next
55 this.setProgress(0, -1);
56 this._updateProgressTimeout = setTimeout(
57 this.updateProgress.bind(this),
62 disconnectedCallback() {
67 if (!this.notification) {
70 this.notification.options.installs.forEach(function(aInstall) {
71 aInstall.removeListener(this);
74 clearTimeout(this._updateProgressTimeout);
77 setProgress(aProgress, aMaxProgress) {
78 if (aMaxProgress == -1) {
79 this.progressmeter.removeAttribute("value");
81 this.progressmeter.setAttribute(
83 (aProgress * 100) / aMaxProgress
89 if (!this.notification.lastUpdate) {
90 this.notification.lastUpdate = now;
91 this.notification.lastProgress = aProgress;
95 let delta = now - this.notification.lastUpdate;
96 if (delta < 400 && aProgress < aMaxProgress) {
100 // Set min. time delta to avoid division by zero in the upcoming speed calculation
101 delta = Math.max(delta, 400);
104 // This algorithm is the same used by the downloads code.
105 let speed = (aProgress - this.notification.lastProgress) / delta;
106 if (this.notification.speed) {
107 speed = speed * 0.9 + this.notification.speed * 0.1;
110 this.notification.lastUpdate = now;
111 this.notification.lastProgress = aProgress;
112 this.notification.speed = speed;
115 [status, this.notification.last] = DownloadUtils.getDownloadStatus(
119 this.notification.last
121 this.progresstext.setAttribute("value", status);
122 this.progresstext.setAttribute("tooltiptext", status);
126 let installs = this.notification.options.installs;
127 installs.forEach(function(aInstall) {
131 // Cancel will throw if the download has already failed
135 PopupNotifications.remove(this.notification);
139 if (!this.notification) {
143 let downloadingCount = 0;
147 this.notification.options.installs.forEach(function(aInstall) {
148 if (aInstall.maxProgress == -1) {
151 progress += aInstall.progress;
152 if (maxProgress >= 0) {
153 maxProgress += aInstall.maxProgress;
155 if (aInstall.state < AddonManager.STATE_DOWNLOADED) {
160 if (downloadingCount == 0) {
162 this.progressmeter.removeAttribute("value");
163 let status = gNavigatorBundle.getString("addonDownloadVerifying");
164 this.progresstext.setAttribute("value", status);
165 this.progresstext.setAttribute("tooltiptext", status);
167 this.setProgress(progress, maxProgress);
171 onDownloadProgress() {
172 this.updateProgress();
176 this.updateProgress();
179 onDownloadCancelled() {
180 this.updateProgress();
184 this.updateProgress();
189 // Removes a doorhanger notification if all of the installs it was notifying
190 // about have ended in some way.
191 function removeNotificationOnEnd(notification, installs) {
192 let count = installs.length;
194 function maybeRemove(install) {
195 install.removeListener(this);
198 // Check that the notification is still showing
199 let current = PopupNotifications.getNotification(
203 if (current === notification) {
204 notification.remove();
209 for (let install of installs) {
210 install.addListener({
211 onDownloadCancelled: maybeRemove,
212 onDownloadFailed: maybeRemove,
213 onInstallFailed: maybeRemove,
214 onInstallEnded: maybeRemove,
219 var gXPInstallObserver = {
220 _findChildShell(aDocShell, aSoughtShell) {
221 if (aDocShell == aSoughtShell) {
225 var node = aDocShell.QueryInterface(Ci.nsIDocShellTreeItem);
226 for (var i = 0; i < node.childCount; ++i) {
227 var docShell = node.getChildAt(i);
228 docShell = this._findChildShell(docShell, aSoughtShell);
229 if (docShell == aSoughtShell) {
236 _getBrowser(aDocShell) {
237 for (let browser of gBrowser.browsers) {
238 if (this._findChildShell(browser.docShell, aDocShell)) {
245 pendingInstalls: new WeakMap(),
247 showInstallConfirmation(browser, installInfo, height = undefined) {
248 // If the confirmation notification is already open cache the installInfo
249 // and the new confirmation will be shown later
251 PopupNotifications.getNotification("addon-install-confirmation", browser)
253 let pending = this.pendingInstalls.get(browser);
255 pending.push(installInfo);
257 this.pendingInstalls.set(browser, [installInfo]);
262 let showNextConfirmation = () => {
263 // Make sure the browser is still alive.
264 if (!gBrowser.browsers.includes(browser)) {
268 let pending = this.pendingInstalls.get(browser);
269 if (pending && pending.length) {
270 this.showInstallConfirmation(browser, pending.shift());
274 // If all installs have already been cancelled in some way then just show
275 // the next confirmation
277 installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)
279 showNextConfirmation();
283 // Make notifications persistent
285 displayURI: installInfo.originatingURI,
290 if (gUnifiedExtensions.isEnabled) {
291 options.popupOptions = {
292 position: "bottomright topright",
296 let acceptInstallation = () => {
297 for (let install of installInfo.installs) {
303 .getHistogramById("SECURITY_UI")
305 Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH
309 let cancelInstallation = () => {
311 for (let install of installInfo.installs) {
312 // The notification may have been closed because the add-ons got
313 // cancelled elsewhere, only try to cancel those that are still
315 if (install.state != AddonManager.STATE_CANCELLED) {
321 showNextConfirmation();
324 let unsigned = installInfo.installs.filter(
325 i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
328 !!unsigned.length && unsigned.length < installInfo.installs.length;
330 options.eventCallback = aEvent => {
333 cancelInstallation();
336 let addonList = document.getElementById(
337 "addon-install-confirmation-content"
339 while (addonList.firstChild) {
340 addonList.firstChild.remove();
343 for (let install of installInfo.installs) {
344 let container = document.createXULElement("hbox");
346 let name = document.createXULElement("label");
347 name.setAttribute("value", install.addon.name);
348 name.setAttribute("class", "addon-install-confirmation-name");
349 container.appendChild(name);
353 install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
355 let unsignedLabel = document.createXULElement("label");
356 unsignedLabel.setAttribute(
358 gNavigatorBundle.getString("addonInstall.unsigned")
360 unsignedLabel.setAttribute(
362 "addon-install-confirmation-unsigned"
364 container.appendChild(unsignedLabel);
367 addonList.appendChild(container);
373 options.learnMoreURL = Services.urlFormatter.formatURLPref(
374 "app.support.baseURL"
378 let notification = document.getElementById(
379 "addon-install-confirmation-notification"
381 if (unsigned.length == installInfo.installs.length) {
382 // None of the add-ons are verified
383 messageString = gNavigatorBundle.getString(
384 "addonConfirmInstallUnsigned.message"
386 notification.setAttribute("warning", "true");
387 options.learnMoreURL += "unsigned-addons";
388 } else if (!unsigned.length) {
389 // All add-ons are verified or don't need to be verified
390 messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
391 notification.removeAttribute("warning");
392 options.learnMoreURL += "find-and-install-add-ons";
394 // Some of the add-ons are unverified, the list of names will indicate
396 messageString = gNavigatorBundle.getString(
397 "addonConfirmInstallSomeUnsigned.message"
399 notification.setAttribute("warning", "true");
400 options.learnMoreURL += "unsigned-addons";
403 let brandBundle = document.getElementById("bundle_brand");
404 let brandShortName = brandBundle.getString("brandShortName");
406 messageString = PluralForm.get(installInfo.installs.length, messageString);
407 messageString = messageString.replace("#1", brandShortName);
408 messageString = messageString.replace("#2", installInfo.installs.length);
411 label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
412 accessKey: gNavigatorBundle.getString(
413 "addonInstall.acceptButton2.accesskey"
415 callback: acceptInstallation,
418 let secondaryAction = {
419 label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
420 accessKey: gNavigatorBundle.getString(
421 "addonInstall.cancelButton.accesskey"
427 notification.style.minHeight = height + "px";
430 let tab = gBrowser.getTabForBrowser(browser);
432 gBrowser.selectedTab = tab;
435 let popup = PopupNotifications.show(
437 "addon-install-confirmation",
439 gUnifiedExtensions.getPopupAnchorID(browser, window),
445 removeNotificationOnEnd(popup, installInfo.installs);
448 .getHistogramById("SECURITY_UI")
449 .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
452 // IDs of addon install related notifications
454 "addon-install-blocked",
455 "addon-install-complete",
456 "addon-install-confirmation",
457 "addon-install-failed",
458 "addon-install-origin-blocked",
459 "addon-install-webapi-blocked",
460 "addon-install-policy-blocked",
462 "addon-webext-permissions",
463 "xpinstall-disabled",
467 * Remove all opened addon installation notifications
469 * @param {*} browser - Browser to remove notifications for
470 * @returns {boolean} - true if notifications have been removed.
472 removeAllNotifications(browser) {
473 let notifications = this.NOTIFICATION_IDS.map(id =>
474 PopupNotifications.getNotification(id, browser)
475 ).filter(notification => notification != null);
477 PopupNotifications.remove(notifications, true);
479 return !!notifications.length;
482 logWarningFullScreenInstallBlocked() {
483 // If notifications have been removed, log a warning to the website console
484 let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
487 let message = gBrowserBundle.GetStringFromName(
488 "addonInstallFullScreenBlocked"
490 consoleMsg.initWithWindowID(
492 gBrowser.currentURI.spec,
496 Ci.nsIScriptError.warningFlag,
498 gBrowser.selectedBrowser.innerWindowID
500 Services.console.logMessage(consoleMsg);
503 observe(aSubject, aTopic, aData) {
504 var brandBundle = document.getElementById("bundle_brand");
505 var installInfo = aSubject.wrappedJSObject;
506 var browser = installInfo.browser;
508 // Make sure the browser is still alive.
509 if (!browser || !gBrowser.browsers.includes(browser)) {
513 var messageString, action;
514 var brandShortName = brandBundle.getString("brandShortName");
516 var notificationID = aTopic;
517 // Make notifications persistent
519 displayURI: installInfo.originatingURI,
522 timeout: Date.now() + 30000,
525 if (gUnifiedExtensions.isEnabled) {
526 options.popupOptions = {
527 position: "bottomright topright",
532 case "addon-install-disabled": {
533 notificationID = "xpinstall-disabled";
534 let secondaryActions = null;
536 if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
537 messageString = gNavigatorBundle.getString(
538 "xpinstallDisabledMessageLocked"
541 messageString = gNavigatorBundle.getString(
542 "xpinstallDisabledMessage"
546 label: gNavigatorBundle.getString("xpinstallDisabledButton"),
547 accessKey: gNavigatorBundle.getString(
548 "xpinstallDisabledButton.accesskey"
550 callback: function editPrefs() {
551 Services.prefs.setBoolPref("xpinstall.enabled", true);
557 label: gNavigatorBundle.getString(
558 "addonInstall.cancelButton.label"
560 accessKey: gNavigatorBundle.getString(
561 "addonInstall.cancelButton.accesskey"
568 PopupNotifications.show(
572 gUnifiedExtensions.getPopupAnchorID(browser, window),
579 case "addon-install-fullscreen-blocked": {
580 // AddonManager denied installation because we are in DOM fullscreen
581 this.logWarningFullScreenInstallBlocked();
584 case "addon-install-webapi-blocked":
585 case "addon-install-policy-blocked":
586 case "addon-install-origin-blocked": {
587 if (aTopic == "addon-install-policy-blocked") {
588 messageString = gNavigatorBundle.getString(
589 "addonDomainBlockedByPolicy"
592 messageString = gNavigatorBundle.getFormattedString(
593 "xpinstallPromptMessage",
598 if (Services.policies) {
599 let extensionSettings = Services.policies.getExtensionSettings("*");
602 "blocked_install_message" in extensionSettings
604 messageString += " " + extensionSettings.blocked_install_message;
608 options.removeOnDismissal = true;
609 options.persistent = false;
611 let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
613 Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
615 let popup = PopupNotifications.show(
619 gUnifiedExtensions.getPopupAnchorID(browser, window),
624 removeNotificationOnEnd(popup, installInfo.installs);
627 case "addon-install-blocked": {
628 // Dismiss the progress notification. Note that this is bad if
629 // there are multiple simultaneous installs happening, see
630 // bug 1329884 for a longer explanation.
631 let progressNotification = PopupNotifications.getNotification(
635 if (progressNotification) {
636 progressNotification.remove();
639 let hasHost = !!options.displayURI;
641 messageString = gNavigatorBundle.getFormattedString(
642 "xpinstallPromptMessage.header",
645 options.name = options.displayURI.displayHost;
647 messageString = gNavigatorBundle.getString(
648 "xpinstallPromptMessage.header.unknown"
651 // displayURI becomes it's own label, so we unset it for this panel. It will become part of the
652 // messageString above.
653 options.displayURI = undefined;
655 options.eventCallback = topic => {
656 if (topic !== "showing") {
659 let doc = browser.ownerDocument;
660 let message = doc.getElementById("addon-install-blocked-message");
661 // We must remove any prior use of this panel message in this window.
662 while (message.firstChild) {
663 message.firstChild.remove();
666 let text = gNavigatorBundle.getString(
667 "xpinstallPromptMessage.message"
669 let b = doc.createElementNS("http://www.w3.org/1999/xhtml", "b");
670 b.textContent = options.name;
671 let fragment = BrowserUIUtils.getLocalizedFragment(doc, text, b);
672 message.appendChild(fragment);
674 message.textContent = gNavigatorBundle.getString(
675 "xpinstallPromptMessage.message.unknown"
678 let learnMore = doc.getElementById("addon-install-blocked-info");
679 learnMore.textContent = gNavigatorBundle.getString(
680 "xpinstallPromptMessage.learnMore"
682 learnMore.setAttribute(
684 Services.urlFormatter.formatURLPref("app.support.baseURL") +
685 "unlisted-extensions-risks"
689 let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
691 label: gNavigatorBundle.getString("xpinstallPromptMessage.install"),
692 accessKey: gNavigatorBundle.getString(
693 "xpinstallPromptMessage.install.accesskey"
697 Ci.nsISecurityUITelemetry
698 .WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
700 installInfo.install();
703 let dontAllowAction = {
704 label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
705 accessKey: gNavigatorBundle.getString(
706 "xpinstallPromptMessage.dontAllow.accesskey"
709 for (let install of installInfo.installs) {
710 if (install.state != AddonManager.STATE_CANCELLED) {
714 if (installInfo.cancel) {
715 installInfo.cancel();
719 let neverAllowAction = {
720 label: gNavigatorBundle.getString(
721 "xpinstallPromptMessage.neverAllow"
723 accessKey: gNavigatorBundle.getString(
724 "xpinstallPromptMessage.neverAllow.accesskey"
727 SitePermissions.setForPrincipal(
728 browser.contentPrincipal,
730 SitePermissions.BLOCK
732 for (let install of installInfo.installs) {
733 if (install.state != AddonManager.STATE_CANCELLED) {
737 if (installInfo.cancel) {
738 installInfo.cancel();
744 Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
746 let popup = PopupNotifications.show(
750 gUnifiedExtensions.getPopupAnchorID(browser, window),
752 [dontAllowAction, neverAllowAction],
755 removeNotificationOnEnd(popup, installInfo.installs);
758 case "addon-install-started": {
759 let needsDownload = function needsDownload(aInstall) {
760 return aInstall.state != AddonManager.STATE_DOWNLOADED;
762 // If all installs have already been downloaded then there is no need to
763 // show the download progress
764 if (!installInfo.installs.some(needsDownload)) {
767 notificationID = "addon-progress";
768 messageString = gNavigatorBundle.getString(
769 "addonDownloadingAndVerifying"
771 messageString = PluralForm.get(
772 installInfo.installs.length,
775 messageString = messageString.replace(
777 installInfo.installs.length
779 options.installs = installInfo.installs;
780 options.contentWindow = browser.contentWindow;
781 options.sourceURI = browser.currentURI;
782 options.eventCallback = function(aEvent) {
785 options.contentWindow = null;
786 options.sourceURI = null;
791 label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
792 accessKey: gNavigatorBundle.getString(
793 "addonInstall.acceptButton2.accesskey"
798 let secondaryAction = {
799 label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
800 accessKey: gNavigatorBundle.getString(
801 "addonInstall.cancelButton.accesskey"
804 for (let install of installInfo.installs) {
805 if (install.state != AddonManager.STATE_CANCELLED) {
811 let notification = PopupNotifications.show(
815 gUnifiedExtensions.getPopupAnchorID(browser, window),
820 notification._startTime = Date.now();
824 case "addon-install-failed": {
825 options.removeOnDismissal = true;
826 options.persistent = false;
828 // TODO This isn't terribly ideal for the multiple failure case
829 for (let install of installInfo.installs) {
832 host = options.displayURI.host;
834 // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
839 install.sourceURI instanceof Ci.nsIStandardURL &&
840 install.sourceURI.host;
843 // Construct the l10n ID for the error, e.g. "addonInstallError-3"
845 host || install.error == 0
846 ? "addonInstallError"
847 : "addonLocalInstallError";
849 if (install.error < 0) {
850 // Append the error code for the installation failure to get the
851 // matching translation of the error. The error code is defined in
852 // AddonManager's _errors Map. Not all error codes listed there are
853 // translated, since errors that are only triggered during updates
854 // will never reach this code.
855 error += install.error;
856 args = [brandShortName, install.name];
858 install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED
860 error += "Blocklisted";
861 args = [install.name];
863 error += "Incompatible";
864 args = [brandShortName, Services.appinfo.version, install.name];
869 !Services.policies.mayInstallAddon(install.addon)
871 error = "addonInstallBlockedByPolicy";
872 let extensionSettings = Services.policies.getExtensionSettings(
878 "blocked_install_message" in extensionSettings
880 message = " " + extensionSettings.blocked_install_message;
882 args = [install.name, install.addon.id, message];
885 // Add Learn More link when refusing to install an unsigned add-on
886 if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
887 options.learnMoreURL =
888 Services.urlFormatter.formatURLPref("app.support.baseURL") +
892 messageString = gNavigatorBundle.getFormattedString(error, args);
894 PopupNotifications.show(
898 gUnifiedExtensions.getPopupAnchorID(browser, window),
904 // Can't have multiple notifications with the same ID, so stop here.
907 this._removeProgressNotification(browser);
910 case "addon-install-confirmation": {
911 let showNotification = () => {
912 let height = undefined;
914 if (PopupNotifications.isPanelOpen) {
916 .getElementById("addon-progress-notification")
917 .getBoundingClientRect();
918 height = rect.height;
921 this._removeProgressNotification(browser);
922 this.showInstallConfirmation(browser, installInfo, height);
925 let progressNotification = PopupNotifications.getNotification(
929 if (progressNotification) {
930 let downloadDuration = Date.now() - progressNotification._startTime;
932 Services.prefs.getIntPref("security.dialog_enable_delay") -
934 if (securityDelay > 0) {
936 // The download may have been cancelled during the security delay
938 PopupNotifications.getNotification("addon-progress", browser)
949 case "addon-install-complete": {
950 let secondaryActions = null;
951 let numAddons = installInfo.installs.length;
953 if (numAddons == 1) {
954 messageString = gNavigatorBundle.getFormattedString(
956 [installInfo.installs[0].name]
959 messageString = gNavigatorBundle.getString("addonsGenericInstalled");
960 messageString = PluralForm.get(numAddons, messageString);
961 messageString = messageString.replace("#1", numAddons);
965 options.removeOnDismissal = true;
966 options.persistent = false;
968 PopupNotifications.show(
972 gUnifiedExtensions.getPopupAnchorID(browser, window),
981 _removeProgressNotification(aBrowser) {
982 let notification = PopupNotifications.getNotification(
987 notification.remove();
992 var gExtensionsNotifications = {
996 this.boundUpdate = this.updateAlerts.bind(this);
997 ExtensionsUI.on("change", this.boundUpdate);
998 this.initialized = true;
1002 // uninit() can race ahead of init() in some cases, if that happens,
1003 // we have no handler to remove.
1004 if (!this.initialized) {
1007 ExtensionsUI.off("change", this.boundUpdate);
1010 _createAddonButton(text, icon, callback) {
1011 let button = document.createXULElement("toolbarbutton");
1012 button.setAttribute("wrap", "true");
1013 button.setAttribute("label", text);
1014 button.setAttribute("tooltiptext", text);
1015 const DEFAULT_EXTENSION_ICON =
1016 "chrome://mozapps/skin/extensions/extensionGeneric.svg";
1017 button.setAttribute("image", icon || DEFAULT_EXTENSION_ICON);
1018 button.className = "addon-banner-item subviewbutton";
1020 button.addEventListener("command", callback);
1021 PanelUI.addonNotificationContainer.appendChild(button);
1025 let sideloaded = ExtensionsUI.sideloaded;
1026 let updates = ExtensionsUI.updates;
1028 let container = PanelUI.addonNotificationContainer;
1030 while (container.firstChild) {
1031 container.firstChild.remove();
1035 for (let update of updates) {
1039 let text = gNavigatorBundle.getFormattedString(
1040 "webextPerms.updateMenuItem",
1043 this._createAddonButton(text, update.addon.iconURL, evt => {
1044 ExtensionsUI.showUpdate(gBrowser, update);
1049 for (let addon of sideloaded) {
1054 let brandBundle = document.getElementById("bundle_brand");
1055 appName = brandBundle.getString("brandShortName");
1058 let text = gNavigatorBundle.getFormattedString(
1059 "webextPerms.sideloadMenuItem",
1060 [addon.name, appName]
1062 this._createAddonButton(text, addon.iconURL, evt => {
1063 // We need to hide the main menu manually because the toolbarbutton is
1064 // removed immediately while processing this event, and PanelUI is
1065 // unable to identify which panel should be closed automatically.
1067 ExtensionsUI.showSideloaded(gBrowser, addon);
1073 var BrowserAddonUI = {
1074 async promptRemoveExtension(addon) {
1075 let { name } = addon;
1076 let title = await document.l10n.formatValue("addon-removal-title", {
1079 let { getFormattedString, getString } = gNavigatorBundle;
1080 let btnTitle = getString("webext.remove.confirmation.button");
1082 BUTTON_TITLE_IS_STRING: titleString,
1083 BUTTON_TITLE_CANCEL: titleCancel,
1087 } = Services.prompt;
1088 let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
1089 let checkboxState = { value: false };
1090 let checkboxMessage = null;
1092 // Enable abuse report checkbox in the remove extension dialog,
1093 // if enabled by the about:config prefs and the addon type
1094 // is currently supported.
1096 gAddonAbuseReportEnabled &&
1097 ["extension", "theme"].includes(addon.type)
1099 checkboxMessage = await document.l10n.formatValue(
1100 "addon-removal-abuse-report-checkbox"
1106 if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
1107 message = getFormattedString("webext.remove.confirmation.message", [
1109 document.getElementById("bundle_brand").getString("brandShorterName"),
1113 let result = confirmEx(
1125 return { remove: result === 0, report: checkboxState.value };
1128 async reportAddon(addonId, reportEntryPoint) {
1129 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1134 const win = await BrowserOpenAddonsMgr("addons://list/extension");
1136 win.openAbuseReport({ addonId, reportEntryPoint });
1139 async removeAddon(addonId, eventObject) {
1140 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1141 if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
1145 let { remove, report } = await this.promptRemoveExtension(addon);
1147 AMTelemetry.recordActionEvent({
1148 object: eventObject,
1149 action: "uninstall",
1150 value: remove ? "accepted" : "cancelled",
1155 // Leave the extension in pending uninstall if we are also reporting the
1157 await addon.uninstall(report);
1160 await this.reportAddon(addon.id, "uninstall");
1165 async manageAddon(addonId, eventObject) {
1166 let addon = addonId && (await AddonManager.getAddonByID(addonId));
1171 BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
1172 AMTelemetry.recordActionEvent({
1173 object: eventObject,
1175 extra: { addonId: addon.id },
1181 * The `unified-extensions-item` custom element is used to manage an extension
1182 * in the list of extensions, which is displayed when users click the unified
1183 * extensions (toolbar) button.
1185 * This custom element must be initialized with `setAddon()`:
1188 * let item = document.createElement("unified-extensions-item");
1189 * item.setAddon(addon);
1190 * document.body.appendChild(item);
1193 customElements.define(
1194 "unified-extensions-item",
1195 class extends HTMLElement {
1197 * Set the add-on for this item. The item will be populated based on the
1198 * add-on when it is rendered into the DOM.
1200 * @param {AddonWrapper} addon The add-on to use.
1206 connectedCallback() {
1207 if (this._openMenuButton) {
1211 const template = document.getElementById(
1212 "unified-extensions-item-template"
1214 this.appendChild(template.content.cloneNode(true));
1216 this._actionButton = this.querySelector(
1217 ".unified-extensions-item-action"
1219 this._openMenuButton = this.querySelector(
1220 ".unified-extensions-item-open-menu"
1223 this._openMenuButton.addEventListener("blur", this);
1224 this._openMenuButton.addEventListener("focus", this);
1226 this.addEventListener("command", this);
1227 this.addEventListener("mouseout", this);
1228 this.addEventListener("mouseover", this);
1233 handleEvent(event) {
1234 const { target } = event;
1236 switch (event.type) {
1238 if (target === this._openMenuButton) {
1239 const popup = target.ownerDocument.getElementById(
1240 "unified-extensions-context-menu"
1247 true /* isContextMenu */,
1248 false /* attributesOverride */,
1251 } else if (target === this._actionButton) {
1252 const extension = WebExtensionPolicy.getByID(this.addon.id)
1258 const win = event.target.ownerGlobal;
1259 const tab = win.gBrowser.selectedTab;
1261 extension.tabManager.addActiveTabPermission(tab);
1262 extension.tabManager.activateScripts(tab);
1268 if (target === this._openMenuButton) {
1269 this.removeAttribute("secondary-button-hovered");
1270 } else if (target === this._actionButton) {
1271 this._updateStateMessage();
1277 if (target === this._openMenuButton) {
1278 this.setAttribute("secondary-button-hovered", true);
1279 } else if (target === this._actionButton) {
1280 this._updateStateMessage({ hover: true });
1286 async _updateStateMessage({ hover = false } = {}) {
1287 const policy = WebExtensionPolicy.getByID(this.addon.id);
1289 const messages = lazy.OriginControls.getStateMessageIDs(
1291 this.ownerGlobal.gBrowser.currentURI
1297 const messageElement = this.querySelector(
1298 ".unified-extensions-item-message-default"
1301 // We only want to adjust the height of an item in the panel when we
1302 // first draw it, and not on hover (even if the hover message is longer,
1303 // which shouldn't happen in practice but even if it was, we don't want
1304 // to change the height on hover).
1305 let adjustMinHeight = false;
1306 if (hover && messages.onHover) {
1307 this.ownerDocument.l10n.setAttributes(messageElement, messages.onHover);
1308 } else if (messages.default) {
1309 this.ownerDocument.l10n.setAttributes(messageElement, messages.default);
1310 adjustMinHeight = true;
1313 await document.l10n.translateElements([messageElement]);
1315 if (adjustMinHeight) {
1316 const contentsElement = this.querySelector(
1317 ".unified-extensions-item-contents"
1319 const { height } = getComputedStyle(contentsElement);
1320 contentsElement.style.minHeight = height;
1325 const policy = WebExtensionPolicy.getByID(this.addon.id);
1326 const state = lazy.OriginControls.getState(
1328 this.ownerGlobal.gBrowser.currentURI
1331 return state && state.whenClicked && !state.hasAccess;
1337 "unified-extensions-item requires an add-on, forgot to call setAddon()?"
1341 this.setAttribute("extension-id", this.addon.id);
1343 let policy = WebExtensionPolicy.getByID(this.addon.id);
1346 lazy.OriginControls.getAttention(policy, this.ownerGlobal)
1350 ".unified-extensions-item-name"
1351 ).textContent = this.addon.name;
1353 const iconURL = AddonManager.getPreferredIconURL(this.addon, 32, window);
1355 this.querySelector(".unified-extensions-item-icon").setAttribute(
1361 this._actionButton.disabled = !this._hasAction();
1363 this._openMenuButton.dataset.extensionId = this.addon.id;
1364 this._openMenuButton.setAttribute(
1366 JSON.stringify({ extensionName: this.addon.name })
1369 this._updateStateMessage();
1374 // We must declare `gUnifiedExtensions` using `var` below to avoid a
1375 // "redeclaration" syntax error.
1376 var gUnifiedExtensions = {
1377 _initialized: false,
1380 if (this._initialized) {
1384 if (this.isEnabled) {
1385 MozXULElement.insertFTLIfNeeded("preview/originControls.ftl");
1386 MozXULElement.insertFTLIfNeeded("preview/unifiedExtensions.ftl");
1388 this._button = document.getElementById("unified-extensions-button");
1389 // TODO: Bug 1778684 - Auto-hide button when there is no active extension.
1390 this._button.hidden = false;
1393 .getElementById("nav-bar")
1394 .setAttribute("unifiedextensionsbuttonshown", true);
1396 gBrowser.addTabsProgressListener(this);
1397 window.addEventListener("TabSelect", () => this.updateAttention());
1399 this.permListener = () => this.updateAttention();
1400 lazy.ExtensionPermissions.addListener(this.permListener);
1403 this._initialized = true;
1407 if (this.permListener) {
1408 lazy.ExtensionPermissions.removeListener(this.permListener);
1409 this.permListener = null;
1414 return Services.prefs.getBoolPref(
1415 "extensions.unifiedExtensions.enabled",
1420 onLocationChange(browser, webProgress, _request, _uri, flags) {
1421 // Only update on top-level cross-document navigations in the selected tab.
1423 webProgress.isTopLevel &&
1424 browser === gBrowser.selectedBrowser &&
1425 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
1427 this.updateAttention();
1431 // Update the attention indicator for the whole unified extensions button.
1432 async updateAttention() {
1433 for (let addon of await this.getActiveExtensions()) {
1434 let policy = WebExtensionPolicy.getByID(addon.id);
1435 let widget = this.browserActionFor(policy)?.widget;
1437 // Only show for extensions which are not already visible in the toolbar.
1438 if (!widget || widget.areaType !== CustomizableUI.TYPE_TOOLBAR) {
1439 if (lazy.OriginControls.getAttention(policy, window)) {
1440 this.button.setAttribute("attention", true);
1445 this.button.setAttribute("attention", false);
1448 getPopupAnchorID(aBrowser, aWindow) {
1449 if (this.isEnabled) {
1450 const anchorID = "unified-extensions-button";
1451 const attr = anchorID + "popupnotificationanchor";
1453 if (!aBrowser[attr]) {
1454 // A hacky way of setting the popup anchor outside the usual url bar
1455 // icon box, similar to how it was done for CFR.
1456 // See: https://searchfox.org/mozilla-central/rev/847b64cc28b74b44c379f9bff4f415b97da1c6d7/toolkit/modules/PopupNotifications.jsm#42
1457 aBrowser[attr] = aWindow.document.getElementById(
1459 // Anchor on the toolbar icon to position the popup right below the
1461 ).firstElementChild;
1467 return "addons-notification-icon";
1471 return this._button;
1475 * Gets a list of active AddonWrapper instances of type "extension", sorted
1476 * alphabetically based on add-on's names.
1478 * @return {Array<AddonWrapper>} An array of active extensions.
1480 async getActiveExtensions() {
1481 // TODO: Bug 1778682 - Use a new method on `AddonManager` so that we get
1482 // the same list of extensions as the one in `about:addons`.
1484 // We only want to display active and visible extensions, and we want to
1485 // list them alphabetically.
1486 let addons = await AddonManager.getAddonsByTypes(["extension"]);
1487 addons = addons.filter(addon => !addon.hidden && addon.isActive);
1488 addons.sort((a1, a2) => a1.name.localeCompare(a2.name));
1493 handleEvent(event) {
1494 switch (event.type) {
1495 case "ViewShowing": {
1496 this.onPanelViewShowing(event.target);
1499 case "ViewHiding": {
1500 this.onPanelViewHiding(event.target);
1505 async onPanelViewShowing(panelview) {
1506 const list = panelview.querySelector(".unified-extensions-list");
1507 const extensions = await this.getActiveExtensions();
1509 for (const extension of extensions) {
1510 const item = document.createElement("unified-extensions-item");
1511 item.setAddon(extension);
1512 list.appendChild(item);
1516 onPanelViewHiding(panelview) {
1517 const list = panelview.querySelector(".unified-extensions-list");
1518 while (list.lastChild) {
1519 list.lastChild.remove();
1523 async togglePanel(aEvent) {
1524 if (!CustomizationHandler.isCustomizing()) {
1525 // The button should directly open `about:addons` when there is no active
1526 // extension to show in the panel.
1527 if ((await this.getActiveExtensions()).length === 0) {
1528 await BrowserOpenAddonsMgr("addons://discover/");
1532 if (!this._listView) {
1533 this._listView = PanelMultiView.getViewNode(
1535 "unified-extensions-view"
1537 this._listView.addEventListener("ViewShowing", this);
1538 this._listView.addEventListener("ViewHiding", this);
1540 // Lazy-load the l10n strings.
1542 .getElementById("unified-extensions-context-menu")
1543 .querySelectorAll("[data-lazy-l10n-id]")
1547 el.getAttribute("data-lazy-l10n-id")
1549 el.removeAttribute("data-lazy-l10n-id");
1553 if (this._button.open) {
1554 PanelMultiView.hidePopup(this._listView.closest("panel"));
1555 this._button.open = false;
1557 PanelUI.showSubView("unified-extensions-view", this._button, aEvent);
1561 // We always dispatch an event (useful for testing purposes).
1562 window.dispatchEvent(new CustomEvent("UnifiedExtensionsTogglePanel"));
1565 async updateContextMenu(menu, event) {
1566 // When the context menu is open, `onpopupshowing` is called when menu
1567 // items open sub-menus. We don't want to update the context menu in this
1569 if (event.target.id !== "unified-extensions-context-menu") {
1573 const id = this._getExtensionId(menu);
1574 const addon = await AddonManager.getAddonByID(id);
1576 const removeButton = menu.querySelector(
1577 ".unified-extensions-context-menu-remove-extension"
1579 const reportButton = menu.querySelector(
1580 ".unified-extensions-context-menu-report-extension"
1583 reportButton.hidden = !gAddonAbuseReportEnabled;
1584 removeButton.disabled = !(
1585 addon.permissions & AddonManager.PERM_CAN_UNINSTALL
1588 ExtensionsUI.originControlsMenu(menu, id);
1590 const browserAction = this.browserActionFor(WebExtensionPolicy.getByID(id));
1591 if (browserAction) {
1592 browserAction.updateContextMenu(menu);
1596 browserActionFor(policy) {
1597 // Ideally, we wouldn't do that because `browserActionFor()` will only be
1598 // defined in `global` when at least one extension has required loading the
1599 // `ext-browserAction` code.
1600 let method = lazy.ExtensionParent.apiManager.global.browserActionFor;
1601 return method?.(policy?.extension);
1604 async manageExtension(menu) {
1605 const id = this._getExtensionId(menu);
1607 await this.togglePanel();
1608 await BrowserAddonUI.manageAddon(id, "unifiedExtensions");
1611 async removeExtension(menu) {
1612 const id = this._getExtensionId(menu);
1614 await this.togglePanel();
1615 await BrowserAddonUI.removeAddon(id, "unifiedExtensions");
1618 async reportExtension(menu) {
1619 const id = this._getExtensionId(menu);
1621 await this.togglePanel();
1622 await BrowserAddonUI.reportAddon(id, "unified_context_menu");
1625 _getExtensionId(menu) {
1626 const { triggerNode } = menu;
1627 return triggerNode.dataset.extensionId;