<!DOCTYPE html>
<html>
<head>
  <script src="/resources/testharness.js"></script>
  <script src="/resources/testharnessreport.js"></script>
  <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script';">
</head>
<body>
<div id="container"></div>
<script id="script1">"hello world!";</script>
<script id="trigger"></script>
<script>
  var container = document.querySelector("#container");
  const policy_dict = {
    createScript: s => (s.includes("fail") ? null : s.replace("default", "count")),
    createHTML: s => s.replace(/2/g, "3"),
  };
  const policy = trustedTypes.createPolicy("policy", policy_dict);

  // Regression tests for 'Bypass via insertAdjacentText', reported at
  // https://github.com/w3c/trusted-types/issues/133

  // We are trying to assert that scripts do _not_ get executed. We
  // accomplish by having the script under examination containing a
  // postMessage, and to send a second guaranteed-to-execute postMessage
  // so there's a point in time when we're sure the first postMessage
  // must have arrived (if indeed it had been sent).
  //
  // We'll interpret the message data as follows:
  // - includes "block": error (this message should have been blocked by TT)
  // - includes "count": Count these, and later check against expect_count.
  // - includes "done": Unregister the event handler and finish the test.
  // - all else: Reject, as this is probably an error in the test.
  function checkMessage(expect_count) {
    postMessage("done", "*");
    return new Promise((resolve, reject) => {
      let count = 0;
      globalThis.addEventListener("message", function handler(e) {
        if (e.data.includes("block")) {
          reject(`'block' received (${e.data})`);
        } else if (e.data.includes("count")) {
          count = count + 1;
        } else if (e.data.includes("done")) {
          globalThis.removeEventListener("message", handler);
          if (expect_count && count != expect_count) {
            reject(
                `'done' received, but unexpected counts: expected ${expect_count} != actual ${count} (${e.data})`);
          } else {
            resolve(e.data);
          }
        } else {
          reject("unexpected message received: " + e.data);
        }
      });
    });
  }

  function checkSecurityPolicyViolationEvent(expect_count) {
    return new Promise((resolve, reject) => {
      let count = 0;
      document.addEventListener("securitypolicyviolation", e => {
        if (e.sample.includes("trigger")) {
          if (expect_count && count != expect_count) {
            reject(
                `'trigger' received, but unexpected counts: expected ${expect_count} != actual ${count}`);
          } else {
            resolve();
          }
        } else {
          count = count + 1;
        }
      });
      try {
        document.getElementById("trigger").text = "trigger fail";
      } catch(e) { }
    });
  }

  // Original report:
  promise_test(t => {
    // Setup: Create a <script> element with a <p> child.
    let s = document.createElement("script");

    // Sanity check: Element child content (<p>) doesn't count as source text.
    let p = document.createElement("p");
    p.text = "hello('world');";
    s.appendChild(p);
    container.appendChild(s);
    assert_equals(s.text, "");

    // Try to insertAdjacentText into the <script>, starting from the <p>
    p.insertAdjacentText("beforebegin",
                         "postMessage('beforebegin should be blocked', '*');");
    assert_true(s.text.includes("postMessage"));
    p.insertAdjacentText("afterend",
                         "postMessage('afterend should be blocked', '*');");
    assert_true(s.text.includes("after"));
    return checkMessage();
  }, "Regression test: Bypass via insertAdjacentText, initial comment.");

  const script_elements = {
    "script": doc => doc.createElement("script"),
    "svg:script": doc => doc.createElementNS("http://www.w3.org/2000/svg", "script"),
  };
  for (let [element, create_element] of Object.entries(script_elements)) {
    // Like the "Original Report" test case above, but avoids use of the "text"
    // accessor that <svg:script> doesn't support.
    promise_test(t => {
      let s = create_element(document);

      // Sanity check: Element child content (<p>) doesn't count as source text.
      let p = document.createElement("p");
      p.textContent = "hello('world');";
      s.appendChild(p);
      container.appendChild(s);

      // Try to insertAdjacentText into the <script>, starting from the <p>
      p.insertAdjacentText("beforebegin",
                           "postMessage('beforebegin should be blocked', '*');");
      assert_true(s.textContent.includes("postMessage"));
      p.insertAdjacentText("afterend",
                           "postMessage('afterend should be blocked', '*');");
      assert_true(s.textContent.includes("after"));
      return checkMessage();
    }, "Regression test: Bypass via insertAdjacentText, initial comment. " + element);

    // Variant: Create a <script> element and create & insert a text node. Then
    // insert it into the document container to make it live.
    promise_test(t => {
      // Setup: Create a <script> element, and insert text via a text node.
      let s = create_element(document);
      let text = document.createTextNode("postMessage('appendChild with a " +
                                         "text node should be blocked', '*');");
      s.appendChild(text);
      container.appendChild(s);
      return checkMessage();
    }, "Regression test: Bypass via appendChild into off-document script element" + element);

    // Variant: Create a <script> element and insert it into the document. Then
    // create a text node and insert it into the live <script> element.
    promise_test(t => {
      // Setup: Create a <script> element, insert it into the doc, and then create
      // and insert text via a text node.
      let s = create_element(document);
      container.appendChild(s);
      let text = document.createTextNode("postMessage('appendChild with a live " +
                                         "text node should be blocked', '*');");
      s.appendChild(text);
      return checkMessage();
    }, "Regression test: Bypass via appendChild into live script element " + element);
  }

  promise_test(t => {
    return checkSecurityPolicyViolationEvent();
  }, "Prep for subsequent tests: Clear SPV event queue.");

  promise_test(t => {
    const inserted_script = document.getElementById("script1");
    assert_throws_js(TypeError, _ => {
        inserted_script.innerHTML = "2+2";
    });

    let new_script = document.createElement("script");
    assert_throws_js(TypeError, _ => {
      new_script.innerHTML = "2+2";
      container.appendChild(new_script);
    });

    const trusted_html = policy.createHTML("2*4");
    assert_equals("" + trusted_html, "3*4");
    inserted_script.innerHTML = trusted_html;
    assert_equals(inserted_script.textContent, "3*4");

    new_script = document.createElement("script");
    new_script.innerHTML = trusted_html;
    container.appendChild(new_script);
    assert_equals(inserted_script.textContent, "3*4");

    // We expect 3 SPV events: two for the two assert_throws_js cases, and one
    // for script element, which will be rejected at the time of execution.
    return checkSecurityPolicyViolationEvent(3);
  }, "Spot tests around script + innerHTML interaction.");


  // Create default policy. Wrapped in a promise_test to ensure it runs only
  // after the other tests.
  let default_policy;
  promise_test(t => {
    default_policy = trustedTypes.createPolicy("default", policy_dict);
    return Promise.resolve();
  }, "Prep for subsequent tests: Create default policy.");

  for (let [element, create_element] of Object.entries(script_elements)) {
    promise_test(t => {
      let s = create_element(document);
      let text = document.createTextNode("postMessage('default', '*');");
      s.appendChild(text);
      container.appendChild(s);
      return Promise.all([checkMessage(1),
                            checkSecurityPolicyViolationEvent(0)]);
    }, "Test that default policy applies. " + element);

    promise_test(t => {
      let s = create_element(document);
      let text = document.createTextNode("fail");
      s.appendChild(text);
      container.appendChild(s);
      return Promise.all([checkMessage(0),
                          checkSecurityPolicyViolationEvent(1)]);
    }, "Test a failing default policy. " + element);
  }

  promise_test(t => {
    const inserted_script = document.getElementById("script1");
    inserted_script.innerHTML = "2+2";
    assert_equals(inserted_script.textContent, "3+3");

    let new_script = document.createElement("script");
    new_script.innerHTML = "2+2";
    container.appendChild(new_script);
    assert_equals(inserted_script.textContent, "3+3");

    const trusted_html = default_policy.createHTML("2*4");
    assert_equals("" + trusted_html, "3*4");
    inserted_script.innerHTML = trusted_html;
    assert_equals(inserted_script.textContent, "3*4");

    new_script = document.createElement("script");
    new_script.innerHTML = trusted_html;
    container.appendChild(new_script);
    assert_equals(inserted_script.textContent, "3*4");

    return checkSecurityPolicyViolationEvent(0);
  }, "Spot tests around script + innerHTML interaction with default policy.");
</script>
</body>
</html>

