Skip to content

Commit a3825f8

Browse files
Merge pull request #2 from Laragear/fix/precognitive
[1.x] Fixes precognitive and adds more helpers
2 parents f33e150 + 41e7845 commit a3825f8

7 files changed

Lines changed: 338 additions & 47 deletions

File tree

README.md

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ When issuing a form, you have three alternatives to ensure the Turnstile challen
184184
185185
### Validating with Request
186186

187-
The easiest and less intrusive way to check the Turnstile challenge is to use the `Laragear\Turnstile\Http\Requests\TurnstileRequest` instance in your controller. This is great if you only have a few controllers where you want to check for a successful Turnstile Challenge.
187+
The easiest and least intrusive way to check the Turnstile Challenge is to use the `Laragear\Turnstile\Http\Requests\TurnstileRequest` instance in your controller. This is great if you only have a few controllers where you want to stop bots.
188188

189189
```php
190190
use App\Models\Comment;
@@ -200,7 +200,7 @@ Route::post('comment', function (TurnstileRequest $request) {
200200
})
201201
```
202202

203-
You can have access to the Cloudflare Turnstile Challenge object through the `challenge()` method. For example, you may use it to double-check if the action is equal to something you expect.
203+
You can have access to the Cloudflare Turnstile Challenge object through the `challenge()` method, plus additional helpers for the Challenge instance itself. For example, you may use it to double-check if the action is equal to something you expect.
204204

205205
```php
206206
use App\Models\Comment;
@@ -212,7 +212,7 @@ Route::post('comment', function (TurnstileRequest $request) {
212212
'body' => 'required|string'
213213
]);
214214

215-
if ($request->challenge()->isAction('comment:store')) {
215+
if ($request->isAction('comment:store')) {
216216
return back()->withErrors('Invalid action');
217217
}
218218

@@ -222,7 +222,87 @@ Route::post('comment', function (TurnstileRequest $request) {
222222

223223
> [!IMPORTANT]
224224
>
225-
> The Request will check for the `cf-turnstile-response` key [by default](#form-key), plus a successful Challenge. If you need more fine-tuning, consider using the [middleware](#validating-with-middleware), [rule](#validating-with-rule), or [validating manually](#validating-manually).
225+
> The Request will check for the `cf-turnstile-response` key [by default](#form-key), plus a successful Challenge. If you need more fine-tuning, you may [extend the form request class](#extending-the-form-request). Alternatively, you may also use a [middleware](#validating-with-middleware), [rule](#validating-with-rule), or [validate manually](#validating-manually).
226+
227+
#### Extending the Form Request
228+
229+
If you need to create a form request and also validate the Turnstile Challenge, you may safely extend the `TurnstileRequest` instead of the base `FormRequest`. The class runs the validation _before_ your form request authorization and rules to avoid running side effects.
230+
231+
```php
232+
namespace App\Http\Requests;
233+
234+
use Laragear\Turnstile\Http\Requests\TurnstileRequest;
235+
236+
class CommentStoreRequest extends TurnstileRequest
237+
{
238+
public function rules(): array
239+
{
240+
return [
241+
'body' => 'required|string'
242+
];
243+
}
244+
}
245+
```
246+
247+
This means your controller can safely retrieve the validated data using `$request->validated()`, as the token won't be considered part of the Request itself.
248+
249+
```php
250+
use App\Models\Comment;
251+
use Illuminate\Support\Facades\Route;
252+
use App\Http\Requests\CommentStoreRequest;
253+
254+
Route::post('comment', function (CommentStoreRequest $request) {
255+
return Comment::create($request->validated());
256+
})
257+
```
258+
259+
#### Custom key and rules
260+
261+
You may also edit the key and the rules where to find and check the Response Token in the request. For the case of rules, ensure you're using [the `turnstile` rule](#validating-with-rule).
262+
263+
```php
264+
namespace App\Http\Requests;
265+
266+
use Laragear\Turnstile\Http\Requests\TurnstileRequest;
267+
268+
class CommentStoreRequest extends TurnstileRequest
269+
{
270+
public function getTurnstileKey()
271+
{
272+
return '_cf-custom-key';
273+
}
274+
275+
protected function getTurnstileRules()
276+
{
277+
// Skip if the user is authenticated.
278+
return 'turnstile:auth'
279+
}
280+
281+
// ...
282+
}
283+
```
284+
285+
#### Precognitive request
286+
287+
The `TurnstileRequest` won't check for the Challenge Token on [Precognitive Requests](https://laravel.com/docs/12.x/precognition), which is useful to not disrupt [live-validation](https://laravel.com/docs/12.x/precognition#live-validation).
288+
289+
If you require custom validation on precognitive requests, you may override the `skipChallengeWhenPrecognitive()` method.
290+
291+
```php
292+
namespace App\Http\Requests;
293+
294+
use Laragear\Turnstile\Http\Requests\TurnstileRequest;
295+
296+
class CommentStoreRequest extends TurnstileRequest
297+
{
298+
protected function skipChallengeWhenPrecognitive(): bool
299+
{
300+
return $this->hasHeader('Requires-CF-Turnstile-Challenge');
301+
}
302+
303+
// ...
304+
}
305+
```
226306

227307
#### Extending the Form Request
228308

@@ -273,7 +353,7 @@ Route::post('comment', function (Request $request) {
273353
>
274354
> Is not suggested to use the middleware on `GET` methods or similar. Some browsers (or extensions) may _cache_ or _inspect_ ahead document links.
275355
276-
If you want to configure the middleware behaviour, you should use the `TurnstileMiddleware` class and the static methods.
356+
If you want to configure the middleware behaviour, you should use the `TurnstileMiddleware` class and the static helper methods.
277357

278358
```php
279359
use Illuminate\Http\Request;
@@ -366,9 +446,24 @@ Route::post('comment', function (Request $request) {
366446
})->middleware(TurnstileMiddleware::action('comment:store'));
367447
```
368448

449+
#### Validating on Precognitive
450+
451+
By default, the middleware will [skip running on Precognitive requests](https://laravel.com/docs/12.x/precognition#managing-side-effects). If you want to run it, set the `TurnstileMiddleware::onPrecognitive()` option, especially if your validation has side effects.
452+
453+
```php
454+
use Illuminate\Http\Request;
455+
use Illuminate\Support\Facades\Route;
456+
use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;
457+
458+
Route::post('comment', function (Request $request) {
459+
// ...
460+
})->middleware(TurnstileMiddleware::onPrecognitive());
461+
```
462+
463+
369464
### Validating with Rule
370465

371-
You can use the `turnstile` rule to check if the Turnstile challenge is present and is successful. The easiest way is to unpack the default rule contained in the `rules()` method of the `Turnstile` facade.
466+
You can use the `turnstile` rule to check if the Turnstile challenge is present and is successful in the data to validate. The easiest way is to unpack the default rule contained in the `rules()` method of the `Turnstile` facade.
372467

373468
```php
374469
use Illuminate\Http\Request;
@@ -454,9 +549,9 @@ public function create(Request $request)
454549

455550
> [!IMPORTANT]
456551
>
457-
> The challenge is automatically retrieved by the [request](#validating-with-request), [middleware](#validating-with-middleware) and [rule](#validating-with-rule). If that's case, you may [use the `challenge()` method](#retrieving-an-already-received-challenge).
552+
> The challenge is automatically retrieved by the [request](#validating-with-request), [middleware](#validating-with-middleware) and [rule](#validating-with-rule). If that's case, you may [use the `challenge()` method](#retrieving-an-already-received-challenge) instead.
458553
459-
To validate the Challenge manually, first you require the Turnstile Response Token that is sent by the frontend, and optionally the IP of the Request.
554+
To validate the Challenge manually, you require the Turnstile Response Token that is sent by the frontend, and optionally the IP of the Request.
460555

461556
Once identified, you should use the `getChallenge()` method of `Turnstile` facade to retrieve the Challenge from Cloudflare Turnstile servers.
462557

src/Enums/SecretKey.php

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

33
namespace Laragear\Turnstile\Enums;
44

5+
use Illuminate\Support\Collection;
6+
57
enum SecretKey: string
68
{
79
case Passing = '1x0000000000000000000000000000000AA';
810
case Fails = '2x0000000000000000000000000000000AA';
911
case Spent = '3x0000000000000000000000000000000AA';
12+
13+
/**
14+
* Returns the enum cases as a Collection.
15+
*
16+
* @return \Illuminate\Support\Collection<int, static>
17+
*/
18+
public static function collect(): Collection
19+
{
20+
return new Collection(self::cases());
21+
}
1022
}

src/Enums/SiteKey.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@
22

33
namespace Laragear\Turnstile\Enums;
44

5+
use Illuminate\Support\Collection;
6+
57
enum SiteKey: string
68
{
79
case VisiblePassing = '1x00000000000000000000AA';
810
case VisibleBlocks = '2x00000000000000000000AB';
911
case InvisiblePassing = '1x00000000000000000000BB';
1012
case InvisibleBlocks = '2x00000000000000000000BB';
1113
case ForceInteraction = '3x00000000000000000000FF';
14+
15+
/**
16+
* Returns the enum cases as a Collection.
17+
*
18+
* @return \Illuminate\Support\Collection<int, static>
19+
*/
20+
public static function collect(): Collection
21+
{
22+
return new Collection(self::cases());
23+
}
1224
}

src/Http/Middleware/TurnstileMiddleware.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* @method static \Laragear\Turnstile\Http\Middleware\TurnstileMiddlewareDefinition input(string $name)
1818
* @method static \Laragear\Turnstile\Http\Middleware\TurnstileMiddlewareDefinition acceptFailed()
1919
* @method static \Laragear\Turnstile\Http\Middleware\TurnstileMiddlewareDefinition action(string $action)
20+
* @method static \Laragear\Turnstile\Http\Middleware\TurnstileMiddlewareDefinition onPrecognitive()
2021
*/
2122
class TurnstileMiddleware
2223
{

src/Http/Requests/TurnstileRequest.php

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ class TurnstileRequest extends FormRequest
1010
{
1111
/**
1212
* Validate the class instance.
13-
*
14-
* @return void
15-
* @throws \Illuminate\Contracts\Container\BindingResolutionException
1613
*/
1714
public function validateResolved(): void
1815
{
@@ -24,30 +21,56 @@ public function validateResolved(): void
2421
/**
2522
* Checks if the Cloudflare Turnstile challenge is valid through the validation rule.
2623
*
27-
* @return void
24+
* @internal
2825
* @throws \Illuminate\Contracts\Container\BindingResolutionException
2926
*/
3027
protected function checkTurnstileChallenge(): void
3128
{
29+
// Avoid depleting the challenge response if is precognitive.
30+
if ($this->isPrecognitive() && $this->skipChallengeWhenPrecognitive()) {
31+
return;
32+
}
33+
3234
/** @var \Laragear\Turnstile\Turnstile $turnstile */
3335
$turnstile = $this->container->make(Turnstile::class);
3436

35-
// We are going to copy-and-paste the "getValidatorInstance" but using our data and rules.
36-
/** @var \Illuminate\Contracts\Validation\Factory $factory */
37-
$factory = $this->container->make('validator');
37+
$key = $this->getTurnstileKey() ?: $turnstile->key();
3838

39-
$validator = $factory->make( // @phpstan-ignore-line
40-
$this->only($turnstile->key()),
41-
$turnstile->rules(),
39+
// Create a new validator with overridable the messages and attribute names.
40+
$this->container->make('validator')->make(
41+
$this->only($key),
42+
[$key => $this->getTurnstileRules() ?: $turnstile->rules()],
4243
$this->messages(),
4344
$this->attributes(),
44-
)->stopOnFirstFailure();
45+
)->validate();
46+
}
4547

46-
if ($this->isPrecognitive()) {
47-
$validator->setRules($this->filterPrecognitiveRules($validator->getRulesWithoutPlaceholders()));
48-
}
48+
/**
49+
* Returns the default Turnstile Response token key to find in the request. When falsy, the default will be used.
50+
*
51+
* @return void|string
52+
*/
53+
protected function getTurnstileKey()
54+
{
55+
// ...
56+
}
57+
58+
/**
59+
* Returns the rules that will be used against the Turnstile Response token. When falsy, the defaults will be used.
60+
*
61+
* @return void|string|array<\Illuminate\Contracts\Validation\ValidationRule|string>
62+
*/
63+
protected function getTurnstileRules()
64+
{
65+
// ...
66+
}
4967

50-
$validator->validate();
68+
/**
69+
* If the Precognitive Request should check for the Turnstile Challenge.
70+
*/
71+
protected function skipChallengeWhenPrecognitive(): bool
72+
{
73+
return true;
5174
}
5275

5376
/**

src/Turnstile.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ class Turnstile
3232
*/
3333
public const HEADER = 'CF-Connecting-IP';
3434

35+
/**
36+
* The proper name for the Cloudflare Turnstile Challenge attribute.
37+
*
38+
* @const string
39+
*/
40+
public const ATTRIBUTE = 'Cloudflare Turnstile Challenge';
41+
3542
/**
3643
* The default key where the Cloudflare Turnstile response is set.
3744
*
@@ -66,7 +73,7 @@ public function __construct(
6673
protected array $fakedResponse = [],
6774
protected bool $shouldFake = false
6875
) {
69-
// ...
76+
//
7077
}
7178

7279
/**

0 commit comments

Comments
 (0)