Add tentative WPT to verify download in sandbox
General testing idea:
For a network request, the server stores a token in stash.
And after a fixed period of time, we validate the token in the
stash to verify a download has happened. Also assert that no
additional navigation should happen.
In the case of <a download> where the decision of download can be made
before resource fetching, the server sets the token immediately.
In the case of navigation to a download, the server will stream a
response over 1 seconds and set the token at the end only when the
socket has been connected. So it is able to detect any download
cancellation while fetching the resource.
Bug: 539938, 927183
Change-Id: I7b90d46504603f60938a46acee9fbd7d1483988b
Reviewed-on: https://chromium-review.googlesource.com/c/1446395
Reviewed-by: Mike West <mkwst@chromium.org>
Commit-Queue: Yao Xiao <yaoxia@chromium.org>
Cr-Commit-Position: refs/heads/master@{#634249}
diff --git a/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_allow_downloads_without_user_activation.sub.tentative.html b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_allow_downloads_without_user_activation.sub.tentative.html
new file mode 100644
index 0000000..32409f2
--- /dev/null
+++ b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_allow_downloads_without_user_activation.sub.tentative.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title><a download> triggered download in sandbox is allowed by allow-downloads-without-user-activation.</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#attr-iframe-sandbox">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-iframe-element">
+<script src="/resources/testharness.js"></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src="support/iframe_sandbox_download_helper.js"></script>
+<body>
+<script>
+"use strict";
+
+async_test(t => {
+ const token = "{{$id:uuid()}}";
+ var iframe = document.createElement("iframe");
+ iframe.srcdoc = "<a>Download</a>";
+ iframe.sandbox = "allow-same-origin allow-downloads-without-user-activation";
+ iframe.onload = t.step_func(function () {
+ iframe.contentWindow.addEventListener(
+ "unload", t.unreached_func("Unexpected navigation."));
+ var anchor = iframe.contentDocument.getElementsByTagName('a')[0];
+ anchor.href = "support/download_stash.py?token=" + token;
+ anchor.download = null;
+ anchor.click();
+ AssertDownloadSuccess(t, token, DownloadVerifyDelay());
+ });
+
+ document.body.appendChild(iframe);
+}, "<a download> triggered download in sandbox is allowed by allow-downloads-without-user-activation.");
+</script>
+</body>
diff --git a/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_block_downloads_without_user_activation.sub.tentative.html b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_block_downloads_without_user_activation.sub.tentative.html
new file mode 100644
index 0000000..abd4d7c
--- /dev/null
+++ b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_block_downloads_without_user_activation.sub.tentative.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title><a download> triggered download in sandbox is blocked.</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#attr-iframe-sandbox">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-iframe-element">
+<script src="/resources/testharness.js"></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src="support/iframe_sandbox_download_helper.js"></script>
+<body>
+<script>
+"use strict";
+
+async_test(t => {
+ const token = "{{$id:uuid()}}";
+ var iframe = document.createElement("iframe");
+ iframe.srcdoc = "<a>Download</a>";
+ iframe.sandbox = "allow-same-origin";
+ iframe.onload = t.step_func(function () {
+ iframe.contentWindow.addEventListener(
+ "unload", t.unreached_func("Unexpected navigation."));
+ var anchor = iframe.contentDocument.getElementsByTagName('a')[0];
+ anchor.href = "support/download_stash.py?token=" + token;
+ anchor.download = null;
+ anchor.click();
+ AssertDownloadFailure(t, token, DownloadVerifyDelay());
+ });
+
+ document.body.appendChild(iframe);
+}, "<a download> triggered download in sandbox is blocked.");
+</script>
+</body>
diff --git a/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_allow_downloads_without_user_activation.sub.tentative.html b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_allow_downloads_without_user_activation.sub.tentative.html
new file mode 100644
index 0000000..4fee27f
--- /dev/null
+++ b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_allow_downloads_without_user_activation.sub.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation resulted download in sandbox is allowed by allow-downloads-without-user-activation.</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#attr-iframe-sandbox">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-iframe-element">
+<script src="/resources/testharness.js"></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src="support/iframe_sandbox_download_helper.js"></script>
+<body>
+<script>
+"use strict";
+
+async_test(t => {
+ const token = "{{$id:uuid()}}";
+ var iframe = document.createElement("iframe");
+ iframe.srcdoc = "<a>Download</a>";
+ iframe.sandbox = "allow-same-origin allow-downloads-without-user-activation";
+ iframe.onload = t.step_func(function () {
+ iframe.contentWindow.addEventListener(
+ "unload", t.unreached_func("Unexpected navigation."));
+ var anchor = iframe.contentDocument.getElementsByTagName('a')[0];
+ // Set |finish-delay| to let the server stream a response over a period
+ // of time, so it's able to catch potential download cancellation by
+ // detecting a socket close.
+ anchor.href = "support/download_stash.py?token=" + token + "&finish-delay=" + StreamDownloadFinishDelay();
+ anchor.click();
+ AssertDownloadSuccess(t, token, StreamDownloadFinishDelay() + DownloadVerifyDelay());
+ });
+
+ document.body.appendChild(iframe);
+}, "Navigation resulted download in sandbox is allowed by allow-downloads-without-user-activation.");
+</script>
+</body>
diff --git a/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_block_downloads_without_user_activation.sub.tentative.html b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_block_downloads_without_user_activation.sub.tentative.html
new file mode 100644
index 0000000..9b9246c
--- /dev/null
+++ b/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_block_downloads_without_user_activation.sub.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation resulted download in sandbox is blocked.</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#attr-iframe-sandbox">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-iframe-element">
+<script src="/resources/testharness.js"></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src="support/iframe_sandbox_download_helper.js"></script>
+<body>
+<script>
+"use strict";
+
+async_test(t => {
+ const token = "{{$id:uuid()}}";
+ var iframe = document.createElement("iframe");
+ iframe.srcdoc = "<a>Download</a>";
+ iframe.sandbox = "allow-same-origin";
+ iframe.onload = t.step_func(function () {
+ iframe.contentWindow.addEventListener(
+ "unload", t.unreached_func("Unexpected navigation."));
+ var anchor = iframe.contentDocument.getElementsByTagName('a')[0];
+ // Set |finish-delay| to let the server stream a response over a period
+ // of time, so it's able to catch potential download cancellation by
+ // detecting a socket close.
+ anchor.href = "support/download_stash.py?token=" + token + "&finish-delay=" + StreamDownloadFinishDelay();
+ anchor.click();
+ AssertDownloadFailure(t, token, StreamDownloadFinishDelay() + DownloadVerifyDelay());
+ });
+
+ document.body.appendChild(iframe);
+}, "Navigation resulted download in sandbox is blocked.");
+</script>
+</body>
diff --git a/html/semantics/embedded-content/the-iframe-element/support/download_stash.py b/html/semantics/embedded-content/the-iframe-element/support/download_stash.py
new file mode 100644
index 0000000..24e1dfd
--- /dev/null
+++ b/html/semantics/embedded-content/the-iframe-element/support/download_stash.py
@@ -0,0 +1,28 @@
+import time
+
+def main(request, response):
+ token = request.GET["token"]
+ response.status = 200
+ response.headers.append("Content-Type", "text/html")
+ if "verify-token" in request.GET:
+ if request.server.stash.take(token):
+ return 'TOKEN_SET'
+ return 'TOKEN_NOT_SET'
+
+ if "finish-delay" not in request.GET:
+ # <a download>
+ request.server.stash.put(token, True)
+ return
+
+ # navigation to download
+ response.headers.append("Content-Disposition", "attachment")
+ response.write_status_headers()
+ finish_delay = float(request.GET["finish-delay"]) / 1E3
+ count = 10
+ single_delay = finish_delay / count
+ for i in range(count): # pylint: disable=unused-variable
+ time.sleep(single_delay)
+ response.writer.write_content("\n")
+ if not response.writer.flush():
+ return
+ request.server.stash.put(token, True)
diff --git a/html/semantics/embedded-content/the-iframe-element/support/iframe_sandbox_download_helper.js b/html/semantics/embedded-content/the-iframe-element/support/iframe_sandbox_download_helper.js
new file mode 100644
index 0000000..7090e766
--- /dev/null
+++ b/html/semantics/embedded-content/the-iframe-element/support/iframe_sandbox_download_helper.js
@@ -0,0 +1,37 @@
+function StreamDownloadFinishDelay() {
+ return 1000;
+}
+
+function DownloadVerifyDelay() {
+ return 1000;
+}
+
+function VerifyDownload(test_obj, token, timeout, expect_download) {
+ var verify_token = test_obj.step_func(function () {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', 'support/download_stash.py?verify-token&token=' + token);
+ xhr.onload = test_obj.step_func(function(e) {
+ if (expect_download) {
+ if (xhr.response != "TOKEN_SET") {
+ // Always retry, and rely on the test timeout to conclude that
+ // download didn't happen and to fail the test.
+ test_obj.step_timeout(verify_token, DownloadVerifyDelay());
+ return;
+ }
+ } else {
+ assert_equals(xhr.response, "TOKEN_NOT_SET", "Expect no download to happen, but got one.");
+ }
+ test_obj.done();
+ });
+ xhr.send();
+ });
+ test_obj.step_timeout(verify_token, timeout);
+}
+
+function AssertDownloadSuccess(test_obj, token, timeout) {
+ VerifyDownload(test_obj, token, timeout, true);
+}
+
+function AssertDownloadFailure(test_obj, token, timeout) {
+ VerifyDownload(test_obj, token, timeout, false);
+}