tor-browser

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

permissions.js (25825B)


      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 var { AppConstants } = ChromeUtils.importESModule(
      6  "resource://gre/modules/AppConstants.sys.mjs"
      7 );
      8 
      9 var { XPCOMUtils } = ChromeUtils.importESModule(
     10  "resource://gre/modules/XPCOMUtils.sys.mjs"
     11 );
     12 
     13 const lazy = {};
     14 
     15 XPCOMUtils.defineLazyServiceGetter(
     16  lazy,
     17  "contentBlockingAllowList",
     18  "@mozilla.org/content-blocking-allow-list;1",
     19  Ci.nsIContentBlockingAllowList
     20 );
     21 
     22 const permissionExceptionsL10n = {
     23  trackingprotection: {
     24    window: "permissions-exceptions-etp-window2",
     25    description: "permissions-exceptions-manage-etp-desc",
     26  },
     27  cookie: {
     28    window: "permissions-exceptions-cookie-window2",
     29    description: "permissions-exceptions-cookie-desc",
     30  },
     31  popup: {
     32    window: "permissions-exceptions-popup-window3",
     33    description: "permissions-exceptions-popup-desc2",
     34  },
     35  "login-saving": {
     36    window: "permissions-exceptions-saved-passwords-window",
     37    description: "permissions-exceptions-saved-passwords-desc",
     38  },
     39  "https-only-load-insecure": {
     40    window: "permissions-exceptions-https-only-window2",
     41    description: "permissions-exceptions-https-only-desc2",
     42  },
     43  install: {
     44    window: "permissions-exceptions-addons-window2",
     45    description: "permissions-exceptions-addons-desc",
     46  },
     47  "ipp-vpn": {
     48    window: "ip-protection-exceptions-dialog-window",
     49    description: "ip-protection-exclusions-desc",
     50  },
     51 };
     52 
     53 function Permission(principal, type, capability) {
     54  this.principal = principal;
     55  this.origin = principal.origin;
     56  this.type = type;
     57  this.capability = capability;
     58 }
     59 
     60 var gPermissionManager = {
     61  _type: "",
     62  _isObserving: false,
     63  _permissions: new Map(),
     64  _permissionsToAdd: new Map(),
     65  _permissionsToDelete: new Map(),
     66  _bundle: null,
     67  _list: null,
     68  _removeButton: null,
     69  _removeAllButton: null,
     70  _forcedHTTP: null,
     71  _capabilityFilter: null,
     72 
     73  onLoad() {
     74    let params = window.arguments[0];
     75    document.mozSubdialogReady = this.init(params);
     76  },
     77 
     78  /**
     79   * @param {object} params
     80   * @param {string} params.permissionType Permission type for which the dialog should be shown
     81   * @param {string} params.prefilledHost The value which the URL field should initially contain
     82   * @param {boolean} params.blockVisible Display the "Block" button in the dialog
     83   * @param {boolean} params.sessionVisible Display the "Allow for Session" button in the dialog (Only for Cookie & HTTPS-Only permissions)
     84   * @param {boolean} params.allowVisible Display the "Allow" button in the dialog
     85   * @param {boolean} params.disableETPVisible Display the "Add Exception" button in the dialog (Only for ETP permissions)
     86   * @param {boolean} params.addVisible Display the "Add" button in the dialog (Only for ipp-vpn permissions)
     87   * @param {boolean} params.hideStatusColumn Hide the "Status" column in the dialog
     88   * @param {boolean} params.forcedHTTP Save inputs whose URI has a HTTPS scheme with a HTTP scheme (Used by HTTPS-Only)
     89   * @param {number} params.capabilityFilter Display permissions that have the specified capability only. See Ci.nsIPermissionManager.
     90   */
     91  async init(params) {
     92    if (!this._isObserving) {
     93      Services.obs.addObserver(this, "perm-changed");
     94      this._isObserving = true;
     95    }
     96 
     97    document.addEventListener("dialogaccept", () => this.onApplyChanges());
     98 
     99    this._type = params.permissionType;
    100    this._list = document.getElementById("permissionsBox");
    101    this._removeButton = document.getElementById("removePermission");
    102    this._removeAllButton = document.getElementById("removeAllPermissions");
    103 
    104    this._btnCookieSession = document.getElementById("btnCookieSession");
    105    this._btnBlock = document.getElementById("btnBlock");
    106    this._btnDisableETP = document.getElementById("btnDisableETP");
    107    this._btnAllow = document.getElementById("btnAllow");
    108    this._btnHttpsOnlyOff = document.getElementById("btnHttpsOnlyOff");
    109    this._btnHttpsOnlyOffTmp = document.getElementById("btnHttpsOnlyOffTmp");
    110    this._btnAdd = document.getElementById("btnAdd");
    111 
    112    this._capabilityFilter = params.capabilityFilter;
    113 
    114    let permissionsText = document.getElementById("permissionsText");
    115 
    116    let l10n = permissionExceptionsL10n[this._type];
    117 
    118    document.l10n.setAttributes(permissionsText, l10n.description);
    119    document.l10n.setAttributes(document.documentElement, l10n.window);
    120 
    121    let urlFieldVisible =
    122      params.blockVisible ||
    123      params.sessionVisible ||
    124      params.allowVisible ||
    125      params.disableETPVisible ||
    126      params.addVisible;
    127 
    128    this._urlField = document.getElementById("url");
    129    this._urlField.value = params.prefilledHost;
    130    this._urlField.hidden = !urlFieldVisible;
    131 
    132    this._forcedHTTP = params.forcedHTTP;
    133 
    134    await document.l10n.translateElements([
    135      permissionsText,
    136      document.documentElement,
    137    ]);
    138 
    139    document.getElementById("btnDisableETP").hidden = !params.disableETPVisible;
    140    document.getElementById("btnBlock").hidden = !params.blockVisible;
    141    document.getElementById("btnCookieSession").hidden = !(
    142      params.sessionVisible && this._type == "cookie"
    143    );
    144    document.getElementById("btnHttpsOnlyOff").hidden = !(
    145      this._type == "https-only-load-insecure"
    146    );
    147    document.getElementById("btnHttpsOnlyOffTmp").hidden = !(
    148      params.sessionVisible && this._type == "https-only-load-insecure"
    149    );
    150    document.getElementById("btnAllow").hidden = !params.allowVisible;
    151    document.getElementById("btnAdd").hidden = !params.addVisible;
    152 
    153    this.onHostInput(this._urlField);
    154 
    155    let urlLabel = document.getElementById("urlLabel");
    156    urlLabel.hidden = !urlFieldVisible;
    157 
    158    this._hideStatusColumn = params.hideStatusColumn;
    159    let statusCol = document.getElementById("statusCol");
    160    statusCol.hidden = this._hideStatusColumn;
    161    const siteCol = document.getElementById("siteCol");
    162    if (this._hideStatusColumn) {
    163      statusCol.removeAttribute("data-isCurrentSortCol");
    164      siteCol.setAttribute("data-isCurrentSortCol", "true");
    165    }
    166 
    167    window.addEventListener("unload", () => {
    168      gPermissionManager.uninit();
    169    });
    170    window.addEventListener("keypress", event => {
    171      gPermissionManager.onWindowKeyPress(event);
    172    });
    173    document
    174      .getElementById("permissionsDialogCloseKey")
    175      .addEventListener("command", () => {
    176        window.close();
    177      });
    178    this._list.addEventListener("keypress", event => {
    179      gPermissionManager.onPermissionKeyPress(event);
    180    });
    181    this._list.addEventListener("select", () => {
    182      gPermissionManager.onPermissionSelect();
    183    });
    184    this.addCommandListeners();
    185    this._urlField.addEventListener("input", event => {
    186      gPermissionManager.onHostInput(event.target);
    187    });
    188    this._urlField.addEventListener("keypress", event => {
    189      gPermissionManager.onHostKeyPress(event);
    190    });
    191    statusCol.addEventListener("click", event => {
    192      gPermissionManager.buildPermissionsList(event.target);
    193    });
    194    siteCol.addEventListener("click", event => {
    195      gPermissionManager.buildPermissionsList(event.target);
    196    });
    197 
    198    Services.obs.notifyObservers(null, "flush-pending-permissions", this._type);
    199 
    200    this._loadPermissions();
    201    this.buildPermissionsList();
    202 
    203    this._urlField.focus();
    204  },
    205 
    206  addCommandListeners() {
    207    window.addEventListener("command", event => {
    208      switch (event.target.id) {
    209        case "removePermission":
    210          gPermissionManager.onPermissionDelete();
    211          break;
    212        case "removeAllPermissions":
    213          gPermissionManager.onAllPermissionsDelete();
    214          break;
    215        case "btnCookieSession":
    216          gPermissionManager.addPermission(
    217            Ci.nsICookiePermission.ACCESS_SESSION
    218          );
    219          break;
    220        case "btnBlock":
    221          gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);
    222          break;
    223        case "btnDisableETP":
    224          gPermissionManager.addPermission(
    225            Ci.nsIPermissionManager.ALLOW_ACTION
    226          );
    227          break;
    228        case "btnAllow":
    229          gPermissionManager.addPermission(
    230            Ci.nsIPermissionManager.ALLOW_ACTION
    231          );
    232          break;
    233        case "btnHttpsOnlyOff":
    234          gPermissionManager.addPermission(
    235            Ci.nsIPermissionManager.ALLOW_ACTION
    236          );
    237          break;
    238        case "btnHttpsOnlyOffTmp":
    239          gPermissionManager.addPermission(
    240            Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
    241          );
    242          break;
    243        case "btnAdd":
    244          // This button is for ipp-vpn, which only supports
    245          // site exclusions at this time.
    246          gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);
    247      }
    248    });
    249  },
    250 
    251  uninit() {
    252    if (this._isObserving) {
    253      Services.obs.removeObserver(this, "perm-changed");
    254      this._isObserving = false;
    255    }
    256  },
    257 
    258  observe(subject, topic, data) {
    259    if (topic !== "perm-changed") {
    260      return;
    261    }
    262 
    263    let permission = subject.QueryInterface(Ci.nsIPermission);
    264 
    265    // Ignore unrelated permission types.
    266    if (permission.type !== this._type) {
    267      return;
    268    }
    269 
    270    if (data == "added") {
    271      this._addPermissionToList(permission);
    272      this.buildPermissionsList();
    273    } else if (data == "changed") {
    274      let p = this._permissions.get(permission.principal.origin);
    275      // Maybe this item has been excluded before because it had an invalid capability.
    276      if (p) {
    277        p.capability = permission.capability;
    278        this._handleCapabilityChange(p);
    279      } else {
    280        this._addPermissionToList(permission);
    281      }
    282      this.buildPermissionsList();
    283    } else if (data == "deleted") {
    284      this._removePermissionFromList(permission.principal.origin);
    285    }
    286  },
    287 
    288  _handleCapabilityChange(perm) {
    289    let permissionlistitem = document.getElementsByAttribute(
    290      "origin",
    291      perm.origin
    292    )[0];
    293    document.l10n.setAttributes(
    294      permissionlistitem.querySelector(".website-capability-value"),
    295      this._getCapabilityL10nId(perm.capability)
    296    );
    297  },
    298 
    299  _isCapabilitySupported(capability) {
    300    return (
    301      capability == Ci.nsIPermissionManager.ALLOW_ACTION ||
    302      capability == Ci.nsIPermissionManager.DENY_ACTION ||
    303      capability == Ci.nsICookiePermission.ACCESS_SESSION ||
    304      // Bug 1753600 there are still a few legacy cookies around that have the capability 9,
    305      // _getCapabilityL10nId will throw if it receives a capability of 9
    306      // that is not in combination with the type https-only-load-insecure
    307      (capability ==
    308        Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION &&
    309        this._type == "https-only-load-insecure")
    310    );
    311  },
    312 
    313  _getCapabilityL10nId(capability) {
    314    // HTTPS-Only Mode phrases exceptions as turning it off
    315    if (this._type == "https-only-load-insecure") {
    316      return this._getHttpsOnlyCapabilityL10nId(capability);
    317    }
    318 
    319    switch (capability) {
    320      case Ci.nsIPermissionManager.ALLOW_ACTION:
    321        return "permissions-capabilities-listitem-allow";
    322      case Ci.nsIPermissionManager.DENY_ACTION:
    323        return "permissions-capabilities-listitem-block";
    324      case Ci.nsICookiePermission.ACCESS_SESSION:
    325        return "permissions-capabilities-listitem-allow-session";
    326      default:
    327        throw new Error(`Unknown capability: ${capability}`);
    328    }
    329  },
    330 
    331  _getHttpsOnlyCapabilityL10nId(capability) {
    332    switch (capability) {
    333      case Ci.nsIPermissionManager.ALLOW_ACTION:
    334        return "permissions-capabilities-listitem-off";
    335      case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION:
    336        return "permissions-capabilities-listitem-off-temporarily";
    337      default:
    338        throw new Error(`Unknown HTTPS-Only Mode capability: ${capability}`);
    339    }
    340  },
    341 
    342  _addPermissionToList(perm) {
    343    if (perm.type !== this._type) {
    344      return;
    345    }
    346    if (!this._isCapabilitySupported(perm.capability)) {
    347      return;
    348    }
    349 
    350    // If filtering is enabled, don't bother showing permissions that don't have
    351    // the capability we want.
    352    if (this._capabilityFilter && perm.capability !== this._capabilityFilter) {
    353      return;
    354    }
    355 
    356    // Skip private browsing session permissions.
    357    if (
    358      perm.principal.privateBrowsingId !==
    359        Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID &&
    360      perm.expireType === Services.perms.EXPIRE_SESSION
    361    ) {
    362      return;
    363    }
    364 
    365    let p = new Permission(perm.principal, perm.type, perm.capability);
    366    this._permissions.set(p.origin, p);
    367  },
    368 
    369  _addOrModifyPermission(principal, capability) {
    370    // check whether the permission already exists, if not, add it
    371    let permissionParams = { principal, type: this._type, capability };
    372    let existingPermission = this._permissions.get(principal.origin);
    373    if (!existingPermission) {
    374      this._permissionsToAdd.set(principal.origin, permissionParams);
    375      this._addPermissionToList(permissionParams);
    376      this.buildPermissionsList();
    377    } else if (existingPermission.capability != capability) {
    378      existingPermission.capability = capability;
    379      this._permissionsToAdd.set(principal.origin, permissionParams);
    380      this._handleCapabilityChange(existingPermission);
    381    }
    382  },
    383 
    384  _addNewPrincipalToList(list, uri) {
    385    list.push(Services.scriptSecurityManager.createContentPrincipal(uri, {}));
    386    // If we have ended up with an unknown scheme, the following will throw.
    387    list[list.length - 1].origin;
    388  },
    389 
    390  addPermission(capability) {
    391    let textbox = document.getElementById("url");
    392    let input_url = textbox.value.trim(); // trim any leading and trailing space
    393    let principals = [];
    394    try {
    395      // The origin accessor on the principal object will throw if the
    396      // principal doesn't have a canonical origin representation. This will
    397      // help catch cases where the URI parser parsed something like
    398      // `localhost:8080` as having the scheme `localhost`, rather than being
    399      // an invalid URI. A canonical origin representation is required by the
    400      // permission manager for storage, so this won't prevent any valid
    401      // permissions from being entered by the user.
    402      try {
    403        let uri = Services.io.newURI(input_url);
    404        if (this._forcedHTTP && uri.schemeIs("https")) {
    405          uri = uri.mutate().setScheme("http").finalize();
    406        }
    407        let principal = Services.scriptSecurityManager.createContentPrincipal(
    408          uri,
    409          {}
    410        );
    411        if (principal.origin.startsWith("moz-nullprincipal:")) {
    412          throw new Error("Null principal");
    413        }
    414        principals.push(principal);
    415      } catch (ex) {
    416        // If the `input_url` already starts with http:// or https://, it is
    417        // definetely invalid here and can't be fixed by prefixing it with
    418        // http:// or https://.
    419        if (
    420          input_url.startsWith("http://") ||
    421          input_url.startsWith("https://")
    422        ) {
    423          throw ex;
    424        }
    425        this._addNewPrincipalToList(
    426          principals,
    427          Services.io.newURI("http://" + input_url)
    428        );
    429        if (!this._forcedHTTP) {
    430          this._addNewPrincipalToList(
    431            principals,
    432            Services.io.newURI("https://" + input_url)
    433          );
    434        }
    435      }
    436    } catch (ex) {
    437      document.l10n
    438        .formatValues([
    439          { id: "permissions-invalid-uri-title" },
    440          { id: "permissions-invalid-uri-label" },
    441        ])
    442        .then(([title, message]) => {
    443          Services.prompt.alert(window, title, message);
    444        });
    445      return;
    446    }
    447    // In case of an ETP exception we compute the contentBlockingAllowList principal
    448    // to align with the allow list behavior triggered by the protections panel
    449    if (this._type == "trackingprotection") {
    450      principals = principals.map(
    451        lazy.contentBlockingAllowList.computeContentBlockingAllowListPrincipal
    452      );
    453    }
    454    for (let principal of principals) {
    455      this._addOrModifyPermission(principal, capability);
    456    }
    457 
    458    textbox.value = "";
    459    textbox.focus();
    460 
    461    // covers a case where the site exists already, so the buttons don't disable
    462    this.onHostInput(textbox);
    463 
    464    // enable "remove all" button as needed
    465    this._setRemoveButtonState();
    466  },
    467 
    468  _removePermission(permission) {
    469    this._removePermissionFromList(permission.origin);
    470 
    471    // If this permission was added during this session, let's remove
    472    // it from the pending adds list to prevent calls to the
    473    // permission manager.
    474    let isNewPermission = this._permissionsToAdd.delete(permission.origin);
    475    if (!isNewPermission) {
    476      this._permissionsToDelete.set(permission.origin, permission);
    477    }
    478  },
    479 
    480  _removePermissionFromList(origin) {
    481    this._permissions.delete(origin);
    482    let permissionlistitem = document.getElementsByAttribute(
    483      "origin",
    484      origin
    485    )[0];
    486    if (permissionlistitem) {
    487      permissionlistitem.remove();
    488    }
    489  },
    490 
    491  _loadPermissions() {
    492    // load permissions into a table.
    493    for (let nextPermission of Services.perms.all) {
    494      this._addPermissionToList(nextPermission);
    495    }
    496  },
    497 
    498  _createPermissionListItem(permission) {
    499    let disabledByPolicy = this._permissionDisabledByPolicy(permission);
    500    let richlistitem = document.createXULElement("richlistitem");
    501    richlistitem.setAttribute("origin", permission.origin);
    502    let row = document.createXULElement("hbox");
    503    row.setAttribute("style", "flex: 1");
    504 
    505    let hbox = document.createXULElement("hbox");
    506    let website = document.createXULElement("label");
    507    website.toggleAttribute("disabled", disabledByPolicy);
    508    website.setAttribute("class", "website-name-value");
    509    website.setAttribute("value", permission.origin);
    510    hbox.setAttribute("class", "website-name");
    511    hbox.setAttribute("style", "flex: 3 3; width: 0");
    512    hbox.appendChild(website);
    513    row.appendChild(hbox);
    514 
    515    if (!this._hideStatusColumn) {
    516      hbox = document.createXULElement("hbox");
    517      let capability = document.createXULElement("label");
    518      capability.toggleAttribute("disabled", disabledByPolicy);
    519      capability.setAttribute("class", "website-capability-value");
    520      document.l10n.setAttributes(
    521        capability,
    522        this._getCapabilityL10nId(permission.capability)
    523      );
    524      hbox.setAttribute("class", "website-name");
    525      hbox.setAttribute("style", "flex: 1; width: 0");
    526      hbox.appendChild(capability);
    527      row.appendChild(hbox);
    528    }
    529 
    530    richlistitem.appendChild(row);
    531    return richlistitem;
    532  },
    533 
    534  onWindowKeyPress(event) {
    535    // Prevent dialog.js from closing the dialog when the user submits the input
    536    // field via the return key.
    537    if (
    538      event.keyCode == KeyEvent.DOM_VK_RETURN &&
    539      document.activeElement == this._urlField
    540    ) {
    541      event.preventDefault();
    542    }
    543  },
    544 
    545  onPermissionKeyPress(event) {
    546    if (!this._list.selectedItem) {
    547      return;
    548    }
    549 
    550    if (
    551      event.keyCode == KeyEvent.DOM_VK_DELETE ||
    552      (AppConstants.platform == "macosx" &&
    553        event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
    554    ) {
    555      this.onPermissionDelete();
    556      event.preventDefault();
    557    }
    558  },
    559 
    560  onHostKeyPress(event) {
    561    if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
    562      if (!document.getElementById("btnAllow").hidden) {
    563        document.getElementById("btnAllow").click();
    564      } else if (!document.getElementById("btnBlock").hidden) {
    565        document.getElementById("btnBlock").click();
    566      } else if (!document.getElementById("btnHttpsOnlyOff").hidden) {
    567        document.getElementById("btnHttpsOnlyOff").click();
    568      } else if (!document.getElementById("btnDisableETP").hidden) {
    569        document.getElementById("btnDisableETP").click();
    570      } else if (!document.getElementById("btnAdd").hidden) {
    571        document.getElementById("btnAdd").click();
    572      }
    573    }
    574  },
    575 
    576  onHostInput(siteField) {
    577    this._btnCookieSession.disabled =
    578      this._btnCookieSession.hidden || !siteField.value;
    579    this._btnHttpsOnlyOff.disabled =
    580      this._btnHttpsOnlyOff.hidden || !siteField.value;
    581    this._btnHttpsOnlyOffTmp.disabled =
    582      this._btnHttpsOnlyOffTmp.hidden || !siteField.value;
    583    this._btnBlock.disabled = this._btnBlock.hidden || !siteField.value;
    584    this._btnDisableETP.disabled =
    585      this._btnDisableETP.hidden || !siteField.value;
    586    this._btnAllow.disabled = this._btnAllow.hidden || !siteField.value;
    587    this._btnAdd.disabled = this._btnAdd.hidden || !siteField.value;
    588  },
    589 
    590  _setRemoveButtonState() {
    591    if (!this._list) {
    592      return;
    593    }
    594 
    595    let hasSelection = this._list.selectedIndex >= 0;
    596 
    597    let disabledByPolicy = false;
    598    if (Services.policies.status === Services.policies.ACTIVE && hasSelection) {
    599      let origin = this._list.selectedItem.getAttribute("origin");
    600      disabledByPolicy = this._permissionDisabledByPolicy(
    601        this._permissions.get(origin)
    602      );
    603    }
    604 
    605    this._removeButton.disabled = !hasSelection || disabledByPolicy;
    606    let disabledItems = this._list.querySelectorAll(
    607      "label.website-name-value[disabled='true']"
    608    );
    609 
    610    this._removeAllButton.disabled =
    611      this._list.itemCount == disabledItems.length;
    612  },
    613 
    614  onPermissionDelete() {
    615    let richlistitem = this._list.selectedItem;
    616    let origin = richlistitem.getAttribute("origin");
    617    let permission = this._permissions.get(origin);
    618    if (this._permissionDisabledByPolicy(permission)) {
    619      return;
    620    }
    621 
    622    this._removePermission(permission);
    623 
    624    this._setRemoveButtonState();
    625  },
    626 
    627  onAllPermissionsDelete() {
    628    for (let permission of this._permissions.values()) {
    629      if (this._permissionDisabledByPolicy(permission)) {
    630        continue;
    631      }
    632      this._removePermission(permission);
    633    }
    634 
    635    this._setRemoveButtonState();
    636  },
    637 
    638  onPermissionSelect() {
    639    this._setRemoveButtonState();
    640  },
    641 
    642  onApplyChanges() {
    643    // Stop observing permission changes since we are about
    644    // to write out the pending adds/deletes and don't need
    645    // to update the UI
    646    this.uninit();
    647 
    648    for (let p of this._permissionsToDelete.values()) {
    649      Services.perms.removeFromPrincipal(p.principal, p.type);
    650    }
    651 
    652    for (let p of this._permissionsToAdd.values()) {
    653      // If this sets the HTTPS-Only exemption only for this
    654      // session, then the expire-type has to be set.
    655      if (
    656        p.capability ==
    657        Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
    658      ) {
    659        Services.perms.addFromPrincipal(
    660          p.principal,
    661          p.type,
    662          p.capability,
    663          Ci.nsIPermissionManager.EXPIRE_SESSION
    664        );
    665      } else {
    666        Services.perms.addFromPrincipal(p.principal, p.type, p.capability);
    667      }
    668    }
    669  },
    670 
    671  buildPermissionsList(sortCol) {
    672    // Clear old entries.
    673    let oldItems = this._list.querySelectorAll("richlistitem");
    674    for (let item of oldItems) {
    675      item.remove();
    676    }
    677    let frag = document.createDocumentFragment();
    678 
    679    let permissions = Array.from(this._permissions.values());
    680 
    681    for (let permission of permissions) {
    682      let richlistitem = this._createPermissionListItem(permission);
    683      frag.appendChild(richlistitem);
    684    }
    685 
    686    // Sort permissions.
    687    this._sortPermissions(this._list, frag, sortCol);
    688 
    689    this._list.appendChild(frag);
    690 
    691    this._setRemoveButtonState();
    692  },
    693 
    694  _permissionDisabledByPolicy(permission) {
    695    let permissionObject = Services.perms.getPermissionObject(
    696      permission.principal,
    697      this._type,
    698      false
    699    );
    700    return (
    701      permissionObject?.expireType == Ci.nsIPermissionManager.EXPIRE_POLICY
    702    );
    703  },
    704 
    705  _sortPermissions(list, frag, column) {
    706    let sortDirection;
    707 
    708    if (!column) {
    709      column = document.querySelector("treecol[data-isCurrentSortCol=true]");
    710      sortDirection =
    711        column.getAttribute("data-last-sortDirection") || "ascending";
    712    } else {
    713      sortDirection = column.getAttribute("data-last-sortDirection");
    714      sortDirection =
    715        sortDirection === "ascending" ? "descending" : "ascending";
    716    }
    717 
    718    let sortFunc = null;
    719    switch (column.id) {
    720      case "siteCol":
    721        sortFunc = (a, b) => {
    722          return comp.compare(
    723            a.getAttribute("origin"),
    724            b.getAttribute("origin")
    725          );
    726        };
    727        break;
    728 
    729      case "statusCol":
    730        sortFunc = (a, b) => {
    731          // The capabilities values ("Allow" and "Block") are localized asynchronously.
    732          // Sort based on the guaranteed-present localization ID instead, note that the
    733          // ascending/descending arrow may be pointing the wrong way.
    734          return (
    735            a
    736              .querySelector(".website-capability-value")
    737              .getAttribute("data-l10n-id") >
    738            b
    739              .querySelector(".website-capability-value")
    740              .getAttribute("data-l10n-id")
    741          );
    742        };
    743        break;
    744    }
    745 
    746    let comp = new Services.intl.Collator(undefined, {
    747      usage: "sort",
    748    });
    749 
    750    let items = Array.from(frag.querySelectorAll("richlistitem"));
    751 
    752    if (sortDirection === "descending") {
    753      items.sort((a, b) => sortFunc(b, a));
    754    } else {
    755      items.sort(sortFunc);
    756    }
    757 
    758    // Re-append items in the correct order:
    759    items.forEach(item => frag.appendChild(item));
    760 
    761    let cols = list.previousElementSibling.querySelectorAll("treecol");
    762    cols.forEach(c => {
    763      c.removeAttribute("data-isCurrentSortCol");
    764      c.removeAttribute("sortDirection");
    765    });
    766    column.setAttribute("data-isCurrentSortCol", "true");
    767    column.setAttribute("sortDirection", sortDirection);
    768    column.setAttribute("data-last-sortDirection", sortDirection);
    769  },
    770 };
    771 
    772 window.addEventListener("load", () => {
    773  gPermissionManager.onLoad();
    774 });