PlzDedicatedWorker: send COEP violation reports to an environment settings object owner

This CL makes a dedicated worker sends COEP violation reports to its
an environment settings object owner during the worker initialization
process when cross-origin-embedder-policy is invalid.
For PlzDedicatedWorker, the owner is the creator document or worker
which starts the worker directly.
For non-PlzDedicatedWorker, the owner is the nearest ancestor frame to
keep the current behavior.

Bug: 1060837, 1197041
Change-Id: I088470e7355c94f48b6ac8d51b5cf813b9d181ad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2794353
Commit-Queue: Asami Doi <asamidoi@chromium.org>
Reviewed-by: Matt Falkenhagen <falken@chromium.org>
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Reviewed-by: Yutaka Hirano <yhirano@chromium.org>
Cr-Commit-Position: refs/heads/master@{#870917}
diff --git a/html/cross-origin-embedder-policy/reporting-to-frame-owner.https.html b/html/cross-origin-embedder-policy/reporting-to-frame-owner.https.html
new file mode 100644
index 0000000..331ad89
--- /dev/null
+++ b/html/cross-origin-embedder-policy/reporting-to-frame-owner.https.html
@@ -0,0 +1,87 @@
+<!doctype html>
+<html>
+<head>
+<title>Check COEP reports are sent to iframe for 'new Worker()' failure</title>
+</head>
+<body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const {ORIGIN} = get_host_info();
+const RESOURCES_PATH= new URL("resources", location).pathname;
+const iframe_path = "worker-owner-frame.html?pipe=";
+const worker_path = "universal-worker.js?pipe=";
+
+const coep_header= {
+  "coep-none"         : "",
+  "coep-report-only"  :
+    "header(Cross-Origin-Embedder-Policy-Report-Only,require-corp)",
+  "coep-require-corp" : "|header(Cross-Origin-Embedder-Policy,require-corp)",
+};
+
+function checkReport(report, url, blocked_url, disposition) {
+  assert_equals(report.type, "coep");
+  assert_equals(report.url, url);
+  assert_equals(report.body.type, "worker initialization");
+  assert_equals(report.body.blockedURL, blocked_url);
+  assert_equals(report.body.disposition, disposition);
+}
+
+// Test parameters:
+// - `owner_coep` the COEP header of the iframe document's response.
+// - `worker_coep` the COEP header of the DedicatedWorker's script response.
+//
+// Test expectations:
+// - `length` the length of reports.
+// - `disposition`  the disposition in a report's body. Empty string if the
+//                  length of reports is expected to be 0.
+function check(
+  // Test parameters:
+  owner_coep,
+  worker_coep,
+  // Test expectations:
+  length,
+  disposition) {
+  promise_test(async (t) => {
+    const worker_url = worker_path + coep_header[worker_coep];
+    const iframe_url = iframe_path + coep_header[owner_coep];
+    const iframe = await with_iframe("./resources/" + iframe_url);
+    t.add_cleanup(() => iframe.remove());
+
+    const iframe_response = new Promise(resolve => window.onmessage = resolve);
+    iframe.contentWindow.startWorkerAndObserveReports(worker_url, length > 0);
+
+    const {data} = await iframe_response;
+    assert_equals(data.length, length);
+    if (data.length > 0) {
+      const blocked_url = `${ORIGIN}${RESOURCES_PATH}/${worker_url}`;
+      const url = `${ORIGIN}${RESOURCES_PATH}/${iframe_url}`;
+      checkReport(
+        data[0],
+        url,
+        blocked_url,
+        disposition
+      );
+    }
+  }, `Reporting to ${owner_coep} frame with ${worker_coep} worker`);
+}
+
+// -----------------------------------------------------------------------------
+//    owner_coep          , worker_coep         , length  , disposition
+// -----------------------------------------------------------------------------
+check("coep-none"         , "coep-none"         , 0       , "");
+check("coep-none"         , "coep-report-only"  , 0       , "");
+check("coep-none"         , "coep-require-corp" , 0       , "");
+check("coep-report-only"  , "coep-none"         , 1       , "reporting");
+check("coep-report-only"  , "coep-report-only"  , 1       , "reporting");
+check("coep-report-only"  , "coep-require-corp" , 0       , "");
+check("coep-require-corp" , "coep-none"         , 1       , "enforce");
+check("coep-require-corp" , "coep-report-only"  , 1       , "enforce");
+check("coep-require-corp" , "coep-require-corp" , 0       , "");
+
+</script>
+</body>
+</html>
+
diff --git a/html/cross-origin-embedder-policy/reporting-to-worker-owner.https.html b/html/cross-origin-embedder-policy/reporting-to-worker-owner.https.html
new file mode 100644
index 0000000..c001087
--- /dev/null
+++ b/html/cross-origin-embedder-policy/reporting-to-worker-owner.https.html
@@ -0,0 +1,89 @@
+<!doctype html>
+<html>
+<head>
+<title>Check COEP reports are sent to parent worker for 'new Worker()' failure</title>
+</head>
+<body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const {ORIGIN} = get_host_info();
+const RESOURCES_PATH= new URL("resources", location).pathname;
+const parent_worker_path = "worker-owner.js?pipe=";
+const worker_path = "universal-worker.js?pipe=";
+
+const coep_header= {
+  "coep-none"         : "",
+  "coep-report-only"  :
+    "header(Cross-Origin-Embedder-Policy-Report-Only,require-corp)",
+  "coep-require-corp" : "|header(Cross-Origin-Embedder-Policy,require-corp)",
+};
+
+function checkReport(report, url, blocked_url, disposition) {
+  assert_equals(report.type, "coep");
+  assert_equals(report.url, url);
+  assert_equals(report.body.type, "worker initialization");
+  assert_equals(report.body.blockedURL, blocked_url);
+  assert_equals(report.body.disposition, disposition);
+}
+
+// Test parameters:
+// - `owner_coep` the COEP header of the parent DedicatedWorker's script
+//                response.
+// - `worker_coep` the COEP header of the DedicatedWorker's script response.
+//
+// Test expectations:
+// - `length` the length of reports.
+// - `disposition`  the disposition in a report's body. Empty string if the
+//                  length of reports is expected to be 0.
+function check(
+  // Test parameters:
+  owner_coep,
+  worker_coep,
+  // Test expectations:
+  length,
+  disposition) {
+  promise_test(async (t) => {
+    const worker_url = worker_path + coep_header[worker_coep];
+    const parent_worker_url = parent_worker_path + coep_header[owner_coep];
+    const parent_worker = new Worker('./resources/' + parent_worker_url);
+
+    const worker_response =
+        new Promise(resolve => parent_worker.onmessage = resolve);
+    parent_worker.postMessage(
+        {worker_url: worker_url, wait_for_report: length > 0});
+
+    const {data} = await worker_response;
+    assert_equals(data.length, length);
+    if (data.length > 0) {
+      const blocked_url = `${ORIGIN}${RESOURCES_PATH}/${worker_url}`;
+      const url = `${ORIGIN}${RESOURCES_PATH}/${parent_worker_url}`;
+      checkReport(
+        data[0],
+        url,
+        blocked_url,
+        disposition
+      );
+    }
+  }, `Reporting to ${owner_coep} worker with ${worker_coep} worker`);
+}
+
+// -----------------------------------------------------------------------------
+//    owner_coep          , worker_coep         , length  , disposition
+// -----------------------------------------------------------------------------
+check("coep-none"         , "coep-none"         , 0       , "");
+check("coep-none"         , "coep-report-only"  , 0       , "");
+check("coep-none"         , "coep-require-corp" , 0       , "");
+check("coep-report-only"  , "coep-none"         , 1       , "reporting");
+check("coep-report-only"  , "coep-report-only"  , 1       , "reporting");
+check("coep-report-only"  , "coep-require-corp" , 0       , "");
+check("coep-require-corp" , "coep-none"         , 1       , "enforce");
+check("coep-require-corp" , "coep-report-only"  , 1       , "enforce");
+check("coep-require-corp" , "coep-require-corp" , 0       , "");
+
+</script>
+</body>
+</html>
+
diff --git a/html/cross-origin-embedder-policy/resources/worker-owner-frame.html b/html/cross-origin-embedder-policy/resources/worker-owner-frame.html
new file mode 100644
index 0000000..509c904
--- /dev/null
+++ b/html/cross-origin-embedder-policy/resources/worker-owner-frame.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<script src="worker-owner.js"></script>
diff --git a/html/cross-origin-embedder-policy/resources/worker-owner.js b/html/cross-origin-embedder-policy/resources/worker-owner.js
new file mode 100644
index 0000000..d1f172a
--- /dev/null
+++ b/html/cross-origin-embedder-policy/resources/worker-owner.js
@@ -0,0 +1,36 @@
+const is_worker = !('window' in self);
+const parent_or_self = is_worker ? self : self.parent;
+
+function startWorkerAndObserveReports(worker_url, wait_for_report) {
+  const worker = new Worker(worker_url);
+  const result_promise = new Promise(resolve => {
+    worker.onmessage = _ => resolve('success');
+    worker.onerror = _ => resolve('error');
+  });
+  worker.postMessage("postMessage('reply to owner from worker');");
+
+  const report_promise = new Promise(resolve => {
+    const observer = new ReportingObserver(reports => {
+      observer.disconnect();
+      resolve(reports.map(r => r.toJSON()));
+    });
+    observer.observe();
+  });
+
+  if (wait_for_report) {
+    Promise.all([result_promise, report_promise]).then(results => {
+      parent_or_self.postMessage(results[1]);
+    });
+  } else {
+    result_promise.then(result => {
+      parent_or_self.postMessage([]);
+    });
+  }
+}
+
+if (is_worker) {
+  onmessage = e => {
+    startWorkerAndObserveReports(e.data.worker_url, e.data.wait_for_report);
+  };
+}
+