From c5f1dfa35c7083006765c4c02ea6a8bf0b9ec20c Mon Sep 17 00:00:00 2001
From: Sebastian Markbage <sebastian@calyptus.eu>
Date: Tue, 22 Aug 2023 18:24:39 -0400
Subject: [PATCH] Fix escaping in action error URL

---
 .../src/client/ReactDOMComponent.js           |  2 +-
 .../src/__tests__/ReactDOMForm-test.js        | 64 +++++++++++++++++++
 2 files changed, 65 insertions(+), 1 deletion(-)

diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js
index 6454ee7c2d4245..b0f452b29c59e6 100644
--- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js
+++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js
@@ -501,7 +501,7 @@ function setProp(
             // eslint-disable-next-line no-script-url
             "javascript:throw new Error('" +
               'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
-              "consider using form.requestSubmit() instead. If you're trying to use " +
+              "consider using form.requestSubmit() instead. If you\\'re trying to use " +
               'event.stopPropagation() in a submit event handler, consider also calling ' +
               'event.preventDefault().' +
               "')",
diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js
index 50ef3d0212875e..abcdfac69cb596 100644
--- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js
@@ -190,6 +190,8 @@ describe('ReactDOMForm', () => {
         (submitter && submitter.getAttribute('formaction')) || form.action;
       if (!/\s*javascript:/i.test(action)) {
         throw new Error('Navigate to: ' + action);
+      } else {
+        Function(action.substr(11))();
       }
     });
   }
@@ -922,4 +924,66 @@ describe('ReactDOMForm', () => {
     await act(() => resolveText('Wait'));
     assertLog(['Async action finished', 'No pending action']);
   });
+
+  function emulateForceSubmit(submitter) {
+    const form = submitter.form || submitter;
+    const action =
+      (submitter && submitter.getAttribute('formaction')) || form.action;
+    if (!/\s*javascript:/i.test(action)) {
+      throw new Error('Navigate to: ' + action);
+    } else {
+      Function(action.substr(11))();
+    }
+  }
+
+  // @gate enableFormActions
+  it('should error if submitting a form manually', async () => {
+    const ref = React.createRef();
+    let foo;
+
+    function action(formData) {
+      foo = formData.get('foo');
+    }
+
+    let error = null;
+    let result = null;
+
+    function emulateForceSubmit(submitter) {
+      const form = submitter.form || submitter;
+      const action =
+        (submitter && submitter.getAttribute('formaction')) || form.action;
+      if (!/\s*javascript:/i.test(action)) {
+        throw new Error('Navigate to: ' + action);
+      } else {
+        try {
+          result = Function(action.substr(11))();
+        } catch (x) {
+          error = x;
+        }
+      }
+    }
+
+    const root = ReactDOMClient.createRoot(container);
+    await act(async () => {
+      root.render(
+        <form
+          action={action}
+          ref={ref}
+          onSubmit={e => {
+            e.preventDefault();
+            emulateForceSubmit(e.target);
+          }}>
+          <input type="text" name="foo" defaultValue="bar" />
+        </form>,
+      );
+    });
+
+    // This submits the form, which gets blocked and then resubmitted. It's a somewhat
+    // common idiom but we don't support this pattern unless it uses requestSubmit().
+    await submit(ref.current);
+    expect(result).toBe(null);
+    expect(error.message).toContain(
+      'A React form was unexpectedly submitted. If you called form.submit()',
+    );
+  });
 });