/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; const { newURI } = Services.io; add_task(async function test_WebExtensionPolicy() { const id = "foo@bar.baz"; const uuid = "ca9d3f23-125c-4b24-abfc-1ca2692b0610"; const baseURL = "file:///foo/"; const mozExtURL = `moz-extension://${uuid}/`; const mozExtURI = newURI(mozExtURL); let policy = new WebExtensionPolicy({ id, mozExtensionHostname: uuid, baseURL, localizeCallback(str) { return `<${str}>`; }, allowedOrigins: new MatchPatternSet(["http://foo.bar/", "*://*.baz/"], { ignorePath: true, }), permissions: [""], webAccessibleResources: [ { resources: ["/foo/*", "/bar.baz"].map(glob => new MatchGlob(glob)), }, ], }); equal(policy.active, false, "Active attribute should initially be false"); // GetURL equal( policy.getURL(), mozExtURL, "getURL() should return the correct root URL" ); equal( policy.getURL("path/foo.html"), `${mozExtURL}path/foo.html`, "getURL(path) should return the correct URL" ); // Permissions deepEqual( policy.permissions, [""], "Initial permissions should be correct" ); ok( policy.hasPermission(""), "hasPermission should match existing permission" ); ok( !policy.hasPermission("history"), "hasPermission should not match nonexistent permission" ); Assert.throws( () => { policy.permissions[0] = "foo"; }, TypeError, "Permissions array should be frozen" ); policy.permissions = ["history"]; deepEqual( policy.permissions, ["history"], "Permissions should be updateable as a set" ); ok( policy.hasPermission("history"), "hasPermission should match existing permission" ); ok( !policy.hasPermission(""), "hasPermission should not match nonexistent permission" ); // Origins ok( policy.canAccessURI(newURI("http://foo.bar/quux")), "Should be able to access permitted URI" ); ok( policy.canAccessURI(newURI("https://x.baz/foo")), "Should be able to access permitted URI" ); ok( !policy.canAccessURI(newURI("https://foo.bar/quux")), "Should not be able to access non-permitted URI" ); policy.allowedOrigins = new MatchPatternSet(["https://foo.bar/"], { ignorePath: true, }); ok( policy.canAccessURI(newURI("https://foo.bar/quux")), "Should be able to access updated permitted URI" ); ok( !policy.canAccessURI(newURI("https://x.baz/foo")), "Should not be able to access removed permitted URI" ); // Web-accessible resources ok( policy.isWebAccessiblePath("/foo/bar"), "Web-accessible glob should be web-accessible" ); ok( policy.isWebAccessiblePath("/bar.baz"), "Web-accessible path should be web-accessible" ); ok( !policy.isWebAccessiblePath("/bar.baz/quux"), "Non-web-accessible path should not be web-accessible" ); ok( policy.sourceMayAccessPath(mozExtURI, "/bar.baz"), "Web-accessible path should be web-accessible to self" ); // Localization equal( policy.localize("foo"), "", "Localization callback should work as expected" ); // Protocol and lookups. let proto = Services.io .getProtocolHandler("moz-extension", uuid) .QueryInterface(Ci.nsISubstitutingProtocolHandler); deepEqual( WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions" ); equal( WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active" ); equal( WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active" ); Assert.throws( () => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/, "URL should not resolve when not active" ); policy.active = true; equal(policy.active, true, "Active attribute should be updated"); let exts = WebExtensionPolicy.getActiveExtensions(); equal(exts.length, 1, "Should have one active extension"); equal(exts[0], policy, "Should have the correct active extension"); equal( WebExtensionPolicy.getByID(id), policy, "ID lookup should return extension when active" ); equal( WebExtensionPolicy.getByHostname(uuid), policy, "Hostname lookup should return extension when active" ); equal( proto.resolveURI(mozExtURI), baseURL, "URL should resolve correctly while active" ); policy.active = false; equal(policy.active, false, "Active attribute should be updated"); deepEqual( WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions" ); equal( WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active" ); equal( WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active" ); Assert.throws( () => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/, "URL should not resolve when not active" ); // Conflicting policies. // This asserts in debug builds, so only test in non-debug builds. if (!AppConstants.DEBUG) { policy.active = true; let attrs = [ { id, uuid }, { id, uuid: "d916886c-cfdf-482e-b7b1-d7f5b0facfa5" }, { id: "foo@quux", uuid }, ]; // eslint-disable-next-line no-shadow for (let { id, uuid } of attrs) { let policy2 = new WebExtensionPolicy({ id, mozExtensionHostname: uuid, baseURL: "file://bar/", localizeCallback() {}, allowedOrigins: new MatchPatternSet([]), }); Assert.throws( () => { policy2.active = true; }, /NS_ERROR_UNEXPECTED/, `Should not be able to activate conflicting policy: ${id} ${uuid}` ); } policy.active = false; } }); // mozExtensionHostname is normalized to lower case when using // policy.getURL whereas using policy.getByHostname does // not. Tests below will fail without case insensitive // comparisons in ExtensionPolicyService add_task(async function test_WebExtensionPolicy_case_sensitivity() { const id = "policy-case@mochitest"; const uuid = "BAD93A23-125C-4B24-ABFC-1CA2692B0610"; const baseURL = "file:///foo/"; const mozExtURL = `moz-extension://${uuid}/`; const mozExtURI = newURI(mozExtURL); let policy = new WebExtensionPolicy({ id: id, mozExtensionHostname: uuid, baseURL, localizeCallback() {}, allowedOrigins: new MatchPatternSet([]), permissions: [""], }); policy.active = true; equal( WebExtensionPolicy.getByHostname(uuid)?.mozExtensionHostname, policy.mozExtensionHostname, "Hostname lookup should match policy" ); equal( WebExtensionPolicy.getByHostname(uuid.toLowerCase())?.mozExtensionHostname, policy.mozExtensionHostname, "Hostname lookup should match policy" ); equal(policy.getURL(), mozExtURI.spec, "Urls should match policy"); ok( policy.sourceMayAccessPath(mozExtURI, "/bar.baz"), "Extension path should be accessible to self" ); policy.active = false; }); add_task(async function test_WebExtensionPolicy_V3() { const id = "foo@bar.baz"; const uuid = "ca9d3f23-125c-4b24-abfc-1ca2692b0610"; const id2 = "foo-2@bar.baz"; const uuid2 = "89383c45-7db4-4999-83f7-f4cc246372cd"; const id3 = "foo-3@bar.baz"; const uuid3 = "56652231-D7E2-45D1-BDBD-BD3BFF80927E"; const baseURL = "file:///foo/"; const mozExtURL = `moz-extension://${uuid}/`; const mozExtURI = newURI(mozExtURL); const fooSite = newURI("http://foo.bar/"); const exampleSite = newURI("https://example.com/"); let policy = new WebExtensionPolicy({ id, mozExtensionHostname: uuid, baseURL, manifestVersion: 3, localizeCallback(str) { return `<${str}>`; }, allowedOrigins: new MatchPatternSet(["http://foo.bar/", "*://*.baz/"], { ignorePath: true, }), permissions: [""], webAccessibleResources: [ { resources: ["/foo/*", "/bar.baz"].map(glob => new MatchGlob(glob)), matches: ["http://foo.bar/"], extension_ids: [id3], }, { resources: ["/foo.bar.baz"].map(glob => new MatchGlob(glob)), extension_ids: ["*"], }, ], }); policy.active = true; equal( WebExtensionPolicy.getByHostname(uuid), policy, "Hostname lookup should match policy" ); let policy2 = new WebExtensionPolicy({ id: id2, mozExtensionHostname: uuid2, baseURL, localizeCallback() {}, allowedOrigins: new MatchPatternSet([]), permissions: [""], }); policy2.active = true; equal( WebExtensionPolicy.getByHostname(uuid2), policy2, "Hostname lookup should match policy" ); let policy3 = new WebExtensionPolicy({ id: id3, mozExtensionHostname: uuid3, baseURL, localizeCallback() {}, allowedOrigins: new MatchPatternSet([]), permissions: [""], }); policy3.active = true; equal( WebExtensionPolicy.getByHostname(uuid3), policy3, "Hostname lookup should match policy" ); ok( policy.isWebAccessiblePath("/bar.baz"), "Web-accessible path should be web-accessible" ); ok( !policy.isWebAccessiblePath("/bar.baz/quux"), "Non-web-accessible path should not be web-accessible" ); // Extension can always access itself ok( policy.sourceMayAccessPath(mozExtURI, "/bar.baz"), "Web-accessible path should be accessible to self" ); ok( policy.sourceMayAccessPath(mozExtURI, "/foo.bar.baz"), "Web-accessible path should be accessible to self" ); ok( !policy.sourceMayAccessPath(newURI(`https://${uuid}/`), "/bar.baz"), "Web-accessible path should not be accessible due to scheme mismatch" ); // non-matching site cannot access url ok( policy.sourceMayAccessPath(fooSite, "/bar.baz"), "Web-accessible path should be accessible to foo.bar site" ); ok( !policy.sourceMayAccessPath(fooSite, "/foo.bar.baz"), "Web-accessible path should not be accessible to foo.bar site" ); // non-matching site cannot access url ok( !policy.sourceMayAccessPath(exampleSite, "/bar.baz"), "Web-accessible path should not be accessible to example.com" ); ok( !policy.sourceMayAccessPath(exampleSite, "/foo.bar.baz"), "Web-accessible path should not be accessible to example.com" ); let extURI = newURI(policy2.getURL("")); ok( !policy.sourceMayAccessPath(extURI, "/bar.baz"), "Web-accessible path should not be accessible to other extension" ); ok( policy.sourceMayAccessPath(extURI, "/foo.bar.baz"), "Web-accessible path should be accessible to other extension" ); extURI = newURI(policy3.getURL("")); ok( policy.sourceMayAccessPath(extURI, "/bar.baz"), "Web-accessible path should be accessible to other extension" ); ok( policy.sourceMayAccessPath(extURI, "/foo.bar.baz"), "Web-accessible path should be accessible to other extension" ); policy.active = false; policy2.active = false; policy3.active = false; }); add_task(async function test_WebExtensionPolicy_registerContentScripts() { const id = "foo@bar.baz"; const uuid = "77a7b9d3-e73c-4cf3-97fb-1824868fe00f"; const id2 = "foo-2@bar.baz"; const uuid2 = "89383c45-7db4-4999-83f7-f4cc246372cd"; const baseURL = "file:///foo/"; const mozExtURL = `moz-extension://${uuid}/`; const mozExtURL2 = `moz-extension://${uuid2}/`; let policy = new WebExtensionPolicy({ id, mozExtensionHostname: uuid, baseURL, localizeCallback() {}, allowedOrigins: new MatchPatternSet([]), permissions: [""], }); let policy2 = new WebExtensionPolicy({ id: id2, mozExtensionHostname: uuid2, baseURL, localizeCallback() {}, allowedOrigins: new MatchPatternSet([]), permissions: [""], }); let script1 = new WebExtensionContentScript(policy, { run_at: "document_end", js: [`${mozExtURL}/registered-content-script.js`], matches: new MatchPatternSet(["http://localhost/data/*"]), }); let script2 = new WebExtensionContentScript(policy, { run_at: "document_end", css: [`${mozExtURL}/registered-content-style.css`], matches: new MatchPatternSet(["http://localhost/data/*"]), }); let script3 = new WebExtensionContentScript(policy2, { run_at: "document_end", css: [`${mozExtURL2}/registered-content-style.css`], matches: new MatchPatternSet(["http://localhost/data/*"]), }); deepEqual( policy.contentScripts, [], "The policy contentScripts is initially empty" ); policy.registerContentScript(script1); deepEqual( policy.contentScripts, [script1], "script1 has been added to the policy contentScripts" ); Assert.throws( () => policy.registerContentScript(script1), e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to register a script more than once" ); Assert.throws( () => policy.registerContentScript(script3), e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to register a script related to " + "a different extension" ); Assert.throws( () => policy.unregisterContentScript(script3), e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to unregister a script related to " + "a different extension" ); deepEqual( policy.contentScripts, [script1], "script1 has not been added twice" ); policy.registerContentScript(script2); deepEqual( policy.contentScripts, [script1, script2], "script2 has the last item of the policy contentScripts array" ); policy.unregisterContentScript(script1); deepEqual( policy.contentScripts, [script2], "script1 has been removed from the policy contentscripts" ); Assert.throws( () => policy.unregisterContentScript(script1), e => e.result == Cr.NS_ERROR_ILLEGAL_VALUE, "Got the expected NS_ERROR_ILLEGAL_VALUE when trying to unregister a script more than once" ); deepEqual( policy.contentScripts, [script2], "the policy contentscripts is unmodified when unregistering an unknown contentScript" ); policy.unregisterContentScript(script2); deepEqual( policy.contentScripts, [], "script2 has been removed from the policy contentScripts" ); }); add_task(async function test_WebExtensionPolicy_static_themes_resources() { const uuid = "0e7ae607-b5b3-4204-9838-c2138c14bc3c"; const mozExtURL = `moz-extension://${uuid}/`; const mozExtURI = newURI(mozExtURL); let policy = new WebExtensionPolicy({ id: "test-extension@mochitest", mozExtensionHostname: uuid, baseURL: "file:///foo/foo/", localizeCallback() {}, allowedOrigins: new MatchPatternSet([]), permissions: [], }); policy.active = true; let staticThemePolicy = new WebExtensionPolicy({ id: "statictheme@bar.baz", mozExtensionHostname: "164d05dc-b45b-4731-aefc-7c1691bae9a4", baseURL: "file:///static_theme/", type: "theme", allowedOrigins: new MatchPatternSet([]), localizeCallback() {}, }); staticThemePolicy.active = true; ok( staticThemePolicy.sourceMayAccessPath(mozExtURI, "/someresource.ext"), "Active extensions should be allowed to access the static themes resources" ); policy.active = false; ok( !staticThemePolicy.sourceMayAccessPath(mozExtURI, "/someresource.ext"), "Disabled extensions should be disallowed the static themes resources" ); ok( !staticThemePolicy.sourceMayAccessPath( Services.io.newURI("http://example.com"), "/someresource.ext" ), "Web content should be disallowed the static themes resources" ); staticThemePolicy.active = false; });