Magento 2 - How to add captcha to a custom form

You need follow some step for using magento captcha into custom module.

Step 1:

Create Vendor/Module/etc/config.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <customer>
            <captcha>
                <shown_to_logged_in_user>
                    <custom_form>1</custom_form>
                </shown_to_logged_in_user>
                <always_for>
                    <custom_form>1</custom_form>
                </always_for>
            </captcha>
        </customer>
        <captcha translate="label">
            <frontend>
                <areas>
                    <custom_form>
                        <label>Custom Form</label>
                    </custom_form>
                </areas>
            </frontend>
        </captcha>
    </default>
</config>

Step 2:

Goto Admin -> Stores -> Configuration -> Customer -> Customer Configuration -> Captcha and configure. You can able to see new forms value Custom Form

Step 3:

Create Vendor/Module/view/frontend/layout/yourroutid_index_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <title>Custom Form</title>
    </head>
    <body>
        <referenceContainer name="content">
            <block class="Vendor\Module\Block\CaptchaForm" name="contactForm" template="Vendor_Module::captchaform.phtml">
                <container name="form.additional.info" label="Form Additional Info">
                    <block class="Magento\Captcha\Block\Captcha" name="captcha" after="-" cacheable="false">
                        <action method="setFormId">
                            <argument name="formId" xsi:type="string">custom_form</argument>
                        </action>
                        <action method="setImgWidth">
                            <argument name="width" xsi:type="string">230</argument>
                        </action>
                        <action method="setImgHeight">
                            <argument name="width" xsi:type="string">50</argument>
                        </action>
                    </block>
                </container>
            </block>
        </referenceContainer>
        <referenceBlock name="head.components">
            <block class="Magento\Framework\View\Element\Js\Components" name="captcha_page_head_components" template="Magento_Captcha::js/components.phtml"/>
        </referenceBlock>
    </body>
</page>

Step 4:

Create Vendor/Module/Block/CaptchaForm.php

namespace Vendor\Module\Block;


class CaptchaForm extends \Magento\Framework\View\Element\Template
{
    public function getFormAction()
    {
        return $this->getUrl('yourroute/index/post', ['_secure' => true]);
    }
}

Step 5:

Create Vendor/Moduel/view/frontend/templates/captchaform.phtml

<form class="form contact"
      action="<?php /* @escapeNotVerified */ echo $block->getFormAction(); ?>"
      id="contact-form"
      method="post"
      data-hasrequired="<?php /* @escapeNotVerified */ echo __('* Required Fields') ?>"
      data-mage-init='{"validation":{}}'>
    <fieldset class="fieldset">
        <legend class="legend"><span><?php /* @escapeNotVerified */ echo __('Write Us') ?></span></legend><br />

        <div class="field name required">
            <label class="label" for="name"><span><?php /* @escapeNotVerified */ echo __('Name') ?></span></label>
            <div class="control">
                <input name="name" id="name" title="<?php /* @escapeNotVerified */ echo __('Name') ?>" value="" class="input-text" type="text" data-validate="{required:true}"/>
            </div>
        </div>
        <div class="field email required">
            <label class="label" for="email"><span><?php /* @escapeNotVerified */ echo __('Email') ?></span></label>
            <div class="control">
                <input name="email" id="email" title="<?php /* @escapeNotVerified */ echo __('Email') ?>" value="" class="input-text" type="email" data-validate="{required:true, 'validate-email':true}"/>
            </div>
        </div>
        <?php echo $block->getChildHtml('form.additional.info'); ?>
    </fieldset>
    <div class="actions-toolbar">
        <div class="primary">
            <input type="hidden" name="hideit" id="hideit" value="" />
            <button type="submit" title="<?php /* @escapeNotVerified */ echo __('Submit') ?>" class="action submit primary">
                <span><?php /* @escapeNotVerified */ echo __('Submit') ?></span>
            </button>
        </div>
    </div>
</form>

Now you can able to see captcha into your form. Now need to validation your captcha using observer. So I use post controller predispatch event for validation.

Step 6:

Create Vendor/Module/etc/frontend/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="controller_action_predispatch_yourroute_index_post">
        <observer name="captcha_custom_form" instance="Vendor\Module\Observer\CheckCustomFormObserver" />
    </event>
</config>

Step 7:

Create Vendor/Module/Observer/CheckCustomFormObserver.php

namespace Vendor\Module\Observer;

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\App\Request\DataPersistorInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Captcha\Observer\CaptchaStringResolver;

class CheckCustomFormObserver implements ObserverInterface
{
    /**
     * @var \Magento\Captcha\Helper\Data
     */
    protected $_helper;

    /**
     * @var \Magento\Framework\App\ActionFlag
     */
    protected $_actionFlag;

    /**
     * @var \Magento\Framework\Message\ManagerInterface
     */
    protected $messageManager;

    /**
     * @var \Magento\Framework\App\Response\RedirectInterface
     */
    protected $redirect;

    /**
     * @var CaptchaStringResolver
     */
    protected $captchaStringResolver;

    /**
     * @var DataPersistorInterface
     */
    private $dataPersistor;

    /**
     * @param \Magento\Captcha\Helper\Data $helper
     * @param \Magento\Framework\App\ActionFlag $actionFlag
     * @param \Magento\Framework\Message\ManagerInterface $messageManager
     * @param \Magento\Framework\App\Response\RedirectInterface $redirect
     * @param CaptchaStringResolver $captchaStringResolver
     */
    public function __construct(
        \Magento\Captcha\Helper\Data $helper,
        \Magento\Framework\App\ActionFlag $actionFlag,
        \Magento\Framework\Message\ManagerInterface $messageManager,
        \Magento\Framework\App\Response\RedirectInterface $redirect,
        CaptchaStringResolver $captchaStringResolver
    ) {
        $this->_helper = $helper;
        $this->_actionFlag = $actionFlag;
        $this->messageManager = $messageManager;
        $this->redirect = $redirect;
        $this->captchaStringResolver = $captchaStringResolver;
    }

    /**
     * Check CAPTCHA on Custom Form
     *
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        $formId = 'custom_form';
        $captcha = $this->_helper->getCaptcha($formId);
        if ($captcha->isRequired()) {
            /** @var \Magento\Framework\App\Action\Action $controller */
            $controller = $observer->getControllerAction();
            if (!$captcha->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) {
                $this->messageManager->addError(__('Incorrect CAPTCHA.'));
                $this->getDataPersistor()->set($formId, $controller->getRequest()->getPostValue());
                $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
                $this->redirect->redirect($controller->getResponse(), 'yourroute/index/index');
            }
        }
    }

    /**
     * Get Data Persistor
     *
     * @return DataPersistorInterface
     */
    private function getDataPersistor()
    {
        if ($this->dataPersistor === null) {
            $this->dataPersistor = ObjectManager::getInstance()
                ->get(DataPersistorInterface::class);
        }

        return $this->dataPersistor;
    }
}

Implementing Google reCAPTCHA

Magento 2.3 requires the module MSP_ReCaptcha which can be used to implement reCAPTCHA in your custom forms. To do so you can extend the module with the following steps:

1. First off add a configuration option to the admin backend:

app/code/Vendor/Module/etc/adminhtml/system.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="msp_securitysuite_recaptcha">
            <group id="frontend">
                <label>Frontend</label>
                <field id="enabled_custom" translate="label" type="select" sortOrder="300" showInDefault="1"
                       showInWebsite="1" showInStore="0" canRestore="1">
                    <label>Use in Custom Form</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                    <depends>
                        <field id="enabled">1</field>
                    </depends>
                </field>
            </group>
        </section>
    </system>
</config>

Note: It here you need to change the field id.

2. Add a default configuration:

app/code/Vendor/Module/etc/config.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <msp_securitysuite_recaptcha>
            <frontend>
                <enabled_custom>1</enabled_custom>
            </frontend>
        </msp_securitysuite_recaptcha>
    </default>
</config>

Note: Set <enabled_custom> to the id you set in the first step.

3. You need to setup virtual types which will be used by the module:

app/code/Vendor/Module/etc/frontend/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- Module Form ReCaptcha -->
    <virtualType name="Vendor\Module\Model\Provider\Failure\RedirectUrl\CustomForm"
                 type="MSP\ReCaptcha\Model\Provider\Failure\RedirectUrl\SimpleUrlProvider">
        <arguments>
            <argument name="urlPath" xsi:type="string">custom/index/index</argument>
        </arguments>
    </virtualType>
    <virtualType name="Vendor\Module\Model\Provider\Failure\CustomFormObserver"
                 type="MSP\ReCaptcha\Model\Provider\Failure\ObserverRedirectFailure">
        <arguments>
            <argument name="redirectUrlProvider"
                      xsi:type="object">Vendor\Module\Model\Provider\Failure\RedirectUrl\CustomForm</argument>
        </arguments>
    </virtualType>
    <virtualType name="Vendor\Module\Model\Provider\IsCheckRequired\Frontend\CustomForm"
                 type="MSP\ReCaptcha\Model\IsCheckRequired">
        <arguments>
            <argument name="enableConfigFlag"
                      xsi:type="string">msp_securitysuite_recaptcha/frontend/enabled_custom</argument>
            <argument name="area" xsi:type="string">frontend</argument>
        </arguments>
    </virtualType>
    <virtualType name="Vendor\Module\Observer\Frontend\CustomFormObserver"
                 type="MSP\ReCaptcha\Observer\ReCaptchaObserver">
        <arguments>
            <argument name="isCheckRequired"
                      xsi:type="object">Vendor\Module\Model\Provider\IsCheckRequired\Frontend\CustomForm</argument>
            <argument name="failureProvider"
                      xsi:type="object">Vendor\Module\Model\Provider\Failure\CustomFormObserver</argument>
        </arguments>
    </virtualType>
</config>

Note: You have to change <argument name="urlPath" xsi:type="string">custom/index/index</argument> to the path you want your user to be redirected back in case of failure.

4. Define a observer that will handle the validation:

app/code/Vendor/Module/etc/frontend/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="controller_action_predispatch_custom_index_index">
        <observer name="vendor_module_recaptcha"
                  instance="Vendor\Module\Observer\Frontend\CustomFormObserver"/>
    </event>
</config>

Note: Make sure you use the observer defined in di.xml in step 3. You have to set the correct event name too!

5. Add the MSP_ReCaptcha block to your form block:

app/code/Vendor/Module/view/frontend/layout/custom_index_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="custom_form_block">
            <block class="MSP\ReCaptcha\Block\Frontend\ReCaptcha" name="msp-recaptcha" after="-"
                   template="MSP_ReCaptcha::msp_recaptcha.phtml"
                   cacheable="false"
                   ifconfig="msp_securitysuite_recaptcha/frontend/enabled_custom">

                <arguments>
                    <argument name="jsLayout" xsi:type="array">
                        <item name="components" xsi:type="array">
                            <item name="msp-recaptcha" xsi:type="array">
                                <item name="component" xsi:type="string">MSP_ReCaptcha/js/reCaptcha</item>
                                <item name="zone" xsi:type="string">custom_form</item>
                            </item>
                        </item>
                    </argument>
                </arguments>
            </block>
        </referenceBlock>
    </body>
</page>

6. The last step is to output the MSP_ReCaptcha block within your form:

app/code/Vendor/Module/view/frontend/templates/custom_form.phtml

<form>
    <?= $block->getChildHtml('msp-recaptcha') ?>
</form>

These steps are based on a form in the frontend area. You can apply reCAPTCHA to you adminhtml forms too. To do so you need to change the area for di.xml, events.xml, layout.xml and template.phtml to adminhtml.


For those of you that cannot get this to work you may need to do what I did:

The reason you captcha may not be displaying is because the base settings are to use the Default captcha block which in the _toHtml does a check to see if the captcha is required.

If you have you settings for captcha to always show than you probably did not run into this issue however if it is not set to always show captchas and you do not want to always show captchas (ie account create/ login etc) than you need to set the logic for only your custom captcha to "Always be required".

on line 69 of vendor/magento/module-captcha/Block/Captcha/DefaultCaptcha.php you will see:

    /**
 * Renders captcha HTML (if required)
 *
 * @return string
 */
protected function _toHtml()
{

    if ($this->getCaptchaModel()->isRequired()) {
        $this->getCaptchaModel()->generate();
        return parent::_toHtml();
    }
    return '';
}

$this->getCaptchaModel() calls $this->_captchaData->getCaptcha() which is in vendor/magento/module-captcha/Helper/Data.php

    /**
 * Get Captcha
 *
 * @param string $formId
 * @return \Magento\Captcha\Model\CaptchaInterface
 */
public function getCaptcha($formId)
{
    if (!array_key_exists($formId, $this->_captcha)) {
        $captchaType = ucfirst($this->getConfig('type'));
        if (!$captchaType) {
            $captchaType = self::DEFAULT_CAPTCHA_TYPE;
        } elseif ($captchaType == 'Default') {
            $captchaType = $captchaType . 'Model';
        }

        $this->_captcha[$formId] = $this->_factory->create($captchaType, $formId);
    }
    return $this->_captcha[$formId];
}

Here the getCaptcha method checks the config value for the type of captcha to render and loads its factory in with $this->_factory->create()

However stepping into this factory class you will see

 public function create($captchaType, $formId)
{
    $className = 'Magento\Captcha\Model\\' . ucfirst($captchaType);

    $instance = $this->_objectManager->create($className, ['formId' => $formId]);
    if (!$instance instanceof \Magento\Captcha\Model\CaptchaInterface) {
        throw new \InvalidArgumentException(
            $className . ' does not implement \Magento\Captcha\Model\CaptchaInterface'
        );
    }
    return $instance;
}

The issue here is that no matter what the factory will look in the Magento Captcha module for any Factory model.. so

We need to create a plugin to wrap around the helper and check for our form key and if it is our form key being used we need to create a new factory class that loads our model that extends \Magento\Captcha\Model\DefaultModel and overides the isRequired() method. Something that looks like this:

in \Your\Module\etc\di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

<!--Custom Captcha-->
<type name="\Magento\Captcha\Helper\Data">
    <plugin name="custom-captcha" type="Your\Module\Plugin\Helper\CaptchaData" />
</type>

in Your\Module\Plugin\Helper\CaptchaData

<?php

namespace Your\Module\Plugin\Helper;

class CaptchaData
{
protected $_captcha = [];

public function __construct(
    \Your\Module\Model\CaptchaFactory $captchaFactory
) {
    $this->captchaFactory = $captchaFactory;
}

/**
 * @param \Magento\Captcha\Helper\Data $subject
 * @param \Closure $proceed
 * @param $formId
 * @return mixed
 */
public function aroundGetCaptcha(\Magento\Captcha\Helper\Data $subject, \Closure $proceed, $formId)
{
    if ($formId == 'your_form_key') {
        $this->_captcha[$formId] = $this->captchaFactory->create();
        return $this->_captcha[$formId];

    }
    return $proceed($formId);

}

}

in \Your\Module\Model\CaptchaFactory

<?php
/**
* Captcha model factory
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Your\Module\Model;

class CaptchaFactory
{
/**
 * @var \Magento\Framework\ObjectManagerInterface
 */
protected $_objectManager;

/**
 * @param \Magento\Framework\ObjectManagerInterface $objectManager
 */
public function __construct(\Magento\Framework\ObjectManagerInterface $objectManager)
{
    $this->_objectManager = $objectManager;
}

/**
 * Get captcha instance
 *
 * @param string $captchaType
 * @param string $formId
 * @return \Magento\Captcha\Model\CaptchaInterface
 * @throws \InvalidArgumentException
 */
public function create()
{
    $instance = $this->_objectManager->create('Your\Module\Model\Captcha', ['formId' => 'event_subscriber']);
    if (!$instance instanceof \Magento\Captcha\Model\CaptchaInterface) {
        throw new \InvalidArgumentException(
            'Your\Module\Model\Captcha does not implement \Magento\Captcha\Model\CaptchaInterface'
        );
    }
    return $instance;
}
}

and finally your model to overide the is required param in \Your\Module\Model\Captcha:

<?php

namespace Your\Module\Model;

class Captcha extends \Magento\Captcha\Model\DefaultModel
{
    public function isRequired($login = null)
    {
        return true;
    }
 }