Source code
Revision control
Copy as Markdown
Other Tools
/* 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
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-passwords-window",
description: "permissions-exceptions-saved-passwords-desc",
},
"https-only-load-insecure": {
window: "permissions-exceptions-https-only-window2",
description: "permissions-exceptions-https-only-desc2",
},
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,
_forcedHTTP: null,
onLoad() {
let params = window.arguments[0];
document.mozSubdialogReady = this.init(params);
},
/**
* @param {Object} params
* @param {string} params.permissionType Permission type for which the dialog should be shown
* @param {string} params.prefilledHost The value which the URL field should initially contain
* @param {boolean} params.blockVisible Display the "Block" button in the dialog
* @param {boolean} params.sessionVisible Display the "Allow for Session" button in the dialog (Only for Cookie & HTTPS-Only permissions)
* @param {boolean} params.allowVisible Display the "Allow" button in the dialog
* @param {boolean} params.disableETPVisible Display the "Add Exception" button in the dialog (Only for ETP permissions)
* @param {boolean} params.hideStatusColumn Hide the "Status" column in the dialog
* @param {boolean} params.forcedHTTP Save inputs whose URI has a HTTPS scheme with a HTTP scheme (Used by HTTPS-Only)
*/
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;
this._forcedHTTP = params.forcedHTTP;
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 ||
// _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);
if (this._forcedHTTP && uri.schemeIs("https")) {
uri = uri.mutate().setScheme("http").finalize();
}
let principal = Services.scriptSecurityManager.createContentPrincipal(
uri,
{}
);
if (principal.origin.startsWith("moz-nullprincipal:")) {
throw new Error("Null principal");
}
principals.push(principal);
} catch (ex) {
// If the `input_url` already starts with http:// or https://, it is
// definetely invalid here and can't be fixed by prefixing it with
// http:// or https://.
if (
input_url.startsWith("http://") ||
input_url.startsWith("https://")
) {
throw ex;
}
this._addNewPrincipalToList(
principals,
Services.io.newURI("http://" + input_url)
);
if (!this._forcedHTTP) {
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);
},
};