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