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