Fix a bug that could allow duplicate form submissions

Previous to this CL, and after [1], if a form Submit button had an
onclick handler that also called form.submit() and did not call
event.preventDefault(), the form would get submitted twice. The second
request was eventually cancelled, but not before it hit the network.
This behavior is specified in [2], through the "plan to navigate"
mechanism. In the case of this bug, the "click" event occurs first,
and changes the "planned navigation". Then, because the click handler
does not preventDefault(), the default submit action is executed,
which changes the "planned navigation", replacing the navigation
queued by the onclick handler. Therefore, only the default submit
navigation is performed.

Note that there are other potential interactions which are less
clearly specified, and which are not addressed in this CL.
For example:

  <iframe id="test" name="test"></iframe>
  <form id=form1 target="test" action="click.html"></form>
  <a target="test" onclick="form1.submit()" href="href.html">Test</a>

In this case, clicking the <a> link first submits the form (to
click.html), and then queues a navigation to href.html. Because
the navigation to href.html is specified (in [3]) to "queue a
navigation", independently of the planned navigation specified
in [2], it is unclear when/whether the form submission should
take place. The spec ([4]) does have provisions for canceling
existing navigations, but that leaves room for the form to still
get to the network in this case, before getting canceled.

[1] https://chromium.googlesource.com/chromium/src/+/6931ab86f19aa79abbdd0c1062084e16b5c4f0f6
[2] https://www.w3.org/TR/html52/sec-forms.html#form-submission-algorithm
[3] https://html.spec.whatwg.org/#following-hyperlinks
[4] https://html.spec.whatwg.org/#navigating-across-documents

Bug: 977882
Change-Id: I693f3bdccb17c5e64df75c2e569fab589c02e88c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1850358
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Reviewed-by: Mason Freed <masonfreed@chromium.org>
Cr-Commit-Position: refs/heads/master@{#706782}
diff --git a/html/semantics/forms/form-submission-0/form-double-submit-2.html b/html/semantics/forms/form-submission-0/form-double-submit-2.html
new file mode 100644
index 0000000..f0c9471
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/form-double-submit-2.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Mason Freed" href="mailto:masonfreed@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- The onclick submit() should *not* get superseded in this case by the
+     default action submit(), because onclick here calls preventDefault().
+  -->
+
+
+
+
+<label for=frame1 style="display:block">This frame should stay blank</label>
+<iframe name=frame1 id=frame1></iframe>
+<label for=frame2 style="display:block">This frame should navigate (to 404)</label>
+<iframe name=frame2 id=frame2></iframe>
+<form id="form1" target="frame2" action="nonexistent.html">
+  <input type=hidden name=navigated value=1>
+  <input id=submitbutton type=submit>
+</form>
+
+<script>
+let frame1 = document.getElementById('frame1');
+let frame2 = document.getElementById('frame2');
+
+async_test(t => {
+  window.addEventListener('load', () => {
+    frame1.addEventListener('load', t.step_func_done(() => {
+      assert_unreached("Frame1 should not get navigated by this test.");
+    }));
+    frame2.addEventListener('load', t.step_func_done(() => {
+      let params = (new URL(frame2.contentWindow.location)).searchParams;
+      let wasNavigated = !!params.get("navigated");
+      assert_true(wasNavigated);
+    }));
+    form1.addEventListener('click', t.step_func(() => {
+      form1.submit();
+      form1.target='frame1';
+      event.preventDefault();  // Prevent default here
+    }));
+    submitbutton.click();
+  });
+}, 'preventDefault should allow onclick submit() to succeed');
+</script>
diff --git a/html/semantics/forms/form-submission-0/form-double-submit-3.html b/html/semantics/forms/form-submission-0/form-double-submit-3.html
new file mode 100644
index 0000000..1bad232
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/form-double-submit-3.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Mason Freed" href="mailto:masonfreed@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- <button> should have the same double-submit protection that
+     <input type=submit> has.
+  -->
+
+
+
+
+<label for=frame1 style="display:block">This frame should stay blank</label>
+<iframe name=frame1 id=frame1></iframe>
+<label for=frame2 style="display:block">This frame should navigate (to 404)</label>
+<iframe name=frame2 id=frame2></iframe>
+<form id="form1" target="frame1" action="nonexistent.html">
+  <input type=hidden name=navigated value=1>
+  <button id=submitbutton>submit</button>
+</form>
+
+<script>
+let frame1 = document.getElementById('frame1');
+let frame2 = document.getElementById('frame2');
+
+async_test(t => {
+  window.addEventListener('load', () => {
+    frame1.addEventListener('load', t.step_func_done(() => {
+      assert_unreached("Frame1 should not get navigated by this test.");
+    }));
+    frame2.addEventListener('load', t.step_func_done(() => {
+      let params = (new URL(frame2.contentWindow.location)).searchParams;
+      let wasNavigated = !!params.get("navigated");
+      assert_true(wasNavigated)
+    }));
+    form1.addEventListener('click', t.step_func(() => {
+      form1.submit();
+      form1.target='frame2';
+
+    }));
+    submitbutton.click();
+  });
+}, '<button> should have the same double-submit protection as <input type=submit>');
+
+</script>
diff --git a/html/semantics/forms/form-submission-0/form-double-submit.html b/html/semantics/forms/form-submission-0/form-double-submit.html
new file mode 100644
index 0000000..1102e30
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/form-double-submit.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Mason Freed" href="mailto:masonfreed@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- The onclick submit() should get superseded by the default
+     action submit(), which isn't preventDefaulted by onclick here.
+     This is per the Form Submission Algorithm [1], step 22.3, which
+     says that new planned navigations replace old planned navigations.
+    [1] https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
+  -->
+
+<label for=frame1 style="display:block">This frame should stay blank</label>
+<iframe name=frame1 id=frame1></iframe>
+<label for=frame2 style="display:block">This frame should navigate (to 404)</label>
+<iframe name=frame2 id=frame2></iframe>
+<form id="form1" target="frame1" action="nonexistent.html">
+  <input type=hidden name=navigated value=1>
+  <input id=submitbutton type=submit>
+</form>
+
+<script>
+let frame1 = document.getElementById('frame1');
+let frame2 = document.getElementById('frame2');
+
+async_test(t => {
+  window.addEventListener('load', () => {
+    frame1.addEventListener('load', t.step_func_done(() => {
+      assert_unreached("Frame1 should not get navigated by this test.");
+    }));
+    frame2.addEventListener('load', t.step_func_done(() => {
+      let params = (new URL(frame2.contentWindow.location)).searchParams;
+      let wasNavigated = !!params.get("navigated");
+      assert_true(wasNavigated)
+    }));
+    form1.addEventListener('click', t.step_func(() => {
+      form1.submit();
+      form1.target='frame2';
+
+    }));
+    submitbutton.click();
+  });
+}, 'default submit action should supersede onclick submit()');
+
+</script>