0

In practical terms, how do I do an async grecapttcha call before submitting the form?

See all SO questions below, that address a less complex scenario.

Considerations:

  • Form must submit via both a submit button and via the Enter key (so no onClick handlers).

  • Prevent multiple form submissions.

  • Default implementation/use of grecaptcha fires on page ready and expires in a couple of minutes, making long forms fail if user takes their time filling them out. So delay running grecaptcha until the form is submitted.

  • Grecaptcha call is async, while form onSubmit requires an immediate response.

  • Grecaptcha is inaccessible in certain markets (I'm looking at you, China). Allow form submission if grecaptcha server is unavailable for any reason.

The following are similar questions on SO, that all fail to address all of the above:

And because community likes to see some broken code, here's my version that works via the submit button but fails via the Enter key because it relies on onClick handler:

<script>
    var onSubmit = function () {
        $('#btn-submit').prop("disabled", true);

        if (typeof grecaptcha == 'object') { //undefined behind the great firewall
            grecaptcha.execute('@Config.ReCaptchaSiteKey', { action: 'login' }).then(function (token) {
                window.document.getElementById('ReCaptchaToken').value = token;
                $('form').submit();
            });
        } else {
            window.document.getElementById('ReCaptchaToken').value = -1;
            $('form').submit();
        }
    };
</script>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    @Html.HiddenFor(model => model.ReCaptchaToken)

    @Html.EditorFor(model => model.Inventory)

    <input type="button"
    id="btn-submit"
    value="Submit"
    class="btn btn-default"
    onclick="onSubmit()" />
}
rothschild86
  • 1,433
  • 16
  • 22
  • 1
    Voted to close. We don't do your work for you here, but rather show you what you did wrong with your code. No code = no effort. Learn to use [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). – StackSlave Mar 06 '20 at 00:31
  • 1
    @StackSlave I answered my own question because all other similar questions and answers on SO were less complex and could not address all my requirements. Why in the world would I use XMLHttpRequest when I'm trying to intercept a form submission for validation and then resubmit??? – rothschild86 Mar 06 '20 at 09:03

1 Answers1

0

The following MVC implementation hits all the requirements (comments inline):

The key line is onsubmit="return submitWithRecaptcha(event);" That return statement is what bubbles up our true/false responses to the native form handler. True means - we have the token - submit now. False means abort - we will resubmit once there is a token. What this means, is that onSubmit method runs twice, taking a different path each time.

<script src="https://www.google.com/recaptcha/api.js?render=@Config.ReCaptchaSiteKey"></script>

<form method="post" action="@Url.Action()" onsubmit="return submitWithRecaptcha(event);">

    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    @Html.HiddenFor(model => model.ReCaptchaToken)

    @Html.EditorFor(model => model.Question1)

    <input type="submit" value="Submit" />
</form> 

<script>

$(window).bind("pageshow", function () { //listen for BFCache pageshow event - ensures a fresh token if navigating back to this page
    var tokenEl = window.document.getElementById('ReCaptchaToken');
    if (tokenEl)
        tokenEl.value = null;
});

var submitWithRecaptcha = function (e) { //handless submit via button and Enter key

    var tokenEl = window.document.getElementById('ReCaptchaToken');

    if (typeof grecaptcha !== 'object') //undefined behind the great firewall
        tokenEl.value = 'error grecaptcha undefined'; //backend will understand

    if (tokenEl.value)
        return true; //we have the token - break immediately with true (will auto submit the form)

    grecaptcha.execute('@Config.ReCaptchaSiteKey', { action: '@ViewContext.RouteData.Values["action"]' })
        .then(function (token) {
            tokenEl.value = token;
            e.target.submit(); //resubmit now that we have the token - restarts this method from the top
        },
        function (ex) { //do not die - submit without recaptcha
            tokenEl.value = 'error ' + ex; //backend will understand
            e.target.submit(); //resubmit - restarts this method from the top
        });

    return false; //abort submit if we don't have the token (grecaptcha runs async) + prevent multiple submissions (via submit button or Enter key)
};
</script>
rothschild86
  • 1,433
  • 16
  • 22