ReCaptcha

This commit is contained in:
2021-02-26 21:55:30 +01:00
parent 4ec83273eb
commit fb19b06c17
19 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
<?php
namespace ReCaptcha\Action;
use ReCaptcha\Event\ReCaptchaCheckEvent;
use ReCaptcha\Event\ReCaptchaEvents;
use ReCaptcha\ReCaptcha;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class ReCaptchaAction implements EventSubscriberInterface
{
/** @var Request */
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function checkCaptcha(ReCaptchaCheckEvent $event)
{
$requestUrl = "https://www.google.com/recaptcha/api/siteverify";
$secretKey = ReCaptcha::getConfigValue('secret_key');
$requestUrl .= "?secret=$secretKey";
$captchaResponse = $event->getCaptchaResponse();
if (null == $captchaResponse) {
$captchaResponse = $this->request->request->get('g-recaptcha-response');
}
$requestUrl .= "&response=$captchaResponse";
$remoteIp = $event->getRemoteIp();
if (null == $remoteIp) {
$remoteIp = $this->request->server->get('REMOTE_ADDR');
}
$requestUrl .= "&remoteip=$remoteIp";
$result = json_decode(file_get_contents($requestUrl), true);
if ($result['success'] == true) {
$event->setHuman(true);
}
}
public static function getSubscribedEvents()
{
return [
ReCaptchaEvents::CHECK_CAPTCHA_EVENT => ['checkCaptcha', 128],
];
}
}

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns="http://thelia.net/schema/dic/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/config http://thelia.net/schema/dic/config/thelia-1.0.xsd">
<loops>
<!-- sample definition
<loop name="MySuperLoop" class="ReCaptcha\Loop\MySuperLoop" />
-->
</loops>
<forms>
<form name="recaptcha_configuration.form" class="ReCaptcha\Form\ConfigurationForm" />
</forms>
<commands>
<!--
<command class="ReCaptcha\Command\MySuperCommand" />
-->
</commands>
<services>
<service id="recpatcha.action" class="ReCaptcha\Action\ReCaptchaAction">
<tag name="kernel.event_subscriber" />
<argument type="service" id="request"/>
</service>
<service id="thelia.form_validator" class="ReCaptcha\Form\MyTheliaFormValidator">
<argument type="service" id="thelia.translator" />
<argument>%kernel.environment%</argument>
<argument type="service" id="event_dispatcher" />
</service>
</services>
<hooks>
<hook id="recaptcha.hook" class="ReCaptcha\Hook\FrontHook">
<tag name="hook.event_listener" event="main.head-top" type="front" templates="render:recaptcha-js.html" />
<tag name="hook.event_listener" event="recaptcha.js" type="front" templates="render:recaptcha-js.html" />
<tag name="hook.event_listener" event="recaptcha.check" type="front" method="addRecaptchaCheck" />
</hook>
</hooks>
<!--
<exports>
</exports>
-->
<!--
<imports>
</imports>
-->
</config>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://thelia.net/schema/dic/module"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/module http://thelia.net/schema/dic/module/module-2_2.xsd">
<fullnamespace>ReCaptcha\ReCaptcha</fullnamespace>
<descriptive locale="en_US">
<title>ReCaptcha</title>
<!--
<subtitle></subtitle>
<description></description>
<postscriptum></postscriptum>
-->
</descriptive>
<descriptive locale="fr_FR">
<title>ReCaptcha</title>
</descriptive>
<!-- <logo></logo> -->
<!--<images-folder>images</images-folder>-->
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>2.0.2</version>
<authors>
<author>
<name>Vincent Lopes-Vicente</name>
<email>vlopes@openstudio.fr</email>
</author>
</authors>
<type>classic</type>
<!--
module dependencies
<required>
<module version="&gt;=0.1">Front</module>
<module version="~1.0">HookCart</module>
<module version="&gt;0.2">HookSearch</module>
</required>
-->
<thelia>2.3.0</thelia>
<stability>other</stability>
</module>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="admin.recaptcha.config" path="/admin/module/ReCaptcha">
<default key="_controller">ReCaptcha\Controller\ConfigurationController::viewAction</default>
</route>
<route id="admin.recaptcha.config.save" path="/admin/module/recaptcha/configuration" methods="POST">
<default key="_controller">ReCaptcha\Controller\ConfigurationController::saveAction</default>
</route>
</routes>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<database defaultIdMethod="native" name="thelia"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../vendor/propel/propel/resources/xsd/database.xsd" >
<!--
See propel documentation on http://propelorm.org for all information about schema file
<table name="product_rel" namespace="ReCaptcha\Model">
<column autoIncrement="true" name="id" primaryKey="true" required="true" type="INTEGER" />
<column defaultValue="0" name="visible" required="true" type="TINYINT" />
<column defaultValue="0" name="position" required="true" type="INTEGER" />
<column name="title" size="255" type="VARCHAR" />
<column name="description" type="CLOB" />
<column name="chapo" type="LONGVARCHAR" />
<column name="postscriptum" type="LONGVARCHAR" />
<foreign-key foreignTable="product" name="fk_product_id" onDelete="CASCADE" onUpdate="RESTRICT">
<reference foreign="id" local="product_id" />
</foreign-key>
<behavior name="timestampable" />
<behavior name="i18n">
<parameter name="i18n_columns" value="title, description, chapo, postscriptum" />
</behavior>
<behavior name="versionable">
<parameter name="log_created_at" value="true" />
<parameter name="log_created_by" value="true" />
</behavior>
</table>
-->
<external-schema filename="local/config/schema.xml" referenceOnly="true" />
</database>

View File

@@ -0,0 +1,50 @@
<?php
namespace ReCaptcha\Controller;
use ReCaptcha\ReCaptcha;
use Thelia\Controller\Admin\BaseAdminController;
use Thelia\Core\Security\AccessManager;
use Thelia\Core\Security\Resource\AdminResources;
use Thelia\Core\Translation\Translator;
class ConfigurationController extends BaseAdminController
{
public function viewAction()
{
return $this->render(
"recaptcha/configuration"
);
}
public function saveAction()
{
if (null !== $response = $this->checkAuth(array(AdminResources::MODULE), 'ReCaptcha', AccessManager::VIEW)) {
return $response;
}
$form = $this->createForm("recaptcha_configuration.form");
try {
$data = $this->validateForm($form)->getData();
ReCaptcha::setConfigValue('site_key', $data['site_key']);
ReCaptcha::setConfigValue('secret_key', $data['secret_key']);
ReCaptcha::setConfigValue('captcha_style', $data['captcha_style']);
} catch (\Exception $e) {
$this->setupFormErrorContext(
Translator::getInstance()->trans(
"Error",
[],
ReCaptcha::DOMAIN_NAME
),
$e->getMessage(),
$form
);
return $this->viewAction();
}
return $this->generateSuccessRedirect($form);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace ReCaptcha\Event;
use Thelia\Core\Event\ActionEvent;
class ReCaptchaCheckEvent extends ActionEvent
{
protected $captchaResponse = null;
protected $remoteIp = null;
/** @var boolean */
protected $human = false;
public function __construct($captchaResponse = null, $remoteIp = null)
{
if (null !== $captchaResponse) {
$this->captchaResponse = $captchaResponse;
}
if (null !== $remoteIp) {
$this->remoteIp = $remoteIp;
}
}
/**
* @return null
*/
public function getCaptchaResponse()
{
return $this->captchaResponse;
}
/**
* @param null $captchaResponse
* @return ReCaptchaCheckEvent
*/
public function setCaptchaResponse($captchaResponse)
{
$this->captchaResponse = $captchaResponse;
return $this;
}
/**
* @return null
*/
public function getRemoteIp()
{
return $this->remoteIp;
}
/**
* @param null $remoteIp
* @return ReCaptchaCheckEvent
*/
public function setRemoteIp($remoteIp)
{
$this->remoteIp = $remoteIp;
return $this;
}
/**
* @return bool
*/
public function isHuman()
{
return $this->human;
}
/**
* @param bool $human
* @return ReCaptchaCheckEvent
*/
public function setHuman($human)
{
$this->human = $human;
return $this;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace ReCaptcha\Event;
class ReCaptchaEvents
{
const CHECK_CAPTCHA_EVENT = "check_captcha_event";
}

View File

@@ -0,0 +1,55 @@
<?php
namespace ReCaptcha\Form;
use ReCaptcha\ReCaptcha;
use Thelia\Core\Translation\Translator;
use Thelia\Form\BaseForm;
class ConfigurationForm extends BaseForm
{
protected function buildForm()
{
$this->formBuilder
->add(
"site_key",
"text",
[
"data" => ReCaptcha::getConfigValue("site_key"),
"label"=>Translator::getInstance()->trans("Site key", array(), ReCaptcha::DOMAIN_NAME),
"label_attr" => ["for" => "site_key"],
"required" => true
]
)
->add(
"secret_key",
"text",
[
"data" => ReCaptcha::getConfigValue("secret_key"),
"label"=>Translator::getInstance()->trans("Secret key", array(), ReCaptcha::DOMAIN_NAME),
"label_attr" => ["for" => "secret_key"],
"required" => true
]
)
->add(
"captcha_style",
"choice",
[
"data" => ReCaptcha::getConfigValue("captcha_style"),
"label"=>Translator::getInstance()->trans("ReCaptcha style", array(), ReCaptcha::DOMAIN_NAME),
"label_attr" => ["for" => "captcha_style"],
"required" => true,
'choices' => [
'normal'=>'Normal',
'compact'=>'Compact',
'invisible'=>'Invisible'
]
]
);
}
public function getName()
{
return "recaptcha_configuration_form";
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace ReCaptcha\Hook;
use ReCaptcha\ReCaptcha;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
class FrontHook extends BaseHook
{
public function addRecaptchaCheck(HookRenderEvent $event)
{
$siteKey = ReCaptcha::getConfigValue('site_key');
$captchaStyle = ReCaptcha::getConfigValue('captcha_style');
$captchaId= "recaptcha";
$captchaCallback = "";
if ($captchaStyle === 'invisible') {
$captchaCallback = "data-callback='onCompleted'";
$captchaId = $captchaId.'-invisible';
}
$event->add("<div id='$captchaId' class='g-recaptcha' data-sitekey='$siteKey' $captchaCallback data-size='$captchaStyle'></div><input type='hidden' name='captcha' value='1'>");
}
}

View File

@@ -0,0 +1,8 @@
<?php
return array(
'ReCaptcha configuration' => 'ReCaptcha configuration',
'ReCaptcha module configuration' => 'ReCaptcha module configuration',
'These infos are available here : ' => 'Ces infos sont disponibles ici : ',
'reCAPTCHA access :' => 'reCAPTCHA accés :',
);

View File

@@ -0,0 +1,4 @@
<?php
return array(
// 'an english string' => 'The displayed english string',
);

View File

@@ -0,0 +1,7 @@
<?php
return array(
'Error' => 'Erreur',
'Secret key' => 'Clé secrète',
'Site key' => 'Clé du site',
);

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 OpenStudio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,55 @@
<?php
/*************************************************************************************/
/* This file is part of the Thelia package. */
/* */
/* Copyright (c) OpenStudio */
/* email : dev@thelia.net */
/* web : http://www.thelia.net */
/* */
/* For the full copyright and license information, please view the LICENSE.txt */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace ReCaptcha;
use Thelia\Core\Template\TemplateDefinition;
use Thelia\Module\BaseModule;
class ReCaptcha extends BaseModule
{
/** @var string */
const DOMAIN_NAME = 'recaptcha';
/*
* You may now override BaseModuleInterface methods, such as:
* install, destroy, preActivation, postActivation, preDeactivation, postDeactivation
*
* Have fun !
*/
public function getHooks()
{
return [
[
"type" => TemplateDefinition::FRONT_OFFICE,
"code" => "recaptcha.js",
"title" => [
"en_US" => "reCaptcha js",
"fr_FR" => "Js pour recaptcha",
],
"block" => false,
"active" => true,
],
[
"type" => TemplateDefinition::FRONT_OFFICE,
"code" => "recaptcha.check",
"title" => [
"en_US" => "reCaptcha check hook",
"fr_FR" => "reCaptcha check hook",
],
"block" => false,
"active" => true,
],
];
}
}

View File

@@ -0,0 +1,62 @@
# Re Captcha
This module allow you to add easily a reCAPTCHA to your form
## Installation
### Composer
Add it in your main thelia composer.json file
```
composer require thelia/re-captcha-module:~2.0.0
```
## Usage
Before using this module you have to create google api key here http://www.google.com/recaptcha/admin
next configure your reCAPTCHA access here http://your_site.com`/admin/module/ReCaptcha` with keys you obtained in Google's page
and choose which style of captcha you want :
- A standard captcha (or a compact version of this one)
![Checkbox captcha](https://developers.google.com/recaptcha/images/newCaptchaAnchor.gif)
- An invisible captcha
![Invisible captcha](https://developers.google.com/recaptcha/images/invisible_badge.png)
Then you'll need help from a developer to add some hooks in template and dispatch the check events, see details below.
### Hook
First if you don't have `{hook name="main.head-top"}` hook in your template you have to put this hook `{hook name="recaptcha.js"}` in the top of your head
Then add this hook `{hook name="recaptcha.check"}` in every form where you want to check if the user is human,
be careful if you want to use the invisible captcha this hook must be placed directly in the form tag like this :
```
<form id="form-contact" action="{url path="/contact"}" method="post">
{hook name="recaptcha.check"}
// End of the form
</form>
```
### Event
To check in server-side if the captcha is valid you have to dispatch the "CHECK_CAPTCHA_EVENT" like this :
```
$checkCaptchaEvent = new ReCaptchaCheckEvent();
$this->dispatch(ReCaptchaEvents::CHECK_CAPTCHA_EVENT, $checkCaptchaEvent);
```
Then the result of check is available in `$checkCaptchaEvent->isHuman()`as boolean so you can do a test like this :
```
if ($checkCaptchaEvent->isHuman() == false) {
throw new \Exception('Invalid captcha');
}
```
Don't forget to add this use at the top of your class :
```
use ReCaptcha\Event\ReCaptchaCheckEvent;
use ReCaptcha\Event\ReCaptchaEvents;
```

View File

@@ -0,0 +1,11 @@
{
"name": "thelia/re-captcha-module",
"license": "LGPL-3.0+",
"type": "thelia-module",
"require": {
"thelia/installer": "~1.1"
},
"extra": {
"installer-name": "ReCaptcha"
}
}

View File

@@ -0,0 +1,69 @@
{extends file="admin-layout.tpl"}
{block name="after-bootstrap-css"}
<style>
.recaptcha_radio {
cursor: pointer;
}
</style>
{/block}
{block name="no-return-functions"}
{$admin_current_location = 'module'}
{/block}
{block name="page-title"}{intl l='ReCaptcha module configuration' d='recaptcha.bo.default'}{/block}
{block name="check-resource"}admin.module{/block}
{block name="check-access"}view{/block}
{block name="check-module"}ReCaptcha{/block}
{block name="main-content"}
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
{intl l="ReCaptcha configuration" d='recaptcha.bo.default'}
</h4>
</div>
{form name="recaptcha_configuration.form"}
<form action="{url path="/admin/module/recaptcha/configuration"}" method="post">
{form_hidden_fields form=$form}
{if $form_error}
<div class="alert alert-danger">{$form_error_message}</div>
{/if}
{form_field form=$form field='success_url'}
<input type="hidden" name="{$name}" value="{url path={navigate to="current"}}"/>
{/form_field}
<div class="panel-body">
<div class="row">
<h3 class="col-md-12">{intl l="reCAPTCHA access :" d='recaptcha.bo.default'}</h3>
<div class="col-md-6">
{render_form_field form=$form field="site_key" value={$data}}
</div>
<div class="col-md-6">
{render_form_field form=$form field="secret_key" value={$data}}
</div>
<div class="col-md-12">
{render_form_field form=$form field="captcha_style" value={$data}}
</div>
<div class="col-md-12">
<p>{intl l="These infos are available here : " d='recaptcha.bo.default'}<a href="http://www.google.com/recaptcha/admin">http://www.google.com/recaptcha/admin</a></p>
</div>
<div class="col-md-12">
<input type="submit" class="btn btn-success form-control">
</div>
</div>
</div>
</form>
{/form}
</div>
</div>
{/block}
{block name="javascript-initialization"}
{/block}

View File

@@ -0,0 +1,22 @@
<script src="https://www.google.com/recaptcha/api.js?hl={lang attr="code"}" async defer></script>
<script>
window.onload = function() {
var captchaDiv = document.getElementById("recaptcha-invisible");
if (captchaDiv !== null) {
var form = captchaDiv.parentElement;
form.addEventListener("submit", function(event) {
if (!grecaptcha.getResponse()) {
event.preventDefault(); //prevent form submit
grecaptcha.execute();
}
});
onCompleted = function() {
if (form.reportValidity() !== false) {
form.submit();
}
}
}
}
</script>