/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); const PERMISSIONS_FILE_NAME = "permissions.sqlite"; function getPermissionsFile(profile) { let file = profile.clone(); file.append(PERMISSIONS_FILE_NAME); return file; } function getInteractionCount(origin) { let profile = do_get_profile(); let db = Services.storage.openDatabase(getPermissionsFile(profile)); let stmt = db.createStatement( "SELECT COUNT(*) FROM moz_origin_interactions WHERE origin = :origin" ); stmt.bindByName("origin", origin); stmt.executeStep(); let count = stmt.getInt64(0); stmt.finalize(); db.close(); return count; } function getInteractionTime(origin) { let profile = do_get_profile(); let db = Services.storage.openDatabase(getPermissionsFile(profile)); let stmt = db.createStatement( "SELECT lastInteractionTime FROM moz_origin_interactions WHERE origin = :origin" ); stmt.bindByName("origin", origin); let time = 0; if (stmt.executeStep()) { time = stmt.getInt64(0); } stmt.finalize(); db.close(); return time; } function setInteractionTime(origin, timeMs) { let profile = do_get_profile(); let db = Services.storage.openDatabase(getPermissionsFile(profile)); let stmt = db.createStatement( "INSERT OR REPLACE INTO moz_origin_interactions (origin, lastInteractionTime) VALUES (:origin, :time)" ); stmt.bindByName("origin", origin); stmt.bindByName("time", timeMs); stmt.execute(); stmt.finalize(); db.close(); } function getTotalInteractionCount() { let profile = do_get_profile(); let db = Services.storage.openDatabase(getPermissionsFile(profile)); let stmt = db.createStatement("SELECT COUNT(*) FROM moz_origin_interactions"); stmt.executeStep(); let count = stmt.getInt64(0); stmt.finalize(); db.close(); return count; } function hasPermission(pm, principal, type) { return ( pm.testPermissionFromPrincipal(principal, type) !== Services.perms.UNKNOWN_ACTION ); } function expiredTimestamp() { let thresholdMs = Services.prefs.getIntPref("permissions.expireUnusedThresholdSec") * 1000; return Date.now() - thresholdMs - 1000; } // Flush all pending background thread writes (including the auto-recorded // interaction from addFromPrincipal), then overwrite with a timestamp past // the expiry threshold. async function writeOldInteraction(origin) { await Services.perms.testFlushPendingWrites(); setInteractionTime(origin, expiredTimestamp()); } add_task(async function test_schema_migration() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); let profile = do_get_profile(); let pm = Services.perms; Assert.equal(pm.all.length, 0, "No permissions initially"); let db = Services.storage.openDatabase(getPermissionsFile(profile)); if (db.tableExists("moz_origin_interactions")) { db.executeSimpleSQL("DROP TABLE moz_origin_interactions"); } db.schemaVersion = 12; db.close(); Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); // Accessing pm.all forces a synchronous load, triggering the migration. pm.all; db = Services.storage.openDatabase(getPermissionsFile(profile)); Assert.ok( db.tableExists("moz_origin_interactions"), "moz_origin_interactions table should exist after migration" ); Assert.equal(db.schemaVersion, 13, "Schema version should be 13"); db.close(); }); add_task(async function test_interaction_tracking() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.updateLastInteractionForPrincipal(principal); await pm.testFlushPendingWrites(); Assert.greater( getInteractionTime("https://example.com"), 0, "lastInteractionTime should be positive" ); pm.removeAll(); }); add_task(async function test_interaction_stored_without_oa_suffix() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://oa-test.example.com^userContextId=5" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.updateLastInteractionForPrincipal(principal); await pm.testFlushPendingWrites(); Assert.greater( getInteractionTime("https://oa-test.example.com"), 0, "lastInteractionTime should be positive" ); Assert.equal( getInteractionCount("https://oa-test.example.com^userContextId=5"), 0, "Should not have a interaction record with OA suffix" ); pm.removeAll(); }); add_task( async function test_interaction_tracked_without_expirable_permission() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref("permissions.expireUnusedTypes", "geo"); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://nopermission.example.com" ); pm.updateLastInteractionForPrincipal(principal); await pm.testFlushPendingWrites(); Assert.equal( getInteractionCount("https://nopermission.example.com"), 1, "Should have an interaction record even without expirable permission" ); } ); add_task(async function test_default_permission_not_expired() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://default.example.com" ); pm.addDefaultFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); setInteractionTime("https://default.example.com", expiredTimestamp()); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, principal, "desktop-notification"), "Default permission should not be expired" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); }); add_task(async function test_policy_permission_not_expired() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://policy.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION, Services.perms.EXPIRE_POLICY ); await writeOldInteraction("https://policy.example.com"); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, principal, "desktop-notification"), "Policy permission should not be expired" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); }); add_task(async function test_permission_not_in_expirable_types_not_expired() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref("permissions.expireUnusedTypes", "geo"); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://notinlist.example.com" ); pm.addFromPrincipal(principal, "camera", Services.perms.ALLOW_ACTION); await writeOldInteraction("https://notinlist.example.com"); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, principal, "camera"), "Permission not in expirable types should not be expired" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); }); add_task(async function test_recent_interaction_prevents_expiration() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 86400); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://recentvisit.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.updateLastInteractionForPrincipal(principal); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, principal, "desktop-notification"), "Permission with recent interaction should not be expired" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); }); add_task(async function test_old_interaction_allows_expiration() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://oldvisit.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); await writeOldInteraction("https://oldvisit.example.com"); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principal, "desktop-notification"), "Waiting for permission to be expired" ); Assert.ok( !hasPermission(pm, principal, "desktop-notification"), "Permission with old interaction should be expired" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); }); add_task(async function test_permission_grant_records_interaction() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://novisit.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); Assert.equal( getInteractionCount("https://novisit.example.com"), 1, "Should have an interaction record from permission grant" ); // Overwrite with old timestamp so it's eligible for expiry. setInteractionTime("https://novisit.example.com", expiredTimestamp()); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principal, "desktop-notification"), "Waiting for permission to be expired" ); Assert.ok( !hasPermission(pm, principal, "desktop-notification"), "Permission should be expired after threshold exceeded" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); }); add_task(async function test_glean_telemetry_on_expiration() { Services.fog.testResetFOG(); Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://telemetry.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); await writeOldInteraction("https://telemetry.example.com"); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principal, "desktop-notification"), "Waiting for permission to be expired" ); let byType = Glean.permissions.unusedPermissionsExpiredByType.testGetValue(); Assert.ok(byType, "Should have by-type data"); Assert.greaterOrEqual( byType["desktop-notification"], 1, "Should record desktop-notification type" ); let ageDist = Glean.permissions.unusedPermissionAgeAtExpiry.testGetValue(); Assert.ok(ageDist, "Should have age distribution data"); let modAgeDist = Glean.permissions.unusedPermissionModifiedAgeAtExpiry.testGetValue(); Assert.ok(modAgeDist, "Should have modified age distribution data"); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_no_telemetry_when_disabled() { Services.fog.testResetFOG(); Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", false); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://disabled.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); setInteractionTime("https://disabled.example.com", expiredTimestamp()); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, principal, "desktop-notification"), "Permission should not be expired when feature is disabled" ); let byType = Glean.permissions.unusedPermissionsExpiredByType.testGetValue(); Assert.ok( !byType || !Object.keys(byType).length, "Should not record by-type telemetry when disabled" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_glean_by_type_multiple_types() { Services.fog.testResetFOG(); Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification,geo" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://bytype1.example.com" ); let principal2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://bytype2.example.com" ); pm.addFromPrincipal( principal1, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.addFromPrincipal(principal2, "geo", Services.perms.ALLOW_ACTION); await writeOldInteraction("https://bytype1.example.com"); await writeOldInteraction("https://bytype2.example.com"); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principal1, "desktop-notification") && !hasPermission(pm, principal2, "geo"), "Waiting for both permissions to be expired" ); let byType = Glean.permissions.unusedPermissionsExpiredByType.testGetValue(); Assert.ok(byType, "Should have by-type data"); Assert.equal( byType["desktop-notification"], 1, "Should record one desktop-notification expiration" ); Assert.equal(byType.geo, 1, "Should record one geo expiration"); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_interaction_not_tracked_when_disabled() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", false); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://visitdisabled.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.updateLastInteractionForPrincipal(principal); await pm.testFlushPendingWrites(); Assert.equal( getInteractionCount("https://visitdisabled.example.com"), 0, "Should not have a interaction record when feature is disabled" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_double_keyed_permission_expiration() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "open-protocol-handler^" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://doublekey.example.com" ); pm.addFromPrincipal( principal, "open-protocol-handler^zoommtg", Services.perms.ALLOW_ACTION ); pm.addFromPrincipal(principal, "geo", Services.perms.ALLOW_ACTION); Assert.ok( hasPermission(pm, principal, "open-protocol-handler^zoommtg"), "Double-keyed permission should exist" ); Assert.ok(hasPermission(pm, principal, "geo"), "Geo permission should exist"); await writeOldInteraction("https://doublekey.example.com"); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principal, "open-protocol-handler^zoommtg"), "Waiting for double-keyed permission to be expired" ); Assert.ok( hasPermission(pm, principal, "geo"), "Geo permission should NOT be expired (not in expirable types)" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_double_keyed_vs_regular_permission() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref("permissions.expireUnusedTypes", "testperm^"); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://testperm.example.com" ); pm.addFromPrincipal(principal, "testperm", Services.perms.ALLOW_ACTION); pm.addFromPrincipal(principal, "testperm^key", Services.perms.ALLOW_ACTION); await writeOldInteraction("https://testperm.example.com"); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principal, "testperm^key"), "Waiting for double-keyed permission to be expired" ); Assert.ok( hasPermission(pm, principal, "testperm"), "Regular permission should NOT be expired" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_oa_isolated_permission_expiration() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref("permissions.expireUnusedTypes", "geo,cookie"); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); Services.prefs.setBoolPref("permissions.isolateBy.userContext", true); let pm = Services.perms; pm.removeAll(); let principalWithOA = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://oatest.example.com^userContextId=3" ); pm.addFromPrincipal(principalWithOA, "geo", Services.perms.ALLOW_ACTION); pm.addFromPrincipal(principalWithOA, "cookie", Services.perms.ALLOW_ACTION); Assert.ok( hasPermission(pm, principalWithOA, "geo"), "Geo permission should exist" ); Assert.ok( hasPermission(pm, principalWithOA, "cookie"), "Cookie permission should exist" ); // Flush auto-recorded interactions, then overwrite OA interaction // with an old timestamp so it's eligible for expiry. await pm.testFlushPendingWrites(); setInteractionTime( "https://oatest.example.com^userContextId=3", expiredTimestamp() ); // Add a fresh interaction for the base origin only (without OA suffix). setInteractionTime("https://oatest.example.com", Date.now()); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principalWithOA, "geo"), "Waiting for OA-isolated geo permission to be expired" ); Assert.ok( hasPermission(pm, principalWithOA, "cookie"), "OA-stripped permission (cookie) should survive via base origin interaction" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); Services.prefs.clearUserPref("permissions.isolateBy.userContext"); }); add_task( async function test_oa_isolated_permission_with_matching_interaction() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref("permissions.expireUnusedTypes", "geo"); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); Services.prefs.setBoolPref("permissions.isolateBy.userContext", true); let pm = Services.perms; pm.removeAll(); let principalWithOA = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://oamatch.example.com^userContextId=7" ); pm.addFromPrincipal(principalWithOA, "geo", Services.perms.ALLOW_ACTION); await writeOldInteraction("https://oamatch.example.com^userContextId=7"); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => !hasPermission(pm, principalWithOA, "geo"), "Waiting for OA-isolated permission to be expired" ); Assert.ok( !hasPermission(pm, principalWithOA, "geo"), "OA-isolated permission should be expired when matching OA interaction exists" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); Services.prefs.clearUserPref("permissions.isolateBy.userContext"); } ); add_task(async function test_no_interaction_tracking_for_private_browsing() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let privatePrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://private.example.com^privateBrowsingId=1" ); pm.addFromPrincipal( privatePrincipal, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.updateLastInteractionForPrincipal(privatePrincipal); await pm.testFlushPendingWrites(); let profile = do_get_profile(); let db = Services.storage.openDatabase(getPermissionsFile(profile)); let stmt = db.createStatement( "SELECT COUNT(*) FROM moz_origin_interactions WHERE origin LIKE :pattern" ); stmt.bindByName("pattern", "https://private.example.com%"); stmt.executeStep(); Assert.equal( stmt.getInt64(0), 0, "Should not have any interaction record for private browsing principal" ); stmt.finalize(); db.close(); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_restart_does_not_reset_interaction_time() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://restart-test.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); let originalTime = getInteractionTime("https://restart-test.example.com"); Assert.greater(originalTime, 0, "Should have interaction time recorded"); Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); await new Promise(resolve => executeSoon(resolve)); let timeAfterReload = getInteractionTime("https://restart-test.example.com"); Assert.equal( timeAfterReload, originalTime, "Interaction time should not change after reloading from disk" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_migration_seeds_interaction_records() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://migration1.example.com" ); let principal2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://migration2.example.com" ); pm.addFromPrincipal( principal1, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.addFromPrincipal(principal2, "geo", Services.perms.ALLOW_ACTION); await pm.testFlushPendingWrites(); let profile = do_get_profile(); let db = Services.storage.openDatabase(getPermissionsFile(profile)); db.executeSimpleSQL("DROP TABLE moz_origin_interactions"); db.schemaVersion = 12; db.close(); Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); // The migration runs on the DB thread; flush to ensure it completes. await pm.testFlushPendingWrites(); Assert.greater( getInteractionCount("https://migration1.example.com"), 0, "Migration should seed interaction for origin 1" ); Assert.greater( getInteractionCount("https://migration2.example.com"), 0, "Migration should seed interaction for origin 2" ); let time1 = getInteractionTime("https://migration1.example.com"); let time2 = getInteractionTime("https://migration2.example.com"); Assert.greater(time1, 0, "Interaction time 1 should be positive"); Assert.greater(time2, 0, "Interaction time 2 should be positive"); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task( async function test_oa_stripped_permission_cleanup_preserves_oa_interactions() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref("permissions.expireUnusedTypes", "cookie"); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); Services.prefs.setBoolPref("permissions.isolateBy.userContext", true); let pm = Services.perms; pm.removeAll(); let principalWithOA = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://cleanup-oa.example.com^userContextId=4" ); pm.addFromPrincipal(principalWithOA, "cookie", Services.perms.ALLOW_ACTION); await pm.testFlushPendingWrites(); Assert.equal( getInteractionCount("https://cleanup-oa.example.com"), 1, "Should have base origin interaction for OA-stripped permission" ); // Simulate IPC interaction recording with full OA setInteractionTime( "https://cleanup-oa.example.com^userContextId=4", Date.now() ); Assert.equal( getInteractionCount("https://cleanup-oa.example.com^userContextId=4"), 1, "Should have full-OA interaction record" ); // Add another permission to trigger cleanup let otherPrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://other.example.com" ); pm.addFromPrincipal(otherPrincipal, "cookie", Services.perms.ALLOW_ACTION); await pm.testFlushPendingWrites(); pm.removeFromPrincipal(otherPrincipal, "cookie"); await pm.removeOrphanedInteractionRecords(); Assert.equal( getInteractionCount("https://cleanup-oa.example.com^userContextId=4"), 1, "Full-OA interaction should survive cleanup for OA-stripped permission" ); Assert.equal( getInteractionCount("https://cleanup-oa.example.com"), 1, "Base origin interaction should survive cleanup" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.isolateBy.userContext"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); } ); add_task(async function test_removeAll_clears_interactions() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeall1.example.com" ); let principal2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeall2.example.com" ); pm.addFromPrincipal( principal1, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.addFromPrincipal( principal2, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); Assert.greaterOrEqual( getTotalInteractionCount(), 2, "Should have at least 2 interaction records" ); pm.removeAll(); await pm.testFlushPendingWrites(); Assert.equal( getTotalInteractionCount(), 0, "All interaction records should be cleared after removeAll" ); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task( async function test_removeAllExceptTypes_clears_orphaned_interactions() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeexcept1.example.com" ); let principal2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeexcept2.example.com" ); pm.addFromPrincipal( principal1, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.addFromPrincipal(principal2, "cookie", Services.perms.ALLOW_ACTION); await pm.testFlushPendingWrites(); pm.removeAllExceptTypes(["cookie"]); await pm.testFlushPendingWrites(); Assert.equal( getInteractionCount("https://removeexcept1.example.com"), 0, "Interaction for removed desktop-notification permission should be gone" ); Assert.ok( hasPermission(pm, principal2, "cookie"), "Cookie permission should be kept" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); } ); add_task( async function test_removeAllSinceWithTypeExceptions_clears_orphaned_interactions() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removesince1.example.com" ); let principal2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removesince2.example.com" ); pm.addFromPrincipal( principal1, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); let perm1 = pm.getPermissionObject( principal1, "desktop-notification", true ); let midTime = perm1.modificationTime + 1; // Ensure PR_Now() advances past midTime before adding the next permission. // Windows timer resolution can be ~16ms, so we need a real delay. await new Promise(resolve => do_timeout(20, resolve)); pm.addFromPrincipal( principal2, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); pm.removeAllSinceWithTypeExceptions(midTime, []); await pm.testFlushPendingWrites(); Assert.equal( getInteractionCount("https://removesince2.example.com"), 0, "Interaction for removed recent permission should be gone" ); Assert.greater( getInteractionCount("https://removesince1.example.com"), 0, "Interaction for kept older permission should remain" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); } ); add_task( async function test_removePermissionsWithAttributes_clears_orphaned_interactions() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); Services.prefs.setBoolPref("permissions.isolateBy.userContext", true); let pm = Services.perms; pm.removeAll(); let principal1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeoa1.example.com^userContextId=1" ); let principal2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeoa2.example.com" ); pm.addFromPrincipal( principal1, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.addFromPrincipal( principal2, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); pm.removePermissionsWithAttributes('{"userContextId":1}', [], []); await pm.testFlushPendingWrites(); Assert.ok( !hasPermission(pm, principal1, "desktop-notification"), "Permission with userContextId=1 should be removed" ); Assert.equal( getInteractionCount("https://removeoa1.example.com^userContextId=1"), 0, "Interaction for removed permission should be gone" ); Assert.greater( getInteractionCount("https://removeoa2.example.com"), 0, "Interaction for the origin with remaining permission should remain" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); Services.prefs.clearUserPref("permissions.isolateBy.userContext"); } ); add_task(async function test_removePermission_clears_interactions() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeperm1.example.com" ); let principal2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://removeperm2.example.com" ); pm.addFromPrincipal( principal1, "desktop-notification", Services.perms.ALLOW_ACTION ); pm.addFromPrincipal( principal2, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); Assert.equal( getInteractionCount("https://removeperm1.example.com"), 1, "Should have interaction for first origin" ); Assert.equal( getInteractionCount("https://removeperm2.example.com"), 1, "Should have interaction for second origin" ); let perm = pm.getPermissionObject(principal1, "desktop-notification", true); pm.removePermission(perm); await pm.removeOrphanedInteractionRecords(); Assert.equal( getInteractionCount("https://removeperm1.example.com"), 0, "Interaction for removed permission should be gone" ); Assert.equal( getInteractionCount("https://removeperm2.example.com"), 1, "Interaction for remaining permission should still exist" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_idle_daily_cleans_orphaned_interactions() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://idle-cleanup.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); await pm.testFlushPendingWrites(); pm.removeFromPrincipal(principal, "desktop-notification"); Assert.greater( getInteractionCount("https://idle-cleanup.example.com"), 0, "Interaction should still exist after removeFromPrincipal" ); Services.obs.notifyObservers(null, "idle-daily"); await TestUtils.waitForCondition( () => getInteractionCount("https://idle-cleanup.example.com") === 0, "Orphaned interaction should be cleaned up by idle-daily" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_session_permission_not_expired() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://session.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION, Services.perms.EXPIRE_SESSION ); await writeOldInteraction("https://session.example.com"); Assert.ok( hasPermission(pm, principal, "desktop-notification"), "Session permission should exist before idle-daily" ); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, principal, "desktop-notification"), "Session permission should not be expired by unused-permissions logic" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_private_browsing_permission_not_expired() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let privatePrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://pbm-expire.example.com^privateBrowsingId=1" ); // Use addFromPrincipalAndPersistInPrivateBrowsing to create a PBM // permission with EXPIRE_NEVER (bypasses the SESSION override). pm.addFromPrincipalAndPersistInPrivateBrowsing( privatePrincipal, "desktop-notification", Services.perms.ALLOW_ACTION ); Assert.ok( hasPermission(pm, privatePrincipal, "desktop-notification"), "PBM-persisted permission should exist" ); // Manually seed an old interaction so it would be eligible for expiry // if the PBM guard weren't in place. setInteractionTime( "https://pbm-expire.example.com^privateBrowsingId=1", expiredTimestamp() ); setInteractionTime("https://pbm-expire.example.com", expiredTimestamp()); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, privatePrincipal, "desktop-notification"), "PBM permission should not be expired" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); }); add_task(async function test_no_interaction_record_keeps_permission() { Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); Services.prefs.setCharPref( "permissions.expireUnusedTypes", "desktop-notification" ); Services.prefs.setIntPref("permissions.expireUnusedThresholdSec", 1); Services.prefs.setBoolPref("permissions.expireUnused.enabled", true); let pm = Services.perms; pm.removeAll(); let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://no-interaction.example.com" ); pm.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); // Flush the auto-recorded interaction, then delete it so the // permission has no interaction record at all. await pm.testFlushPendingWrites(); let profile = do_get_profile(); let db = Services.storage.openDatabase(getPermissionsFile(profile)); db.executeSimpleSQL( "DELETE FROM moz_origin_interactions WHERE origin = 'https://no-interaction.example.com'" ); db.close(); Assert.equal( getInteractionCount("https://no-interaction.example.com"), 0, "Interaction record should be deleted" ); Services.obs.notifyObservers(null, "idle-daily"); await new Promise(resolve => executeSoon(resolve)); Assert.ok( hasPermission(pm, principal, "desktop-notification"), "Permission with no interaction record should be kept (not expired)" ); pm.removeAll(); Services.prefs.clearUserPref("permissions.expireUnusedThresholdSec"); Services.prefs.clearUserPref("permissions.expireUnused.enabled"); });