Add content-type to PerformanceResourceTiming

This CL introduces a contentType field to Performance Resource
Timing object. This field is behind a Runtime Enabled Flag.

Resource Timing PR : https://github.com/w3c/resource-timing/pull/341
Fetch PR : https://github.com/whatwg/fetch/pull/1481

Bug: 1366706
Change-Id: If4c92ef96d74e3ddbb71420ac61dbea4bfa1163b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3916841
Reviewed-by: Yoav Weiss <yoavweiss@chromium.org>
Reviewed-by: Mike West <mkwst@chromium.org>
Commit-Queue: Abin Paul <abin.paul1@gmail.com>
Cr-Commit-Position: refs/heads/main@{#1063289}
diff --git a/resource-timing/content-type.html b/resource-timing/content-type.html
new file mode 100644
index 0000000..f6b1db7
--- /dev/null
+++ b/resource-timing/content-type.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates the content-type of resources.</title>
+<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/entry-invariants.js"></script>
+<script src="resources/resource-loaders.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<script>
+const {ORIGIN, REMOTE_ORIGIN} = get_host_info();
+const SAME_ORIGIN = location.origin;
+
+
+// Content-type for same origin resources is exposed.
+const run_test = (loader, contentType) => {
+  let path = `/resource-timing/resources/content-type.py?content_type=${contentType}`;
+  const url = new URL(path, ORIGIN);
+  attribute_test(
+    loader, url,
+    entry => {
+        assert_equals(entry.contentType, contentType,
+            `content-type for ${entry.name} should be ${contentType}`);
+    });
+}
+
+// Content-type is empty string when a no-cors request is made for cross
+// origin resource.
+// Content-type is empty for cross origin iframes.
+const run_test_cross_origin = (loader, contentType) => {
+  let path = `/resource-timing/resources/content-type.py?content_type=${contentType}`;
+  const url = new URL(path, REMOTE_ORIGIN);
+  attribute_test(
+    loader, url,
+    entry => {
+        assert_equals(entry.contentType, "",
+            `content-type for ${entry.name} should be ""`);
+    });
+}
+
+const resource_loaders_and_types = [
+  [load.font, ["font/woff", "font/otf"]],
+  [load.image, ["image/png", "image/jpg"]],
+  [load.script, ["application/javascript", "text/javascript"]],
+  [load.stylesheet, ["text/css"]],
+  [load.xhr_async, ["application/x-gzip", "application/pdf"]],
+  [load.iframe, ["text/html"]]
+];
+
+resource_loaders_and_types.forEach(resource => {
+  let loader = resource[0];
+  let content_types = resource[1];
+  content_types.forEach(type => {
+    run_test(loader, type);
+    run_test_cross_origin(loader, type);
+  })
+});
+
+
+// Content-type is exposed for cors request for cross-origin resources.
+const run_test_cross_origin_allow_origin = (loader_with_attr,contentType) => {
+  let path = `/resource-timing/resources/content-type.py?content_type=${contentType}&allow_origin=${ORIGIN}`;
+  const url = new URL(path, REMOTE_ORIGIN);
+  loader_with_crossOrigin_attr = async url => {
+    return loader_with_attr(url, {"crossOrigin": "anonymous"});
+  }
+  attribute_test(
+    loader_with_crossOrigin_attr, url,
+    entry => {
+        assert_equals(entry.contentType, contentType,
+            `content-type for ${entry.name} should be ${contentType}`);
+    });
+}
+
+const resource_loaders_with_attrs_and_types = [
+  [load.image_with_attrs, ["image/gif", "image/jpeg"]],
+  [load.script_with_attrs, ["application/javascript", "text/javascript"]],
+  [load.stylesheet_with_attrs, ["text/css"]],
+]
+
+resource_loaders_with_attrs_and_types.forEach(resource => {
+  let loader = resource[0];
+  let content_types = resource[1];
+  content_types.forEach(type => {
+    run_test_cross_origin_allow_origin(loader, type);
+  })
+});
+
+// Content-type for iframes is empty when cross origin redirects are present.
+var destUrl = `${SAME_ORIGIN}/resource-timing/resources/multi_redirect.py?`;
+destUrl += `page_origin=${SAME_ORIGIN}`;
+destUrl += `&cross_origin=${REMOTE_ORIGIN}`;
+destUrl += `&final_resource=/resource-timing/resources/content-type.py?content_type=text/html`;
+attribute_test(
+    load.iframe, new URL(destUrl),
+    entry => {
+        assert_equals(entry.contentType, "",
+            `content-type should be empty for iframes having cross origin redirects`);
+});
+
+
+// Content-type for iframes is exposed for same origin redirects.
+var destUrl = `${SAME_ORIGIN}/resource-timing/resources/redirect-cors.py`;
+destUrl += `?location=${SAME_ORIGIN}/resource-timing/resources/content-type.py?content_type=text/html`;
+attribute_test(
+    load.iframe, new URL(destUrl),
+    entry => {
+        assert_equals(entry.contentType, "text/html",
+            `content-type should be exposed for iframes having only same origin redirects`);
+});
+
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/resource-timing/resources/content-type.py b/resource-timing/resources/content-type.py
new file mode 100644
index 0000000..23a4f0d
--- /dev/null
+++ b/resource-timing/resources/content-type.py
@@ -0,0 +1,5 @@
+def main(request, response):
+    if b'content_type' in request.GET:
+      response.headers.set(b'content-type', request.GET.first(b'content_type'))
+    if b'allow_origin' in request.GET:
+      response.headers.set(b'access-control-allow-origin', request.GET.first(b'allow_origin'))
\ No newline at end of file
diff --git a/resource-timing/tojson.html b/resource-timing/tojson.html
index 19da9c0..2564b85 100644
--- a/resource-timing/tojson.html
+++ b/resource-timing/tojson.html
@@ -49,6 +49,7 @@
         'decodedBodySize',
         'renderBlockingStatus',
         'responseStatus',
+        'contentType',
       ];
       for (const key of performanceResourceTimingKeys) {
         try {