PermissionUI.sys.mjs (48716B)
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 /** 6 * PermissionUI is responsible for exposing both a prototype 7 * PermissionPrompt that can be used by arbitrary browser 8 * components and add-ons, but also hosts the implementations of 9 * built-in permission prompts. 10 * 11 * If you're developing a feature that requires web content to ask 12 * for special permissions from the user, this module is for you. 13 * 14 * Suppose a system add-on wants to add a new prompt for a new request 15 * for getting more low-level access to the user's sound card, and the 16 * permission request is coming up from content by way of the 17 * nsContentPermissionHelper. The system add-on could then do the following: 18 * 19 * const { Integration } = ChromeUtils.importESModule( 20 * "resource://gre/modules/Integration.sys.mjs" 21 * ); 22 * const { PermissionUI } = ChromeUtils.importESModule( 23 * "resource:///modules/PermissionUI.sys.mjs" 24 * ); 25 * 26 * const SoundCardIntegration = base => { 27 * let soundCardObj = { 28 * createPermissionPrompt(type, request) { 29 * if (type != "sound-api") { 30 * return super.createPermissionPrompt(...arguments); 31 * } 32 * 33 * let permissionPrompt = { 34 * get permissionKey() { 35 * return "sound-permission"; 36 * } 37 * // etc - see the documentation for PermissionPrompt for 38 * // a better idea of what things one can and should override. 39 * }; 40 * Object.setPrototypeOf( 41 * permissionPrompt, 42 * PermissionUI.PermissionPromptForRequest 43 * ); 44 * return permissionPrompt; 45 * }, 46 * }; 47 * Object.setPrototypeOf(soundCardObj, base); 48 * return soundCardObj; 49 * }; 50 * 51 * // Add-on startup: 52 * Integration.contentPermission.register(SoundCardIntegration); 53 * // ... 54 * // Add-on shutdown: 55 * Integration.contentPermission.unregister(SoundCardIntegration); 56 * 57 * Note that PermissionPromptForRequest must be used as the 58 * prototype, since the prompt is wrapping an nsIContentPermissionRequest, 59 * and going through nsIContentPermissionPrompt. 60 * 61 * It is, however, possible to take advantage of PermissionPrompt without 62 * having to go through nsIContentPermissionPrompt or with a 63 * nsIContentPermissionRequest. The PermissionPrompt can be 64 * imported, subclassed, and have prompt() called directly, without 65 * the caller having called into createPermissionPrompt. 66 */ 67 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 68 69 const lazy = {}; 70 71 ChromeUtils.defineESModuleGetters(lazy, { 72 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 73 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 74 SitePermissions: "resource:///modules/SitePermissions.sys.mjs", 75 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 76 setTimeout: "resource://gre/modules/Timer.sys.mjs", 77 }); 78 79 XPCOMUtils.defineLazyServiceGetter( 80 lazy, 81 "IDNService", 82 "@mozilla.org/network/idn-service;1", 83 Ci.nsIIDNService 84 ); 85 XPCOMUtils.defineLazyServiceGetter( 86 lazy, 87 "ContentPrefService2", 88 "@mozilla.org/content-pref/service;1", 89 Ci.nsIContentPrefService2 90 ); 91 92 ChromeUtils.defineLazyGetter(lazy, "gBrandBundle", function () { 93 return Services.strings.createBundle( 94 "chrome://branding/locale/brand.properties" 95 ); 96 }); 97 98 ChromeUtils.defineLazyGetter(lazy, "gBrowserBundle", function () { 99 return Services.strings.createBundle( 100 "chrome://browser/locale/browser.properties" 101 ); 102 }); 103 104 ChromeUtils.defineLazyGetter(lazy, "gFluentStrings", function () { 105 return new Localization(["browser/permissions.ftl"], true /* aSync */); 106 }); 107 108 import { SITEPERMS_ADDON_PROVIDER_PREF } from "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs"; 109 110 XPCOMUtils.defineLazyPreferenceGetter( 111 lazy, 112 "sitePermsAddonsProviderEnabled", 113 SITEPERMS_ADDON_PROVIDER_PREF, 114 false 115 ); 116 117 XPCOMUtils.defineLazyPreferenceGetter( 118 lazy, 119 "lnaPromptTimeoutMs", 120 "network.lna.prompt.timeout", 121 300000 122 ); 123 124 /** 125 * PermissionPrompt should be subclassed by callers that 126 * want to display prompts to the user. See each method and property 127 * below for guidance on what to override. 128 * 129 * Note that if you're creating a prompt for an 130 * nsIContentPermissionRequest, you'll want to subclass 131 * PermissionPromptForRequest instead. 132 */ 133 class PermissionPrompt { 134 /** 135 * Returns the associated <xul:browser> for the request. This should 136 * work for the e10s and non-e10s case. 137 * 138 * Subclasses must override this. 139 * 140 * @return {<xul:browser>} 141 */ 142 get browser() { 143 throw new Error("Not implemented."); 144 } 145 146 /** 147 * Returns the nsIPrincipal associated with the request. 148 * 149 * Subclasses must override this. 150 * 151 * @return {nsIPrincipal} 152 */ 153 get principal() { 154 throw new Error("Not implemented."); 155 } 156 157 /** 158 * Indicates the type of the permission request from content. This type might 159 * be different from the permission key used in the permissions database. 160 */ 161 get type() { 162 return undefined; 163 } 164 165 /** 166 * If the nsIPermissionManager is being queried and written 167 * to for this permission request, set this to the key to be 168 * used. If this is undefined, no integration with temporary 169 * permissions infrastructure will be provided. 170 * 171 * Note that if a permission is set, in any follow-up 172 * prompting within the expiry window of that permission, 173 * the prompt will be skipped and the allow or deny choice 174 * will be selected automatically. 175 */ 176 get permissionKey() { 177 return undefined; 178 } 179 180 /** 181 * If true, user permissions will be read from and written to. 182 * When this is false, we still provide integration with 183 * infrastructure such as temporary permissions. permissionKey should 184 * still return a valid name in those cases for that integration to work. 185 */ 186 get usePermissionManager() { 187 return true; 188 } 189 190 /** 191 * Indicates what URI should be used as the scope when using temporary 192 * permissions. If undefined, it defaults to the browser.currentURI. 193 */ 194 get temporaryPermissionURI() { 195 return undefined; 196 } 197 198 /** 199 * These are the options that will be passed to the PopupNotification when it 200 * is shown. See the documentation of `PopupNotifications_show` in 201 * PopupNotifications.sys.mjs for details. 202 * 203 * Note that prompt() will automatically set displayURI to 204 * be the URI of the requesting pricipal, unless the displayURI is exactly 205 * set to false. 206 */ 207 get popupOptions() { 208 return {}; 209 } 210 211 /** 212 * If true, automatically denied permission requests will 213 * spawn a "post-prompt" that allows the user to correct the 214 * automatic denial by giving permanent permission access to 215 * the site. 216 * 217 * Note that if this function returns true, the permissionKey 218 * and postPromptActions attributes must be implemented. 219 */ 220 get postPromptEnabled() { 221 return false; 222 } 223 224 /** 225 * If true, the prompt will be cancelled automatically unless 226 * request.hasValidTransientUserGestureActivation is true. 227 */ 228 get requiresUserInput() { 229 return false; 230 } 231 232 /** 233 * PopupNotification requires a unique ID to open the notification. 234 * You must return a unique ID string here, for which PopupNotification 235 * will then create a <xul:popupnotification> node with the ID 236 * "<notificationID>-notification". 237 * 238 * If there's a custom <xul:popupnotification> you're hoping to show, 239 * then you need to make sure its ID has the "-notification" suffix, 240 * and then return the prefix here. 241 * 242 * See PopupNotifications.sys.mjs for more details. 243 * 244 * @return {string} 245 * The unique ID that will be used to as the 246 * "<unique ID>-notification" ID for the <xul:popupnotification> 247 * to use or create. 248 */ 249 get notificationID() { 250 throw new Error("Not implemented."); 251 } 252 253 /** 254 * The ID of the element to anchor the PopupNotification to. 255 * 256 * @return {string} 257 */ 258 get anchorID() { 259 return "default-notification-icon"; 260 } 261 262 /** 263 * The message to show to the user in the PopupNotification, see 264 * `PopupNotifications_show` in PopupNotifications.sys.mjs. 265 * 266 * Subclasses must override this. 267 * 268 * @return {string} 269 */ 270 get message() { 271 throw new Error("Not implemented."); 272 } 273 274 /** 275 * The hint text to show to the user in the PopupNotification, see 276 * `PopupNotifications_show` in PopupNotifications.sys.mjs. 277 * By default, no hint is shown. 278 * 279 * @return {string} 280 */ 281 get hintText() { 282 return undefined; 283 } 284 285 /** 286 * Provides the preferred name to use in the permission popups, 287 * based on the principal URI (the URI.hostPort for any URI scheme 288 * besides the moz-extension one which should default to the 289 * extension name). 290 */ 291 getPrincipalName(principal = this.principal) { 292 if (principal.addonPolicy) { 293 return principal.addonPolicy.name; 294 } 295 296 return principal.hostPort; 297 } 298 299 /** 300 * This will be called if the request is to be cancelled. 301 * 302 * Subclasses only need to override this if they provide a 303 * permissionKey. 304 */ 305 cancel() { 306 throw new Error("Not implemented."); 307 } 308 309 /** 310 * This will be called if the request is to be allowed. 311 * 312 * Subclasses only need to override this if they provide a 313 * permissionKey. 314 */ 315 allow() { 316 throw new Error("Not implemented."); 317 } 318 319 /** 320 * The actions that will be displayed in the PopupNotification 321 * via a dropdown menu. The first item in this array will be 322 * the default selection. Each action is an Object with the 323 * following properties: 324 * 325 * label (string): 326 * The label that will be displayed for this choice. 327 * accessKey (string): 328 * The access key character that will be used for this choice. 329 * action (SitePermissions state) 330 * The action that will be associated with this choice. 331 * This should be either SitePermissions.ALLOW or SitePermissions.BLOCK. 332 * scope (SitePermissions scope) 333 * The scope of the associated action (e.g. SitePermissions.SCOPE_PERSISTENT) 334 * 335 * callback (function, optional) 336 * A callback function that will fire if the user makes this choice, with 337 * a single parameter, state. State is an Object that contains the property 338 * checkboxChecked, which identifies whether the checkbox to remember this 339 * decision was checked. 340 */ 341 get promptActions() { 342 return []; 343 } 344 345 /** 346 * The actions that will be displayed in the PopupNotification 347 * for post-prompt notifications via a dropdown menu. 348 * The first item in this array will be the default selection. 349 * Each action is an Object with the following properties: 350 * 351 * label (string): 352 * The label that will be displayed for this choice. 353 * accessKey (string): 354 * The access key character that will be used for this choice. 355 * action (SitePermissions state) 356 * The action that will be associated with this choice. 357 * This should be either SitePermissions.ALLOW or SitePermissions.BLOCK. 358 * Note that the scope of this action will always be persistent. 359 * 360 * callback (function, optional) 361 * A callback function that will fire if the user makes this choice. 362 */ 363 get postPromptActions() { 364 return null; 365 } 366 367 /** 368 * If the prompt will be shown to the user, this callback will 369 * be called just before. Subclasses may want to override this 370 * in order to, for example, bump a counter Telemetry probe for 371 * how often a particular permission request is seen. 372 * 373 * If this returns false, it cancels the process of showing the prompt. In 374 * that case, it is the responsibility of the onBeforeShow() implementation 375 * to ensure that allow() or cancel() are called on the object appropriately. 376 */ 377 onBeforeShow() { 378 return true; 379 } 380 381 /** 382 * If the prompt was shown to the user, this callback will be called just 383 * after it's been shown. 384 */ 385 onShown() {} 386 387 /** 388 * If the prompt was shown to the user, this callback will be called just 389 * after it's been hidden. 390 */ 391 onAfterShow() {} 392 393 /** 394 * Will determine if a prompt should be shown to the user, and if so, 395 * will show it. 396 * 397 * If a permissionKey is defined prompt() might automatically 398 * allow or cancel itself based on the user's current 399 * permission settings without displaying the prompt. 400 * 401 * If the permission is not already set and the <xul:browser> that the request 402 * is associated with does not belong to a browser window with the 403 * PopupNotifications global set, the prompt request is ignored. 404 */ 405 prompt() { 406 // We ignore requests from non-nsIStandardURLs 407 let requestingURI = this.principal.URI; 408 if (!(requestingURI instanceof Ci.nsIStandardURL)) { 409 return; 410 } 411 412 if (this.usePermissionManager && this.permissionKey) { 413 // If we're reading and setting permissions, then we need 414 // to check to see if we already have a permission setting 415 // for this particular principal. 416 let { state } = lazy.SitePermissions.getForPrincipal( 417 this.principal, 418 this.permissionKey, 419 this.browser, 420 this.temporaryPermissionURI 421 ); 422 423 if (state == lazy.SitePermissions.BLOCK) { 424 this.cancel(); 425 return; 426 } 427 428 if ( 429 state == lazy.SitePermissions.ALLOW && 430 !this.request.isRequestDelegatedToUnsafeThirdParty 431 ) { 432 this.allow(); 433 return; 434 } 435 } else if (this.permissionKey) { 436 // If we're reading a permission which already has a temporary value, 437 // see if we can use the temporary value. 438 let { state } = lazy.SitePermissions.getForPrincipal( 439 null, 440 this.permissionKey, 441 this.browser, 442 this.temporaryPermissionURI 443 ); 444 445 if (state == lazy.SitePermissions.BLOCK) { 446 this.cancel(); 447 return; 448 } 449 } 450 451 if ( 452 this.requiresUserInput && 453 !this.request.hasValidTransientUserGestureActivation 454 ) { 455 if (this.postPromptEnabled) { 456 this.postPrompt(); 457 } 458 this.cancel(); 459 return; 460 } 461 462 let chromeWin = this.browser.ownerGlobal; 463 if (!chromeWin.PopupNotifications) { 464 this.cancel(); 465 return; 466 } 467 468 // Transform the PermissionPrompt actions into PopupNotification actions. 469 let popupNotificationActions = []; 470 for (let promptAction of this.promptActions) { 471 let action = { 472 label: promptAction.label, 473 accessKey: promptAction.accessKey, 474 callback: state => { 475 if (promptAction.callback) { 476 promptAction.callback(); 477 } 478 479 if (this.usePermissionManager && this.permissionKey) { 480 if ( 481 (state && state.checkboxChecked && state.source != "esc-press") || 482 promptAction.scope == lazy.SitePermissions.SCOPE_PERSISTENT 483 ) { 484 // Permanently store permission. 485 let scope = lazy.SitePermissions.SCOPE_PERSISTENT; 486 // Only remember permission for session if in PB mode. 487 if (lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser)) { 488 scope = lazy.SitePermissions.SCOPE_SESSION; 489 } 490 lazy.SitePermissions.setForPrincipal( 491 this.principal, 492 this.permissionKey, 493 promptAction.action, 494 scope 495 ); 496 } else { 497 lazy.SitePermissions.setForPrincipal( 498 this.principal, 499 this.permissionKey, 500 promptAction.action, 501 lazy.SitePermissions.SCOPE_TEMPORARY, 502 this.browser 503 ); 504 } 505 506 // Grant permission if action is ALLOW. 507 if (promptAction.action == lazy.SitePermissions.ALLOW) { 508 this.allow(); 509 } else { 510 this.cancel(); 511 } 512 } else if (this.permissionKey) { 513 lazy.SitePermissions.setForPrincipal( 514 null, 515 this.permissionKey, 516 promptAction.action, 517 lazy.SitePermissions.SCOPE_TEMPORARY, 518 this.browser 519 ); 520 } 521 }, 522 }; 523 if (promptAction.dismiss) { 524 action.dismiss = promptAction.dismiss; 525 } 526 527 popupNotificationActions.push(action); 528 } 529 530 this.#showNotification(popupNotificationActions); 531 } 532 533 postPrompt() { 534 let browser = this.browser; 535 let principal = this.principal; 536 let chromeWin = browser.ownerGlobal; 537 if (!chromeWin.PopupNotifications) { 538 return; 539 } 540 541 if (!this.permissionKey) { 542 throw new Error("permissionKey is required to show a post-prompt"); 543 } 544 545 if (!this.postPromptActions) { 546 throw new Error("postPromptActions are required to show a post-prompt"); 547 } 548 549 // Transform the PermissionPrompt actions into PopupNotification actions. 550 let popupNotificationActions = []; 551 for (let promptAction of this.postPromptActions) { 552 let action = { 553 label: promptAction.label, 554 accessKey: promptAction.accessKey, 555 callback: () => { 556 if (promptAction.callback) { 557 promptAction.callback(); 558 } 559 560 // Post-prompt permissions are stored permanently by default. 561 // Since we can not reply to the original permission request anymore, 562 // the page will need to listen for permission changes which are triggered 563 // by permanent entries in the permission manager. 564 let scope = lazy.SitePermissions.SCOPE_PERSISTENT; 565 // Only remember permission for session if in PB mode. 566 if (lazy.PrivateBrowsingUtils.isBrowserPrivate(browser)) { 567 scope = lazy.SitePermissions.SCOPE_SESSION; 568 } 569 lazy.SitePermissions.setForPrincipal( 570 principal, 571 this.permissionKey, 572 promptAction.action, 573 scope 574 ); 575 }, 576 }; 577 popupNotificationActions.push(action); 578 } 579 580 // Post-prompt animation 581 if (!chromeWin.gReduceMotion) { 582 let anchor = chromeWin.document.getElementById(this.anchorID); 583 // Only show the animation on the first request, not after e.g. tab switching. 584 anchor.addEventListener( 585 "animationend", 586 () => anchor.removeAttribute("animate"), 587 { once: true } 588 ); 589 anchor.setAttribute("animate", "true"); 590 } 591 592 this.#showNotification(popupNotificationActions, true); 593 } 594 595 #showNotification(actions, postPrompt = false) { 596 let chromeWin = this.browser.ownerGlobal; 597 let mainAction = actions.length ? actions[0] : null; 598 let secondaryActions = actions.splice(1); 599 600 let options = this.popupOptions; 601 602 if (!options.hasOwnProperty("displayURI") || options.displayURI) { 603 options.displayURI = this.principal.URI; 604 } 605 606 if (!postPrompt) { 607 // Permission prompts are always persistent; the close button is controlled by a pref. 608 options.persistent = true; 609 options.hideClose = true; 610 } 611 612 options.eventCallback = (topic, nextRemovalReason, isCancel) => { 613 // When the docshell of the browser is aboout to be swapped to another one, 614 // the "swapping" event is called. Returning true causes the notification 615 // to be moved to the new browser. 616 if (topic == "swapping") { 617 return true; 618 } 619 // The prompt has been shown, notify the PermissionUI. 620 // onShown() is currently not called for post-prompts, 621 // because there is no prompt that would make use of this. 622 // You can remove this restriction if you need it, but be 623 // mindful of other consumers. 624 if (topic == "shown" && !postPrompt) { 625 this.onShown(); 626 } 627 // The prompt has been removed, notify the PermissionUI. 628 // onAfterShow() is currently not called for post-prompts, 629 // because there is no prompt that would make use of this. 630 // You can remove this restriction if you need it, but be 631 // mindful of other consumers. 632 if (topic == "removed" && !postPrompt) { 633 if (isCancel) { 634 this.cancel(); 635 } 636 this.onAfterShow(); 637 } 638 return false; 639 }; 640 641 options.hintText = this.hintText; 642 // Post-prompts show up as dismissed. 643 options.dismissed = postPrompt; 644 645 // onBeforeShow() is currently not called for post-prompts, 646 // because there is no prompt that would make use of this. 647 // You can remove this restriction if you need it, but be 648 // mindful of other consumers. 649 if (postPrompt || this.onBeforeShow() !== false) { 650 chromeWin.PopupNotifications.show( 651 this.browser, 652 this.notificationID, 653 this.message, 654 this.anchorID, 655 mainAction, 656 secondaryActions, 657 options 658 ); 659 } 660 } 661 } 662 663 /** 664 * A subclass of PermissionPrompt that assumes 665 * that this.request is an nsIContentPermissionRequest 666 * and fills in some of the required properties on the 667 * PermissionPrompt. For callers that are wrapping an 668 * nsIContentPermissionRequest, this should be subclassed 669 * rather than PermissionPrompt. 670 */ 671 class PermissionPromptForRequest extends PermissionPrompt { 672 get browser() { 673 // In the e10s-case, the <xul:browser> will be at request.element. 674 // In the single-process case, we have to use some XPCOM incantations 675 // to resolve to the <xul:browser>. 676 if (this.request.element) { 677 return this.request.element; 678 } 679 return this.request.window.docShell.chromeEventHandler; 680 } 681 682 get principal() { 683 let request = this.request.QueryInterface(Ci.nsIContentPermissionRequest); 684 return request.getDelegatePrincipal(this.type); 685 } 686 687 cancel() { 688 this.request.cancel(); 689 } 690 691 allow(choices) { 692 this.request.allow(choices); 693 } 694 } 695 696 /** 697 * A subclass of PermissionPromptForRequest that prompts 698 * for a Synthetic SitePermsAddon addon type and starts a synthetic 699 * addon install flow. 700 */ 701 class SitePermsAddonInstallRequest extends PermissionPromptForRequest { 702 prompt() { 703 // fallback to regular permission prompt for localhost, 704 // or when the SitePermsAddonProvider is not enabled. 705 if (this.principal.isLoopbackHost || !lazy.sitePermsAddonsProviderEnabled) { 706 super.prompt(); 707 return; 708 } 709 710 // Otherwise, we'll use the addon install flow. 711 lazy.AddonManager.installSitePermsAddonFromWebpage( 712 this.browser, 713 this.principal, 714 this.permName 715 ).then( 716 () => { 717 this.allow(); 718 }, 719 err => { 720 this.cancel(); 721 722 // Print an error message in the console to give more information to the developer. 723 let scriptErrorClass = Cc["@mozilla.org/scripterror;1"]; 724 let errorMessage = 725 this.getInstallErrorMessage(err) || 726 `${this.permName} access was rejected: ${err.message}`; 727 728 let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError); 729 scriptError.initWithWindowID( 730 errorMessage, 731 null, 732 0, 733 0, 734 0, 735 "content javascript", 736 this.browser.browsingContext.currentWindowGlobal.innerWindowId 737 ); 738 Services.console.logMessage(scriptError); 739 } 740 ); 741 } 742 743 /** 744 * Returns an error message that will be printed to the console given a passed Component.Exception. 745 * This should be overriden by children classes. 746 * 747 * @param {Components.Exception} err 748 * @returns {string} The error message 749 */ 750 getInstallErrorMessage() { 751 return null; 752 } 753 } 754 755 /** 756 * Creates a PermissionPrompt for a nsIContentPermissionRequest for 757 * the GeoLocation API. 758 * 759 * @param request (nsIContentPermissionRequest) 760 * The request for a permission from content. 761 */ 762 class GeolocationPermissionPrompt extends PermissionPromptForRequest { 763 constructor(request) { 764 super(); 765 this.request = request; 766 let types = request.types.QueryInterface(Ci.nsIArray); 767 let perm = types.queryElementAt(0, Ci.nsIContentPermissionType); 768 if (perm.options.length) { 769 this.systemPermissionMsg = perm.options.queryElementAt( 770 0, 771 Ci.nsISupportsString 772 ); 773 } 774 } 775 776 get type() { 777 return "geo"; 778 } 779 780 get permissionKey() { 781 return "geo"; 782 } 783 784 get popupOptions() { 785 let pref = "browser.geolocation.warning.infoURL"; 786 let options = { 787 learnMoreURL: Services.urlFormatter.formatURLPref(pref), 788 displayURI: false, 789 name: this.getPrincipalName(), 790 }; 791 792 // Don't offer "always remember" action in PB mode 793 options.checkbox = { 794 show: !lazy.PrivateBrowsingUtils.isWindowPrivate( 795 this.browser.ownerGlobal 796 ), 797 }; 798 799 if (this.request.isRequestDelegatedToUnsafeThirdParty) { 800 // Second name should be the third party origin 801 options.secondName = this.getPrincipalName(this.request.principal); 802 options.checkbox = { show: false }; 803 } 804 805 if (options.checkbox.show) { 806 options.checkbox.label = lazy.gBrowserBundle.GetStringFromName( 807 "geolocation.remember" 808 ); 809 } 810 811 return options; 812 } 813 814 get notificationID() { 815 return "geolocation"; 816 } 817 818 get anchorID() { 819 return "geo-notification-icon"; 820 } 821 822 get message() { 823 if (this.principal.schemeIs("file")) { 824 return lazy.gBrowserBundle.GetStringFromName( 825 "geolocation.shareWithFile4" 826 ); 827 } 828 829 if (this.request.isRequestDelegatedToUnsafeThirdParty) { 830 return lazy.gBrowserBundle.formatStringFromName( 831 "geolocation.shareWithSiteUnsafeDelegation2", 832 ["<>", "{}"] 833 ); 834 } 835 836 return lazy.gBrowserBundle.formatStringFromName( 837 "geolocation.shareWithSite4", 838 ["<>"] 839 ); 840 } 841 842 get hintText() { 843 let productName = lazy.gBrandBundle.GetStringFromName("brandShortName"); 844 845 if (this.systemPermissionMsg == "sysdlg") { 846 return lazy.gBrowserBundle.formatStringFromName( 847 "geolocation.systemWillRequestPermission", 848 [productName] 849 ); 850 } 851 852 if (this.systemPermissionMsg == "syssetting") { 853 return lazy.gBrowserBundle.formatStringFromName( 854 "geolocation.needsSystemSetting", 855 [productName] 856 ); 857 } 858 859 return undefined; 860 } 861 862 get promptActions() { 863 return [ 864 { 865 label: lazy.gBrowserBundle.GetStringFromName("geolocation.allow"), 866 accessKey: lazy.gBrowserBundle.GetStringFromName( 867 "geolocation.allow.accesskey" 868 ), 869 action: lazy.SitePermissions.ALLOW, 870 }, 871 { 872 label: lazy.gBrowserBundle.GetStringFromName("geolocation.block"), 873 accessKey: lazy.gBrowserBundle.GetStringFromName( 874 "geolocation.block.accesskey" 875 ), 876 action: lazy.SitePermissions.BLOCK, 877 }, 878 ]; 879 } 880 881 #updateGeoSharing(state) { 882 let gBrowser = this.browser.ownerGlobal.gBrowser; 883 if (gBrowser == null) { 884 return; 885 } 886 gBrowser.updateBrowserSharing(this.browser, { geo: state }); 887 888 // Update last access timestamp 889 let host; 890 try { 891 host = this.browser.currentURI.host; 892 } catch (e) { 893 return; 894 } 895 if (host == null || host == "") { 896 return; 897 } 898 lazy.ContentPrefService2.set( 899 this.browser.currentURI.host, 900 "permissions.geoLocation.lastAccess", 901 new Date().toString(), 902 this.browser.loadContext 903 ); 904 } 905 906 allow(...args) { 907 this.#updateGeoSharing(true); 908 super.allow(...args); 909 } 910 911 cancel(...args) { 912 this.#updateGeoSharing(false); 913 super.cancel(...args); 914 } 915 } 916 917 /** 918 * Creates a PermissionPrompt for a nsIContentPermissionRequest for 919 * the WebXR API. 920 * 921 * @param request (nsIContentPermissionRequest) 922 * The request for a permission from content. 923 */ 924 class XRPermissionPrompt extends PermissionPromptForRequest { 925 constructor(request) { 926 super(); 927 this.request = request; 928 } 929 930 get type() { 931 return "xr"; 932 } 933 934 get permissionKey() { 935 return "xr"; 936 } 937 938 get popupOptions() { 939 let pref = "browser.xr.warning.infoURL"; 940 let options = { 941 learnMoreURL: Services.urlFormatter.formatURLPref(pref), 942 displayURI: false, 943 name: this.getPrincipalName(), 944 }; 945 946 // Don't offer "always remember" action in PB mode 947 options.checkbox = { 948 show: !lazy.PrivateBrowsingUtils.isWindowPrivate( 949 this.browser.ownerGlobal 950 ), 951 }; 952 953 if (options.checkbox.show) { 954 options.checkbox.label = 955 lazy.gBrowserBundle.GetStringFromName("xr.remember"); 956 } 957 958 return options; 959 } 960 961 get notificationID() { 962 return "xr"; 963 } 964 965 get anchorID() { 966 return "xr-notification-icon"; 967 } 968 969 get message() { 970 if (this.principal.schemeIs("file")) { 971 return lazy.gBrowserBundle.GetStringFromName("xr.shareWithFile4"); 972 } 973 974 return lazy.gBrowserBundle.formatStringFromName("xr.shareWithSite4", [ 975 "<>", 976 ]); 977 } 978 979 get promptActions() { 980 return [ 981 { 982 label: lazy.gBrowserBundle.GetStringFromName("xr.allow2"), 983 accessKey: lazy.gBrowserBundle.GetStringFromName("xr.allow2.accesskey"), 984 action: lazy.SitePermissions.ALLOW, 985 }, 986 { 987 label: lazy.gBrowserBundle.GetStringFromName("xr.block"), 988 accessKey: lazy.gBrowserBundle.GetStringFromName("xr.block.accesskey"), 989 action: lazy.SitePermissions.BLOCK, 990 }, 991 ]; 992 } 993 994 #updateXRSharing(state) { 995 let gBrowser = this.browser.ownerGlobal.gBrowser; 996 if (gBrowser == null) { 997 return; 998 } 999 gBrowser.updateBrowserSharing(this.browser, { xr: state }); 1000 1001 let devicePermOrigins = this.browser.getDevicePermissionOrigins("xr"); 1002 if (!state) { 1003 devicePermOrigins.delete(this.principal.origin); 1004 return; 1005 } 1006 devicePermOrigins.add(this.principal.origin); 1007 } 1008 1009 allow(...args) { 1010 this.#updateXRSharing(true); 1011 super.allow(...args); 1012 } 1013 1014 cancel(...args) { 1015 this.#updateXRSharing(false); 1016 super.cancel(...args); 1017 } 1018 } 1019 1020 /** 1021 * Base class for Local Network Access (LNA) permission prompts. 1022 * Provides automatic timeout handling for LNA prompts. 1023 * 1024 * If the user doesn't respond to the prompt within the timeout period, 1025 * the prompt is automatically cancelled and the network request fails. 1026 */ 1027 class LNAPermissionPromptBase extends PermissionPromptForRequest { 1028 static DEFAULT_PROMPT_TIMEOUT_MS = 300000; 1029 1030 #timeoutTimer = null; 1031 1032 constructor(request) { 1033 super(); 1034 this.request = request; 1035 } 1036 1037 onBeforeShow() { 1038 // Notify LNAPermissionRequest that the prompt is being shown. 1039 // This triggers telemetry recording and notifies nsHttpChannel. 1040 if (typeof this.request.notifyShown === "function") { 1041 this.request.notifyShown(); 1042 } 1043 return true; 1044 } 1045 1046 onShown() { 1047 this.#startTimeoutTimer(); 1048 } 1049 1050 onAfterShow() { 1051 this.#clearTimeoutTimer(); 1052 } 1053 1054 cancel() { 1055 super.cancel(); 1056 } 1057 1058 allow(choices) { 1059 super.allow(choices); 1060 } 1061 1062 #startTimeoutTimer() { 1063 this.#clearTimeoutTimer(); 1064 1065 this.#timeoutTimer = lazy.setTimeout(() => { 1066 let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( 1067 Ci.nsIScriptError 1068 ); 1069 scriptError.initWithWindowID( 1070 `LNA permission prompt timed out after ${lazy.lnaPromptTimeoutMs / 1000} seconds`, 1071 null, 1072 0, 1073 0, 1074 Ci.nsIScriptError.warningFlag, 1075 "content javascript", 1076 this.browser.browsingContext.currentWindowGlobal.innerWindowId 1077 ); 1078 Services.console.logMessage(scriptError); 1079 1080 this.#removePrompt(); 1081 this.cancel(); 1082 }, lazy.lnaPromptTimeoutMs); 1083 } 1084 1085 #removePrompt() { 1086 let chromeWin = this.browser?.ownerGlobal; 1087 let notification = chromeWin?.PopupNotifications.getNotification( 1088 this.notificationID, 1089 this.browser 1090 ); 1091 if (notification) { 1092 chromeWin.PopupNotifications.remove(notification); 1093 } 1094 } 1095 1096 #clearTimeoutTimer() { 1097 if (this.#timeoutTimer) { 1098 lazy.clearTimeout(this.#timeoutTimer); 1099 this.#timeoutTimer = null; 1100 } 1101 } 1102 } 1103 1104 /** 1105 * Creates a PermissionPrompt for a nsIContentPermissionRequest for 1106 * the Local Host Access. 1107 * 1108 * @param request (nsIContentPermissionRequest) 1109 * The request for a permission from content. 1110 */ 1111 class LocalHostPermissionPrompt extends LNAPermissionPromptBase { 1112 get type() { 1113 return "localhost"; 1114 } 1115 1116 get permissionKey() { 1117 return "localhost"; 1118 } 1119 1120 get popupOptions() { 1121 let options = { 1122 learnMoreURL: Services.urlFormatter.formatURLPref( 1123 "browser.lna.warning.infoURL" 1124 ), 1125 displayURI: false, 1126 name: this.getPrincipalName(), 1127 }; 1128 1129 // Don't offer "always remember" action in PB mode 1130 options.checkbox = { 1131 show: !lazy.PrivateBrowsingUtils.isWindowPrivate( 1132 this.browser.ownerGlobal 1133 ), 1134 }; 1135 1136 if (this.request.isRequestDelegatedToUnsafeThirdParty) { 1137 // Second name should be the third party origin 1138 options.secondName = this.getPrincipalName(this.request.principal); 1139 options.checkbox = { show: false }; 1140 } 1141 1142 if (options.checkbox.show) { 1143 options.checkbox.label = lazy.gBrowserBundle.GetStringFromName( 1144 "localhost.remember2" 1145 ); 1146 } 1147 1148 return options; 1149 } 1150 1151 get notificationID() { 1152 return "localhost"; 1153 } 1154 1155 get anchorID() { 1156 return "localhost-notification-icon"; 1157 } 1158 1159 get message() { 1160 return lazy.gBrowserBundle.formatStringFromName( 1161 "localhost.allowWithSite2", 1162 ["<>"] 1163 ); 1164 } 1165 1166 get promptActions() { 1167 return [ 1168 { 1169 label: lazy.gBrowserBundle.GetStringFromName("localhost.allowlabel"), 1170 accessKey: lazy.gBrowserBundle.GetStringFromName( 1171 "localhost.allow.accesskey" 1172 ), 1173 action: lazy.SitePermissions.ALLOW, 1174 }, 1175 { 1176 label: lazy.gBrowserBundle.GetStringFromName("localhost.blocklabel"), 1177 accessKey: lazy.gBrowserBundle.GetStringFromName( 1178 "localhost.block.accesskey" 1179 ), 1180 action: lazy.SitePermissions.BLOCK, 1181 }, 1182 ]; 1183 } 1184 } 1185 1186 /** 1187 * Creates a PermissionPrompt for a nsIContentPermissionRequest for 1188 * the Desktop Notification API. 1189 * 1190 * @param request (nsIContentPermissionRequest) 1191 * The request for a permission from content. 1192 * @return {PermissionPrompt} (see documentation in header) 1193 */ 1194 class DesktopNotificationPermissionPrompt extends PermissionPromptForRequest { 1195 constructor(request) { 1196 super(); 1197 this.request = request; 1198 1199 XPCOMUtils.defineLazyPreferenceGetter( 1200 this, 1201 "requiresUserInput", 1202 "dom.webnotifications.requireuserinteraction" 1203 ); 1204 XPCOMUtils.defineLazyPreferenceGetter( 1205 this, 1206 "postPromptEnabled", 1207 "permissions.desktop-notification.postPrompt.enabled" 1208 ); 1209 XPCOMUtils.defineLazyPreferenceGetter( 1210 this, 1211 "notNowEnabled", 1212 "permissions.desktop-notification.notNow.enabled" 1213 ); 1214 } 1215 1216 get type() { 1217 return "desktop-notification"; 1218 } 1219 1220 get permissionKey() { 1221 return "desktop-notification"; 1222 } 1223 1224 get popupOptions() { 1225 let learnMoreURL = 1226 Services.urlFormatter.formatURLPref("app.support.baseURL") + "push"; 1227 1228 return { 1229 learnMoreURL, 1230 displayURI: false, 1231 name: this.getPrincipalName(), 1232 }; 1233 } 1234 1235 get notificationID() { 1236 return "web-notifications"; 1237 } 1238 1239 get anchorID() { 1240 return "web-notifications-notification-icon"; 1241 } 1242 1243 get message() { 1244 return lazy.gBrowserBundle.formatStringFromName( 1245 "webNotifications.receiveFromSite3", 1246 ["<>"] 1247 ); 1248 } 1249 1250 get promptActions() { 1251 let actions = [ 1252 { 1253 label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"), 1254 accessKey: lazy.gBrowserBundle.GetStringFromName( 1255 "webNotifications.allow2.accesskey" 1256 ), 1257 action: lazy.SitePermissions.ALLOW, 1258 scope: lazy.SitePermissions.SCOPE_PERSISTENT, 1259 }, 1260 ]; 1261 if (this.notNowEnabled) { 1262 actions.push({ 1263 label: lazy.gBrowserBundle.GetStringFromName("webNotifications.notNow"), 1264 accessKey: lazy.gBrowserBundle.GetStringFromName( 1265 "webNotifications.notNow.accesskey" 1266 ), 1267 action: lazy.SitePermissions.BLOCK, 1268 }); 1269 } 1270 1271 let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( 1272 this.browser 1273 ); 1274 actions.push({ 1275 label: isBrowserPrivate 1276 ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block") 1277 : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"), 1278 accessKey: isBrowserPrivate 1279 ? lazy.gBrowserBundle.GetStringFromName( 1280 "webNotifications.block.accesskey" 1281 ) 1282 : lazy.gBrowserBundle.GetStringFromName( 1283 "webNotifications.alwaysBlock.accesskey" 1284 ), 1285 action: lazy.SitePermissions.BLOCK, 1286 scope: isBrowserPrivate 1287 ? lazy.SitePermissions.SCOPE_SESSION 1288 : lazy.SitePermissions.SCOPE_PERSISTENT, 1289 }); 1290 return actions; 1291 } 1292 1293 get postPromptActions() { 1294 let actions = [ 1295 { 1296 label: lazy.gBrowserBundle.GetStringFromName("webNotifications.allow2"), 1297 accessKey: lazy.gBrowserBundle.GetStringFromName( 1298 "webNotifications.allow2.accesskey" 1299 ), 1300 action: lazy.SitePermissions.ALLOW, 1301 }, 1302 ]; 1303 1304 let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( 1305 this.browser 1306 ); 1307 actions.push({ 1308 label: isBrowserPrivate 1309 ? lazy.gBrowserBundle.GetStringFromName("webNotifications.block") 1310 : lazy.gBrowserBundle.GetStringFromName("webNotifications.alwaysBlock"), 1311 accessKey: isBrowserPrivate 1312 ? lazy.gBrowserBundle.GetStringFromName( 1313 "webNotifications.block.accesskey" 1314 ) 1315 : lazy.gBrowserBundle.GetStringFromName( 1316 "webNotifications.alwaysBlock.accesskey" 1317 ), 1318 action: lazy.SitePermissions.BLOCK, 1319 }); 1320 return actions; 1321 } 1322 } 1323 1324 /** 1325 * Creates a PermissionPrompt for a nsIContentPermissionRequest for 1326 * the Local Network Access. 1327 * 1328 * @param request (nsIContentPermissionRequest) 1329 * The request for a permission from content. 1330 */ 1331 class LocalNetworkPermissionPrompt extends LNAPermissionPromptBase { 1332 get type() { 1333 return "local-network"; 1334 } 1335 1336 get permissionKey() { 1337 return "local-network"; 1338 } 1339 1340 get popupOptions() { 1341 let options = { 1342 learnMoreURL: Services.urlFormatter.formatURLPref( 1343 "browser.lna.warning.infoURL" 1344 ), 1345 displayURI: false, 1346 name: this.getPrincipalName(), 1347 }; 1348 1349 // Don't offer "always remember" action in PB mode 1350 options.checkbox = { 1351 show: !lazy.PrivateBrowsingUtils.isWindowPrivate( 1352 this.browser.ownerGlobal 1353 ), 1354 }; 1355 1356 if (this.request.isRequestDelegatedToUnsafeThirdParty) { 1357 // Second name should be the third party origin 1358 options.secondName = this.getPrincipalName(this.request.principal); 1359 options.checkbox = { show: false }; 1360 } 1361 1362 if (options.checkbox.show) { 1363 options.checkbox.label = lazy.gBrowserBundle.GetStringFromName( 1364 "localNetwork.remember2" 1365 ); 1366 } 1367 1368 return options; 1369 } 1370 1371 get notificationID() { 1372 return "local-network"; 1373 } 1374 1375 get anchorID() { 1376 return "local-network-notification-icon"; 1377 } 1378 1379 get message() { 1380 return lazy.gBrowserBundle.formatStringFromName( 1381 "localNetwork.allowWithSite2", 1382 ["<>"] 1383 ); 1384 } 1385 1386 get promptActions() { 1387 return [ 1388 { 1389 label: lazy.gBrowserBundle.GetStringFromName("localNetwork.allowLabel"), 1390 accessKey: lazy.gBrowserBundle.GetStringFromName( 1391 "localNetwork.allow.accesskey" 1392 ), 1393 action: lazy.SitePermissions.ALLOW, 1394 }, 1395 { 1396 label: lazy.gBrowserBundle.GetStringFromName("localNetwork.blockLabel"), 1397 accessKey: lazy.gBrowserBundle.GetStringFromName( 1398 "localNetwork.block.accesskey" 1399 ), 1400 action: lazy.SitePermissions.BLOCK, 1401 }, 1402 ]; 1403 } 1404 } 1405 /** 1406 * Creates a PermissionPrompt for a nsIContentPermissionRequest for 1407 * the persistent-storage API. 1408 * 1409 * @param request (nsIContentPermissionRequest) 1410 * The request for a permission from content. 1411 */ 1412 class PersistentStoragePermissionPrompt extends PermissionPromptForRequest { 1413 constructor(request) { 1414 super(); 1415 this.request = request; 1416 } 1417 1418 get type() { 1419 return "persistent-storage"; 1420 } 1421 1422 get permissionKey() { 1423 return "persistent-storage"; 1424 } 1425 1426 get popupOptions() { 1427 let learnMoreURL = 1428 Services.urlFormatter.formatURLPref("app.support.baseURL") + 1429 "storage-permissions"; 1430 let options = { 1431 learnMoreURL, 1432 displayURI: false, 1433 name: this.getPrincipalName(), 1434 }; 1435 1436 options.checkbox = { 1437 show: !lazy.PrivateBrowsingUtils.isWindowPrivate( 1438 this.browser.ownerGlobal 1439 ), 1440 }; 1441 1442 if (options.checkbox.show) { 1443 options.checkbox.label = lazy.gFluentStrings.formatValueSync( 1444 "perm-persistent-storage-remember" 1445 ); 1446 } 1447 1448 return options; 1449 } 1450 1451 get notificationID() { 1452 return "persistent-storage"; 1453 } 1454 1455 get anchorID() { 1456 return "persistent-storage-notification-icon"; 1457 } 1458 1459 get message() { 1460 return lazy.gBrowserBundle.formatStringFromName( 1461 "persistentStorage.allowWithSite2", 1462 ["<>"] 1463 ); 1464 } 1465 1466 get promptActions() { 1467 return [ 1468 { 1469 label: lazy.gBrowserBundle.GetStringFromName("persistentStorage.allow"), 1470 accessKey: lazy.gBrowserBundle.GetStringFromName( 1471 "persistentStorage.allow.accesskey" 1472 ), 1473 action: Ci.nsIPermissionManager.ALLOW_ACTION, 1474 scope: lazy.SitePermissions.SCOPE_PERSISTENT, 1475 }, 1476 { 1477 label: lazy.gBrowserBundle.GetStringFromName( 1478 "persistentStorage.block.label" 1479 ), 1480 accessKey: lazy.gBrowserBundle.GetStringFromName( 1481 "persistentStorage.block.accesskey" 1482 ), 1483 action: lazy.SitePermissions.BLOCK, 1484 }, 1485 ]; 1486 } 1487 } 1488 1489 /** 1490 * Creates a PermissionPrompt for a nsIContentPermissionRequest for 1491 * the WebMIDI API. 1492 * 1493 * @param request (nsIContentPermissionRequest) 1494 * The request for a permission from content. 1495 */ 1496 class MIDIPermissionPrompt extends SitePermsAddonInstallRequest { 1497 constructor(request) { 1498 super(); 1499 this.request = request; 1500 let types = request.types.QueryInterface(Ci.nsIArray); 1501 let perm = types.queryElementAt(0, Ci.nsIContentPermissionType); 1502 this.isSysexPerm = 1503 !!perm.options.length && 1504 perm.options.queryElementAt(0, Ci.nsISupportsString) == "sysex"; 1505 this.permName = "midi"; 1506 if (this.isSysexPerm) { 1507 this.permName = "midi-sysex"; 1508 } 1509 } 1510 1511 get type() { 1512 return "midi"; 1513 } 1514 1515 get permissionKey() { 1516 return this.permName; 1517 } 1518 1519 get popupOptions() { 1520 // TODO (bug 1433235) We need a security/permissions explanation URL for this 1521 let options = { 1522 displayURI: false, 1523 name: this.getPrincipalName(), 1524 }; 1525 1526 // Don't offer "always remember" action in PB mode 1527 options.checkbox = { 1528 show: !lazy.PrivateBrowsingUtils.isWindowPrivate( 1529 this.browser.ownerGlobal 1530 ), 1531 }; 1532 1533 if (options.checkbox.show) { 1534 options.checkbox.label = 1535 lazy.gBrowserBundle.GetStringFromName("midi.remember"); 1536 } 1537 1538 return options; 1539 } 1540 1541 get notificationID() { 1542 return "midi"; 1543 } 1544 1545 get anchorID() { 1546 return "midi-notification-icon"; 1547 } 1548 1549 get message() { 1550 let message; 1551 if (this.principal.schemeIs("file")) { 1552 if (this.isSysexPerm) { 1553 message = lazy.gBrowserBundle.GetStringFromName( 1554 "midi.shareSysexWithFile" 1555 ); 1556 } else { 1557 message = lazy.gBrowserBundle.GetStringFromName("midi.shareWithFile"); 1558 } 1559 } else if (this.isSysexPerm) { 1560 message = lazy.gBrowserBundle.formatStringFromName( 1561 "midi.shareSysexWithSite", 1562 ["<>"] 1563 ); 1564 } else { 1565 message = lazy.gBrowserBundle.formatStringFromName("midi.shareWithSite", [ 1566 "<>", 1567 ]); 1568 } 1569 return message; 1570 } 1571 1572 get promptActions() { 1573 return [ 1574 { 1575 label: lazy.gBrowserBundle.GetStringFromName("midi.allow.label"), 1576 accessKey: lazy.gBrowserBundle.GetStringFromName( 1577 "midi.allow.accesskey" 1578 ), 1579 action: Ci.nsIPermissionManager.ALLOW_ACTION, 1580 }, 1581 { 1582 label: lazy.gBrowserBundle.GetStringFromName("midi.block.label"), 1583 accessKey: lazy.gBrowserBundle.GetStringFromName( 1584 "midi.block.accesskey" 1585 ), 1586 action: Ci.nsIPermissionManager.DENY_ACTION, 1587 }, 1588 ]; 1589 } 1590 1591 /** 1592 * @override 1593 * @param {Components.Exception} err 1594 * @returns {string} 1595 */ 1596 getInstallErrorMessage(err) { 1597 return `WebMIDI access request was denied: ❝${err.message}❞. See https://developer.mozilla.org/docs/Web/API/Navigator/requestMIDIAccess for more information`; 1598 } 1599 } 1600 1601 class StorageAccessPermissionPrompt extends PermissionPromptForRequest { 1602 #permissionKey; 1603 1604 constructor(request) { 1605 super(); 1606 this.request = request; 1607 this.siteOption = null; 1608 this.#permissionKey = `3rdPartyStorage${lazy.SitePermissions.PERM_KEY_DELIMITER}${this.principal.origin}`; 1609 1610 let types = this.request.types.QueryInterface(Ci.nsIArray); 1611 let perm = types.queryElementAt(0, Ci.nsIContentPermissionType); 1612 let options = perm.options.QueryInterface(Ci.nsIArray); 1613 // If we have an option, the permission request is different in some way. 1614 // We may be in a call from requestStorageAccessUnderSite or a frame-scoped 1615 // request, which means that the embedding principal is not the current top-level 1616 // or the permission key is different. 1617 if (options.length != 2) { 1618 return; 1619 } 1620 1621 let topLevelOption = options.queryElementAt(0, Ci.nsISupportsString).data; 1622 if (topLevelOption) { 1623 this.siteOption = topLevelOption; 1624 } 1625 let frameOption = options.queryElementAt(1, Ci.nsISupportsString).data; 1626 if (frameOption) { 1627 // We replace the permission key with a frame-specific one that only has a site after the delimiter 1628 this.#permissionKey = `3rdPartyFrameStorage${lazy.SitePermissions.PERM_KEY_DELIMITER}${this.principal.siteOrigin}`; 1629 } 1630 } 1631 1632 get usePermissionManager() { 1633 return false; 1634 } 1635 1636 get type() { 1637 return "storage-access"; 1638 } 1639 1640 get permissionKey() { 1641 // Make sure this name is unique per each third-party tracker 1642 return this.#permissionKey; 1643 } 1644 1645 get temporaryPermissionURI() { 1646 if (this.siteOption) { 1647 return Services.io.newURI(this.siteOption); 1648 } 1649 return undefined; 1650 } 1651 1652 prettifyHostPort(hostport) { 1653 let [host, port] = hostport.split(":"); 1654 host = lazy.IDNService.convertToDisplayIDN(host); 1655 if (port) { 1656 return `${host}:${port}`; 1657 } 1658 return host; 1659 } 1660 1661 get popupOptions() { 1662 let learnMoreURL = 1663 Services.urlFormatter.formatURLPref("app.support.baseURL") + 1664 "third-party-cookies"; 1665 let hostPort = this.prettifyHostPort(this.principal.hostPort); 1666 let hintText = lazy.gBrowserBundle.formatStringFromName( 1667 "storageAccess1.hintText", 1668 [hostPort] 1669 ); 1670 return { 1671 learnMoreURL, 1672 displayURI: false, 1673 hintText, 1674 escAction: "secondarybuttoncommand", 1675 }; 1676 } 1677 1678 get notificationID() { 1679 return "storage-access"; 1680 } 1681 1682 get anchorID() { 1683 return "storage-access-notification-icon"; 1684 } 1685 1686 get message() { 1687 let embeddingHost = this.topLevelPrincipal.host; 1688 1689 if (this.siteOption) { 1690 embeddingHost = this.siteOption.split("://").at(-1); 1691 } 1692 1693 return lazy.gBrowserBundle.formatStringFromName("storageAccess4.message", [ 1694 this.prettifyHostPort(this.principal.hostPort), 1695 this.prettifyHostPort(embeddingHost), 1696 ]); 1697 } 1698 1699 get promptActions() { 1700 let self = this; 1701 1702 return [ 1703 { 1704 label: lazy.gBrowserBundle.GetStringFromName( 1705 "storageAccess1.Allow.label" 1706 ), 1707 accessKey: lazy.gBrowserBundle.GetStringFromName( 1708 "storageAccess1.Allow.accesskey" 1709 ), 1710 action: Ci.nsIPermissionManager.ALLOW_ACTION, 1711 callback() { 1712 self.allow({ "storage-access": "allow" }); 1713 }, 1714 }, 1715 { 1716 label: lazy.gBrowserBundle.GetStringFromName( 1717 "storageAccess1.DontAllow.label" 1718 ), 1719 accessKey: lazy.gBrowserBundle.GetStringFromName( 1720 "storageAccess1.DontAllow.accesskey" 1721 ), 1722 action: Ci.nsIPermissionManager.DENY_ACTION, 1723 callback() { 1724 self.cancel(); 1725 }, 1726 }, 1727 ]; 1728 } 1729 1730 get topLevelPrincipal() { 1731 return this.request.topLevelPrincipal; 1732 } 1733 } 1734 1735 export const PermissionUI = { 1736 PermissionPromptForRequest, 1737 GeolocationPermissionPrompt, 1738 XRPermissionPrompt, 1739 DesktopNotificationPermissionPrompt, 1740 PersistentStoragePermissionPrompt, 1741 MIDIPermissionPrompt, 1742 StorageAccessPermissionPrompt, 1743 LocalHostPermissionPrompt, 1744 LocalNetworkPermissionPrompt, 1745 };