Files
sousa-gecko/browser/modules/test/unit/test_SitePermissions.js
T
Emma Zuehlcke 05cf85a9af Bug 2049138 - Surface the clear-on-shutdown exception as a "Keep site data when closing" permission in the Page Info window and the site permission panel. r=desktop-theme-reviewers,timhuang,Itiel
Registers persist-data-on-shutdown in SitePermissions so users can add a
per-site exception from Page Info (Ctrl+i) and the lock-icon permission
panel, instead of only the prefs "Manage Exceptions…" dialog. Shown only
while clear-on-shutdown is enabled.

Differential Revision: https://phabricator.services.mozilla.com/D309066
2026-07-02 10:33:30 +00:00

650 lines
19 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { SitePermissions } = ChromeUtils.importESModule(
"resource:///modules/SitePermissions.sys.mjs"
);
const RESIST_FINGERPRINTING_ENABLED = Services.prefs.getBoolPref(
"privacy.resistFingerprinting"
);
const MIDI_ENABLED = Services.prefs.getBoolPref("dom.webmidi.enabled");
const WEBSERIAL_ENABLED = Services.prefs.getBoolPref("dom.webserial.enabled");
const SPEAKER_SELECTION_ENABLED = Services.prefs.getBoolPref(
"media.setsinkid.enabled"
);
const LOCAL_NETWORK_ACCESS_ENABLED = Services.prefs.getBoolPref(
"network.lna.blocking"
);
const SANITIZE_ON_SHUTDOWN_ENABLED = Services.prefs.getBoolPref(
"privacy.sanitize.sanitizeOnShutdown"
);
add_task(async function testPermissionsListing() {
let expectedPermissions = [
"autoplay-media",
"camera",
"cookie",
"desktop-notification",
"focus-tab-by-prompt",
"geo",
"install",
"microphone",
"popup",
"screen",
"shortcuts",
"persistent-storage",
"storage-access",
"xr",
"3rdPartyStorage",
"3rdPartyFrameStorage",
"open-protocol-handler",
];
if (RESIST_FINGERPRINTING_ENABLED) {
// Canvas permission should be hidden unless privacy.resistFingerprinting
// is true.
expectedPermissions.push("canvas");
}
if (MIDI_ENABLED) {
// Should remove this checking and add it as default after it is fully pref'd-on.
expectedPermissions.push("midi");
expectedPermissions.push("midi-sysex");
}
if (WEBSERIAL_ENABLED) {
expectedPermissions.push("serial");
}
if (SPEAKER_SELECTION_ENABLED) {
expectedPermissions.push("speaker");
}
if (LOCAL_NETWORK_ACCESS_ENABLED) {
expectedPermissions.push("loopback-network");
expectedPermissions.push("local-network");
}
if (SANITIZE_ON_SHUTDOWN_ENABLED) {
// The clear-on-shutdown exception is hidden unless data is cleared on
// shutdown.
expectedPermissions.push("persist-data-on-shutdown");
}
Assert.deepEqual(
SitePermissions.listPermissions().sort(),
expectedPermissions.sort(),
"Correct list of all permissions"
);
});
add_task(async function testGetAllByPrincipal() {
// check that it returns an empty array on an invalid principal
// like a principal with an about URI, which doesn't support site permissions
let wrongPrincipal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"about:config"
);
Assert.deepEqual(SitePermissions.getAllByPrincipal(wrongPrincipal), []);
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://example.com"
);
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
SitePermissions.setForPrincipal(principal, "camera", SitePermissions.ALLOW);
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
{
id: "camera",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_PERSISTENT,
},
]);
SitePermissions.setForPrincipal(
principal,
"microphone",
SitePermissions.ALLOW,
SitePermissions.SCOPE_SESSION
);
Services.prefs.setBoolPref("network.lna.blocking", true);
SitePermissions.setForPrincipal(
principal,
"loopback-network",
SitePermissions.ALLOW,
SitePermissions.SCOPE_SESSION
);
SitePermissions.setForPrincipal(
principal,
"local-network",
SitePermissions.ALLOW,
SitePermissions.SCOPE_SESSION
);
SitePermissions.setForPrincipal(
principal,
"desktop-notification",
SitePermissions.BLOCK
);
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
{
id: "camera",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_PERSISTENT,
},
{
id: "microphone",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_SESSION,
},
{
id: "loopback-network",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_SESSION,
},
{
id: "local-network",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_SESSION,
},
{
id: "desktop-notification",
state: SitePermissions.BLOCK,
scope: SitePermissions.SCOPE_PERSISTENT,
},
]);
SitePermissions.removeFromPrincipal(principal, "microphone");
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
{
id: "camera",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_PERSISTENT,
},
{
id: "loopback-network",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_SESSION,
},
{
id: "local-network",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_SESSION,
},
{
id: "desktop-notification",
state: SitePermissions.BLOCK,
scope: SitePermissions.SCOPE_PERSISTENT,
},
]);
SitePermissions.removeFromPrincipal(principal, "camera");
SitePermissions.removeFromPrincipal(principal, "desktop-notification");
SitePermissions.removeFromPrincipal(principal, "loopback-network");
SitePermissions.removeFromPrincipal(principal, "local-network");
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
Assert.equal(Services.prefs.getIntPref("permissions.default.shortcuts"), 0);
SitePermissions.setForPrincipal(
principal,
"shortcuts",
SitePermissions.BLOCK
);
// Customized preference should have been enabled, but the default should not.
Assert.equal(Services.prefs.getIntPref("permissions.default.shortcuts"), 0);
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
{
id: "shortcuts",
state: SitePermissions.BLOCK,
scope: SitePermissions.SCOPE_PERSISTENT,
},
]);
SitePermissions.removeFromPrincipal(principal, "shortcuts");
Services.prefs.clearUserPref("permissions.default.shortcuts");
Services.prefs.clearUserPref("network.lna.blocking");
});
add_task(async function testGetAvailableStates() {
Assert.deepEqual(SitePermissions.getAvailableStates("camera"), [
SitePermissions.UNKNOWN,
SitePermissions.ALLOW,
SitePermissions.BLOCK,
]);
// Test available states with a default permission set.
Services.prefs.setIntPref(
"permissions.default.camera",
SitePermissions.ALLOW
);
Assert.deepEqual(SitePermissions.getAvailableStates("camera"), [
SitePermissions.PROMPT,
SitePermissions.ALLOW,
SitePermissions.BLOCK,
]);
Services.prefs.clearUserPref("permissions.default.camera");
Assert.deepEqual(SitePermissions.getAvailableStates("cookie"), [
SitePermissions.ALLOW,
SitePermissions.ALLOW_COOKIES_FOR_SESSION,
SitePermissions.BLOCK,
]);
Assert.deepEqual(SitePermissions.getAvailableStates("popup"), [
SitePermissions.ALLOW,
SitePermissions.BLOCK,
]);
});
add_task(async function testPersistDataOnShutdownPermission() {
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://example.com"
);
// The permission is hidden from the listing while clear-on-shutdown is off.
Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", false);
Assert.ok(
!SitePermissions.listPermissions().includes("persist-data-on-shutdown"),
"Hidden when data is not cleared on shutdown"
);
// Turning clear-on-shutdown on surfaces it.
Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
registerCleanupFunction(() =>
Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown")
);
Assert.ok(
SitePermissions.listPermissions().includes("persist-data-on-shutdown"),
"Shown when data is cleared on shutdown"
);
Assert.equal(
SitePermissions.getDefault("persist-data-on-shutdown"),
SitePermissions.UNKNOWN,
"Defaults to no exception (cleared on shutdown)"
);
Assert.deepEqual(
SitePermissions.getAvailableStates("persist-data-on-shutdown"),
[SitePermissions.UNKNOWN, SitePermissions.ALLOW],
"Exposes the default and keep states"
);
Assert.equal(
SitePermissions.getPermissionLabel("persist-data-on-shutdown"),
"Keep site data when closing"
);
Assert.equal(
SitePermissions.getMultichoiceStateLabel(
"persist-data-on-shutdown",
SitePermissions.UNKNOWN
),
"Clear when closing"
);
Assert.equal(
SitePermissions.getMultichoiceStateLabel(
"persist-data-on-shutdown",
SitePermissions.ALLOW
),
"Always keep site data"
);
// Setting ALLOW persists the exception.
SitePermissions.setForPrincipal(
principal,
"persist-data-on-shutdown",
SitePermissions.ALLOW
);
Assert.equal(
SitePermissions.getForPrincipal(principal, "persist-data-on-shutdown")
.state,
SitePermissions.ALLOW
);
// Setting back to the default removes the exception.
SitePermissions.setForPrincipal(
principal,
"persist-data-on-shutdown",
SitePermissions.UNKNOWN
);
Assert.equal(
SitePermissions.getForPrincipal(principal, "persist-data-on-shutdown")
.state,
SitePermissions.UNKNOWN
);
Assert.equal(
Services.perms.getPermissionObject(
principal,
"persist-data-on-shutdown",
false
),
null,
"Permission is fully removed, not stored"
);
});
add_task(async function testExactHostMatch() {
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://example.com"
);
let subPrincipal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://test1.example.com"
);
let exactHostMatched = [
"autoplay-media",
"desktop-notification",
"focus-tab-by-prompt",
"camera",
"loopback-network",
"local-network",
"microphone",
"screen",
"geo",
"xr",
"persistent-storage",
"open-protocol-handler",
];
if (RESIST_FINGERPRINTING_ENABLED) {
// Canvas permission should be hidden unless privacy.resistFingerprinting
// is true.
exactHostMatched.push("canvas");
}
if (MIDI_ENABLED) {
// WebMIDI is only pref'd on in nightly.
// Should remove this checking and add it as default after it is fully pref-on.
exactHostMatched.push("midi");
exactHostMatched.push("midi-sysex");
}
if (WEBSERIAL_ENABLED) {
exactHostMatched.push("serial");
}
if (SPEAKER_SELECTION_ENABLED) {
exactHostMatched.push("speaker");
}
let nonExactHostMatched = [
"cookie",
"popup",
"install",
"shortcuts",
"persist-data-on-shutdown",
"storage-access",
"3rdPartyStorage",
"3rdPartyFrameStorage",
];
let permissions = SitePermissions.listPermissions();
for (let permission of permissions) {
SitePermissions.setForPrincipal(
principal,
permission,
SitePermissions.ALLOW
);
if (exactHostMatched.includes(permission)) {
// Check that the sub-origin does not inherit the permission from its parent.
Assert.equal(
SitePermissions.getForPrincipal(subPrincipal, permission).state,
SitePermissions.getDefault(permission),
`${permission} should exact-host match`
);
} else if (nonExactHostMatched.includes(permission)) {
// Check that the sub-origin does inherit the permission from its parent.
Assert.equal(
SitePermissions.getForPrincipal(subPrincipal, permission).state,
SitePermissions.ALLOW,
`${permission} should not exact-host match`
);
} else {
Assert.ok(
false,
`Found an unknown permission ${permission} in exact host match test.` +
"Please add new permissions from SitePermissions.sys.mjs to this test."
);
}
// Check that the permission can be made specific to the sub-origin.
SitePermissions.setForPrincipal(
subPrincipal,
permission,
SitePermissions.PROMPT
);
Assert.equal(
SitePermissions.getForPrincipal(subPrincipal, permission).state,
SitePermissions.PROMPT
);
Assert.equal(
SitePermissions.getForPrincipal(principal, permission).state,
SitePermissions.ALLOW
);
SitePermissions.removeFromPrincipal(subPrincipal, permission);
SitePermissions.removeFromPrincipal(principal, permission);
}
});
add_task(async function testDefaultPrefs() {
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://example.com"
);
// Check that without a pref the default return value is UNKNOWN.
Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
state: SitePermissions.UNKNOWN,
scope: SitePermissions.SCOPE_PERSISTENT,
});
// Check that the default return value changed after setting the pref.
Services.prefs.setIntPref(
"permissions.default.camera",
SitePermissions.BLOCK
);
Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
state: SitePermissions.BLOCK,
scope: SitePermissions.SCOPE_PERSISTENT,
});
// Check that other permissions still return UNKNOWN.
Assert.deepEqual(SitePermissions.getForPrincipal(principal, "microphone"), {
state: SitePermissions.UNKNOWN,
scope: SitePermissions.SCOPE_PERSISTENT,
});
Assert.deepEqual(
SitePermissions.getForPrincipal(principal, "loopback-network"),
{
state: SitePermissions.UNKNOWN,
scope: SitePermissions.SCOPE_PERSISTENT,
}
);
Assert.deepEqual(
SitePermissions.getForPrincipal(principal, "local-network"),
{
state: SitePermissions.UNKNOWN,
scope: SitePermissions.SCOPE_PERSISTENT,
}
);
// Check that the default return value changed after changing the pref.
Services.prefs.setIntPref(
"permissions.default.camera",
SitePermissions.ALLOW
);
Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_PERSISTENT,
});
// Check that the preference is ignored if there is a value.
SitePermissions.setForPrincipal(principal, "camera", SitePermissions.BLOCK);
Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
state: SitePermissions.BLOCK,
scope: SitePermissions.SCOPE_PERSISTENT,
});
// The preference should be honored again, after resetting the permissions.
SitePermissions.removeFromPrincipal(principal, "camera");
Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_PERSISTENT,
});
// Should be UNKNOWN after clearing the pref.
Services.prefs.clearUserPref("permissions.default.camera");
Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
state: SitePermissions.UNKNOWN,
scope: SitePermissions.SCOPE_PERSISTENT,
});
});
add_task(async function testCanvasPermission() {
let resistFingerprinting = Services.prefs.getBoolPref(
"privacy.resistFingerprinting",
false
);
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://example.com"
);
SitePermissions.setForPrincipal(principal, "canvas", SitePermissions.ALLOW);
// Canvas permission is hidden when privacy.resistFingerprinting is false.
Services.prefs.setBoolPref("privacy.resistFingerprinting", false);
Assert.equal(SitePermissions.listPermissions().indexOf("canvas"), -1);
Assert.equal(
SitePermissions.getAllByPrincipal(principal).filter(
permission => permission.id === "canvas"
).length,
0
);
// Canvas permission is visible when privacy.resistFingerprinting is true.
Services.prefs.setBoolPref("privacy.resistFingerprinting", true);
Assert.notEqual(SitePermissions.listPermissions().indexOf("canvas"), -1);
Assert.notEqual(
SitePermissions.getAllByPrincipal(principal).filter(
permission => permission.id === "canvas"
).length,
0
);
SitePermissions.removeFromPrincipal(principal, "canvas");
Services.prefs.setBoolPref(
"privacy.resistFingerprinting",
resistFingerprinting
);
});
add_task(async function testLocalHostPermission() {
let lnaEnabled = Services.prefs.getBoolPref("network.lna.blocking", false);
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://example.com"
);
SitePermissions.setForPrincipal(
principal,
"loopback-network",
SitePermissions.ALLOW
);
Services.prefs.setBoolPref("network.lna.blocking", false);
Assert.ok(
!SitePermissions.listPermissions().includes("loopback-network"),
"No 'loopback-network' permission should be present"
);
Assert.ok(
!SitePermissions.getAllByPrincipal(principal).some(
permission => permission.id === "loopback-network"
),
"No 'loopback-network' permission should be present"
);
Services.prefs.setBoolPref("network.lna.blocking", true);
Assert.ok(
SitePermissions.listPermissions().includes("loopback-network"),
"'loopback-network' should be in listPermissions when blocking is enabled"
);
Assert.ok(
SitePermissions.getAllByPrincipal(principal).some(
permission => permission.id === "loopback-network"
),
"'loopback-network' permission should be present for principal when blocking is enabled"
);
SitePermissions.removeFromPrincipal(principal, "loopback-network");
Services.prefs.setBoolPref("network.lna.blocking", lnaEnabled);
});
add_task(async function testLocalNetworkPermission() {
let lnaEnabled = Services.prefs.getBoolPref("network.lna.blocking", false);
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"https://example.com"
);
SitePermissions.setForPrincipal(
principal,
"local-network",
SitePermissions.ALLOW
);
Services.prefs.setBoolPref("network.lna.blocking", false);
Assert.ok(
!SitePermissions.listPermissions().includes("local-network"),
"'local-network' should not be in listPermissions when blocking is disabled"
);
Assert.ok(
!SitePermissions.getAllByPrincipal(principal).some(
permission => permission.id === "local-network"
),
"'local-network' permission should not be present for principal when blocking is disabled"
);
Services.prefs.setBoolPref("network.lna.blocking", true);
Assert.ok(
SitePermissions.listPermissions().includes("local-network"),
"'local-network' should be in listPermissions when blocking is enabled"
);
Assert.ok(
SitePermissions.getAllByPrincipal(principal).some(
permission => permission.id === "local-network"
),
"'local-network' permission should be present for principal when blocking is enabled"
);
SitePermissions.removeFromPrincipal(principal, "local-network");
Services.prefs.setBoolPref("network.lna.blocking", lnaEnabled);
});
add_task(async function testFilePermissions() {
let principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"file:///example.js"
);
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
SitePermissions.setForPrincipal(principal, "camera", SitePermissions.ALLOW);
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
{
id: "camera",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_PERSISTENT,
},
]);
SitePermissions.removeFromPrincipal(principal, "camera");
Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
});