Skip to content

Commit

Permalink
Friendly captcha (#455)
Browse files Browse the repository at this point in the history
* add friendly captcha form type [up-port]
* p11 adjustments
  • Loading branch information
solverat committed Jun 28, 2024
1 parent 13674d9 commit 88c6cdc
Show file tree
Hide file tree
Showing 24 changed files with 433 additions and 5 deletions.
3 changes: 3 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Upgrade Notes

## 5.1.0
- **[SECURITY FEATURE]** Add [friendly captcha field](/docs/03_SpamProtection.md#friendly-captcha)

## 5.0.7
- Remove `editable_root` restriction from mail editor
Expand Down
4 changes: 4 additions & 0 deletions config/install/translations/admin.csv
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"form_builder_type.time_type","Time Type","Uhrzeit Element"
"form_builder_type.birthday_type","Birthday Type","Geburtstag Element"
"form_builder_type.recaptcha_v3","reCAPTCHA v3 Field","reCAPTCHA v3 Feld"
"form_builder_type.friendly_captcha","Friendly Captcha Field","Friendly Captcha Feld"
"form_builder_type_tab.default","Default","Standard"
"form_builder_type_display_group.base","Base","Base"
"form_builder_type_display_group.hook","Hook","Hook"
Expand Down Expand Up @@ -108,6 +109,9 @@
"form_builder_type_field.date_with_seconds","With Seconds","Sekunden darstellen"
"form_builder_type_field.date_format","Format","Format"
"form_builder_type_field.recaptcha_v3.action_name","Action","Aktion"
"form_builder_type_field.friendly_captcha.start","Start","Start"
"form_builder_type_field.friendly_captcha.darkmode","Darkmode","Darkmode"
"form_builder_type_field.friendly_captcha.callback","Callback","Callback"
"form_builder_type_template.default","Default","Default"
"form_builder_form_template.bootstrap_3_horizontal_layout","Bootstrap 3 Horizontal Layout","Bootstrap 3 Horizontales Layout"
"form_builder_form_template.bootstrap_3_layout","Bootstrap 3 Layout","Bootstrap 3 Layout"
Expand Down
8 changes: 8 additions & 0 deletions config/services/forms/forms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ services:
tags:
- { name: form.type }

FormBuilderBundle\Form\Type\FriendlyCaptchaType:
public: false
arguments:
- '@request_stack'
- '@FormBuilderBundle\Configuration\Configuration'
tags:
- { name: form.type }

FormBuilderBundle\Form\Type\DynamicFormType:
public: false
arguments:
Expand Down
4 changes: 4 additions & 0 deletions config/services/system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ services:
FormBuilderBundle\Tool\ReCaptchaProcessorInterface: '@FormBuilderBundle\Tool\ReCaptchaProcessor'
FormBuilderBundle\Tool\ReCaptchaProcessor: ~

# tool: friendly captcha processor
FormBuilderBundle\Tool\FriendlyCaptchaProcessorInterface: '@FormBuilderBundle\Tool\FriendlyCaptchaProcessor'
FormBuilderBundle\Tool\FriendlyCaptchaProcessor: ~

# configuration
FormBuilderBundle\Configuration\Configuration: ~

Expand Down
5 changes: 5 additions & 0 deletions config/services/validator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ services:
- { name: validator.constraint_validator }

FormBuilderBundle\Validator\Constraints\Recaptcha3Validator:
public: false
tags:
- { name: validator.constraint_validator }

FormBuilderBundle\Validator\Constraints\FriendlyCaptchaValidator:
public: false
tags:
- { name: validator.constraint_validator }
35 changes: 35 additions & 0 deletions config/types/type/friendly_captcha.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
form_builder:
types:
friendly_captcha:
class: FormBuilderBundle\Form\Type\FriendlyCaptchaType
backend:
form_type_group: security_fields
label: 'form_builder_type.friendly_captcha'
icon_class: 'form_builder_icon_friendly_captcha'
constraints: false
fields:
optional.email_label: ~
options.help_text: ~
options.data: ~
options.value: ~
options.start:
display_group_id: attributes
type: select
label: 'form_builder_type_field.friendly_captcha.start'
config:
options:
- ['Focus', 'focus']
- ['Auto', 'auto']
- ['None', 'none']
options.darkmode:
display_group_id: attributes
type: checkbox
label: 'form_builder_type_field.friendly_captcha.darkmode'
config:
default_value: null
options.callback:
display_group_id: attributes
type: textfield
label: 'form_builder_type_field.friendly_captcha.callback'
config:
default_value: null
20 changes: 20 additions & 0 deletions docs/03_SpamProtection.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,24 @@ form_builder:

3. Add the reCAPTCHA field to your form
4. Enable the reCAPTCHA [javascript module](./91_Javascript.md)
4. Done

## Friendly Captcha
Friendly Captcha is a system for preventing spam on your website.
You can add the Friendly Captcha widget to your form to fight spam, with little impact to the user experience.

1. Set your application: https://docs.friendlycaptcha.com/#/installation?id=_1-generating-a-sitekey
2. Add site and secret key to your formbuilder settings:

```yaml
form_builder:
spam_protection:
friendly_captcha:
secret_key: 'YOUR_SECRET_KEY'
site_key: 'YOUR_SITE_KEY'
eu_only: false # see https://docs.friendlycaptcha.com/#/eu_endpoint (enterprise only)
```

3. Add the "Friendly Captcha" field to your form
4. Enable the FriendlyCaptcha [javascript module](./91_Javascript.md)
4. Done
4 changes: 4 additions & 0 deletions public/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@
background: url(/bundles/formbuilder/img/recaptcha_v3.svg) left center no-repeat !important;
}

.form_builder_icon_friendly_captcha {
background: url(/bundles/formbuilder/img/friendly_captcha.svg) left center no-repeat !important;
}

/*
Conditional Logic
*/
Expand Down
6 changes: 6 additions & 0 deletions public/img/friendly_captcha.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,28 @@ private function buildSpamProductionNode(): NodeDefinition
->scalarNode('secret_key')->defaultNull()->end()
->end()
->end()
->arrayNode('friendly_captcha')
->addDefaultsIfNotSet()
->children()
->scalarNode('site_key')->defaultNull()->end()
->scalarNode('secret_key')->defaultNull()->end()
->scalarNode('eu_only')->defaultFalse()->end()
->arrayNode('puzzle')
->addDefaultsIfNotSet()
->children()
->scalarNode('global_endpoint')->defaultValue('https://api.friendlycaptcha.com/api/v1/puzzle')->end()
->scalarNode('eu_endpoint')->defaultValue('https://eu-api.friendlycaptcha.eu/api/v1/puzzle')->end()
->end()
->end()
->arrayNode('verification')
->addDefaultsIfNotSet()
->children()
->scalarNode('global_endpoint')->defaultValue('https://api.friendlycaptcha.com/api/v1/siteverify')->end()
->scalarNode('eu_endpoint')->defaultValue('https://eu-api.friendlycaptcha.eu/api/v1/siteverify')->end()
->end()
->end()
->end()
->end()
->end();

return $rootNode;
Expand Down
72 changes: 72 additions & 0 deletions src/Form/Type/FriendlyCaptchaType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace FormBuilderBundle\Form\Type;

use FormBuilderBundle\Configuration\Configuration;
use FormBuilderBundle\Validator\Constraints\FriendlyCaptcha;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class FriendlyCaptchaType extends AbstractType
{
protected Configuration $configuration;
protected RequestStack $requestStack;

public function __construct(RequestStack $requestStack, Configuration $configuration)
{
$this->requestStack = $requestStack;
$this->configuration = $configuration;
}

public function buildView(FormView $view, FormInterface $form, array $options): void
{
$config = $this->configuration->getConfig('spam_protection');
$friendlyCaptchaConfig = $config['friendly_captcha'];

$locale = $options['lang'] ?? null;
if ($locale === null) {
$locale = $this->requestStack->getCurrentRequest()?->getLocale() ?? 'en';
}

$friendlyCaptchaDataAttributes = array_filter([
'sitekey' => $friendlyCaptchaConfig['site_key'],
'lang' => str_contains($locale, '_') ? explode('_', $locale)[0] : $locale,
'start' => $options['start'] ?? 'focus',
'callback' => $options['callback'] ?? null,
'puzzle-endpoint' => $friendlyCaptchaConfig['eu_only'] === true
? $friendlyCaptchaConfig['puzzle']['eu_endpoint']
: $friendlyCaptchaConfig['puzzle']['global_endpoint'],
]);

$view->vars['friendly_captcha_attributes'] = $friendlyCaptchaDataAttributes;
$view->vars['darkmode'] = $options['darkmode'] ?? false;
}

public function getParent(): string
{
return HiddenType::class;
}

public function getBlockPrefix(): string
{
return 'form_builder_friendly_captcha_type';
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'lang' => null,
'start' => 'focus',
'callback' => null,
'mapped' => false,
'darkmode' => false,
'constraints' => [new FriendlyCaptcha()],
]);

$resolver->setAllowedValues('start', ['auto', 'focus', 'none']);
}
}
31 changes: 31 additions & 0 deletions src/Migrations/Version20240628143429.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace FormBuilderBundle\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use FormBuilderBundle\Tool\Install;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

final class Version20240628143429 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;

public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
$installer = $this->container->get(Install::class);
$installer->updateTranslations();
}

public function down(Schema $schema): void
{
}
}
59 changes: 59 additions & 0 deletions src/Tool/FriendlyCaptcha/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace FormBuilderBundle\Tool\FriendlyCaptcha;

class Response
{
private bool $success;
private ?string $details;
private ?array $errors;

public static function fromJson(string $json): Response
{
$responseData = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

if (!$responseData) {
return new self(false, 'invalid-json', null);
}

$success = $responseData['success'] ?? false;
$details = $responseData['details'] ?? null;
$errors = $responseData['errors'] ?? null;

return new self($success, $details, $errors);
}

public function __construct(
bool $success,
?string $details,
?array $errors
) {
$this->success = $success;
$this->details = $details;
$this->errors = $errors;
}

public function isSuccess(): bool
{
return $this->success;
}

public function getDetails(): ?string
{
return $this->details;
}

public function getErrors(): ?array
{
return $this->errors;
}

public function toArray(): array
{
return [
'success' => $this->isSuccess(),
'details' => $this->getDetails(),
'errors' => $this->getErrors(),
];
}
}
42 changes: 42 additions & 0 deletions src/Tool/FriendlyCaptchaProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace FormBuilderBundle\Tool;

use FormBuilderBundle\Configuration\Configuration;
use FormBuilderBundle\Tool\FriendlyCaptcha\Response;
use GuzzleHttp\Client;
use Symfony\Component\HttpFoundation\RequestStack;

class FriendlyCaptchaProcessor implements FriendlyCaptchaProcessorInterface
{

public function __construct(
protected Configuration $configuration,
protected RequestStack $requestStack
) {
}

public function verify(mixed $value): Response
{
$client = new Client();
$config = $this->configuration->getConfig('spam_protection');
$friendlyCaptchaConfig = $config['friendly_captcha'];

$verificationEndpoint = $friendlyCaptchaConfig['eu_only'] === true
? $friendlyCaptchaConfig['verification']['eu_endpoint']
: $friendlyCaptchaConfig['verification']['global_endpoint'];

$response = $client->post(
$verificationEndpoint,
[
'form_params' => [
'secret' => $friendlyCaptchaConfig['secret_key'],
'sitekey' => $friendlyCaptchaConfig['site_key'],
'solution' => $value,
],
]
);

return Response::fromJson($response->getBody());
}
}
10 changes: 10 additions & 0 deletions src/Tool/FriendlyCaptchaProcessorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace FormBuilderBundle\Tool;

use FormBuilderBundle\Tool\FriendlyCaptcha\Response;

interface FriendlyCaptchaProcessorInterface
{
public function verify(mixed $value): Response;
}
Loading

0 comments on commit 88c6cdc

Please sign in to comment.