diff options
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js | 620 |
1 files changed, 620 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js new file mode 100644 index 0000000000..ff2cc3c2ac --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js @@ -0,0 +1,620 @@ +/* -*- 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: ["<all_urls>"], + 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, + ["<all_urls>"], + "Initial permissions should be correct" + ); + + ok( + policy.hasPermission("<all_urls>"), + "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("<all_urls>"), + "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"), + "<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: ["<all_urls>"], + }); + 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: ["<all_urls>"], + 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: ["<all_urls>"], + }); + 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: ["<all_urls>"], + }); + 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: ["<all_urls>"], + }); + + let policy2 = new WebExtensionPolicy({ + id: id2, + mozExtensionHostname: uuid2, + baseURL, + localizeCallback() {}, + allowedOrigins: new MatchPatternSet([]), + permissions: ["<all_urls>"], + }); + + 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; +}); |