A Guide to Recaptcha V3 for MODX CMS

Dec 28, 2020

Overview

ReCaptchaV2 (and V3) for MODX CMS is a MODX Extra (add-on component) that bootstraps the implementation of Google's Recaptcha in your MODX-powered website. V3 support was launched Jan 1, 2019.

2 years later, it has become apparent that some folks could use more concrete code examples. The default recaptchav3_html chunk is meant as a starting point and only really useful in the most basic use case. If you're looking for help with multiple forms on one page, or setting the recaptcha token on a user-initiated event, you've come to the right place :)

References

For reference, here's a list of resources you might also find helpful:

This is the content of the default recaptchav3_html Chunk:

<script src="https://www.google.com/recaptcha/api.js?render=[[+site_key]]&hl=[[++cultureKey]]"></script>
<input type="hidden" name="[[+token_key]]">
<input type="hidden" name="[[+action_key]]" value="[[+form_id]]">
<script>
    grecaptcha.ready(function() {
        grecaptcha.execute('[[+site_key]]', {action: '[[+form_id]]'}).then(function(token) {
            document.querySelector('[name="[[+token_key]]"]').value = token;
        });
    });
</script>

Someone mentioned in a GitHub issue that Google Pagespeed would like the script to be called with the defer attribute. You can modify the default Chunk to do this, but it's recommended you create a new one, that won't be overwritten on update.

Use Cases

Multiple forms on a page

To support multiple forms on one page, the first thing you'll want to do is move the recaptcha script tag somewhere in your page html where all the form elements will have access to it. Also, change the [[+site_key]] placeholder to reference the System Setting:

<script src="https://www.google.com/recaptcha/api.js?render=[[++recaptchav3.site_key]]&hl=[[++cultureKey]]"></script>

Note: V2 and V3 have distinct System Settings. This guide is about V3.

Next, loop through all your forms—or more precisely, the recaptcha action input element in each form—and call grecaptcha.execute() on each of them.

grecaptcha.ready(function() {
    document.querySelectorAll('[name="[[++recaptchav3.action_key]]"]').forEach((action) => {
        grecaptcha.execute('[[++recaptchav3.site_key]]', {action: action.value}).then(function(token) {
            action.nextElementSibling.value = token;
        });
    });
});

Break it down:

  • When grecaptcha is ready
  • Select all the input elements. I used the name attribute, with the value of the System Setting for recaptchav3.action_key. This may or may not be appropriate for your specific implementation.
  • For each of those input elements, grecaptcha.execute() is called and the nextElementSibling of the input element gets its value set with the token.

IMPORTANT: you'll want your render Chunk to render the token element after the action element, if you do it the way I've done it.

<input type="hidden" name="[[+action_key]]" value="[[+form_id]]">
<input type="hidden" name="[[+token_key]]">

Again, it's good practice to rename customized Snippet template Chunks, so they don't get overwritten on upgrade. For example the Chunk named recaptchav3_html would be overwritten on upgrade but recaptchav3.tpl would not.

Trigger on user-initiated action

An issue was filed pointing out that if you call grecaptcha.execute() on page load, the token might expire before the user fills out the form. A variation of the above could attach an event handler to your form(s).

CAVEAT: the following has not been tested. It might be, or likely is, flawed in some way. It's only a guide.

grecaptcha.ready(() => {
    document.querySelectorAll('form').forEach((form) => {
        form.onmouseenter = (event) => {
            const action = event.target.querySelector('[name="[[++recaptchav3.action_key]]"]');
            grecaptcha.execute('[[++recaptchav3.site_key]]', {action: action.value}).then((token) => {
                action.nextElementSibling.value = token;
            });
        }
    });
});

Break it down:

  • When grecaptcha is ready
  • Select all form elements on the page
  • Attach a handler to the onmouseenter event. It should fire once when the user's mouse pointer enters the form, and again each time it re-enters the form. Also not sure about mobile devices—it's likely better to attach the handler to onfocus for one of your input fields.
  • Select the action input that is child of the form on which the event was triggered.
  • Run the same call to grecaptcha.execute() to set the token.

Disclaimer: none of this code has been tested and there's no guarantee of suitability for any purpose.

Hopefully this helps get you on your way, if you're facing one of the relevant use cases :)