summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/dialogs/permissions.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/preferences/dialogs/permissions.js')
-rw-r--r--browser/components/preferences/dialogs/permissions.js645
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..3ca2664611
--- /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", "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);
+ },
+};