Portals: Implement PortalHost.postMessage

Implements a version of postMessage for PortalHost that can only send
strings. It implements messaging from the portal to the embedding page,
i.e. in the opposite direction to https://crrev.com/c/1423462. Support
for more general message types will be added in a follow up CL.

This CL introduces 2 new mojo interfaces:

blink.mojom.PortalClient: Allows content::Portal in the browser process
to send messages to the corresponding HTMLPortalElement in the outer
renderer process. (This is in the opposite direction as
blink.mojom.Portal, which goes from renderer to browser.)

blink.mojom.PortalHost: Used by the blink::PortalHost object (in the
inner renderer process) to send messages to the content::Portal in the
browser process.

Both the above interfaces together allow blink::PortalHost in the inner
renderer process to send messages to the blink::HTMLPortalElement in
the outer renderer process.

Bug: 914120
Change-Id: If7dbaf6e2817fcc94dc38a1df25afc25d8b75eb3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1432480
Commit-Queue: Adithya Srinivasan <adithyas@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Reviewed-by: Lucas Gadani <lfg@chromium.org>
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#655612}
diff --git a/portals/portals-host-post-message.sub.html b/portals/portals-host-post-message.sub.html
new file mode 100644
index 0000000..a142f55
--- /dev/null
+++ b/portals/portals-host-post-message.sub.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<title>Test postMessage on PortalHost</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+  <script>
+    function createPortal(portalSrc) {
+      var portal = document.createElement("portal");
+      portal.src = portalSrc;
+      return new Promise((resolve, reject) => {
+        portal.addEventListener("message", e => {
+          if (e.data == "loaded")
+            resolve(portal);
+        });
+        document.body.appendChild(portal);
+      });
+    }
+
+    async function createPortalAndLoopMessage(portalSrc, params) {
+      var portal = await createPortal(portalSrc);
+      var waitForResponse = new Promise((resolve, reject) => {
+        portal.addEventListener("message", e => { resolve(e); });
+      });
+      portal.postMessage(params, "*");
+      return waitForResponse;
+    }
+
+    const crossOriginUrl = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-host-post-message.sub.html";
+
+    promise_test(async () => {
+      var {data, origin} = await createPortalAndLoopMessage(
+          "resources/portal-host-post-message.sub.html", ["test", "*"]);
+      assert_equals(data, "test");
+      assert_equals(origin, "http://{{host}}:{{ports[http][0]}}");
+    }, "Message received after postMessage from portal host");
+
+    promise_test(async () => {
+      var {data, origin} = await createPortalAndLoopMessage(
+          crossOriginUrl, ["test", "*"]);
+      assert_equals(data, "test");
+      assert_equals(origin, "http://{{hosts[alt][www]}}:{{ports[http][0]}}");
+    }, "Message received after postMessage from portal host in cross-origin-portal");
+
+    promise_test(async () => {
+      var {data, origin} = await createPortalAndLoopMessage(
+          "resources/portal-host-post-message.sub.html", ["test"]);
+      assert_equals(data, "test");
+      assert_equals(origin, "http://{{host}}:{{ports[http][0]}}");
+    }, "Message received from same-origin portal host with no target origin specified");
+
+    promise_test(async () => {
+      var {data, origin} = await createPortalAndLoopMessage(
+         crossOriginUrl, ["test", "http://{{host}}:{{ports[http][0]}}"]);
+      assert_equals(data, "test");
+    }, "Message received from cross-origin portal host with target origin correctly specified");
+
+    promise_test(async () => {
+       var receiveMessage = new Promise((resolve, reject) => {
+         var bc = new BroadcastChannel("portal-host-post-message-after-activate");
+         bc.onmessage = e => { resolve(e); };
+       });
+       const portalUrl = encodeURIComponent(
+          "portal-host-post-message-after-activate.html");
+       window.open(`resources/portal-embed-and-activate.html?url=${portalUrl}`);
+       var message = await receiveMessage;
+       assert_equals(message.data, "InvalidStateError");
+    }, "Calling postMessage after receiving onactivate event should fail");
+
+    promise_test(() => {
+      var portal = document.createElement("portal");
+      portal.src = "resources/portal-host-post-message-navigate-1.html";
+      var count = 0;
+      var waitForMessages = new Promise((resolve, reject) => {
+        portal.addEventListener("message", e => {
+          count++;
+          if (count == 2)
+            resolve();
+        });
+      });
+      document.body.appendChild(portal);
+      return waitForMessages;
+    }, "postMessage before and after portal navigation should work");
+
+    async_test(t => {
+      createPortalAndLoopMessage("resources/portal-host-post-message.sub.html",
+          ["test", "http://{{hosts[alt][www]}}:{{ports[http][0]}}"])
+          .then(t.step_func(() => { assert_unreached("message delivered"); }));
+      t.step_timeout(t.done, 2000);
+    }, "Message should not be received from portal host with target set to different origin");
+
+    async_test(t => {
+      createPortalAndLoopMessage(crossOriginUrl, ["test"]).then(t.step_func(() => {
+        assert_unreached("message delivered");
+      }));
+      t.step_timeout(t.done, 2000);
+    }, "Message should not be received cross origin-portal host with no target origin set");
+  </script>
+</body>
diff --git a/portals/resources/portal-host-post-message-after-activate.html b/portals/resources/portal-host-post-message-after-activate.html
new file mode 100644
index 0000000..07db1a0
--- /dev/null
+++ b/portals/resources/portal-host-post-message-after-activate.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<script>
+  var bc = new BroadcastChannel("portal");
+  bc.postMessage("loaded");
+  bc.close();
+
+  var ph = window.portalHost;
+
+  window.onportalactivate = e => {
+    var exception_name = ""
+    try {
+      ph.postMessage("message", "*");
+    }
+    catch (error) {
+      exception_name = error.name;
+    }
+    bc = new BroadcastChannel("portal-host-post-message-after-activate");
+    bc.postMessage(exception_name, "*");
+    bc.close();
+  };
+</script>
diff --git a/portals/resources/portal-host-post-message-navigate-1.html b/portals/resources/portal-host-post-message-navigate-1.html
new file mode 100644
index 0000000..3b36eae
--- /dev/null
+++ b/portals/resources/portal-host-post-message-navigate-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+  window.portalHost.postMessage("loaded", "*");
+  window.location.href = "portal-host-post-message-navigate-2.html"
+</script>
diff --git a/portals/resources/portal-host-post-message-navigate-2.html b/portals/resources/portal-host-post-message-navigate-2.html
new file mode 100644
index 0000000..163c42e
--- /dev/null
+++ b/portals/resources/portal-host-post-message-navigate-2.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+  window.portalHost.postMessage("loaded", "*");
+</script>
diff --git a/portals/resources/portal-host-post-message.sub.html b/portals/resources/portal-host-post-message.sub.html
new file mode 100644
index 0000000..8df9830
--- /dev/null
+++ b/portals/resources/portal-host-post-message.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script>
+  window.portalHost.postMessage("loaded", "*");
+  window.portalHost.addEventListener("message", e => {
+    window.portalHost.postMessage(...e.data);
+  });
+</script>