/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const lazy = {}; XPCOMUtils.defineLazyServiceGetter( lazy, "contentBlockingAllowList", "@mozilla.org/content-blocking-allow-list;1", "nsIContentBlockingAllowList" ); const permissionExceptionsL10n = { trackingprotection: { window: "permissions-exceptions-etp-window2", description: "permissions-exceptions-manage-etp-desc", }, cookie: { window: "permissions-exceptions-cookie-window2", description: "permissions-exceptions-cookie-desc", }, popup: { window: "permissions-exceptions-popup-window2", description: "permissions-exceptions-popup-desc", }, "login-saving": { window: "permissions-exceptions-saved-logins-window2", description: "permissions-exceptions-saved-logins-desc", }, "https-only-load-insecure": { window: "permissions-exceptions-https-only-window2", description: "permissions-exceptions-https-only-desc", }, install: { window: "permissions-exceptions-addons-window2", description: "permissions-exceptions-addons-desc", }, }; function Permission(principal, type, capability) { this.principal = principal; this.origin = principal.origin; this.type = type; this.capability = capability; } var gPermissionManager = { _type: "", _isObserving: false, _permissions: new Map(), _permissionsToAdd: new Map(), _permissionsToDelete: new Map(), _bundle: null, _list: null, _removeButton: null, _removeAllButton: null, onLoad() { let params = window.arguments[0]; document.mozSubdialogReady = this.init(params); }, async init(params) { if (!this._isObserving) { Services.obs.addObserver(this, "perm-changed"); this._isObserving = true; } document.addEventListener("dialogaccept", () => this.onApplyChanges()); this._type = params.permissionType; this._list = document.getElementById("permissionsBox"); this._removeButton = document.getElementById("removePermission"); this._removeAllButton = document.getElementById("removeAllPermissions"); this._btnCookieSession = document.getElementById("btnCookieSession"); this._btnBlock = document.getElementById("btnBlock"); this._btnDisableETP = document.getElementById("btnDisableETP"); this._btnAllow = document.getElementById("btnAllow"); this._btnHttpsOnlyOff = document.getElementById("btnHttpsOnlyOff"); this._btnHttpsOnlyOffTmp = document.getElementById("btnHttpsOnlyOffTmp"); let permissionsText = document.getElementById("permissionsText"); let l10n = permissionExceptionsL10n[this._type]; document.l10n.setAttributes(permissionsText, l10n.description); document.l10n.setAttributes(document.documentElement, l10n.window); let urlFieldVisible = params.blockVisible || params.sessionVisible || params.allowVisible || params.disableETPVisible; this._urlField = document.getElementById("url"); this._urlField.value = params.prefilledHost; this._urlField.hidden = !urlFieldVisible; await document.l10n.translateElements([ permissionsText, document.documentElement, ]); document.getElementById("btnDisableETP").hidden = !params.disableETPVisible; document.getElementById("btnBlock").hidden = !params.blockVisible; document.getElementById("btnCookieSession").hidden = !( params.sessionVisible && this._type == "cookie" ); document.getElementById("btnHttpsOnlyOff").hidden = !( this._type == "https-only-load-insecure" ); document.getElementById("btnHttpsOnlyOffTmp").hidden = !( params.sessionVisible && this._type == "https-only-load-insecure" ); document.getElementById("btnAllow").hidden = !params.allowVisible; this.onHostInput(this._urlField); let urlLabel = document.getElementById("urlLabel"); urlLabel.hidden = !urlFieldVisible; this._hideStatusColumn = params.hideStatusColumn; let statusCol = document.getElementById("statusCol"); statusCol.hidden = this._hideStatusColumn; if (this._hideStatusColumn) { statusCol.removeAttribute("data-isCurrentSortCol"); document .getElementById("siteCol") .setAttribute("data-isCurrentSortCol", "true"); } Services.obs.notifyObservers(null, "flush-pending-permissions", this._type); this._loadPermissions(); this.buildPermissionsList(); this._urlField.focus(); }, uninit() { if (this._isObserving) { Services.obs.removeObserver(this, "perm-changed"); this._isObserving = false; } }, observe(subject, topic, data) { if (topic !== "perm-changed") { return; } let permission = subject.QueryInterface(Ci.nsIPermission); // Ignore unrelated permission types. if (permission.type !== this._type) { return; } if (data == "added") { this._addPermissionToList(permission); this.buildPermissionsList(); } else if (data == "changed") { let p = this._permissions.get(permission.principal.origin); // Maybe this item has been excluded before because it had an invalid capability. if (p) { p.capability = permission.capability; this._handleCapabilityChange(p); } else { this._addPermissionToList(permission); } this.buildPermissionsList(); } else if (data == "deleted") { this._removePermissionFromList(permission.principal.origin); } }, _handleCapabilityChange(perm) { let permissionlistitem = document.getElementsByAttribute( "origin", perm.origin )[0]; document.l10n.setAttributes( permissionlistitem.querySelector(".website-capability-value"), this._getCapabilityL10nId(perm.capability) ); }, _isCapabilitySupported(capability) { return ( capability == Ci.nsIPermissionManager.ALLOW_ACTION || capability == Ci.nsIPermissionManager.DENY_ACTION || capability == Ci.nsICookiePermission.ACCESS_SESSION || // Bug 1753600 there are still a few legacy cookies around that have the capability 9, // _getCapabilityL10nId will throw if it receives a capability of 9 // that is not in combination with the type https-only-load-insecure (capability == Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION && this._type == "https-only-load-insecure") ); }, _getCapabilityL10nId(capability) { // HTTPS-Only Mode phrases exceptions as turning it off if (this._type == "https-only-load-insecure") { return this._getHttpsOnlyCapabilityL10nId(capability); } switch (capability) { case Ci.nsIPermissionManager.ALLOW_ACTION: return "permissions-capabilities-listitem-allow"; case Ci.nsIPermissionManager.DENY_ACTION: return "permissions-capabilities-listitem-block"; case Ci.nsICookiePermission.ACCESS_SESSION: return "permissions-capabilities-listitem-allow-session"; default: throw new Error(`Unknown capability: ${capability}`); } }, _getHttpsOnlyCapabilityL10nId(capability) { switch (capability) { case Ci.nsIPermissionManager.ALLOW_ACTION: return "permissions-capabilities-listitem-off"; case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION: return "permissions-capabilities-listitem-off-temporarily"; default: throw new Error(`Unknown HTTPS-Only Mode capability: ${capability}`); } }, _addPermissionToList(perm) { if (perm.type !== this._type) { return; } if (!this._isCapabilitySupported(perm.capability)) { return; } // Skip private browsing session permissions. if ( perm.principal.privateBrowsingId !== Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID && perm.expireType === Services.perms.EXPIRE_SESSION ) { return; } let p = new Permission(perm.principal, perm.type, perm.capability); this._permissions.set(p.origin, p); }, _addOrModifyPermission(principal, capability) { // check whether the permission already exists, if not, add it let permissionParams = { principal, type: this._type, capability }; let existingPermission = this._permissions.get(principal.origin); if (!existingPermission) { this._permissionsToAdd.set(principal.origin, permissionParams); this._addPermissionToList(permissionParams); this.buildPermissionsList(); } else if (existingPermission.capability != capability) { existingPermission.capability = capability; this._permissionsToAdd.set(principal.origin, permissionParams); this._handleCapabilityChange(existingPermission); } }, _addNewPrincipalToList(list, uri) { list.push(Services.scriptSecurityManager.createContentPrincipal(uri, {})); // If we have ended up with an unknown scheme, the following will throw. list[list.length - 1].origin; }, addPermission(capability) { let textbox = document.getElementById("url"); let input_url = textbox.value.trim(); // trim any leading and trailing space let principals = []; try { // The origin accessor on the principal object will throw if the // principal doesn't have a canonical origin representation. This will // help catch cases where the URI parser parsed something like // `localhost:8080` as having the scheme `localhost`, rather than being // an invalid URI. A canonical origin representation is required by the // permission manager for storage, so this won't prevent any valid // permissions from being entered by the user. try { let uri = Services.io.newURI(input_url); let principal = Services.scriptSecurityManager.createContentPrincipal( uri, {} ); if (principal.origin.startsWith("moz-nullprincipal:")) { throw new Error("Null principal"); } principals.push(principal); } catch (ex) { this._addNewPrincipalToList( principals, Services.io.newURI("http://" + input_url) ); this._addNewPrincipalToList( principals, Services.io.newURI("https://" + input_url) ); } } catch (ex) { document.l10n .formatValues([ { id: "permissions-invalid-uri-title" }, { id: "permissions-invalid-uri-label" }, ]) .then(([title, message]) => { Services.prompt.alert(window, title, message); }); return; } // In case of an ETP exception we compute the contentBlockingAllowList principal // to align with the allow list behavior triggered by the protections panel if (this._type == "trackingprotection") { principals = principals.map( lazy.contentBlockingAllowList.computeContentBlockingAllowListPrincipal ); } for (let principal of principals) { this._addOrModifyPermission(principal, capability); } textbox.value = ""; textbox.focus(); // covers a case where the site exists already, so the buttons don't disable this.onHostInput(textbox); // enable "remove all" button as needed this._setRemoveButtonState(); }, _removePermission(permission) { this._removePermissionFromList(permission.origin); // If this permission was added during this session, let's remove // it from the pending adds list to prevent calls to the // permission manager. let isNewPermission = this._permissionsToAdd.delete(permission.origin); if (!isNewPermission) { this._permissionsToDelete.set(permission.origin, permission); } }, _removePermissionFromList(origin) { this._permissions.delete(origin); let permissionlistitem = document.getElementsByAttribute( "origin", origin )[0]; if (permissionlistitem) { permissionlistitem.remove(); } }, _loadPermissions() { // load permissions into a table. for (let nextPermission of Services.perms.all) { this._addPermissionToList(nextPermission); } }, _createPermissionListItem(permission) { let disabledByPolicy = this._permissionDisabledByPolicy(permission); let richlistitem = document.createXULElement("richlistitem"); richlistitem.setAttribute("origin", permission.origin); let row = document.createXULElement("hbox"); row.setAttribute("style", "flex: 1"); let hbox = document.createXULElement("hbox"); let website = document.createXULElement("label"); website.setAttribute("disabled", disabledByPolicy); website.setAttribute("class", "website-name-value"); website.setAttribute("value", permission.origin); hbox.setAttribute("class", "website-name"); hbox.setAttribute("style", "flex: 3 3; width: 0"); hbox.appendChild(website); row.appendChild(hbox); if (!this._hideStatusColumn) { hbox = document.createXULElement("hbox"); let capability = document.createXULElement("label"); capability.setAttribute("disabled", disabledByPolicy); capability.setAttribute("class", "website-capability-value"); document.l10n.setAttributes( capability, this._getCapabilityL10nId(permission.capability) ); hbox.setAttribute("class", "website-name"); hbox.setAttribute("style", "flex: 1; width: 0"); hbox.appendChild(capability); row.appendChild(hbox); } richlistitem.appendChild(row); return richlistitem; }, onWindowKeyPress(event) { // Prevent dialog.js from closing the dialog when the user submits the input // field via the return key. if ( event.keyCode == KeyEvent.DOM_VK_RETURN && document.activeElement == this._urlField ) { event.preventDefault(); } }, onPermissionKeyPress(event) { if (!this._list.selectedItem) { return; } if ( event.keyCode == KeyEvent.DOM_VK_DELETE || (AppConstants.platform == "macosx" && event.keyCode == KeyEvent.DOM_VK_BACK_SPACE) ) { this.onPermissionDelete(); event.preventDefault(); } }, onHostKeyPress(event) { if (event.keyCode == KeyEvent.DOM_VK_RETURN) { if (!document.getElementById("btnAllow").hidden) { document.getElementById("btnAllow").click(); } else if (!document.getElementById("btnBlock").hidden) { document.getElementById("btnBlock").click(); } else if (!document.getElementById("btnHttpsOnlyOff").hidden) { document.getElementById("btnHttpsOnlyOff").click(); } else if (!document.getElementById("btnDisableETP").hidden) { document.getElementById("btnDisableETP").click(); } } }, onHostInput(siteField) { this._btnCookieSession.disabled = this._btnCookieSession.hidden || !siteField.value; this._btnHttpsOnlyOff.disabled = this._btnHttpsOnlyOff.hidden || !siteField.value; this._btnHttpsOnlyOffTmp.disabled = this._btnHttpsOnlyOffTmp.hidden || !siteField.value; this._btnBlock.disabled = this._btnBlock.hidden || !siteField.value; this._btnDisableETP.disabled = this._btnDisableETP.hidden || !siteField.value; this._btnAllow.disabled = this._btnAllow.hidden || !siteField.value; }, _setRemoveButtonState() { if (!this._list) { return; } let hasSelection = this._list.selectedIndex >= 0; let disabledByPolicy = false; if (Services.policies.status === Services.policies.ACTIVE && hasSelection) { let origin = this._list.selectedItem.getAttribute("origin"); disabledByPolicy = this._permissionDisabledByPolicy( this._permissions.get(origin) ); } this._removeButton.disabled = !hasSelection || disabledByPolicy; let disabledItems = this._list.querySelectorAll( "label.website-name-value[disabled='true']" ); this._removeAllButton.disabled = this._list.itemCount == disabledItems.length; }, onPermissionDelete() { let richlistitem = this._list.selectedItem; let origin = richlistitem.getAttribute("origin"); let permission = this._permissions.get(origin); if (this._permissionDisabledByPolicy(permission)) { return; } this._removePermission(permission); this._setRemoveButtonState(); }, onAllPermissionsDelete() { for (let permission of this._permissions.values()) { if (this._permissionDisabledByPolicy(permission)) { continue; } this._removePermission(permission); } this._setRemoveButtonState(); }, onPermissionSelect() { this._setRemoveButtonState(); }, onApplyChanges() { // Stop observing permission changes since we are about // to write out the pending adds/deletes and don't need // to update the UI this.uninit(); for (let p of this._permissionsToDelete.values()) { Services.perms.removeFromPrincipal(p.principal, p.type); } for (let p of this._permissionsToAdd.values()) { // If this sets the HTTPS-Only exemption only for this // session, then the expire-type has to be set. if ( p.capability == Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION ) { Services.perms.addFromPrincipal( p.principal, p.type, p.capability, Ci.nsIPermissionManager.EXPIRE_SESSION ); } else { Services.perms.addFromPrincipal(p.principal, p.type, p.capability); } } }, buildPermissionsList(sortCol) { // Clear old entries. let oldItems = this._list.querySelectorAll("richlistitem"); for (let item of oldItems) { item.remove(); } let frag = document.createDocumentFragment(); let permissions = Array.from(this._permissions.values()); for (let permission of permissions) { let richlistitem = this._createPermissionListItem(permission); frag.appendChild(richlistitem); } // Sort permissions. this._sortPermissions(this._list, frag, sortCol); this._list.appendChild(frag); this._setRemoveButtonState(); }, _permissionDisabledByPolicy(permission) { let permissionObject = Services.perms.getPermissionObject( permission.principal, this._type, false ); return ( permissionObject?.expireType == Ci.nsIPermissionManager.EXPIRE_POLICY ); }, _sortPermissions(list, frag, column) { let sortDirection; if (!column) { column = document.querySelector("treecol[data-isCurrentSortCol=true]"); sortDirection = column.getAttribute("data-last-sortDirection") || "ascending"; } else { sortDirection = column.getAttribute("data-last-sortDirection"); sortDirection = sortDirection === "ascending" ? "descending" : "ascending"; } let sortFunc = null; switch (column.id) { case "siteCol": sortFunc = (a, b) => { return comp.compare( a.getAttribute("origin"), b.getAttribute("origin") ); }; break; case "statusCol": sortFunc = (a, b) => { // The capabilities values ("Allow" and "Block") are localized asynchronously. // Sort based on the guaranteed-present localization ID instead, note that the // ascending/descending arrow may be pointing the wrong way. return ( a .querySelector(".website-capability-value") .getAttribute("data-l10n-id") > b .querySelector(".website-capability-value") .getAttribute("data-l10n-id") ); }; break; } let comp = new Services.intl.Collator(undefined, { usage: "sort", }); let items = Array.from(frag.querySelectorAll("richlistitem")); if (sortDirection === "descending") { items.sort((a, b) => sortFunc(b, a)); } else { items.sort(sortFunc); } // Re-append items in the correct order: items.forEach(item => frag.appendChild(item)); let cols = list.previousElementSibling.querySelectorAll("treecol"); cols.forEach(c => { c.removeAttribute("data-isCurrentSortCol"); c.removeAttribute("sortDirection"); }); column.setAttribute("data-isCurrentSortCol", "true"); column.setAttribute("sortDirection", sortDirection); column.setAttribute("data-last-sortDirection", sortDirection); }, };