Skip to content

Commit 2be0e4c

Browse files
[2.x] Adds livewire helpers.
1 parent 683c465 commit 2be0e4c

10 files changed

Lines changed: 514 additions & 4 deletions

File tree

.github/workflows/php.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ jobs:
6161
- 12.*
6262
- 13.*
6363
dependencies: [ lowest, highest ]
64+
include-filament: [ true ]
65+
exclude:
66+
- laravel-constraint: "13.*"
67+
include-filament: true
6468

6569
steps:
6670
- name: Set up PHP
@@ -73,6 +77,11 @@ jobs:
7377
- name: Checkout code
7478
uses: actions/checkout@v6
7579

80+
# Remove this once Filament PHP v5 gets Laravel 13 compatibility.
81+
- name: Remove Filament for Laravel 13 compatibility
82+
if: matrix.laravel-constraint == '13.*'
83+
run: composer remove "filament/filament" --dev --no-update
84+
7685
- name: Install dependencies
7786
uses: ramsey/composer-install@v3
7887
with:

README.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ $request->validate([
564564
]);
565565
```
566566

567-
#### Rule accepts failed challenges
567+
#### Rule accepts failed challenges
568568

569569
The rule supports not checking if the challenge is successful by setting the `accept-failed` parameter. This can be useful to retrieve the response later and programmatically continue based on the response result through the `sucess()` and `failed()` methods of the `Turnstile` facade.
570570

@@ -840,6 +840,83 @@ return Application::configure(basePath: dirname(__DIR__))
840840
->create();
841841
```
842842
843+
## Livewire & Filament trait
844+
845+
If you're using Liveware or Filament PHP, you may use the `Laragear\Turnstile\Livewire\InteractsWithTurnstile` trait in your Filament Pages or components.
846+
847+
The trait will register a hook _after validation_, that only runs on non-Precognitive requests. This way, the Turnstile Challenge is consumed only when the form is completely validated. It also avoids using an idempotency key based on the request fingerprint, saving you a request to Cloudflare Turnstile servers.
848+
849+
```php
850+
use Filament\Forms\Concerns\InteractsWithForms;
851+
use Filament\Forms\Contracts\HasForms;
852+
use Filament\Pages\Page;
853+
use Filament\Schemas\Schema;
854+
use Laragear\Turnstile\Livewire\InteractsWithTurnstile;
855+
856+
class MyCustomPage extends Page implements HasForms
857+
{
858+
use InteractsWithForms;
859+
use InteractsWithTurnstile;
860+
861+
public function form(Schema $schema) : Schema
862+
{
863+
return $schema->components([
864+
// ...
865+
]);
866+
}
867+
}
868+
```
869+
870+
If you're using a custom challenge key in your form, you may override the `turnstileToken()` method to retrieve the value of the token.
871+
872+
```php
873+
/**
874+
* Return token for the Turnstile Challenge present in the form.
875+
*
876+
* @return string|null|void
877+
*/
878+
protected function turnstileToken(string $key)
879+
{
880+
return $this->data['cloudflare-turnstile-token'];
881+
}
882+
```
883+
884+
> [!IMPORTANT]
885+
>
886+
> The trait injects the Turnstile challenge validation on all forms. If you need more customization, use the [`afterValidate()` hook](https://filamentphp.com/docs/5.x/resources/creating-records#lifecycle-hooks).
887+
888+
### Handling the Turnstile Challenge
889+
890+
You have access to some useful methods to handle if the challenge should be deemed successful or not, and how to handle successes and failures:
891+
892+
* `skipTurnstileValidation()`: Returns if the challenge verification should run when authenticated.
893+
* `handleTurnstileChallengeStatus()`: Returns if the challenge is successful or not.
894+
* `handleSuccessfulTurnstileChallenge()`: Handle a successful Turnstile challenge.
895+
* `handleFailedTurnstileChallenge()`: Handle a failed Turnstile challenge.
896+
897+
For example, for non-admins, you may check if the challenge is successful if it matches the component action:
898+
899+
```php
900+
use Illuminate\Support\Str;
901+
use Laragear\Turnstile\Challenge;
902+
903+
/**
904+
* Check if the Turnstile challenge validation should be skipped if authenticated.
905+
*/
906+
protected function skipTurnstileValidation(): bool
907+
{
908+
return auth('admins')->check();
909+
}
910+
911+
/**
912+
* Handles the Turnstile challenge and returns true or false if it has succeeded or failed.
913+
*/
914+
protected function handleTurnstileChallengeStatus(Challenge $challenge): bool
915+
{
916+
return $challenge->successful && $challenge->isAction(Str::snake(static::class));
917+
}
918+
```
919+
843920
## Advanced configuration
844921

845922
Laragear Turnstile is intended to work out-of-the-box, but you can publish the configuration file for fine-tuning the Challenge verification.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"illuminate/session": "12.*|13.*"
3636
},
3737
"require-dev": {
38+
"filament/filament": "4.*|5.*",
3839
"orchestra/testbench": "10.*|11.*"
3940
},
4041
"autoload": {

lang/en/notification.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
return [
4+
'failed' => [
5+
'title' => 'The Turnstile Challenge is invalid',
6+
'body' => 'Please try again or refresh the page.',
7+
],
8+
];

lang/es/interstitial.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
return [
4+
'title' => 'Comprobando la conexión segura',
5+
'description' => 'Su navegador será redirigido automáticamente',
6+
];

lang/es/notification.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
return [
3+
'failed' => [
4+
'title' => 'El desafío de Turnstile no es válido',
5+
'body' => 'Por favor, inténtalo de nuevo o recarga la página.',
6+
],
7+
];

lang/es/validation.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
return [
4+
'invalid' => 'El desafío de Turnstile es inválido, ausente o falló.',
5+
];

src/Challenge.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace Laragear\Turnstile;
44

5+
use Carbon\CarbonInterface;
56
use ErrorException;
67
use Illuminate\Support\Arr;
7-
use Carbon\CarbonInterface;
88
use Illuminate\Support\Stringable;
99
use InvalidArgumentException;
1010
use function in_array;
@@ -84,7 +84,7 @@ public function isAction(string $action): bool
8484
}
8585

8686
/**
87-
* Check if the action is not the same as the developer expects.
87+
* Check if the action is different from the developer expects.
8888
*/
8989
public function isNotAction(string $action): bool
9090
{
@@ -110,7 +110,7 @@ public function isCustomerData(string|iterable $customerData): bool
110110
}
111111

112112
/**
113-
* Checks if the Customer Data is not the same pattern as the developer expects.
113+
* Checks if the Customer Data is a different pattern as the developer expects.
114114
*
115115
* @param string|iterable<string> $customerData
116116
*/
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace Laragear\Turnstile\Livewire;
4+
5+
use Filament\Notifications\Notification;
6+
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
7+
use Illuminate\Support\Arr;
8+
use Illuminate\Validation\ValidationException;
9+
use Laragear\Turnstile\Challenge;
10+
use Laragear\Turnstile\Turnstile;
11+
use Throwable;
12+
use function __;
13+
use function app;
14+
use function request;
15+
16+
trait InteractsWithTurnstile
17+
{
18+
/**
19+
* Boots the trait.
20+
*/
21+
public function bootInteractsWithTurnstile(): void
22+
{
23+
$this->withValidator(function (ValidatorContract $validator): void {
24+
$validator->after($this->validateTurnstile(...));
25+
});
26+
}
27+
28+
/**
29+
* Check if the Turnstile challenge validation should be skipped.
30+
*/
31+
protected function skipTurnstileValidation(): bool
32+
{
33+
return false;
34+
}
35+
36+
/**
37+
* Validates the Turnstile captcha token provided in the request data.
38+
*/
39+
protected function validateTurnstile(): void
40+
{
41+
if ($this->skipTurnstileValidation() || request()->isPrecognitive()) {
42+
return;
43+
}
44+
45+
$turnstile = app(Turnstile::class);
46+
47+
// If Turnstile is disabled, we don't need to use it at all.
48+
if ($turnstile->isDisabled()) {
49+
return;
50+
}
51+
52+
if (!$token = $this->turnstileToken($turnstile->key())) {
53+
$this->handleFailedTurnstileChallenge();
54+
}
55+
56+
try {
57+
$challenge = $this->retrieveTurnstileChallenge($turnstile, $token);
58+
} catch (Throwable $exception) {
59+
$this->handleTurnstileException($exception);
60+
}
61+
62+
$challenge && $this->handleTurnstileChallengeStatus($challenge)
63+
? $this->handleSuccessfulTurnstileChallenge($challenge)
64+
: $this->handleFailedTurnstileChallenge($challenge);
65+
}
66+
67+
/**
68+
* Return token for the Turnstile Challenge present in the form.
69+
*
70+
* @return string|null|void
71+
*/
72+
protected function turnstileToken(string $key)
73+
{
74+
return Arr::get($this->data ?? $this->form?->getFormSnapshot(), $key);
75+
}
76+
77+
/**
78+
* Retrieves the token challenge.
79+
*/
80+
protected function retrieveTurnstileChallenge(Turnstile $turnstile, string $token): ?Challenge
81+
{
82+
return $turnstile->getChallenge($token);
83+
}
84+
85+
/**
86+
* Handles the Turnstile challenge and returns true or false if it has succeeded or failed.
87+
*/
88+
protected function handleTurnstileChallengeStatus(Challenge $challenge): bool
89+
{
90+
return $challenge->successful;
91+
}
92+
93+
/**
94+
* Handle a successful Turnstile challenge.
95+
*/
96+
protected function handleSuccessfulTurnstileChallenge(Challenge $challenge): void
97+
{
98+
//
99+
}
100+
101+
/**
102+
* Handle a failed Turnstile challenge.
103+
*/
104+
protected function handleFailedTurnstileChallenge(?Challenge $challenge = null): void
105+
{
106+
$this->notifyFailedTurnstileChallenge($challenge);
107+
$this->throwTurnstileValidationError($challenge);
108+
}
109+
110+
/**
111+
* Send a notification to the user if the challenge has failed.
112+
*/
113+
protected function notifyFailedTurnstileChallenge(?Challenge $challenge = null): void
114+
{
115+
Notification::make('turnstile-challenge')
116+
->title(__('turnstile::notification.failed.title'))
117+
->body(__('turnstile::notification.failed.body'))
118+
->danger()
119+
->send();
120+
}
121+
122+
/**
123+
* Throw a Validation exception for the failed Turnstile challenge.
124+
*/
125+
protected function throwTurnstileValidationError(?Challenge $challenge = null): never
126+
{
127+
throw ValidationException::withMessages([
128+
Turnstile::KEY => __('turnstile::validation.invalid'),
129+
]);
130+
}
131+
132+
/**
133+
* Handle a Turnstile challenge exception.
134+
*
135+
* @return void|never
136+
*/
137+
protected function handleTurnstileException(Throwable $exception)
138+
{
139+
throw $exception;
140+
}
141+
}

0 commit comments

Comments
 (0)