Skip to content

Commit 0fa2e5d

Browse files
authored
Supporting different captcha providers and hCaptcha support (#9)
Supporting different captcha providers hCaptcha provider --------- Co-authored-by: Aleks S <[email protected]>
1 parent 8185346 commit 0fa2e5d

15 files changed

+292
-84
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
0.2.2
2+
==
3+
26 Frb 2024
4+
5+
**Added:**
6+
7+
* Supporting different captcha providers
8+
* hCaptcha provider
9+
10+
**Migration guide:**
11+
* Create and run BD migrations to apply new changes (see on https://phpform.dev)
12+
* Check forms and update captcha settings if needed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ developers and businesses looking for a reliable and GDPR-compliant form managem
1313
- **Cost-Effective**: Designed to run smoothly on inexpensive hosting or free cloud services, reducing your operational costs.
1414
- **GDPR Compliant**: We prioritize your data privacy. PHPForm ensures that all your data remains yours, complying fully with GDPR regulations.
1515
- **Browser Push and Email notifications**: Get notified when a new form submission is received.
16-
- **reCaptcha Protection**: Protect your forms from spam and abuse with Google reCaptcha.
16+
- **Captcha Protection**: Protect your forms from spam and abuse with different Captcha providers.
1717
- **Token-based Protection**: Ideal protection for mobile and desktop apps.
1818

1919
# Requirements
@@ -49,7 +49,7 @@ PHPForm is released under MIT, ensuring it remains free and open for use and mod
4949
## Running locally with docker
5050
Use the latest [image from docker hub](https://hub.docker.com/r/phpform/phpform-server) to run it locally:
5151
```bash
52-
docker run --name phpform -d -p 9000:9000 phpform/phpform-server:0.2
52+
docker run --name phpform -d -p 9000:9000 phpform/phpform-server:latest
5353
```
5454
Copy environment file and adjust it to your needs:
5555
```bash

src/Admin/Controller/FormController.php

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
<?php
22
namespace App\Admin\Controller;
33

4-
use App\Admin\Form\FormRecaptchaType;
4+
use App\Admin\Form\FormCaptchaType;
55
use App\Admin\Form\FormSecretType;
6+
use App\Captcha\Captcha;
7+
use App\Captcha\CaptchaProviderInterface;
68
use App\Entity\Form;
79
use App\Admin\Form\FormType;
810
use App\Service\FormMenuCounterService;
@@ -20,6 +22,7 @@ public function __construct(
2022
private readonly FormService $formService,
2123
private readonly FormMenuCounterService $formMenuCounterService,
2224
private readonly FormSubmissionService $formSubmissionService,
25+
private readonly Captcha $captcha,
2326
)
2427
{
2528
}
@@ -75,24 +78,31 @@ public function edit(Request $request, Form $formEntity): Response
7578
]);
7679
}
7780

78-
#[Route('/admin/forms/{id}/recaptcha', name: 'admin_forms_recaptcha')]
79-
public function recaptcha(Request $request, Form $formEntity): Response
81+
#[Route('/admin/forms/{id}/captcha', name: 'admin_forms_captcha')]
82+
public function captcha(Request $request, Form $formEntity): Response
8083
{
81-
$form = $this->createForm(FormRecaptchaType::class, $formEntity);
84+
$form = $this->createForm(FormCaptchaType::class, $formEntity);
8285
$form->handleRequest($request);
8386

8487
if ($form->isSubmitted() && $form->isValid()) {
8588
$this->formService->edit($formEntity);
8689

87-
$this->addFlash('primary', 'Recaptcha Secret Key updated successfully');
90+
$this->addFlash('primary', 'Captcha Secret Key updated successfully');
8891

89-
return $this->redirectToRoute('admin_forms_recaptcha', ['id' => $formEntity->getId()]);
92+
return $this->redirectToRoute('admin_forms_captcha', ['id' => $formEntity->getId()]);
9093
}
9194

92-
return $this->render('@Admin/forms/recaptcha.html.twig', [
95+
return $this->render('@Admin/forms/captcha.html.twig', [
9396
'form' => $form->createView(),
9497
'formEntity' => $formEntity,
9598
'menuCounts' => $this->formMenuCounterService->getAllCountsByFormId($formEntity->getId()),
99+
'providersInfo' => array_map(static function(CaptchaProviderInterface $provider) {
100+
return [
101+
'name' => $provider->getName(),
102+
'homepageUrl' => $provider->getHomePageUrl(),
103+
'documentationUrl' => $provider->getDocumentationUrl(),
104+
];
105+
}, $this->captcha->getProviders())
96106
]);
97107
}
98108

src/Admin/Form/FormRecaptchaType.php renamed to src/Admin/Form/FormCaptchaType.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
<?php
22
namespace App\Admin\Form;
33

4+
use App\Captcha\Captcha;
5+
use App\Captcha\CaptchaProviderInterface;
46
use App\Entity\Form;
57
use Symfony\Component\Form\AbstractType;
68
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
79
use Symfony\Component\Form\FormBuilderInterface;
810
use Symfony\Component\OptionsResolver\OptionsResolver;
11+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
912

10-
class FormRecaptchaType extends AbstractType
13+
class FormCaptchaType extends AbstractType
1114
{
1215
public function buildForm(FormBuilderInterface $builder, array $options): void
1316
{
1417
$builder
15-
->add('recaptcha_token', null, ['label' => 'reCAPTCHA Secret Key'])
18+
->add('captcha_provider', ChoiceType::class, [
19+
'label' => 'Captcha Provider',
20+
'choices' => array_flip(array_map(static function(CaptchaProviderInterface $provider){
21+
return $provider->getName();
22+
}, (new Captcha())->getProviders())),
23+
])
24+
->add('captcha_token', null, ['label' => 'Secret Key'])
1625
->add('save', SubmitType::class, ['label' => 'Save']);
1726
}
1827

src/Admin/Resources/templates/bulma_theme.html.twig

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{% block form_row %}
22
<div class="field">
3-
{{ form_label(form) }}
4-
<div class="control">
3+
<label class="has-text-weight-bold">
4+
{{ form_label(form) }}
5+
</label>
6+
<div class="control mt-1">
57
{{ form_widget(form) }}
68
</div>
79
{{ form_errors(form) }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{% extends '@Admin/forms/page.html.twig' %}
2+
3+
{% block title %}API / {{ formEntity.name }} / Forms{% endblock %}
4+
5+
{% block section %}
6+
<div class="card">
7+
<header class="card-header">
8+
<p class="card-header-title">
9+
Protect form with captcha
10+
</p>
11+
</header>
12+
<div class="card-content" x-data="providersData()">
13+
<div class="content">
14+
<form method="post">
15+
{{ form_start(form) }}
16+
<p>
17+
Generate your Secret Key at <a target="_blank" :href="currentProvider.homepageUrl"><span x-html="currentProvider.name"></span></a>.
18+
Only the Secret Key is required here.
19+
Ensure to implement token retrieval on the frontend and include it in the PHPForm endpoint request under the 'captchaResponse' key.
20+
</p>
21+
{{ form_row(form.captcha_provider, {
22+
'attr': {'@change': 'change($event.target.value)'}
23+
}) }}
24+
{{ form_row(form.captcha_token) }}
25+
<p>
26+
Keep the field empty to disable Captcha Protection.
27+
</p>
28+
<p>
29+
Example of how to include the token in the request:
30+
</p>
31+
<div class="mb-4">
32+
<pre style="overflow:auto; width:100%; max-width: 550px;"><code>fetch('https://your-domain.com/api/forms/{{ formEntity.hash }}', {
33+
method: 'POST',
34+
headers: {
35+
'Content-Type': 'application/json',
36+
},
37+
body: JSON.stringify({
38+
captchaResponse: 'captcha-token-here',
39+
// other form fields
40+
}),
41+
})</code></pre>
42+
</div>
43+
<p>
44+
More information on how to use it can be found <a target="_blank" :href="currentProvider.documentationUrl">here</a>.
45+
</p>
46+
{{ form_row(form.save) }}
47+
{{ form_end(form) }}
48+
</form>
49+
</div>
50+
</div>
51+
</div>
52+
53+
<script>
54+
function providersData() {
55+
const providers = {{ providersInfo|json_encode|raw }};
56+
return {
57+
providers,
58+
currentProvider: providers[0],
59+
change: function (value) {
60+
this.currentProvider = providers[value];
61+
}
62+
};
63+
}
64+
</script>
65+
{% endblock %}

src/Admin/Resources/templates/forms/index.html.twig

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@
6767
</a>
6868
{% endif %}
6969
{% if app.user.isSuperUser %}
70-
{% if (form.recaptchaToken|length == 0 and form.secret|length == 0) %}
71-
<a class="tag is-danger is-light ml-1" title="Please use token or reCaptcha protection" href="{{ path('admin_forms_token', { id: form.id }) }}">
70+
{% if (form.captchaToken|length == 0 and form.secret|length == 0) %}
71+
<a class="tag is-danger is-light ml-1" title="Please use token or captcha protection" href="{{ path('admin_forms_token', { id: form.id }) }}">
7272
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="width: 16px; height: 16px; fill: hsl(348, 100%, 61%);">
7373
<path d="M21.171,15.398l-5.912-9.854C14.483,4.251,13.296,3.511,12,3.511s-2.483,0.74-3.259,2.031l-5.912,9.856 c-0.786,1.309-0.872,2.705-0.235,3.83C3.23,20.354,4.472,21,6,21h12c1.528,0,2.77-0.646,3.406-1.771 C22.043,18.104,21.957,16.708,21.171,15.398z M12,17.549c-0.854,0-1.55-0.695-1.55-1.549c0-0.855,0.695-1.551,1.55-1.551 s1.55,0.696,1.55,1.551C13.55,16.854,12.854,17.549,12,17.549z M13.633,10.125c-0.011,0.031-1.401,3.468-1.401,3.468 c-0.038,0.094-0.13,0.156-0.231,0.156s-0.193-0.062-0.231-0.156l-1.391-3.438C10.289,9.922,10.25,9.712,10.25,9.5 c0-0.965,0.785-1.75,1.75-1.75s1.75,0.785,1.75,1.75C13.75,9.712,13.711,9.922,13.633,10.125z"/>
7474
</svg>

src/Admin/Resources/templates/forms/page.html.twig

+3-3
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@
8282
</li>
8383
<li>
8484
<a
85-
{% if current_path == 'admin_forms_recaptcha' %}
85+
{% if current_path == 'admin_forms_captcha' %}
8686
class="is-active"
8787
{% endif %}
88-
href="{{ path('admin_forms_recaptcha', {'id': formEntity.id}) }}"
88+
href="{{ path('admin_forms_captcha', {'id': formEntity.id}) }}"
8989
>
90-
reCAPTCHA Protection
90+
Captcha Protection
9191
</a>
9292
</li>
9393
<li>

src/Admin/Resources/templates/forms/recaptcha.html.twig

-47
This file was deleted.

src/Captcha/Captcha.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
namespace App\Captcha;
3+
4+
use App\Captcha\Providers\HCaptchaProvider;
5+
use App\Captcha\Providers\ReCaptchaProvider;
6+
7+
final class Captcha
8+
{
9+
public const CAPTCHA_PROVIDER_RECAPTCHA = 0;
10+
public const CAPTCHA_PROVIDER_HCAPTCHA = 1;
11+
12+
public function getProviders(): array
13+
{
14+
return [
15+
self::CAPTCHA_PROVIDER_RECAPTCHA => new ReCaptchaProvider(),
16+
self::CAPTCHA_PROVIDER_HCAPTCHA => new HCaptchaProvider(),
17+
];
18+
}
19+
20+
public function getProvider(int $provider): ?CaptchaProviderInterface
21+
{
22+
return $this->getProviders()[$provider] ?? null;
23+
}
24+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
namespace App\Captcha;
3+
4+
interface CaptchaProviderInterface
5+
{
6+
public function validate(string $response, string $secretKey, ?string $userIp = null): bool;
7+
8+
public function getName(): string;
9+
10+
public function getHomePageUrl(): string;
11+
12+
public function getDocumentationUrl(): string;
13+
}
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
namespace App\Captcha\Providers;
3+
4+
use App\Captcha\CaptchaProviderInterface;
5+
6+
class HCaptchaProvider implements CaptchaProviderInterface
7+
{
8+
private string $verifyUrl = 'https://api.hcaptcha.com/siteverify';
9+
10+
public function validate(string $response, string $secretKey, ?string $userIp = null): bool
11+
{
12+
if (empty($response)) {
13+
return false;
14+
}
15+
16+
$data = [
17+
'secret' => $secretKey,
18+
'response' => $response,
19+
'remoteip' => $userIp
20+
];
21+
22+
$options = [
23+
'http' => [
24+
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
25+
'method' => 'POST',
26+
'content' => http_build_query($data)
27+
]
28+
];
29+
30+
$context = stream_context_create($options);
31+
$response = file_get_contents($this->verifyUrl, false, $context);
32+
if ($response === false) {
33+
return false;
34+
}
35+
36+
$result = json_decode($response);
37+
return $result->success;
38+
}
39+
40+
public function getName(): string
41+
{
42+
return 'hCaptcha';
43+
}
44+
45+
public function getHomePageUrl(): string
46+
{
47+
return 'https://www.hcaptcha.com/';
48+
}
49+
50+
public function getDocumentationUrl(): string
51+
{
52+
return 'https://docs.hcaptcha.com/';
53+
}
54+
}

0 commit comments

Comments
 (0)