From 34dff9ebd1858e095fbbc9436e3a491c11fbea1a Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Tue, 27 Oct 2020 09:17:08 +0000 Subject: [PATCH] Bug 1659218 - Prevent activating disabled inputs via dispatchEvent r=smaug Differential Revision: https://phabricator.services.mozilla.com/D87151 --- dom/html/HTMLInputElement.cpp | 26 ++++++++--- .../dom/events/Event-dispatch-click.html | 46 +++++++++++++++++++ .../Event-dispatch-click.tentative.html | 26 +++++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp index 72bec84bb9a6..a3ba512940c2 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -3107,7 +3107,8 @@ void HTMLInputElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { // we've already toggled the state from onclick since the user could // explicitly dispatch DOMActivate on the element. // - // This is a compatibility hack. + // These are compatibility hacks and are defined as legacy-pre-activation + // and legacy-canceled-activation behavior in HTML. // // Track whether we're in the outermost Dispatch invocation that will @@ -3639,7 +3640,7 @@ nsresult HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { // tell the form that we are about to exit a click handler // so the form knows not to defer subsequent submissions // the pending ones that were created during the handler - // will be flushed or forgoten. + // will be flushed or forgotten. mForm->OnSubmitClickEnd(); break; default: @@ -3647,12 +3648,23 @@ nsresult HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { } } - // now check to see if the event was "cancelled" + bool preventDefault = + aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault; + if (IsDisabled() && oldType != NS_FORM_INPUT_CHECKBOX && + oldType != NS_FORM_INPUT_RADIO) { + // Behave as if defaultPrevented when the element becomes disabled by event + // listeners. Checkboxes and radio buttons should still process clicks for + // web compat. See: + // https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour + preventDefault = true; + } + + // now check to see if the event was canceled if (mCheckedIsToggled && outerActivateEvent) { - if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { - // if it was cancelled and a radio button, then set the old + if (preventDefault) { + // if it was canceled and a radio button, then set the old // selected btn to TRUE. if it is a checkbox then set it to its - // original value + // original value (legacy-canceled-activation) if (oldType == NS_FORM_INPUT_RADIO) { nsCOMPtr content = do_QueryInterface(aVisitor.mItemData); HTMLInputElement* selectedRadioButton = @@ -3704,7 +3716,7 @@ nsresult HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { StepNumberControlForUserEvent(keyEvent->mKeyCode == NS_VK_UP ? 1 : -1); FireChangeEventIfNeeded(); aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; - } else if (nsEventStatus_eIgnore == aVisitor.mEventStatus) { + } else if (!preventDefault) { switch (aVisitor.mEvent->mMessage) { case eFocus: { // see if we should select the contents of the textbox. This happens diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-click.html b/testing/web-platform/tests/dom/events/Event-dispatch-click.html index 148f4f50d417..bbc408df8825 100644 --- a/testing/web-platform/tests/dom/events/Event-dispatch-click.html +++ b/testing/web-platform/tests/dom/events/Event-dispatch-click.html @@ -302,4 +302,50 @@ async_test(function(t) { assert_false(didSubmit) t.done() }, "disconnected form should not submit") + +async_test(t => { + const form = document.createElement("form"); + form.onsubmit = t.step_func(ev => { + ev.preventDefault(); + assert_unreached("The form is unexpectedly submitted."); + }); + dump.append(form); + const input = form.appendChild(document.createElement("input")); + input.type = "submit" + input.disabled = true; + input.dispatchEvent(new MouseEvent("click", { cancelable: true })); + t.done(); +}, "disabled submit button should not activate"); + +async_test(t => { + const form = document.createElement("form"); + form.onsubmit = t.step_func(ev => { + ev.preventDefault(); + assert_unreached("The form is unexpectedly submitted."); + }); + dump.append(form); + const input = form.appendChild(document.createElement("input")); + input.onclick = t.step_func(() => { + input.disabled = true; + }); + input.type = "submit" + input.dispatchEvent(new MouseEvent("click", { cancelable: true })); + t.done(); +}, "submit button should not activate if the event listener disables it"); + +async_test(t => { + const form = document.createElement("form"); + form.onsubmit = t.step_func(ev => { + ev.preventDefault(); + assert_unreached("The form is unexpectedly submitted."); + }); + dump.append(form); + const input = form.appendChild(document.createElement("input")); + input.onclick = t.step_func(() => { + input.type = "submit" + input.disabled = true; + }); + input.click(); + t.done(); +}, "submit button that morphed from checkbox should not activate"); diff --git a/testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html b/testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html index 01ce1936a8b7..cfdae55ef2ab 100644 --- a/testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html +++ b/testing/web-platform/tests/dom/events/Event-dispatch-click.tentative.html @@ -49,4 +49,30 @@ test(t => { label.dispatchEvent(new MouseEvent("click")); assert_false(input.checked); }, "disabled radio should not be checked from label click by dispatchEvent"); + +test(t => { + const checkbox = dump.appendChild(document.createElement("input")); + checkbox.type = "checkbox"; + checkbox.onclick = ev => { + checkbox.type = "date"; + ev.preventDefault(); + }; + checkbox.dispatchEvent(new MouseEvent("click", { cancelable: true })); + assert_false(checkbox.checked); +}, "checkbox morphed into another type should not mutate checked state"); + +test(t => { + const radio1 = dump.appendChild(document.createElement("input")); + const radio2 = dump.appendChild(radio1.cloneNode()); + radio1.type = radio2.type = "radio"; + radio1.name = radio2.name = "foo"; + radio2.checked = true; + radio1.onclick = ev => { + radio1.type = "date"; + ev.preventDefault(); + }; + radio1.dispatchEvent(new MouseEvent("click", { cancelable: true })); + assert_false(radio1.checked); + assert_true(radio2.checked); +}, "radio morphed into another type should not steal the existing checked state");