How to validate Google reCaptcha v2 using phalcon/volt forms?

1.1k views Asked by At

How do you validate the new Google reCaptcha using volt and phalcon techniques?

(Just wanted to share what I did to make it work, see answer below, hope it helps...)

1

There are 1 answers

0
Levite On

What you will need

  • A Validator (RecaptchaValidator in this case)
  • Form implementation
  • Controller implementation (actually no changes needed here, but for completeness...)
  • View implementation
  • (optional) Config entries for your recaptcha keys and url (nicer/cleaner this way)
  • (optional) A recaptcha element for automatic rendering (if you prefer the render method)

The Validator

The validator is the most crucial part of this, all other things are rather intuitive ...

use \Phalcon\Validation\Validator;
use \Phalcon\Validation\ValidatorInterface;
use \Phalcon\Validation\Message;

class RecaptchaValidator extends Validator implements ValidatorInterface
{
    public function validate(\Phalcon\Validation $validation, $attribute) 
    {
        if (!$this->isValid($validation)) {
            $message = $this->getOption('message');
            if ($message) { // Add the custom message defined in the "Form" class
                $validation->appendMessage(new Message($message, $attribute, 'Recaptcha'));
            }
            return false;
        }
        return true;
    }

    /********************************************
     *  isValid - Return Values
     *  =======================
     *  true .... Ok
     *  false ... Not Ok
     *  null .... Error
     */
    public function isValid($validation) 
    {
        try {
            $config = $validation->config->recaptcha; // not needed if you don't use a config
            $value =  $validation->getValue('g-recaptcha-response');
            $ip    =  $validation->request->getClientAddress();

            $url = $config->verifyUrl; // or 'https://www.google.com/recaptcha/api/siteverify'; without config
            $data = ['secret'   => $config->secretKey, // or your secret key directly without using the config
                     'response' => $value,
                     'remoteip' => $ip,
                    ];

            // Prepare POST request
            $options = [
                'http' => [
                    'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
                    'method'  => 'POST',
                    'content' => http_build_query($data),
                ],
            ];

            // Make POST request and evaluate the response
            $context  = stream_context_create($options);
            $result = file_get_contents($url, false, $context);
            return json_decode($result)->success;
        }
        catch (Exception $e) {
            return null;
        }
    }
}

Form (class) implementation

class SignupForm extends Form
{
    public function initialize($entity = null, $options = null) 
    {
        // Name (just as an example of other form fields)
        $name = new Text('name');
        $name->setLabel('Username');
        $name->addValidators(array(
            new PresenceOf(array(
                'message' => 'Please enter your name'
            ))
        ));
        $this->add($name);

        // Google Recaptcha v2
        $recaptcha = new Check('recaptcha');
        $recaptcha->addValidator(new RecaptchaValidator([
            'message' => 'Please confirm that you are human'
        ]));
        $this->add($recaptcha);

        // Other form fields...
}

Controller implementation

Even though the controller is the same as with every other form, for completeness sake here an example...

class SessionController extends \Phalcon\Mvc\Controller
{
    public function signupAction()
    {
        $form = new SignupForm();
        if ($this->request->isPost()) {
            if ($form->isValid($this->request->getPost()) != false) 
            {
                // Add user to database, do other checks, etc.
                // ...
            }
        }

        $this->view->form = $form;
    }
}

View implementation

For the view you can either just put the html there or let it be rendered by the engine. If you want it rendered instead (with e.g. {{ form.render('recaptcha') }}), you will have to also create an Recaptcha Element instead of using one of the defaults (see last point in this answer for that).

...

{{ form('class':'signupForm') }}
<fieldset>

    <div>{{ form.label('name') }}</div>
        {{ form.render('name') }}
        {{ form.messages('name') }}

    <!-- other elements here -->        
    <!-- ...                 -->

    <div class="g-recaptcha" data-sitekey="{{ this.config.recaptcha.publicKey }}"></div>
    {{ form.messages('recaptcha') }}

If you don't wanna use a config for your public key (next section), just set the value of the data-sitekey to your personal (Google reCaptcha) public key.

Also don't forget to include the script (<script src='https://www.google.com/recaptcha/api.js'></script>) somewhere (e.g. in your html head section).

(Optional) Config

If you wanna use the config to store your recaptcha keys, also add the following to your config/config.php ...

// config/config.php

return new \Phalcon\Config([

    'application' => [
        'controllersDir' => __DIR__ . '/../../app/controllers/',
        'modelsDir'      => __DIR__ . '/../../app/models/',
        'formsDir'       => __DIR__ . '/../../app/forms/',
        'viewsDir'       => __DIR__ . '/../../app/views/',
        'pluginsDir'     => __DIR__ . '/../../app/plugins/',
        'libraryDir'     => __DIR__ . '/../../app/library/',
        'cacheDir'       => __DIR__ . '/../../app/cache/',
        'baseUri'        => '/',
    ],

    // other configs here
    // ...

    'recaptcha' => [
        'publicKey' => 'your public key',
        'secretKey' => 'your private key',
        'verifyUrl' => 'https://www.google.com/recaptcha/api/siteverify',
    ],
]);

To be able to access the config in your view, you might also need to add $di->set('config', $config); to your dependency injector (typically within config/services.php).

(Optional) Recaptcha Element

If you want your recaptcha to be rendered for you (instead of you putting the div directly in the view), you will need a separate \Phalcon\Forms\Element\ ...

class Recaptcha extends \Phalcon\Forms\Element
{
    public function render($attributes = null) {
        return '<div class="g-recaptcha" data-sitekey="'
            .$this->config->recaptcha->publicKey
            .'"></div>';
    }
}

You will also have to change your Form accordingly:

// ...
$recaptcha = new Recaptcha('recaptcha');
$recaptcha->addValidator(new RecaptchaValidator([
    'message' => '...'
]));
// ...

And finally also your View:

{{ form.render('recaptcha') }}