SitePermissions.sys.mjs (42490B)
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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 11 setTimeout: "resource://gre/modules/Timer.sys.mjs", 12 }); 13 14 var gStringBundle = Services.strings.createBundle( 15 "chrome://browser/locale/sitePermissions.properties" 16 ); 17 18 /** 19 * A helper module to manage temporary permissions. 20 * 21 * Permissions are keyed by browser, so methods take a Browser 22 * element to identify the corresponding permission set. 23 * 24 * This uses a WeakMap to key browsers, so that entries are 25 * automatically cleared once the browser stops existing 26 * (once there are no other references to the browser object); 27 */ 28 const TemporaryPermissions = { 29 // This is a three level deep map with the following structure: 30 // 31 // Browser => { 32 // <baseDomain|origin>: { 33 // <permissionID>: {state: Number, expireTimeout: Number} 34 // } 35 // } 36 // 37 // Only the top level browser elements are stored via WeakMap. The WeakMap 38 // value is an object with URI baseDomains or origins as keys. The keys of 39 // that object are ids that identify permissions that were set for the 40 // specific URI. The final value is an object containing the permission state 41 // and the id of the timeout which will cause permission expiry. 42 // BLOCK permissions are keyed under baseDomain to prevent bypassing the block 43 // (see Bug 1492668). Any other permissions are keyed under origin. 44 _stateByBrowser: new WeakMap(), 45 46 // Extract baseDomain from uri. Fallback to hostname on conversion error. 47 _uriToBaseDomain(uri) { 48 try { 49 return Services.eTLD.getBaseDomain(uri); 50 } catch (error) { 51 if ( 52 error.result !== Cr.NS_ERROR_HOST_IS_IP_ADDRESS && 53 error.result !== Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS 54 ) { 55 throw error; 56 } 57 return uri.host; 58 } 59 }, 60 61 /** 62 * Generate keys to store temporary permissions under. The strict key is 63 * origin, non-strict is baseDomain. 64 * 65 * @param {nsIPrincipal} principal - principal to derive keys from. 66 * @returns {object} keys - Object containing the generated permission keys. 67 * @returns {string} keys.strict - Key to be used for strict matching. 68 * @returns {string} keys.nonStrict - Key to be used for non-strict matching. 69 * @throws {Error} - Throws if principal is undefined or no valid permission key can 70 * be generated. 71 */ 72 _getKeysFromPrincipal(principal) { 73 return { strict: principal.origin, nonStrict: principal.baseDomain }; 74 }, 75 76 /** 77 * Sets a new permission for the specified browser. 78 * 79 * @returns {boolean} whether the permission changed, effectively. 80 */ 81 set( 82 browser, 83 id, 84 state, 85 expireTimeMS, 86 principal = browser.contentPrincipal, 87 expireCallback 88 ) { 89 if ( 90 !browser || 91 !principal || 92 !SitePermissions.isSupportedPrincipal(principal) 93 ) { 94 return false; 95 } 96 let entry = this._stateByBrowser.get(browser); 97 if (!entry) { 98 entry = { browser: Cu.getWeakReference(browser), uriToPerm: {} }; 99 this._stateByBrowser.set(browser, entry); 100 } 101 let { uriToPerm } = entry; 102 // We store blocked permissions by baseDomain. Other states by origin. 103 let { strict, nonStrict } = this._getKeysFromPrincipal(principal); 104 let setKey; 105 let deleteKey; 106 // Differentiate between block and non-block permissions. If we store a 107 // block permission we need to delete old entries which may be set under 108 // origin before setting the new permission for baseDomain. For non-block 109 // permissions this is swapped. 110 if (state == SitePermissions.BLOCK) { 111 setKey = nonStrict; 112 deleteKey = strict; 113 } else { 114 setKey = strict; 115 deleteKey = nonStrict; 116 } 117 118 if (!uriToPerm[setKey]) { 119 uriToPerm[setKey] = {}; 120 } 121 122 let expireTimeout = uriToPerm[setKey][id]?.expireTimeout; 123 let previousState = uriToPerm[setKey][id]?.state; 124 // If overwriting a permission state. We need to cancel the old timeout. 125 if (expireTimeout) { 126 lazy.clearTimeout(expireTimeout); 127 } 128 // Construct the new timeout to remove the permission once it has expired. 129 expireTimeout = lazy.setTimeout(() => { 130 let entryBrowser = entry.browser.get(); 131 // Exit early if the browser is no longer alive when we get the timeout 132 // callback. 133 if (!entryBrowser || !uriToPerm[setKey]) { 134 return; 135 } 136 delete uriToPerm[setKey][id]; 137 // Notify SitePermissions that a temporary permission has expired. 138 // Get the browser the permission is currently set for. If this.copy was 139 // used this browser is different from the original one passed above. 140 expireCallback(entryBrowser); 141 }, expireTimeMS); 142 uriToPerm[setKey][id] = { 143 expireTimeout, 144 state, 145 }; 146 147 // If we set a permission state for a origin we need to reset the old state 148 // which may be set for baseDomain and vice versa. An individual permission 149 // must only ever be keyed by either origin or baseDomain. 150 let permissions = uriToPerm[deleteKey]; 151 if (permissions) { 152 expireTimeout = permissions[id]?.expireTimeout; 153 if (expireTimeout) { 154 lazy.clearTimeout(expireTimeout); 155 } 156 delete permissions[id]; 157 } 158 159 return state != previousState; 160 }, 161 162 /** 163 * Removes a permission with the specified id for the specified browser. 164 * 165 * @returns {boolean} whether the permission was removed. 166 */ 167 remove(browser, id) { 168 if ( 169 !browser || 170 !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) || 171 !this._stateByBrowser.has(browser) 172 ) { 173 return false; 174 } 175 // Permission can be stored by any of the two keys (strict and non-strict). 176 // getKeysFromURI can throw. We let the caller handle the exception. 177 let { strict, nonStrict } = this._getKeysFromPrincipal( 178 browser.contentPrincipal 179 ); 180 let { uriToPerm } = this._stateByBrowser.get(browser); 181 for (let key of [nonStrict, strict]) { 182 if (uriToPerm[key]?.[id] != null) { 183 let { expireTimeout } = uriToPerm[key][id]; 184 if (expireTimeout) { 185 lazy.clearTimeout(expireTimeout); 186 } 187 delete uriToPerm[key][id]; 188 // Individual permissions can only ever be keyed either strict or 189 // non-strict. If we find the permission via the first key run we can 190 // return early. 191 return true; 192 } 193 } 194 return false; 195 }, 196 197 // Gets a permission with the specified id for the specified browser. 198 get(browser, id) { 199 if ( 200 !browser || 201 !browser.contentPrincipal || 202 !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) || 203 !this._stateByBrowser.has(browser) 204 ) { 205 return null; 206 } 207 let { uriToPerm } = this._stateByBrowser.get(browser); 208 209 let { strict, nonStrict } = this._getKeysFromPrincipal( 210 browser.contentPrincipal 211 ); 212 for (let key of [nonStrict, strict]) { 213 if (uriToPerm[key]) { 214 let permission = uriToPerm[key][id]; 215 if (permission) { 216 return { 217 id, 218 state: permission.state, 219 scope: SitePermissions.SCOPE_TEMPORARY, 220 }; 221 } 222 } 223 } 224 return null; 225 }, 226 227 // Gets all permissions for the specified browser. 228 // Note that only permissions that apply to the current URI 229 // of the passed browser element will be returned. 230 getAll(browser) { 231 let permissions = []; 232 if ( 233 !SitePermissions.isSupportedPrincipal(browser.contentPrincipal) || 234 !this._stateByBrowser.has(browser) 235 ) { 236 return permissions; 237 } 238 let { uriToPerm } = this._stateByBrowser.get(browser); 239 240 let { strict, nonStrict } = this._getKeysFromPrincipal( 241 browser.contentPrincipal 242 ); 243 for (let key of [nonStrict, strict]) { 244 if (uriToPerm[key]) { 245 let perms = uriToPerm[key]; 246 for (let id of Object.keys(perms)) { 247 let permission = perms[id]; 248 if (permission) { 249 permissions.push({ 250 id, 251 state: permission.state, 252 scope: SitePermissions.SCOPE_TEMPORARY, 253 }); 254 } 255 } 256 } 257 } 258 259 return permissions; 260 }, 261 262 // Clears all permissions for the specified browser. 263 // Unlike other methods, this does NOT clear only for 264 // the currentURI but the whole browser state. 265 266 /** 267 * Clear temporary permissions for the specified browser. Unlike other 268 * methods, this does NOT clear only for the currentURI but the whole browser 269 * state. 270 * 271 * @param {Browser} browser - Browser to clear permissions for. 272 * @param {number} [filterState] - Only clear permissions with the given state 273 * value. Defaults to all permissions. 274 */ 275 clear(browser, filterState = null) { 276 let entry = this._stateByBrowser.get(browser); 277 if (!entry?.uriToPerm) { 278 return; 279 } 280 281 let { uriToPerm } = entry; 282 Object.entries(uriToPerm).forEach(([uriKey, permissions]) => { 283 Object.entries(permissions).forEach( 284 ([permId, { state, expireTimeout }]) => { 285 // We need to explicitly check for null or undefined here, because the 286 // permission state may be 0. 287 if (filterState != null) { 288 if (state != filterState) { 289 // Skip permission entry if it doesn't match the filter. 290 return; 291 } 292 delete permissions[permId]; 293 } 294 // For the clear-all case we remove the entire browser entry, so we 295 // only need to clear the timeouts. 296 if (!expireTimeout) { 297 return; 298 } 299 lazy.clearTimeout(expireTimeout); 300 } 301 ); 302 // If there are no more permissions, remove the entry from the URI map. 303 if (filterState != null && !Object.keys(permissions).length) { 304 delete uriToPerm[uriKey]; 305 } 306 }); 307 308 // We're either clearing all permissions or only the permissions with state 309 // == filterState. If we have a filter, we can only clean up the browser if 310 // there are no permission entries left in the map. 311 if (filterState == null || !Object.keys(uriToPerm).length) { 312 this._stateByBrowser.delete(browser); 313 } 314 }, 315 316 // Copies the temporary permission state of one browser 317 // into a new entry for the other browser. 318 copy(browser, newBrowser) { 319 let entry = this._stateByBrowser.get(browser); 320 if (entry) { 321 entry.browser = Cu.getWeakReference(newBrowser); 322 this._stateByBrowser.set(newBrowser, entry); 323 } 324 }, 325 }; 326 327 // This hold a flag per browser to indicate whether we should show the 328 // user a notification as a permission has been requested that has been 329 // blocked globally. We only want to notify the user in the case that 330 // they actually requested the permission within the current page load 331 // so will clear the flag on navigation. 332 const GloballyBlockedPermissions = { 333 _stateByBrowser: new WeakMap(), 334 335 /** 336 * @returns {boolean} whether the permission was removed. 337 */ 338 set(browser, id) { 339 if (!this._stateByBrowser.has(browser)) { 340 this._stateByBrowser.set(browser, {}); 341 } 342 let entry = this._stateByBrowser.get(browser); 343 let origin = browser.contentPrincipal.origin; 344 if (!entry[origin]) { 345 entry[origin] = {}; 346 } 347 348 if (entry[origin][id]) { 349 return false; 350 } 351 entry[origin][id] = true; 352 353 // Clear the flag and remove the listener once the user has navigated. 354 // WebProgress will report various things including hashchanges to us, the 355 // navigation we care about is either leaving the current page or reloading. 356 let { prePath } = browser.currentURI; 357 browser.addProgressListener( 358 { 359 QueryInterface: ChromeUtils.generateQI([ 360 "nsIWebProgressListener", 361 "nsISupportsWeakReference", 362 ]), 363 onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { 364 let hasLeftPage = 365 aLocation.prePath != prePath || 366 !(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); 367 let isReload = !!( 368 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD 369 ); 370 371 if (aWebProgress.isTopLevel && (hasLeftPage || isReload)) { 372 GloballyBlockedPermissions.remove(browser, id, origin); 373 browser.removeProgressListener(this); 374 } 375 }, 376 }, 377 Ci.nsIWebProgress.NOTIFY_LOCATION 378 ); 379 return true; 380 }, 381 382 // Removes a permission with the specified id for the specified browser. 383 remove(browser, id, origin = null) { 384 let entry = this._stateByBrowser.get(browser); 385 if (!origin) { 386 origin = browser.contentPrincipal.origin; 387 } 388 if (entry && entry[origin]) { 389 delete entry[origin][id]; 390 } 391 }, 392 393 // Gets all permissions for the specified browser. 394 // Note that only permissions that apply to the current URI 395 // of the passed browser element will be returned. 396 getAll(browser) { 397 let permissions = []; 398 let entry = this._stateByBrowser.get(browser); 399 let origin = browser.contentPrincipal.origin; 400 if (entry && entry[origin]) { 401 let timeStamps = entry[origin]; 402 for (let id of Object.keys(timeStamps)) { 403 permissions.push({ 404 id, 405 state: gPermissions.get(id).getDefault(), 406 scope: SitePermissions.SCOPE_GLOBAL, 407 }); 408 } 409 } 410 return permissions; 411 }, 412 413 // Copies the globally blocked permission state of one browser 414 // into a new entry for the other browser. 415 copy(browser, newBrowser) { 416 let entry = this._stateByBrowser.get(browser); 417 if (entry) { 418 this._stateByBrowser.set(newBrowser, entry); 419 } 420 }, 421 }; 422 423 /** 424 * A module to manage permanent and temporary permissions 425 * by URI and browser. 426 * 427 * Some methods have the side effect of dispatching a "PermissionStateChange" 428 * event on changes to temporary permissions, as mentioned in the respective docs. 429 */ 430 export var SitePermissions = { 431 // Permission states. 432 UNKNOWN: Services.perms.UNKNOWN_ACTION, 433 ALLOW: Services.perms.ALLOW_ACTION, 434 BLOCK: Services.perms.DENY_ACTION, 435 PROMPT: Services.perms.PROMPT_ACTION, 436 ALLOW_COOKIES_FOR_SESSION: Ci.nsICookiePermission.ACCESS_SESSION, 437 AUTOPLAY_BLOCKED_ALL: Ci.nsIAutoplay.BLOCKED_ALL, 438 439 // Permission scopes. 440 SCOPE_REQUEST: "{SitePermissions.SCOPE_REQUEST}", 441 SCOPE_TEMPORARY: "{SitePermissions.SCOPE_TEMPORARY}", 442 SCOPE_SESSION: "{SitePermissions.SCOPE_SESSION}", 443 SCOPE_PERSISTENT: "{SitePermissions.SCOPE_PERSISTENT}", 444 SCOPE_POLICY: "{SitePermissions.SCOPE_POLICY}", 445 SCOPE_GLOBAL: "{SitePermissions.SCOPE_GLOBAL}", 446 447 // The delimiter used for double keyed permissions. 448 // For example: open-protocol-handler^irc 449 PERM_KEY_DELIMITER: "^", 450 451 _permissionsArray: null, 452 _defaultPrefBranch: Services.prefs.getBranch("permissions.default."), 453 454 // For testing use only. 455 _temporaryPermissions: TemporaryPermissions, 456 457 /** 458 * Gets all custom permissions for a given principal. 459 * Install addon permission is excluded, check bug 1303108. 460 * 461 * @return {Array} a list of objects with the keys: 462 * - id: the permissionId of the permission 463 * - scope: the scope of the permission (e.g. SitePermissions.SCOPE_TEMPORARY) 464 * - state: a constant representing the current permission state 465 * (e.g. SitePermissions.ALLOW) 466 */ 467 getAllByPrincipal(principal) { 468 if (!principal) { 469 throw new Error("principal argument cannot be null."); 470 } 471 if (!this.isSupportedPrincipal(principal)) { 472 return []; 473 } 474 475 // Get all permissions from the permission manager by principal, excluding 476 // the ones set to be disabled. 477 let permissions = Services.perms 478 .getAllForPrincipal(principal) 479 .filter(permission => { 480 let entry = gPermissions.get(permission.type); 481 if (!entry || entry.disabled) { 482 return false; 483 } 484 let type = entry.id; 485 486 /* Hide persistent storage permission when extension principal 487 * have WebExtensions-unlimitedStorage permission. */ 488 if ( 489 type == "persistent-storage" && 490 SitePermissions.getForPrincipal( 491 principal, 492 "WebExtensions-unlimitedStorage" 493 ).state == SitePermissions.ALLOW 494 ) { 495 return false; 496 } 497 498 return true; 499 }); 500 501 return permissions.map(permission => { 502 let scope = this.SCOPE_PERSISTENT; 503 if (permission.expireType == Services.perms.EXPIRE_SESSION) { 504 scope = this.SCOPE_SESSION; 505 } else if (permission.expireType == Services.perms.EXPIRE_POLICY) { 506 scope = this.SCOPE_POLICY; 507 } 508 509 return { 510 id: permission.type, 511 scope, 512 state: permission.capability, 513 }; 514 }); 515 }, 516 517 /** 518 * Returns all custom permissions for a given browser. 519 * 520 * To receive a more detailed, albeit less performant listing see 521 * SitePermissions.getAllPermissionDetailsForBrowser(). 522 * 523 * @param {Browser} browser 524 * The browser to fetch permission for. 525 * 526 * @return {Array} a list of objects with the keys: 527 * - id: the permissionId of the permission 528 * - state: a constant representing the current permission state 529 * (e.g. SitePermissions.ALLOW) 530 * - scope: a constant representing how long the permission will 531 * be kept. 532 */ 533 getAllForBrowser(browser) { 534 let permissions = {}; 535 536 for (let permission of TemporaryPermissions.getAll(browser)) { 537 permission.scope = this.SCOPE_TEMPORARY; 538 permissions[permission.id] = permission; 539 } 540 541 for (let permission of GloballyBlockedPermissions.getAll(browser)) { 542 permissions[permission.id] = permission; 543 } 544 545 for (let permission of this.getAllByPrincipal(browser.contentPrincipal)) { 546 permissions[permission.id] = permission; 547 } 548 549 return Object.values(permissions); 550 }, 551 552 /** 553 * Returns a list of objects with detailed information on all permissions 554 * that are currently set for the given browser. 555 * 556 * @param {Browser} browser 557 * The browser to fetch permission for. 558 * 559 * @return {Array<object>} a list of objects with the keys: 560 * - id: the permissionID of the permission 561 * - state: a constant representing the current permission state 562 * (e.g. SitePermissions.ALLOW) 563 * - scope: a constant representing how long the permission will 564 * be kept. 565 * - label: the localized label, or null if none is available. 566 */ 567 getAllPermissionDetailsForBrowser(browser) { 568 return this.getAllForBrowser(browser).map(({ id, scope, state }) => ({ 569 id, 570 scope, 571 state, 572 label: this.getPermissionLabel(id), 573 })); 574 }, 575 576 /** 577 * Checks whether a UI for managing permissions should be exposed for a given 578 * principal. 579 * 580 * @param {nsIPrincipal} principal 581 * The principal to check. 582 * 583 * @return {boolean} if the principal is supported. 584 */ 585 isSupportedPrincipal(principal) { 586 if (!principal) { 587 return false; 588 } 589 if (!(principal instanceof Ci.nsIPrincipal)) { 590 throw new Error( 591 "Argument passed as principal is not an instance of Ci.nsIPrincipal" 592 ); 593 } 594 return this.isSupportedScheme(principal.scheme); 595 }, 596 597 /** 598 * Checks whether we support managing permissions for a specific scheme. 599 * 600 * @param {string} scheme - Scheme to test. 601 * @returns {boolean} Whether the scheme is supported. 602 */ 603 isSupportedScheme(scheme) { 604 return ["http", "https", "moz-extension", "file"].includes(scheme); 605 }, 606 607 /** 608 * Gets an array of all permission IDs. 609 * 610 * @return {Array<string>} an array of all permission IDs. 611 */ 612 listPermissions() { 613 if (this._permissionsArray === null) { 614 this._permissionsArray = gPermissions.getEnabledPermissions(); 615 } 616 return this._permissionsArray; 617 }, 618 619 /** 620 * Test whether a permission is managed by SitePermissions. 621 * 622 * @param {string} type - Permission type. 623 * @returns {boolean} 624 */ 625 isSitePermission(type) { 626 return gPermissions.has(type); 627 }, 628 629 /** 630 * Called when a preference changes its value. 631 * 632 * @param {string} data 633 * The last argument passed to the preference change observer 634 * @param {string} previous 635 * The previous value of the preference 636 * @param {string} latest 637 * The latest value of the preference 638 */ 639 invalidatePermissionList() { 640 // Ensure that listPermissions() will reconstruct its return value the next 641 // time it's called. 642 this._permissionsArray = null; 643 }, 644 645 /** 646 * Returns an array of permission states to be exposed to the user for a 647 * permission with the given ID. 648 * 649 * @param {string} permissionID 650 * The ID to get permission states for. 651 * 652 * @return {Array<SitePermissions state>} an array of all permission states. 653 */ 654 getAvailableStates(permissionID) { 655 if ( 656 gPermissions.has(permissionID) && 657 gPermissions.get(permissionID).states 658 ) { 659 return gPermissions.get(permissionID).states; 660 } 661 662 /* Since the permissions we are dealing with have adopted the convention 663 * of treating UNKNOWN == PROMPT, we only include one of either UNKNOWN 664 * or PROMPT in this list, to avoid duplicating states. */ 665 if (this.getDefault(permissionID) == this.UNKNOWN) { 666 return [ 667 SitePermissions.UNKNOWN, 668 SitePermissions.ALLOW, 669 SitePermissions.BLOCK, 670 ]; 671 } 672 673 return [ 674 SitePermissions.PROMPT, 675 SitePermissions.ALLOW, 676 SitePermissions.BLOCK, 677 ]; 678 }, 679 680 /** 681 * Returns the default state of a particular permission. 682 * 683 * @param {string} permissionID 684 * The ID to get the default for. 685 * 686 * @return {SitePermissions.state} the default state. 687 */ 688 getDefault(permissionID) { 689 // If the permission has custom logic for getting its default value, 690 // try that first. 691 if ( 692 gPermissions.has(permissionID) && 693 gPermissions.get(permissionID).getDefault 694 ) { 695 return gPermissions.get(permissionID).getDefault(); 696 } 697 698 // Otherwise try to get the default preference for that permission. 699 return this._defaultPrefBranch.getIntPref(permissionID, this.UNKNOWN); 700 }, 701 702 /** 703 * Set the default state of a particular permission. 704 * 705 * @param {string} permissionID 706 * The ID to set the default for. 707 * 708 * @param {string} state 709 * The state to set. 710 */ 711 setDefault(permissionID, state) { 712 if ( 713 gPermissions.has(permissionID) && 714 gPermissions.get(permissionID).setDefault 715 ) { 716 return gPermissions.get(permissionID).setDefault(state); 717 } 718 let key = "permissions.default." + permissionID; 719 return Services.prefs.setIntPref(key, state); 720 }, 721 722 /** 723 * Returns the state and scope of a particular permission for a given principal. 724 * 725 * This method will NOT dispatch a "PermissionStateChange" event on the specified 726 * browser if a temporary permission was removed because it has expired. 727 * 728 * @param {nsIPrincipal} principal 729 * The principal to check. 730 * @param {string} permissionID 731 * The id of the permission. 732 * @param {Browser} [browser] The browser object to check for temporary 733 * permissions. 734 * 735 * @return {object} an object with the keys: 736 * - state: The current state of the permission 737 * (e.g. SitePermissions.ALLOW) 738 * - scope: The scope of the permission 739 * (e.g. SitePermissions.SCOPE_PERSISTENT) 740 */ 741 getForPrincipal(principal, permissionID, browser) { 742 if (!principal && !browser) { 743 throw new Error( 744 "Atleast one of the arguments, either principal or browser should not be null." 745 ); 746 } 747 let defaultState = this.getDefault(permissionID); 748 let result = { state: defaultState, scope: this.SCOPE_PERSISTENT }; 749 if (this.isSupportedPrincipal(principal)) { 750 let permission = null; 751 if ( 752 gPermissions.has(permissionID) && 753 gPermissions.get(permissionID).exactHostMatch 754 ) { 755 permission = Services.perms.getPermissionObject( 756 principal, 757 permissionID, 758 true 759 ); 760 } else { 761 permission = Services.perms.getPermissionObject( 762 principal, 763 permissionID, 764 false 765 ); 766 } 767 768 if (permission) { 769 result.state = permission.capability; 770 if (permission.expireType == Services.perms.EXPIRE_SESSION) { 771 result.scope = this.SCOPE_SESSION; 772 } else if (permission.expireType == Services.perms.EXPIRE_POLICY) { 773 result.scope = this.SCOPE_POLICY; 774 } 775 } 776 } 777 778 if ( 779 result.state == defaultState || 780 result.state == SitePermissions.PROMPT 781 ) { 782 // If there's no persistent permission saved, or if the persistent permission 783 // saved is merely PROMPT (aka "Always Ask" when persisted for camera and 784 // microphone), then check if we have something set temporarily. 785 // 786 // This way, a temporary ALLOW or BLOCK trumps a persisted PROMPT. While 787 // having overlap would be a bug (because any ALLOW or BLOCK user action should 788 // really clear PROMPT), this order seems safer than the other way around. 789 let value = TemporaryPermissions.get(browser, permissionID); 790 791 if (value) { 792 result.state = value.state; 793 result.scope = this.SCOPE_TEMPORARY; 794 } 795 } 796 797 return result; 798 }, 799 800 /** 801 * Sets the state of a particular permission for a given principal or browser. 802 * This method will dispatch a "PermissionStateChange" event on the specified 803 * browser if a temporary permission was set 804 * 805 * @param {nsIPrincipal} [principal] The principal to set the permission for. 806 * When setting temporary permissions passing a principal is optional. 807 * If the principal is still passed here it takes precedence over the 808 * browser's contentPrincipal for permission keying. This can be 809 * helpful in situations where the browser has already navigated away 810 * from a site you want to set a permission for. 811 * @param {string} permissionID The id of the permission. 812 * @param {SitePermissions state} state The state of the permission. 813 * @param {SitePermissions scope} [scope] The scope of the permission. 814 * Defaults to SCOPE_PERSISTENT. 815 * @param {Browser} [browser] The browser object to set temporary permissions 816 * on. This needs to be provided if the scope is SCOPE_TEMPORARY! 817 * @param {number} [expireTimeMS] If setting a temporary permission, how many 818 * milliseconds it should be valid for. The default is controlled by 819 * the 'privacy.temporary_permission_expire_time_ms' pref. 820 */ 821 setForPrincipal( 822 principal, 823 permissionID, 824 state, 825 scope = this.SCOPE_PERSISTENT, 826 browser = null, 827 expireTimeMS = SitePermissions.temporaryPermissionExpireTime 828 ) { 829 if (!principal && !browser) { 830 throw new Error( 831 "Atleast one of the arguments, either principal or browser should not be null." 832 ); 833 } 834 if (scope == this.SCOPE_GLOBAL && state == this.BLOCK) { 835 if (GloballyBlockedPermissions.set(browser, permissionID)) { 836 browser.dispatchEvent( 837 new browser.ownerGlobal.CustomEvent("PermissionStateChange") 838 ); 839 } 840 return; 841 } 842 843 if (state == this.UNKNOWN || state == this.getDefault(permissionID)) { 844 // Because they are controlled by two prefs with many states that do not 845 // correspond to the classical ALLOW/DENY/PROMPT model, we want to always 846 // allow the user to add exceptions to their cookie rules without removing them. 847 if (permissionID != "cookie") { 848 this.removeFromPrincipal(principal, permissionID, browser); 849 return; 850 } 851 } 852 853 if (state == this.ALLOW_COOKIES_FOR_SESSION && permissionID != "cookie") { 854 throw new Error( 855 "ALLOW_COOKIES_FOR_SESSION can only be set on the cookie permission" 856 ); 857 } 858 859 // Save temporary permissions. 860 if (scope == this.SCOPE_TEMPORARY) { 861 if (!browser) { 862 throw new Error( 863 "TEMPORARY scoped permissions require a browser object" 864 ); 865 } 866 if (!Number.isInteger(expireTimeMS) || expireTimeMS <= 0) { 867 throw new Error("expireTime must be a positive integer"); 868 } 869 870 if ( 871 TemporaryPermissions.set( 872 browser, 873 permissionID, 874 state, 875 expireTimeMS, 876 principal ?? browser.contentPrincipal, 877 // On permission expiry 878 origBrowser => { 879 if (!origBrowser.ownerGlobal) { 880 return; 881 } 882 origBrowser.dispatchEvent( 883 new origBrowser.ownerGlobal.CustomEvent("PermissionStateChange") 884 ); 885 } 886 ) 887 ) { 888 browser.dispatchEvent( 889 new browser.ownerGlobal.CustomEvent("PermissionStateChange") 890 ); 891 } 892 } else if (this.isSupportedPrincipal(principal)) { 893 let perms_scope = Services.perms.EXPIRE_NEVER; 894 if (scope == this.SCOPE_SESSION) { 895 perms_scope = Services.perms.EXPIRE_SESSION; 896 } else if (scope == this.SCOPE_POLICY) { 897 perms_scope = Services.perms.EXPIRE_POLICY; 898 } 899 900 Services.perms.addFromPrincipal( 901 principal, 902 permissionID, 903 state, 904 perms_scope 905 ); 906 } 907 }, 908 909 /** 910 * Removes the saved state of a particular permission for a given principal and/or browser. 911 * This method will dispatch a "PermissionStateChange" event on the specified 912 * browser if a temporary permission was removed. 913 * 914 * @param {nsIPrincipal} principal 915 * The principal to remove the permission for. 916 * @param {string} permissionID 917 * The id of the permission. 918 * @param {Browser} browser (optional) 919 * The browser object to remove temporary permissions on. 920 */ 921 removeFromPrincipal(principal, permissionID, browser) { 922 if (!principal && !browser) { 923 throw new Error( 924 "Atleast one of the arguments, either principal or browser should not be null." 925 ); 926 } 927 if (this.isSupportedPrincipal(principal)) { 928 Services.perms.removeFromPrincipal(principal, permissionID); 929 } 930 931 // TemporaryPermissions.get() deletes expired permissions automatically, 932 // if it hasn't expired, remove it explicitly. 933 if (TemporaryPermissions.remove(browser, permissionID)) { 934 // Send a PermissionStateChange event only if the permission hasn't expired. 935 browser.dispatchEvent( 936 new browser.ownerGlobal.CustomEvent("PermissionStateChange") 937 ); 938 } 939 }, 940 941 /** 942 * Clears all block permissions that were temporarily saved. 943 * 944 * @param {Browser} browser 945 * The browser object to clear. 946 */ 947 clearTemporaryBlockPermissions(browser) { 948 TemporaryPermissions.clear(browser, SitePermissions.BLOCK); 949 }, 950 951 /** 952 * Copy all permissions that were temporarily saved on one 953 * browser object to a new browser. 954 * 955 * @param {Browser} browser 956 * The browser object to copy from. 957 * @param {Browser} newBrowser 958 * The browser object to copy to. 959 */ 960 copyTemporaryPermissions(browser, newBrowser) { 961 TemporaryPermissions.copy(browser, newBrowser); 962 GloballyBlockedPermissions.copy(browser, newBrowser); 963 }, 964 965 /** 966 * Returns the localized label for the permission with the given ID, to be 967 * used in a UI for managing permissions. 968 * If a permission is double keyed (has an additional key in the ID), the 969 * second key is split off and supplied to the string formatter as a variable. 970 * 971 * @param {string} permissionID 972 * The permission to get the label for. May include second key. 973 * 974 * @return {string} the localized label or null if none is available. 975 */ 976 getPermissionLabel(permissionID) { 977 let [id, key] = permissionID.split(this.PERM_KEY_DELIMITER); 978 if (!gPermissions.has(id)) { 979 // Permission can't be found. 980 return null; 981 } 982 if ( 983 "labelID" in gPermissions.get(id) && 984 gPermissions.get(id).labelID === null 985 ) { 986 // Permission doesn't support having a label. 987 return null; 988 } 989 if (id == "3rdPartyStorage" || id == "3rdPartyFrameStorage") { 990 // The key is the 3rd party origin or site, which we use for the label. 991 return key; 992 } 993 let labelID = gPermissions.get(id).labelID || id; 994 return gStringBundle.formatStringFromName(`permission.${labelID}.label`, [ 995 key, 996 ]); 997 }, 998 999 /** 1000 * Returns the localized label for the given permission state, to be used in 1001 * a UI for managing permissions. 1002 * 1003 * @param {string} permissionID 1004 * The permission to get the label for. 1005 * 1006 * @param {SitePermissions state} state 1007 * The state to get the label for. 1008 * 1009 * @return {string | null} the localized label or null if an 1010 * unknown state was passed. 1011 */ 1012 getMultichoiceStateLabel(permissionID, state) { 1013 // If the permission has custom logic for getting its default value, 1014 // try that first. 1015 if ( 1016 gPermissions.has(permissionID) && 1017 gPermissions.get(permissionID).getMultichoiceStateLabel 1018 ) { 1019 return gPermissions.get(permissionID).getMultichoiceStateLabel(state); 1020 } 1021 1022 switch (state) { 1023 case this.UNKNOWN: 1024 case this.PROMPT: 1025 return gStringBundle.GetStringFromName("state.multichoice.alwaysAsk"); 1026 case this.ALLOW: 1027 return gStringBundle.GetStringFromName("state.multichoice.allow"); 1028 case this.ALLOW_COOKIES_FOR_SESSION: 1029 return gStringBundle.GetStringFromName( 1030 "state.multichoice.allowForSession" 1031 ); 1032 case this.BLOCK: 1033 return gStringBundle.GetStringFromName("state.multichoice.block"); 1034 default: 1035 return null; 1036 } 1037 }, 1038 1039 /** 1040 * Returns the localized label for a permission's current state. 1041 * 1042 * @param {SitePermissions state} state 1043 * The state to get the label for. 1044 * @param {string} id 1045 * The permission to get the state label for. 1046 * @param {SitePermissions scope} scope (optional) 1047 * The scope to get the label for. 1048 * 1049 * @return {string | null} the localized label or null if an 1050 * unknown state was passed. 1051 */ 1052 getCurrentStateLabel(state, id, scope = null) { 1053 switch (state) { 1054 case this.PROMPT: 1055 return gStringBundle.GetStringFromName("state.current.prompt"); 1056 case this.ALLOW: 1057 if ( 1058 scope && 1059 scope != this.SCOPE_PERSISTENT && 1060 scope != this.SCOPE_POLICY 1061 ) { 1062 return gStringBundle.GetStringFromName( 1063 "state.current.allowedTemporarily" 1064 ); 1065 } 1066 return gStringBundle.GetStringFromName("state.current.allowed"); 1067 case this.ALLOW_COOKIES_FOR_SESSION: 1068 return gStringBundle.GetStringFromName( 1069 "state.current.allowedForSession" 1070 ); 1071 case this.BLOCK: 1072 if ( 1073 scope && 1074 scope != this.SCOPE_PERSISTENT && 1075 scope != this.SCOPE_POLICY && 1076 scope != this.SCOPE_GLOBAL 1077 ) { 1078 return gStringBundle.GetStringFromName( 1079 "state.current.blockedTemporarily" 1080 ); 1081 } 1082 return gStringBundle.GetStringFromName("state.current.blocked"); 1083 default: 1084 return null; 1085 } 1086 }, 1087 }; 1088 1089 let gPermissions = { 1090 _getId(type) { 1091 // Split off second key (if it exists). 1092 let [id] = type.split(SitePermissions.PERM_KEY_DELIMITER); 1093 return id; 1094 }, 1095 1096 has(type) { 1097 return this._getId(type) in this._permissions; 1098 }, 1099 1100 get(type) { 1101 let id = this._getId(type); 1102 let perm = this._permissions[id]; 1103 if (perm) { 1104 perm.id = id; 1105 } 1106 return perm; 1107 }, 1108 1109 getEnabledPermissions() { 1110 return Object.keys(this._permissions).filter( 1111 id => !this._permissions[id].disabled 1112 ); 1113 }, 1114 1115 /* Holds permission ID => options pairs. 1116 * 1117 * Supported options: 1118 * 1119 * - exactHostMatch 1120 * Allows sub domains to have their own permissions. 1121 * Defaults to false. 1122 * 1123 * - getDefault 1124 * Called to get the permission's default state. 1125 * Defaults to UNKNOWN, indicating that the user will be asked each time 1126 * a page asks for that permissions. 1127 * 1128 * - labelID 1129 * Use the given ID instead of the permission name for looking up strings. 1130 * e.g. "desktop-notification2" to use permission.desktop-notification2.label 1131 * 1132 * - states 1133 * Array of permission states to be exposed to the user. 1134 * Defaults to ALLOW, BLOCK and the default state (see getDefault). 1135 * 1136 * - getMultichoiceStateLabel 1137 * Optional method to overwrite SitePermissions#getMultichoiceStateLabel with custom label logic. 1138 */ 1139 _permissions: { 1140 "autoplay-media": { 1141 exactHostMatch: true, 1142 getDefault() { 1143 let pref = Services.prefs.getIntPref( 1144 "media.autoplay.default", 1145 Ci.nsIAutoplay.BLOCKED 1146 ); 1147 if (pref == Ci.nsIAutoplay.ALLOWED) { 1148 return SitePermissions.ALLOW; 1149 } 1150 if (pref == Ci.nsIAutoplay.BLOCKED_ALL) { 1151 return SitePermissions.AUTOPLAY_BLOCKED_ALL; 1152 } 1153 return SitePermissions.BLOCK; 1154 }, 1155 setDefault(value) { 1156 let prefValue = Ci.nsIAutoplay.BLOCKED; 1157 if (value == SitePermissions.ALLOW) { 1158 prefValue = Ci.nsIAutoplay.ALLOWED; 1159 } else if (value == SitePermissions.AUTOPLAY_BLOCKED_ALL) { 1160 prefValue = Ci.nsIAutoplay.BLOCKED_ALL; 1161 } 1162 Services.prefs.setIntPref("media.autoplay.default", prefValue); 1163 }, 1164 labelID: "autoplay", 1165 states: [ 1166 SitePermissions.ALLOW, 1167 SitePermissions.BLOCK, 1168 SitePermissions.AUTOPLAY_BLOCKED_ALL, 1169 ], 1170 getMultichoiceStateLabel(state) { 1171 switch (state) { 1172 case SitePermissions.AUTOPLAY_BLOCKED_ALL: 1173 return gStringBundle.GetStringFromName( 1174 "state.multichoice.autoplayblockall" 1175 ); 1176 case SitePermissions.BLOCK: 1177 return gStringBundle.GetStringFromName( 1178 "state.multichoice.autoplayblock" 1179 ); 1180 case SitePermissions.ALLOW: 1181 return gStringBundle.GetStringFromName( 1182 "state.multichoice.autoplayallow" 1183 ); 1184 } 1185 throw new Error(`Unknown state: ${state}`); 1186 }, 1187 }, 1188 1189 cookie: { 1190 states: [ 1191 SitePermissions.ALLOW, 1192 SitePermissions.ALLOW_COOKIES_FOR_SESSION, 1193 SitePermissions.BLOCK, 1194 ], 1195 getDefault() { 1196 if ( 1197 Services.cookies.getCookieBehavior(false) == 1198 Ci.nsICookieService.BEHAVIOR_REJECT 1199 ) { 1200 return SitePermissions.BLOCK; 1201 } 1202 1203 return SitePermissions.ALLOW; 1204 }, 1205 }, 1206 1207 "desktop-notification": { 1208 exactHostMatch: true, 1209 labelID: "desktop-notification3", 1210 }, 1211 1212 camera: { 1213 exactHostMatch: true, 1214 }, 1215 1216 localhost: { 1217 exactHostMatch: true, 1218 get disabled() { 1219 return !SitePermissions.localNetworkAccessPermissionsEnabled; 1220 }, 1221 }, 1222 1223 "local-network": { 1224 exactHostMatch: true, 1225 get disabled() { 1226 return !SitePermissions.localNetworkAccessPermissionsEnabled; 1227 }, 1228 }, 1229 1230 microphone: { 1231 exactHostMatch: true, 1232 }, 1233 1234 screen: { 1235 exactHostMatch: true, 1236 states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK], 1237 }, 1238 1239 speaker: { 1240 exactHostMatch: true, 1241 states: [SitePermissions.UNKNOWN, SitePermissions.BLOCK], 1242 get disabled() { 1243 return !SitePermissions.setSinkIdEnabled; 1244 }, 1245 }, 1246 1247 popup: { 1248 getDefault() { 1249 return Services.prefs.getBoolPref("dom.disable_open_during_load") 1250 ? SitePermissions.BLOCK 1251 : SitePermissions.ALLOW; 1252 }, 1253 labelID: "popup2", 1254 states: [SitePermissions.ALLOW, SitePermissions.BLOCK], 1255 }, 1256 1257 install: { 1258 getDefault() { 1259 return Services.prefs.getBoolPref("xpinstall.whitelist.required") 1260 ? SitePermissions.UNKNOWN 1261 : SitePermissions.ALLOW; 1262 }, 1263 }, 1264 1265 geo: { 1266 exactHostMatch: true, 1267 }, 1268 1269 "open-protocol-handler": { 1270 labelID: "open-protocol-handler", 1271 exactHostMatch: true, 1272 states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW], 1273 }, 1274 1275 xr: { 1276 exactHostMatch: true, 1277 }, 1278 1279 "focus-tab-by-prompt": { 1280 exactHostMatch: true, 1281 states: [SitePermissions.UNKNOWN, SitePermissions.ALLOW], 1282 }, 1283 "persistent-storage": { 1284 exactHostMatch: true, 1285 }, 1286 1287 shortcuts: { 1288 states: [SitePermissions.ALLOW, SitePermissions.BLOCK], 1289 }, 1290 1291 canvas: { 1292 get disabled() { 1293 return !SitePermissions.resistFingerprinting; 1294 }, 1295 }, 1296 1297 midi: { 1298 exactHostMatch: true, 1299 get disabled() { 1300 return !SitePermissions.midiPermissionEnabled; 1301 }, 1302 }, 1303 1304 "midi-sysex": { 1305 exactHostMatch: true, 1306 get disabled() { 1307 return !SitePermissions.midiPermissionEnabled; 1308 }, 1309 }, 1310 1311 "storage-access": { 1312 labelID: null, 1313 getDefault() { 1314 return SitePermissions.UNKNOWN; 1315 }, 1316 }, 1317 1318 "3rdPartyStorage": {}, 1319 "3rdPartyFrameStorage": {}, 1320 }, 1321 }; 1322 1323 SitePermissions.midiPermissionEnabled = Services.prefs.getBoolPref( 1324 "dom.webmidi.enabled" 1325 ); 1326 1327 XPCOMUtils.defineLazyPreferenceGetter( 1328 SitePermissions, 1329 "temporaryPermissionExpireTime", 1330 "privacy.temporary_permission_expire_time_ms", 1331 3600 * 1000 1332 ); 1333 XPCOMUtils.defineLazyPreferenceGetter( 1334 SitePermissions, 1335 "setSinkIdEnabled", 1336 "media.setsinkid.enabled", 1337 false, 1338 SitePermissions.invalidatePermissionList.bind(SitePermissions) 1339 ); 1340 XPCOMUtils.defineLazyPreferenceGetter( 1341 SitePermissions, 1342 "resistFingerprinting", 1343 "privacy.resistFingerprinting", 1344 false, 1345 SitePermissions.invalidatePermissionList.bind(SitePermissions) 1346 ); 1347 1348 XPCOMUtils.defineLazyPreferenceGetter( 1349 SitePermissions, 1350 "localNetworkAccessPermissionsEnabled", 1351 "network.lna.blocking", 1352 false, 1353 SitePermissions.invalidatePermissionList.bind(SitePermissions) 1354 );