<!DOCTYPE HTML>
<html>
<head>
  <title>Test for content script</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
  <script type="text/javascript" src="head.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>

<script type="text/javascript">
"use strict";

add_task(async function setup() {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.manifestV3.enabled", true]],
  });
});

// Create a test extension with the provided function as the background
// script.  The background script will have a few helpful functions
// available.
/* global awaitLoad, gatherFrameSources */
function makeExtension({
  background,
  useScriptingAPI = false,
  manifest_version = 2,
  host_permissions,
}) {
  // Wait for a webNavigation.onCompleted event where the details for the
  // loaded page match the attributes of `filter`.
  function awaitLoad(filter) {
    return new Promise(resolve => {
      const listener = details => {
        if (Object.keys(filter).every(key => details[key] === filter[key])) {
          browser.webNavigation.onCompleted.removeListener(listener);
          resolve();
        }
      };
      browser.webNavigation.onCompleted.addListener(listener);
    });
  }

  // Return a string with a (sorted) list of the source of all frames
  // in the given tab into which this extension can inject scripts
  // (ie all frames for which it has the activeTab permission).
  // Source is the hostname for frames in http sources, or the full
  // location href in other documents (eg about: pages)
  const gatherFrameSources = useScriptingAPI ?
    async function gatherFrameSources(tabid) {
      let results = await browser.scripting.executeScript({
        target: { tabId: tabid, allFrames: true },
        func: () => window.location.hostname || window.location.href,
      });
      // Adjust `result` so that it looks like the one returned by
      // `tabs.executeScript()`.
      let result = results.map(res => res.result);

      return String(result.sort());
    } : async function gatherFrameSources(tabid) {
      let result = await browser.tabs.executeScript(tabid, {
        allFrames: true,
        matchAboutBlank: true,
        code: "window.location.hostname || window.location.href;",
      });

      return String(result.sort());
    };

  const permissions = ["webNavigation"];
  if (useScriptingAPI) {
    permissions.push("scripting");
  }

  // When host_permissions is passed, test "automatic activeTab" for ungranted
  // host_permissions in mv3, else test with the normal activeTab permission.
  if (!host_permissions) {
    permissions.push("activeTab");
  }

  return ExtensionTestUtils.loadExtension({
    manifest: {
      manifest_version,
      permissions,
      host_permissions,
    },
    background: [
      `const useScriptingAPI = ${useScriptingAPI};`,
      `const manifest_version = ${manifest_version};`,
      `${awaitLoad}`,
      `${gatherFrameSources}`,
      `${ExtensionTestCommon.serializeScript(background)}`,
    ].join("\n")
  });
}

// Helper function to verify that executeScript() fails without the activeTab
// permission (or any specific origin permissions).
const verifyNoActiveTab = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
  let extension = makeExtension({
    async background() {
      const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html";

      let [tab] = await Promise.all([
        browser.tabs.create({url: URL}),
        awaitLoad({frameId: 0}),
      ]);

      await browser.test.assertRejects(
        gatherFrameSources(tab.id),
        /^Missing host permission/,
        "executeScript should fail without activeTab permission"
      );

      await browser.tabs.remove(tab.id);

      browser.test.notifyPass("no-active-tab");
    },
    useScriptingAPI,
    manifest_version,
    host_permissions,
  });

  await extension.startup();
  await extension.awaitFinish("no-active-tab");
  await extension.unload();
};

add_task(async function test_no_activeTab_tabs() {
  await verifyNoActiveTab({ useScriptingAPI: false });
});

add_task(async function test_no_activeTab_scripting() {
  await verifyNoActiveTab({ useScriptingAPI: true });
});

add_task(async function test_no_activeTab_scripting_mv3() {
  await verifyNoActiveTab({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: null,
  });
});

add_task(async function test_no_activeTab_scripting_mv3_autoActiveTab() {
  await verifyNoActiveTab({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: ["http://mochi.test/"],
  });
});

// Test helper to verify that dynamically created iframes do not get the
// activeTab permission.
const verifyDynamicFrames = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
  let extension = makeExtension({
    async background() {
      const BASE_HOST = "www.example.com";

      let [tab] = await Promise.all([
        browser.tabs.create({url: `https://${BASE_HOST}/`}),
        awaitLoad({frameId: 0}),
      ]);

      function inject() {
        let nframes = 4;
        function frameLoaded() {
          nframes--;
          if (nframes == 0) {
            browser.runtime.sendMessage("frames-loaded");
          }
        }

        let frame = document.createElement("iframe");
        frame.addEventListener("load", frameLoaded, {once: true});
        document.body.appendChild(frame);

        let div = document.createElement("div");
        div.innerHTML = "<iframe src='https://test1.example.com/'></iframe>";
        let framelist = div.getElementsByTagName("iframe");
        browser.test.assertEq(1, framelist.length, "Found 1 frame inside div");
        framelist[0].addEventListener("load", frameLoaded, {once: true});
        document.body.appendChild(div);

        let div2 = document.createElement("div");
        div2.innerHTML = "<iframe srcdoc=\"<iframe src='https://test2.example.com/'&gt;</iframe&gt;\"></iframe>";
        framelist = div2.getElementsByTagName("iframe");
        browser.test.assertEq(1, framelist.length, "Found 1 frame inside div");
        framelist[0].addEventListener("load", frameLoaded, {once: true});
        document.body.appendChild(div2);

        const URL = "https://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html";

        let xhr = new XMLHttpRequest();
        xhr.open("GET", URL);
        xhr.responseType = "document";
        xhr.overrideMimeType("text/html");

        xhr.addEventListener("load", () => {
          if (xhr.readyState != 4) {
            return;
          }
          if (xhr.status != 200) {
            browser.runtime.sendMessage("error");
          }

          let frame = xhr.response.getElementById("frame");
          browser.test.assertTrue(frame, "Found frame in response document");
          frame.addEventListener("load", frameLoaded, {once: true});
          document.body.appendChild(frame);
        }, {once: true});
        xhr.addEventListener("error", () => {
          browser.runtime.sendMessage("error");
        }, {once: true});
        xhr.send();
      }

      browser.test.onMessage.addListener(async msg => {
        if (msg !== "go") {
          browser.test.fail(`unexpected message received: ${msg}`);
          return;
        }

        let loadedPromise = new Promise((resolve, reject) => {
          let listener = msg => {
            let unlisten = () => browser.runtime.onMessage.removeListener(listener);
            if (msg == "frames-loaded") {
              unlisten();
              resolve();
            } else if (msg == "error") {
              unlisten();
              reject();
            }
          };
          browser.runtime.onMessage.addListener(listener);
        });

        if (useScriptingAPI) {
          await browser.scripting.executeScript({
            target: { tabId: tab.id },
            func: inject,
          });
        } else {
          await browser.tabs.executeScript(tab.id, {
            code: `(${inject})();`,
          });
        }

        await loadedPromise;

        let result = await gatherFrameSources(tab.id);

        if (manifest_version < 3) {
          browser.test.assertEq(
            String([BASE_HOST]),
            result,
            "Script is not injected into dynamically created frames"
          );
        } else {
          browser.test.assertEq(
            String(["about:blank", "about:srcdoc", BASE_HOST]),
            result,
            `Script injected only into (same origin) about:blank-ish dynamically created frames`
          );
        }

        await browser.tabs.remove(tab.id);

        browser.test.notifyPass("dynamic-frames");
      });

      browser.test.sendMessage("ready", tab.id);
    },
    useScriptingAPI,
    manifest_version,
    host_permissions,
  });

  await extension.startup();

  let tabId = await extension.awaitMessage("ready");
  extension.grantActiveTab(tabId);

  extension.sendMessage("go");
  await extension.awaitFinish("dynamic-frames");

  await extension.unload();
};

add_task(async function test_dynamic_frames_tabs() {
  await verifyDynamicFrames({ useScriptingAPI: false });
});

add_task(async function test_dynamic_frames_scripting() {
  await verifyDynamicFrames({ useScriptingAPI: true });
});

add_task(async function test_dynamic_frames_scripting_mv3() {
  await verifyDynamicFrames({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: null,
  });
});

add_task(async function test_dynamic_frames_scripting_mv3_autoActiveTab() {
  await verifyDynamicFrames({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: ["https://www.example.com/"],
  });
});

// Test helper to verify that an iframe created from an <iframe srcdoc> gets
// the activeTab permission.
const verifySrcdoc = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
  let extension = makeExtension({
    async background() {
      const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html";
      const OUTER_SOURCE = "about:srcdoc";
      const PAGE_SOURCE = "mochi.test";
      const FRAME_SOURCE = "test1.example.com";

      let [tab] = await Promise.all([
        browser.tabs.create({url: URL}),
        awaitLoad({frameId: 0}),
      ]);

      browser.test.onMessage.addListener(async msg => {
        if (msg !== "go") {
          browser.test.fail(`unexpected message received: ${msg}`);
          return;
        }

        let result = await gatherFrameSources(tab.id);

        if (manifest_version < 3) {
          browser.test.assertEq(
            String([OUTER_SOURCE, PAGE_SOURCE, FRAME_SOURCE]),
            result,
            "Script is injected into frame created from <iframe srcdoc>"
          );
        } else {
          browser.test.assertEq(
            String([OUTER_SOURCE, PAGE_SOURCE]),
            result,
            "Script is not injected into cross-origin frame created from <iframe srcdoc>"
          );
        }

        await browser.tabs.remove(tab.id);

        browser.test.notifyPass("srcdoc");
      });

      browser.test.sendMessage("ready", tab.id);
    },
    useScriptingAPI,
    manifest_version,
    host_permissions,
  });

  await extension.startup();

  let tabId = await extension.awaitMessage("ready");
  extension.grantActiveTab(tabId);

  extension.sendMessage("go");
  await extension.awaitFinish("srcdoc");

  await extension.unload();
};

add_task(async function test_srcdoc_tabs() {
  await verifySrcdoc({ useScriptingAPI: false });
});

add_task(async function test_srcdoc_scripting() {
  await verifySrcdoc({ useScriptingAPI: true });
});

add_task(async function test_srcdoc_scripting_mv3() {
  await verifySrcdoc({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: null,
  });
});

add_task(async function test_srcdoc_scripting_mv3_autoActiveTab() {
  await verifySrcdoc({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: ["http://mochi.test/"],
  });
});

// Test helper to verify that navigating frames by setting the src attribute
// from the parent page revokes the activeTab permission.
const verifyNavigateBySrc = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
  let extension = makeExtension({
    async background() {
      const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html";
      const PAGE_SOURCE = "mochi.test";
      const EMPTY_SOURCE = "about:blank";
      const FRAME_SOURCE = "test1.example.com";

      let [tab] = await Promise.all([
        browser.tabs.create({url: URL}),
        awaitLoad({frameId: 0}),
      ]);

      browser.test.onMessage.addListener(async msg => {
        if (msg !== "go") {
          browser.test.fail(`unexpected message received: ${msg}`);
          return;
        }

        let result = await gatherFrameSources(tab.id);
        if (manifest_version < 3) {
          browser.test.assertEq(
            String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]),
            result,
            "In original page, script is injected into base page and original frames"
          );
        } else {
          browser.test.assertEq(
            String([EMPTY_SOURCE, PAGE_SOURCE]),
            result,
            "In original page, script is injected into same-origin frames"
          );
        }

        let loadedPromise = awaitLoad({tabId: tab.id});

        let func = () => {
          document.getElementById('emptyframe').src = 'http://test2.example.com/';
        };

        if (useScriptingAPI) {
          await browser.scripting.executeScript({
            target: { tabId: tab.id },
            func,
          });
        } else {
          await browser.tabs.executeScript(tab.id, { code: `(${func})();` });
        }

        await loadedPromise;


        result = await gatherFrameSources(tab.id);
        if (manifest_version < 3) {
          browser.test.assertEq(
            String([PAGE_SOURCE, FRAME_SOURCE]),
            result,
            "Script is not injected into initially empty frame after navigation"
          );
        } else {
          browser.test.assertEq(
            String([PAGE_SOURCE]),
            result,
            "Script is not injected into initially empty frame after navigation"
          );
        }

        loadedPromise = awaitLoad({tabId: tab.id});

        func = () => {
          document.getElementById('regularframe').src = 'http://mochi.test:8888/';
        };

        if (useScriptingAPI) {
          await browser.scripting.executeScript({
            target: { tabId: tab.id },
            func,
          });
        } else {
          await browser.tabs.executeScript(tab.id, { code: `(${func})();` });
        }

        await loadedPromise;

        result = await gatherFrameSources(tab.id);

        if (manifest_version < 3) {
          browser.test.assertEq(
            String([PAGE_SOURCE]),
            result,
            "Script is not injected into regular frame after navigation"
          );
        } else {
          browser.test.assertEq(
            String([PAGE_SOURCE, PAGE_SOURCE]),
            result,
            "Script injected into frame after navigating to same-origin"
          );
        }

        await browser.tabs.remove(tab.id);
        browser.test.notifyPass("test-scripts");
      });

      browser.test.sendMessage("ready", tab.id);
    },
    useScriptingAPI,
    manifest_version,
    host_permissions,
  });

  await extension.startup();

  let tabId = await extension.awaitMessage("ready");
  extension.grantActiveTab(tabId);

  extension.sendMessage("go");
  await extension.awaitFinish("test-scripts");

  await extension.unload();
};

add_task(async function test_navigate_by_src_tabs() {
  await verifyNavigateBySrc({ useScriptingAPI: false });
});

add_task(async function test_navigate_by_src_scripting() {
  await verifyNavigateBySrc({ useScriptingAPI: true });
});

add_task(async function test_navigate_by_src_scripting_mv3() {
  await verifyNavigateBySrc({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: null,
  });
});

add_task(async function test_navigate_by_src_scripting_mv3_autoActiveTab() {
  await verifyNavigateBySrc({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: ["http://mochi.test/"],
  });
});

// Test helper to verify that navigating frames by setting window.location from
// inside the frame revokes the activeTab permission.
const verifyNavigateByWindowLocation = async ({ useScriptingAPI, manifest_version, host_permissions }) => {
  let extension = makeExtension({
    async background() {
      const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html";
      const PAGE_SOURCE = "mochi.test";
      const EMPTY_SOURCE = "about:blank";
      const FRAME_SOURCE = "test1.example.com";

      let [tab] = await Promise.all([
        browser.tabs.create({url: URL}),
        awaitLoad({frameId: 0}),
      ]);

      browser.test.onMessage.addListener(async msg => {
        if (msg !== "go") {
          browser.test.fail(`unexpected message received: ${msg}`);
          return;
        }

        let result = await gatherFrameSources(tab.id);

        if (manifest_version < 3) {
          browser.test.assertEq(
            String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]),
            result,
            "Script initially injected into all frames"
          );
        } else {
          browser.test.assertEq(
            String([EMPTY_SOURCE, PAGE_SOURCE]),
            result,
            "Script initially injected into all same-origin frames"
          );
        }

        let nframes = 0;
        let frames = await browser.webNavigation.getAllFrames({tabId: tab.id});
        for (let frame of frames) {
          if (frame.parentFrameId == -1) {
            continue;
          }

          if (manifest_version >= 3 && frame.url.includes(FRAME_SOURCE)) {
            // In MV3, can't access cross-origin iframes from the start.

            let invalidPromise = browser.scripting.executeScript({
              target: { tabId: tab.id, frameIds: [frame.frameId] },
              func: () => window.location.hostname,
            });
            await browser.test.assertRejects(
              invalidPromise,
              /^Missing host permission for the tab or frames/,
              "executeScript should fail on cross-origin frame"
            );

            continue;
          }

          let loadPromise = awaitLoad({
            tabId: tab.id,
            frameId: frame.frameId,
          });

          let func = () => {
            window.location.href = 'https://test2.example.com/';
          };

          if (useScriptingAPI) {
            await browser.scripting.executeScript({
              target: { tabId: tab.id, frameIds: [frame.frameId] },
              func,
            });
          } else {
            await browser.tabs.executeScript(tab.id, {
              frameId: frame.frameId,
              matchAboutBlank: true,
              code: `(${func})();`,
            });
          }

          await loadPromise;

          let executePromise;
          func = () => window.location.hostname;

          if (useScriptingAPI) {
            executePromise = browser.scripting.executeScript({
              target: { tabId: tab.id, frameIds: [frame.frameId] },
              func,
            });
          } else {
            executePromise = browser.tabs.executeScript(tab.id, {
              frameId: frame.frameId,
              matchAboutBlank: true,
              code: `(${func})();`,
            });
          }

          await browser.test.assertRejects(
            executePromise,
            /^Missing host permission for the tab or frames/,
            "executeScript should have failed on navigated frame"
          );

          nframes++;
        }

        if (manifest_version < 3) {
          browser.test.assertEq(2, nframes, "Found 2 frames");
        } else {
          browser.test.assertEq(1, nframes, "Found 1 frame");
        }

        await browser.tabs.remove(tab.id);
        browser.test.notifyPass("scripted-navigation");
      });

      browser.test.sendMessage("ready", tab.id);
    },
    useScriptingAPI,
    manifest_version,
    host_permissions,
  });

  await extension.startup();

  let tabId = await extension.awaitMessage("ready");
  extension.grantActiveTab(tabId);

  extension.sendMessage("go");
  await extension.awaitFinish("scripted-navigation");

  await extension.unload();
};

add_task(async function test_navigate_by_window_location_tabs() {
  await verifyNavigateByWindowLocation({ useScriptingAPI: false });
});

add_task(async function test_navigate_by_window_location_scripting() {
  await verifyNavigateByWindowLocation({ useScriptingAPI: true });
});

add_task(async function test_navigate_by_window_location_scripting_mv3() {
  await verifyNavigateByWindowLocation({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: null,
   });
});

add_task(async function test_navigate_by_window_location_scripting_mv3_autoActiveTab() {
  await verifyNavigateByWindowLocation({
    useScriptingAPI: true,
    manifest_version: 3,
    host_permissions: ["http://mochi.test/"],
   });
});

</script>

</body>
</html>
