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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const PERM_NAME = "ipp-vpn";
/**
* Manages site exceptions for IP Protection.
* It communicates with Services.perms to update the ipp-vpn permission type.
* Site exclusions are marked as permissions with DENY capabilities.
*
* While permissions related UI (eg. panels and dialogs) already handle changes to ipp-vpn,
* the intention of this class is to abstract methods for updating ipp-vpn as needed
* from other non-permissions related UI.
*/
class ExceptionsManager extends EventTarget {
#inited = false;
#observer = null;
init() {
if (this.#inited) {
return;
}
// ES6 classes that extend EventTarget cannot be coerced into nsIObserver.
// Work around this by using a function as the observer.
this.#observer = (subject, topic, data) => {
this.observe(subject, topic, data);
};
Services.obs.addObserver(this.#observer, "perm-changed");
this.#inited = true;
}
uninit() {
if (!this.#inited) {
return;
}
Services.obs.removeObserver(this.#observer, "perm-changed");
this.#observer = null;
this.#inited = false;
}
observe(subject, topic, data) {
if (topic !== "perm-changed") {
return;
}
let permission = subject.QueryInterface(Ci.nsIPermission);
if (permission.type !== PERM_NAME) {
return;
}
const isExclusion =
permission.capability === Ci.nsIPermissionManager.DENY_ACTION;
const added = data === "added" && isExclusion;
const removed = data === "deleted" && isExclusion;
if (added || removed) {
if (added) {
Glean.ipprotection.exclusionAdded.add(1);
}
this.dispatchEvent(
new CustomEvent("IPPExceptionsManager:ExclusionChanged")
);
}
}
/**
* Adds a principal to ipp-vpn with capability DENY_ACTION
* for site exclusions.
*
* @param {nsIPrincipal} principal
* The principal that we want to add as a site exception.
*/
addExclusion(principal) {
Services.perms.addFromPrincipal(
principal,
PERM_NAME,
Ci.nsIPermissionManager.DENY_ACTION
);
}
/**
* Removes an existing principal from ipp-vpn.
*
* @param {nsIPrincipal} principal
* The principal that we want to remove as a site exception.
*/
removeExclusion(principal) {
Services.perms.removeFromPrincipal(principal, PERM_NAME);
}
/**
* Returns true if the principal already exists in ipp-vpn
* and is registered as a permission with a DENY_ACTION
* capability (site exclusions).
* Else returns false if no such principal exists.
*
* @param {nsIPrincipal} principal
* The principal that we want to check is saved in ipp-vpn
* as a site exclusion.
* @returns {boolean}
* True if the principal exists as a site exclusion.
*/
hasExclusion(principal) {
let permission = this.getExceptionPermissionObject(principal);
return permission?.capability === Ci.nsIPermissionManager.DENY_ACTION;
}
/**
* Get the permission object for a site exception if it exists in ipp-vpn.
*
* Use exactHost=true to only match the specific origin, not the base domain.
* This ensures that subdomains aren't implicitly excluded when entering
* a site in the about:preferences dialog. It also avoids an issue where we
* try to remove a subdomain as an exclusion when the site doesn't exist in ipp-vpn
* (see Bug 2016676).
*
* Eg. if we enter "example.com" in the dialog, "www.example.com" and
* "subdomain.example.com" won't be considered exclusions.
*
* @param {nsIPrincipal} principal
* The principal that we want to check is saved in ipp-vpn.
*
* @returns {nsIPermission}
* The permission object for a site exception, or null if unavailable.
*/
getExceptionPermissionObject(principal) {
let permissionObject = Services.perms.getPermissionObject(
principal,
PERM_NAME,
true /* exactHost */
);
return permissionObject;
}
/**
* Gets the total number of site exclusions added to ipp-vpn.
*
* @returns {number}
* The count of site exclusions in ipp-vpn.
*/
getExclusionCount() {
let count = 0;
for (let perm of Services.perms.getAllByTypes([PERM_NAME])) {
if (perm.capability === Ci.nsIPermissionManager.DENY_ACTION) {
count++;
}
}
return count;
}
/**
* Sets the given principal as an exclusion or non exclusion.
*
* @param {nsIPrincipal} principal
* The principal we want to update for the exclusion state.
* @param {boolean} shouldExclude
* True to set the principal as an exclusion. Otherwise false.
*
* @example
* // Assuming the principal represents a site https://www.example.com,
* // this line sets https://www.example.com as an exclusion
* // in ipp-vpn.
* IPPExceptionsManager.setExclusion(nsIPrincipal, true);
*/
setExclusion(principal, shouldExclude) {
if (!principal) {
return;
}
const isExclusion = this.hasExclusion(principal);
// Early return if already in desired state
if ((shouldExclude && isExclusion) || (!shouldExclude && !isExclusion)) {
return;
}
if (shouldExclude) {
this.addExclusion(principal);
} else {
this.removeExclusion(principal);
}
}
}
const IPPExceptionsManager = new ExceptionsManager();
export { IPPExceptionsManager };