"use strict"; const { AddonManager } = ChromeUtils.importESModule( "resource://gre/modules/AddonManager.sys.mjs" ); const { permissionToL10nId } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionPermissionMessages.sys.mjs" ); const { ExtensionPermissions } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionPermissions.sys.mjs" ); Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); // ExtensionParent.jsm is being imported lazily because when it is imported Services.appinfo will be // retrieved and cached (as a side-effect of Schemas.jsm being imported), and so Services.appinfo // will not be returning the version set by AddonTestUtils.createAppInfo and this test will // fail on non-nightly builds (because the cached appinfo.version will be undefined and // AddonManager startup will fail). ChromeUtils.defineESModuleGetters(this, { ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", }); const l10n = new Localization([ "toolkit/global/extensions.ftl", "toolkit/global/extensionPermissions.ftl", "branding/brand.ftl", ]); // Localization resources need to be first iterated outside a test l10n.formatValue("webext-perms-sideload-text"); AddonTestUtils.init(this); AddonTestUtils.overrideCertDB(); AddonTestUtils.createAppInfo( "xpcshell@tests.mozilla.org", "XPCShell", "1", "42" ); add_setup(async () => { // Bug 1646182: Force ExtensionPermissions to run in rkv mode, the legacy // storage mode will run in xpcshell-legacy-ep.ini await ExtensionPermissions._uninit(); optionalPermissionsPromptHandler.init(); await AddonTestUtils.promiseStartupManager(); AddonTestUtils.usePrivilegedSignatures = false; }); add_task(async function test_permissions_on_startup() { let extensionId = "@permissionTest"; let extension = ExtensionTestUtils.loadExtension({ manifest: { browser_specific_settings: { gecko: { id: extensionId }, }, permissions: ["tabs"], }, useAddonManager: "permanent", async background() { let perms = await browser.permissions.getAll(); browser.test.sendMessage("permissions", perms); }, }); let adding = { permissions: ["internal:privateBrowsingAllowed"], origins: [], }; await extension.startup(); let perms = await extension.awaitMessage("permissions"); equal(perms.permissions.length, 1, "one permission"); equal(perms.permissions[0], "tabs", "internal permission not present"); const { StartupCache } = ExtensionParent; // StartupCache.permissions will not contain the extension permissions. let manifestData = await StartupCache.permissions.get(extensionId, () => { return { permissions: [], origins: [] }; }); equal(manifestData.permissions.length, 0, "no permission"); perms = await ExtensionPermissions.get(extensionId); equal(perms.permissions.length, 0, "no permissions"); await ExtensionPermissions.add(extensionId, adding); // Restart the extension and re-test the permissions. await ExtensionPermissions._uninit(); await AddonTestUtils.promiseRestartManager(); let restarted = extension.awaitMessage("permissions"); await extension.awaitStartup(); perms = await restarted; manifestData = await StartupCache.permissions.get(extensionId, () => { return { permissions: [], origins: [] }; }); deepEqual( manifestData.permissions, adding.permissions, "StartupCache.permissions contains permission" ); equal(perms.permissions.length, 1, "one permission"); equal(perms.permissions[0], "tabs", "internal permission not present"); let added = await ExtensionPermissions._get(extensionId); deepEqual(added, adding, "permissions were retained"); await extension.unload(); }); async function test_permissions({ manifest_version, granted_host_permissions, useAddonManager, expectAllGranted, }) { const REQUIRED_PERMISSIONS = ["downloads"]; const REQUIRED_ORIGINS = ["*://site.com/", "*://*.domain.com/"]; const REQUIRED_ORIGINS_EXPECTED = expectAllGranted ? ["*://site.com/*", "*://*.domain.com/*"] : []; const OPTIONAL_PERMISSIONS = ["idle", "clipboardWrite"]; const OPTIONAL_ORIGINS = [ "http://optionalsite.com/", "https://*.optionaldomain.com/", ]; const OPTIONAL_ORIGINS_NORMALIZED = [ "http://optionalsite.com/*", "https://*.optionaldomain.com/*", ]; function background() { browser.test.onMessage.addListener(async (method, arg) => { if (method == "getAll") { let perms = await browser.permissions.getAll(); let url = browser.runtime.getURL("*"); perms.origins = perms.origins.filter(i => i != url); browser.test.sendMessage("getAll.result", perms); } else if (method == "contains") { let result = await browser.permissions.contains(arg); browser.test.sendMessage("contains.result", result); } else if (method == "request") { try { let result = await browser.permissions.request(arg); browser.test.sendMessage("request.result", { status: "success", result, }); } catch (err) { browser.test.sendMessage("request.result", { status: "error", message: err.message, }); } } else if (method == "remove") { let result = await browser.permissions.remove(arg); browser.test.sendMessage("remove.result", result); } }); } let extension = ExtensionTestUtils.loadExtension({ background, manifest: { manifest_version, permissions: REQUIRED_PERMISSIONS, host_permissions: REQUIRED_ORIGINS, optional_permissions: [...OPTIONAL_PERMISSIONS, ...OPTIONAL_ORIGINS], granted_host_permissions, }, useAddonManager, }); await extension.startup(); function call(method, arg) { extension.sendMessage(method, arg); return extension.awaitMessage(`${method}.result`); } let result = await call("getAll"); deepEqual(result.permissions, REQUIRED_PERMISSIONS); deepEqual(result.origins, REQUIRED_ORIGINS_EXPECTED); for (let perm of REQUIRED_PERMISSIONS) { result = await call("contains", { permissions: [perm] }); equal(result, true, `contains() returns true for fixed permission ${perm}`); } for (let origin of REQUIRED_ORIGINS) { result = await call("contains", { origins: [origin] }); equal( result, expectAllGranted, `contains() returns true for fixed origin ${origin}` ); } // None of the optional permissions should be available yet for (let perm of OPTIONAL_PERMISSIONS) { result = await call("contains", { permissions: [perm] }); equal(result, false, `contains() returns false for permission ${perm}`); } for (let origin of OPTIONAL_ORIGINS) { result = await call("contains", { origins: [origin] }); equal(result, false, `contains() returns false for origin ${origin}`); } result = await call("contains", { permissions: [...REQUIRED_PERMISSIONS, ...OPTIONAL_PERMISSIONS], }); equal( result, false, "contains() returns false for a mix of available and unavailable permissions" ); let perm = OPTIONAL_PERMISSIONS[0]; result = await call("request", { permissions: [perm] }); equal( result.status, "error", "request() fails if not called from an event handler" ); ok( /request may only be called from a user input handler/.test(result.message), "error message for calling request() outside an event handler is reasonable" ); result = await call("contains", { permissions: [perm] }); equal( result, false, "Permission requested outside an event handler was not granted" ); await withHandlingUserInput(extension, async () => { result = await call("request", { permissions: ["notifications"] }); equal( result.status, "error", "request() for permission not in optional_permissions should fail" ); ok( /since it was not declared in optional_permissions/.test(result.message), "error message for undeclared optional_permission is reasonable" ); // Check request() when the prompt is canceled. optionalPermissionsPromptHandler.acceptPrompt = false; result = await call("request", { permissions: [perm] }); equal(result.status, "success", "request() returned cleanly"); equal( result.result, false, "request() returned false for rejected permission" ); result = await call("contains", { permissions: [perm] }); equal(result, false, "Rejected permission was not granted"); // Call request() and accept the prompt optionalPermissionsPromptHandler.acceptPrompt = true; let allOptional = { permissions: OPTIONAL_PERMISSIONS, origins: OPTIONAL_ORIGINS, }; result = await call("request", allOptional); equal(result.status, "success", "request() returned cleanly"); equal( result.result, true, "request() returned true for accepted permissions" ); // Verify that requesting a permission/origin in the wrong field fails let originsAsPerms = { permissions: OPTIONAL_ORIGINS, }; let permsAsOrigins = { origins: OPTIONAL_PERMISSIONS, }; result = await call("request", originsAsPerms); equal( result.status, "error", "Requesting an origin as a permission should fail" ); ok( /Type error for parameter permissions \(Error processing permissions/.test( result.message ), "Error message for origin as permission is reasonable" ); result = await call("request", permsAsOrigins); equal( result.status, "error", "Requesting a permission as an origin should fail" ); ok( /Type error for parameter permissions \(Error processing origins/.test( result.message ), "Error message for permission as origin is reasonable" ); }); let allPermissions = { permissions: [...REQUIRED_PERMISSIONS, ...OPTIONAL_PERMISSIONS], origins: [...REQUIRED_ORIGINS_EXPECTED, ...OPTIONAL_ORIGINS_NORMALIZED], }; result = await call("getAll"); deepEqual( result, allPermissions, "getAll() returns required and runtime requested permissions" ); result = await call("contains", allPermissions); equal( result, true, "contains() returns true for runtime requested permissions" ); async function restart() { if (useAddonManager === "permanent") { await AddonTestUtils.promiseRestartManager(); } else { // Manually reload for temporarily loaded. await extension.addon.reload(); } await extension.awaitBackgroundStarted(); } // Restart extension, verify permissions are still present. await restart(); result = await call("getAll"); deepEqual( result, allPermissions, "Runtime requested permissions are still present after restart" ); // Check remove() result = await call("remove", { permissions: OPTIONAL_PERMISSIONS }); equal(result, true, "remove() succeeded"); let perms = { permissions: REQUIRED_PERMISSIONS, origins: [...REQUIRED_ORIGINS_EXPECTED, ...OPTIONAL_ORIGINS_NORMALIZED], }; result = await call("getAll"); deepEqual(result, perms, "Expected permissions remain after removing some"); result = await call("remove", { origins: OPTIONAL_ORIGINS }); equal(result, true, "remove() succeeded"); perms.origins = REQUIRED_ORIGINS_EXPECTED; result = await call("getAll"); deepEqual(result, perms, "Back to default permissions after removing more"); if (granted_host_permissions && expectAllGranted) { // Check that all (granted) host permissions in MV3 can be revoked. result = await call("remove", { origins: REQUIRED_ORIGINS }); equal(result, true, "remove() succeeded"); perms.origins = []; result = await call("getAll"); deepEqual( result, perms, "Expected only api permissions remain after removing all origins in mv3." ); } // Clear cache to confirm same result after rebuilding it (after an update). await ExtensionParent.StartupCache.clearAddonData(extension.id); // Restart again, verify optional permissions state is still preserved. await restart(); result = await call("getAll"); deepEqual(result, perms, "Expected the same permissions after restart."); await extension.unload(); } add_task(function test_normal_mv2() { return test_permissions({ manifest_version: 2, useAddonManager: "permanent", expectAllGranted: true, }); }); add_task(function test_normal_mv3() { return test_permissions({ manifest_version: 3, useAddonManager: "permanent", expectAllGranted: false, }); }); add_task(function test_granted_for_temporary_mv3() { return test_permissions({ manifest_version: 3, granted_host_permissions: true, useAddonManager: "temporary", expectAllGranted: true, }); }); add_task(async function test_granted_only_for_privileged_mv3() { try { // For permanent non-privileged, granted_host_permissions does nothing. await test_permissions({ manifest_version: 3, granted_host_permissions: true, useAddonManager: "permanent", expectAllGranted: false, }); // Make extensions loaded with addon manager privileged. AddonTestUtils.usePrivilegedSignatures = true; await test_permissions({ manifest_version: 3, granted_host_permissions: true, useAddonManager: "permanent", expectAllGranted: true, }); } finally { AddonTestUtils.usePrivilegedSignatures = false; } }); add_task(async function test_startup() { async function background() { browser.test.onMessage.addListener(async perms => { await browser.permissions.request(perms); browser.test.sendMessage("requested"); }); let all = await browser.permissions.getAll(); let url = browser.runtime.getURL("*"); all.origins = all.origins.filter(i => i != url); browser.test.sendMessage("perms", all); } const PERMS1 = { permissions: ["clipboardRead", "tabs"], }; const PERMS2 = { origins: ["https://site2.com/*"], }; let extension1 = ExtensionTestUtils.loadExtension({ background, manifest: { optional_permissions: PERMS1.permissions, }, useAddonManager: "permanent", }); let extension2 = ExtensionTestUtils.loadExtension({ background, manifest: { optional_permissions: PERMS2.origins, }, useAddonManager: "permanent", }); await extension1.startup(); await extension2.startup(); let perms = await extension1.awaitMessage("perms"); perms = await extension2.awaitMessage("perms"); await withHandlingUserInput(extension1, async () => { extension1.sendMessage(PERMS1); await extension1.awaitMessage("requested"); }); await withHandlingUserInput(extension2, async () => { extension2.sendMessage(PERMS2); await extension2.awaitMessage("requested"); }); // Restart everything, and force the permissions store to be // re-read on startup await ExtensionPermissions._uninit(); await AddonTestUtils.promiseRestartManager(); await extension1.awaitStartup(); await extension2.awaitStartup(); async function checkPermissions(extension, permissions) { perms = await extension.awaitMessage("perms"); let expect = Object.assign({ permissions: [], origins: [] }, permissions); deepEqual(perms, expect, "Extension got correct permissions on startup"); } await checkPermissions(extension1, PERMS1); await checkPermissions(extension2, PERMS2); await extension1.unload(); await extension2.unload(); }); // Test that we don't prompt for permissions an extension already has. async function test_alreadyGranted(manifest_version) { const REQUIRED_PERMISSIONS = ["geolocation"]; const REQUIRED_ORIGINS = [ "*://required-host.com/", "*://*.required-domain.com/", ]; const OPTIONAL_PERMISSIONS = [ ...REQUIRED_PERMISSIONS, ...REQUIRED_ORIGINS, "clipboardRead", "*://optional-host.com/", "*://*.optional-domain.com/", ]; function pageScript() { browser.test.onMessage.addListener(async (msg, arg) => { if (msg == "request") { let result = await browser.permissions.request(arg); browser.test.sendMessage("request.result", result); } else if (msg == "remove") { let result = await browser.permissions.remove(arg); browser.test.sendMessage("remove.result", result); } else if (msg == "close") { window.close(); } }); browser.test.sendMessage("page-ready"); } let extension = ExtensionTestUtils.loadExtension({ background() { browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); }, manifest: { manifest_version, permissions: REQUIRED_PERMISSIONS, host_permissions: REQUIRED_ORIGINS, optional_permissions: OPTIONAL_PERMISSIONS, granted_host_permissions: true, }, temporarilyInstalled: true, startupReason: "ADDON_INSTALL", files: { "page.html": `