"use strict"; // The purpose of this test is to show that the XMLHttpRequest API behaves // similarly in MV2 and MV3, except for intentional differences related to // permission handling. Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); const server = createHttpServer({ hosts: ["example.com", "example.net", "example.org"], }); server.registerPathHandler("/dummy", (req, res) => { res.setStatusLine(req.httpVersion, 200, "OK"); res.setHeader("Content-Type", "text/html; charset=utf-8"); // A very strict CSP. res.setHeader( "Content-Security-Policy", "default-src; script-src 'nonce-kindasecret'; connect-src http:" ); res.write( ` ` ); }); server.registerPathHandler("/dummy.json", (req, res) => { res.write(`{"mykey": "kvalue"}`); }); server.registerPathHandler("/nocors", (req, res) => { res.write("no cors"); }); server.registerPathHandler("/cors-enabled", (req, res) => { res.setHeader("Access-Control-Allow-Origin", "http://example.com"); res.write("cors_response"); }); server.registerPathHandler("/return-origin", (req, res) => { res.setHeader("Content-Type", "text/plain"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "*"); res.write(req.hasHeader("Origin") ? req.getHeader("Origin") : "undefined"); }); // We just need to test XHR; fetch is already covered by test_ext_secfetch.js. async function test_xhr({ manifest_version }) { async function contentScript(manifest_version) { function runXHR(url, extraXHRProps, method = "GET") { return new Promise(resolve => { let x = new XMLHttpRequest(); x.open(method, url); Object.assign(x, extraXHRProps); x.onloadend = () => resolve(x); x.send(); }); } async function checkXHR({ description, url, extraXHRProps, method, expected, }) { let { status, response } = expected; let x = await runXHR(url, extraXHRProps, method); browser.test.assertEq(status, x.status, `${description} - status`); browser.test.assertEq(response, x.response, `${description} - body`); } await checkXHR({ description: "Same-origin", url: "http://example.com/nocors", expected: { status: 200, response: "no cors" }, }); await checkXHR({ description: "Cross-origin without CORS", url: "http://example.org/nocors", expected: { status: 0, response: "" }, }); await checkXHR({ description: "Cross-origin with CORS", url: "http://example.org/cors-enabled", expected: manifest_version === 2 ? // Bug 1605197: MV2 cannot fall back to CORS. { status: 0, response: "" } : { status: 200, response: "cors_response" }, }); // MV2 allowed cross-origin requests in content scripts with host // permissions, but MV3 does not. await checkXHR({ description: "Cross-origin without CORS, with permission", url: "http://example.net/nocors", expected: manifest_version === 2 ? { status: 200, response: "no cors" } : { status: 0, response: "" }, }); await checkXHR({ description: "Cross-origin with CORS (and permission)", url: "http://example.net/cors-enabled", expected: { status: 200, response: "cors_response" }, }); // MV2 has a XMLHttpRequest instance specific to the sandbox. // MV3 uses the page's XMLHttpRequest and currently enforces the page's CSP. // TODO bug 1766813: Enforce content script CSP instead. await checkXHR({ description: "data:-URL while page blocks data: via CSP", url: "data:,data-url", expected: // Should be "data-url" in MV3 too. manifest_version === 2 ? { status: 200, response: "data-url" } : { status: 0, response: "" }, }); { let x = await runXHR("http://example.com/dummy.json", { responseType: "json", }); browser.test.assertTrue(x.response instanceof Object, "is JSON object"); browser.test.assertEq(x.response.mykey, "kvalue", "can read parsed JSON"); } { let x = await runXHR("http://example.com/dummy", { responseType: "document", }); browser.test.assertTrue(HTMLDocument.isInstance(x.response), "is doc"); browser.test.assertTrue( x.response.querySelector("#id_of_some_element"), "got parsed document" ); } await checkXHR({ description: "Same-origin Origin header", url: "http://example.com/return-origin", expected: { status: 200, response: "undefined" }, }); await checkXHR({ description: "Same-origin POST Origin header", url: "http://example.com/return-origin", method: "POST", expected: manifest_version === 2 ? { status: 200, response: "undefined" } : { status: 200, response: "http://example.com" }, }); await checkXHR({ description: "Cross-origin (CORS) Origin header", url: "http://example.org/return-origin", expected: manifest_version === 2 ? // Bug 1605197: MV2 cannot fall back to CORS. { status: 0, response: "" } : { status: 200, response: "http://example.com" }, }); await checkXHR({ description: "Cross-origin (CORS) POST Origin header", url: "http://example.org/return-origin", method: "POST", expected: manifest_version === 2 ? // Bug 1605197: MV2 cannot fall back to CORS. { status: 0, response: "" } : { status: 200, response: "http://example.com" }, }); browser.test.sendMessage("done"); } let extension = ExtensionTestUtils.loadExtension({ temporarilyInstalled: true, // Needed for granted_host_permissions manifest: { manifest_version, granted_host_permissions: true, // Test-only: grant permissions in MV3. host_permissions: [ "http://example.net/", // Work-around for bug 1766752. "http://example.com/", // "http://example.org/" is intentionally missing. ], content_scripts: [ { matches: ["http://example.com/dummy"], run_at: "document_end", js: ["contentscript.js"], }, ], }, files: { "contentscript.js": `(${contentScript})(${manifest_version})`, }, }); await extension.startup(); let contentPage = await ExtensionTestUtils.loadContentPage( "http://example.com/dummy" ); await extension.awaitMessage("done"); await contentPage.close(); await extension.unload(); } add_task(async function test_XHR_MV2() { await test_xhr({ manifest_version: 2 }); }); add_task(async function test_XHR_MV3() { await test_xhr({ manifest_version: 3 }); });