tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 );