Skip to content

Commit 5f7c67b

Browse files
authored
add math captcha type (#514)
1 parent e340c3c commit 5f7c67b

23 files changed

+484
-1
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
## Installation
2222

2323
```bash
24-
composer require "dachcom-digital/formbuilder":"~5.2.0"
24+
composer require "dachcom-digital/formbuilder":"~5.3.0"
2525
```
2626

2727
Add Bundle to `bundles.php`:
@@ -56,6 +56,7 @@ Nothing to tell here, it's just [Symfony](https://symfony.com/doc/current/templa
5656
- [FriendlyCaptcha](docs/03_SpamProtection.md#friendly-captcha)
5757
- [Honeypot](docs/03_SpamProtection.md#honeypot)
5858
- [ReCaptcha V3](docs/03_SpamProtection.md#recaptcha-v3)
59+
- [Math Captcha](docs/03_SpamProtection.md#math-captcha)
5960
- [Email Checker](docs/03_SpamProtection.md#email-checker)
6061
- [Double-Opt-In Feature](docs/04_DoubleOptIn.md)
6162
- [Output Workflows](docs/OutputWorkflow/0_Usage.md)

UPGRADE.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 5.3.0
44
- **[IMPROVEMENT]** New field added to `form_builder_form_attributes`: `autocomplete`
55
- **[BUGFIX]** Solidify check for empty value in output transformer [#486](https://github.com/dachcom-digital/pimcore-formbuilder/issues/508)
6+
- **[SECURITY FEATURE]** Math CAPTCHA Field [#511](https://github.com/dachcom-digital/pimcore-formbuilder/issues/511), read more about it [here](./docs/03_SpamProtection.md#math-captcha)
67

78
## 5.2.0
89
- **[LICENSE]** Dual-License with GPL and Dachcom Commercial License (DCL) added

config/install/translations/admin.csv

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
"form_builder_type.time_type","Time Type","Uhrzeit Element"
6363
"form_builder_type.birthday_type","Birthday Type","Geburtstag Element"
6464
"form_builder_type.recaptcha_v3","reCAPTCHA v3 Field","reCAPTCHA v3 Feld"
65+
"form_builder_type.math_captcha","Math Captcha Field","Mathematisches Captcha Feld"
66+
"form_builder_type_field.math_captcha.validation_message_trans_note","Validation messages will be translated via pimcore translation manager: 'The given answer is not correct', 'Captcha has expired due to inactivity. Please refresh the page and try again.'","Validierungsmeldungen werden mit dem Pimcore Translation-Manager übersetzt: 'The given answer is not correct', 'Captcha has expired due to inactivity. Please refresh the page and try again.'"
67+
"form_builder_type_field.math_captcha.difficulty","Difficulty","Schwierigkeit"
6568
"form_builder_type.friendly_captcha","Friendly Captcha Field","Friendly Captcha Feld"
6669
"form_builder_type.cloudflare_turnstile","Cloudflare Turnstile Field","Cloudflare Turnstile Feld"
6770
"form_builder_type_tab.default","Default","Standard"

config/services/forms/forms.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ services:
2222
tags:
2323
- { name: form.type }
2424

25+
FormBuilderBundle\Form\Type\MathCaptchaType:
26+
public: false
27+
arguments:
28+
- '@FormBuilderBundle\Tool\MathCaptchaProcessor'
29+
tags:
30+
- { name: form.type }
31+
2532
FormBuilderBundle\Form\Type\DynamicFormType:
2633
public: false
2734
arguments:

config/services/system.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ services:
3131
FormBuilderBundle\Tool\CloudflareTurnstileProcessorInterface: '@FormBuilderBundle\Tool\CloudflareTurnstileProcessor'
3232
FormBuilderBundle\Tool\CloudflareTurnstileProcessor: ~
3333

34+
# tool: math captcha processor
35+
FormBuilderBundle\Tool\MathCaptchaProcessorInterface: '@FormBuilderBundle\Tool\MathCaptchaProcessor'
36+
FormBuilderBundle\Tool\MathCaptchaProcessor:
37+
arguments:
38+
- '%pimcore.encryption.secret%'
39+
- '@FormBuilderBundle\Configuration\Configuration'
40+
3441
# configuration
3542
FormBuilderBundle\Configuration\Configuration: ~
3643

config/services/validator.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ services:
1414
tags:
1515
- { name: validator.constraint_validator }
1616

17+
FormBuilderBundle\Validator\Constraints\MathCaptchaValidator:
18+
public: false
19+
tags:
20+
- { name: validator.constraint_validator }
21+
1722
FormBuilderBundle\Validator\Constraints\CloudflareTurnstileValidator:
1823
public: false
1924
tags:

config/types/type/math_captcha.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
form_builder:
2+
types:
3+
math_captcha:
4+
class: FormBuilderBundle\Form\Type\MathCaptchaType
5+
backend:
6+
form_type_group: security_fields
7+
label: 'form_builder_type.math_captcha'
8+
icon_class: 'form_builder_icon_math_captcha'
9+
constraints: false
10+
fields:
11+
optional.email_label: ~
12+
options.help_text: ~
13+
options.data: ~
14+
options.value: ~
15+
options.difficulty:
16+
display_group_id: base
17+
type: select
18+
label: 'form_builder_type_field.math_captcha.difficulty'
19+
config:
20+
options:
21+
- ['easy','easy']
22+
- ['normal','normal']
23+
- ['hard','hard']
24+
options.validation_message_trans_note:
25+
display_group_id: base
26+
type: label
27+
label: 'form_builder_type_field.math_captcha.validation_message_trans_note'

docs/03_SpamProtection.md

+43
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ form_builder:
6060
4. Enable the FriendlyCaptcha [javascript module](./91_Javascript.md)
6161
4. Done
6262
63+
***
64+
6365
## Cloudflare Turnstile
6466
Turnstile delivers frustration-free, CAPTCHA-free web experiences to website visitors - with just a simple snippet of free code.
6567
Moreover, Turnstile stops abuse and confirms visitors are real without the data privacy concerns or awful user experience of CAPTCHAs.
@@ -80,6 +82,47 @@ form_builder:
8082
4. Enable the CloudFlareTurnstile [javascript module](./91_Javascript.md)
8183
4. Done
8284

85+
***
86+
87+
## Math Captcha
88+
This simple math captcha form provides a lightweight and session-free way to prevent automated submissions.
89+
It offers three difficulty levels, allowing customization based on security needs.
90+
The captcha generates random math problems that users must solve before submitting the form.
91+
Since no session storage is required, it can be easily integrated with minimal overhead.
92+
93+
> [!CAUTION]
94+
> The Math captcha doesn't solve the problem of figuring out that a user has previously solved the captcha.
95+
> It only protects individual requests and might help if you're in the middle of a spam attack.
96+
> However, it will give you time, to set up stronger spam protection like
97+
> recaptcha, turnstile or friendly captcha (which are all supported by this bundle) to get rid of smart bots!
98+
99+
### Encryption Secret
100+
101+
> [!IMPORTANT]
102+
> Math Captcha requires a valid encryption key!
103+
> It uses the `%pimcore.encryption.secret%` as default, but you're able to set a dedicated one:
104+
105+
```yaml
106+
form_builder:
107+
spam_protection:
108+
math_captcha:
109+
encryption_secret: 'my-very-long-encryption-secret'
110+
```
111+
112+
### Math Captcha TTL | Expiration
113+
To prevent replay attacks in a long-term view, a math captcha gets invalided after 30 minutes.
114+
115+
If you want to change the value, you need to update the configuration
116+
117+
```yaml
118+
form_builder:
119+
spam_protection:
120+
math_captcha:
121+
hash_ttl: 30 # 30 minutes (default value)
122+
```
123+
124+
***
125+
83126
## Email Checker
84127
The Email Checker Validator is available, if you've added at least one service. Per default, no service is registered by default.
85128

public/css/admin.css

+4
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@
202202
background: url(/bundles/formbuilder/img/friendly_captcha.svg) left center no-repeat !important;
203203
}
204204

205+
.form_builder_icon_math_captcha {
206+
background: url(/bundles/formbuilder/img/math_captcha.svg) left center no-repeat !important;
207+
}
208+
205209
/*
206210
Conditional Logic
207211
*/

public/img/math_captcha.svg

+14
Loading

src/DependencyInjection/Configuration.php

+8
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,14 @@ private function buildSpamProductionNode(): NodeDefinition
662662
->end()
663663
->end()
664664

665+
->arrayNode('math_captcha')
666+
->addDefaultsIfNotSet()
667+
->children()
668+
->scalarNode('encryption_secret')->defaultNull()->end()
669+
->integerNode('hash_ttl')->defaultValue(30)->end()
670+
->end()
671+
->end()
672+
665673
->arrayNode('email_checker')
666674
->addDefaultsIfNotSet()
667675
->children()

src/Form/Type/MathCaptchaType.php

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This source file is available under two different licenses:
5+
* - GNU General Public License version 3 (GPLv3)
6+
* - DACHCOM Commercial License (DCL)
7+
* Full copyright and license information is available in
8+
* LICENSE.md which is distributed with this source code.
9+
*
10+
* @copyright Copyright (c) DACHCOM.DIGITAL AG (https://www.dachcom-digital.com)
11+
* @license GPLv3 and DCL
12+
*/
13+
14+
namespace FormBuilderBundle\Form\Type;
15+
16+
use FormBuilderBundle\Tool\MathCaptchaProcessor;
17+
use FormBuilderBundle\Validator\Constraints\MathCaptcha;
18+
use Symfony\Component\Form\AbstractType;
19+
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
20+
use Symfony\Component\Form\Extension\Core\Type\TextType;
21+
use Symfony\Component\Form\FormBuilderInterface;
22+
use Symfony\Component\Form\FormInterface;
23+
use Symfony\Component\Form\FormView;
24+
use Symfony\Component\OptionsResolver\OptionsResolver;
25+
26+
final class MathCaptchaType extends AbstractType
27+
{
28+
public function __construct(protected MathCaptchaProcessor $mathCaptchaProcessor)
29+
{
30+
}
31+
32+
public function buildForm(FormBuilderInterface $builder, array $options): void
33+
{
34+
$stamp = $this->mathCaptchaProcessor->generateStamp();
35+
$challenge = $this->mathCaptchaProcessor->generateChallenge($options['difficulty'], $stamp);
36+
37+
$challengeFieldOptions = [
38+
'label' => $challenge['user_challenge'],
39+
'label_attr' => [
40+
'class' => 'math-captcha-challenge-label'
41+
],
42+
];
43+
44+
if ($challenge['hash'] === null) {
45+
$challengeFieldOptions['attr']['disabled'] = true;
46+
$challengeFieldOptions['label'] = 'No encryption secret found. cannot create challenge.';
47+
}
48+
49+
$builder->add('challenge', TextType::class, $challengeFieldOptions);
50+
$builder->add('hash', HiddenType::class, ['data' => $challenge['hash']]);
51+
$builder->add('stamp', HiddenType::class, ['data' => $stamp]);
52+
}
53+
54+
public function buildView(FormView $view, FormInterface $form, array $options): void
55+
{
56+
$view->vars['attr']['class'] = 'math-captcha';
57+
}
58+
59+
public function getBlockPrefix(): string
60+
{
61+
return 'form_builder_math_captcha_type';
62+
}
63+
64+
public function configureOptions(OptionsResolver $resolver): void
65+
{
66+
$resolver->setDefaults([
67+
'mapped' => false,
68+
'difficulty' => 'easy',
69+
'constraints' => [new MathCaptcha()],
70+
]);
71+
}
72+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This source file is available under two different licenses:
7+
* - GNU General Public License version 3 (GPLv3)
8+
* - DACHCOM Commercial License (DCL)
9+
* Full copyright and license information is available in
10+
* LICENSE.md which is distributed with this source code.
11+
*
12+
* @copyright Copyright (c) DACHCOM.DIGITAL AG (https://www.dachcom-digital.com)
13+
* @license GPLv3 and DCL
14+
*/
15+
16+
namespace FormBuilderBundle\Migrations;
17+
18+
use Doctrine\DBAL\Schema\Schema;
19+
use Doctrine\Migrations\AbstractMigration;
20+
use FormBuilderBundle\Tool\Install;
21+
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
22+
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
23+
24+
final class Version20250306140903 extends AbstractMigration implements ContainerAwareInterface
25+
{
26+
use ContainerAwareTrait;
27+
28+
public function getDescription(): string
29+
{
30+
return '';
31+
}
32+
33+
public function up(Schema $schema): void
34+
{
35+
$installer = $this->container->get(Install::class);
36+
$installer->updateTranslations();
37+
}
38+
39+
public function down(Schema $schema): void
40+
{
41+
}
42+
}

0 commit comments

Comments
 (0)