diff options
Diffstat (limited to 'browser/components/preferences/dialogs/permissions.js')
-rw-r--r-- | browser/components/preferences/dialogs/permissions.js | 645 |
1 files changed, 645 insertions, 0 deletions
diff --git a/browser/components/preferences/dialogs/permissions.js b/browser/components/preferences/dialogs/permissions.js new file mode 100644 index 0000000000..1d43f6f810 --- /dev/null +++ b/browser/components/preferences/dialogs/permissions.js @@ -0,0 +1,645 @@ +/* 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", "-moz-box-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", "-moz-box-flex: 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", "-moz-box-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); + }, +}; |