Skip to content

Commit 7cbed9d

Browse files
authored
Merge pull request #12 from Webotvorba/development
- Add support for reCAPTCHA v3 Enterprise in README and implementation
2 parents 4b9f1ff + c39b79b commit 7cbed9d

5 files changed

Lines changed: 118 additions & 8 deletions

File tree

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ version you are going to implement.
2222

2323
This package supports the following versions. Note that each version requires a different sitekey/secretkey pair:
2424

25-
| **Version** | **Docs** | **Notes** |
26-
|----------------------|-------------------------------------------------------------------|-----------------------------|
27-
| **v3** (recommended) | [V3 Docs](https://developers.google.com/recaptcha/docs/v3) | |
28-
| **v2** | [V2 Docs](https://developers.google.com/recaptcha/docs/display) | |
29-
| **v2 invisible** | [V2 Docs](https://developers.google.com/recaptcha/docs/invisible) | Use `'size' => 'invisible'` |
25+
| **Version** | **Docs** | **Notes** |
26+
|----------------------|-------------------------------------------------------------------|-----------------------------------|
27+
| **v3** (recommended) | [V3 Docs](https://developers.google.com/recaptcha/docs/v3) | |
28+
| **v3** (enterprise) | [V3 Docs](https://developers.google.com/recaptcha/docs/v3) | Use `'version' => 'v3-enterprise'` |
29+
| **v2** | [V2 Docs](https://developers.google.com/recaptcha/docs/display) | |
30+
| **v2 invisible** | [V2 Docs](https://developers.google.com/recaptcha/docs/invisible) | Use `'size' => 'invisible'` |
3031

3132
Your options should reside in the `config/services.php` file:
3233

@@ -38,6 +39,7 @@ Your options should reside in the `config/services.php` file:
3839
'secret_key' => env('GOOGLE_RECAPTCHA_SECRET_KEY'),
3940
'version' => 'v3',
4041
'score' => 0.5, // An integer between 0 and 1, that indicates the minimum score to pass the Captcha challenge.
42+
'endpoint' => 'https://www.google.com/recaptcha/api/siteverify', // For enterprise users, fill in your URL from Google Console.
4143
],
4244
],
4345

src/ValidatesRecaptcha.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ class ValidatesRecaptcha extends LivewireAttribute
1616
public function __construct(
1717
public ?string $secretKey = null,
1818
public ?float $score = null,
19+
public ?string $endpoint = null,
1920
) {
2021
$this->secretKey ??= config('services.google.recaptcha.secret_key');
2122
$this->score ??= config('services.google.recaptcha.score') ?? 0.5;
23+
$this->endpoint ??= config('services.google.recaptcha.endpoint', 'https://www.google.com/recaptcha/api/siteverify');
2224
}
2325

2426
/**
@@ -28,8 +30,10 @@ public function __construct(
2830
*/
2931
public function call(array $params, Closure $returnEarly): void
3032
{
33+
assert(is_string($this->endpoint));
34+
3135
if (isset($this->component->gRecaptchaResponse)) {
32-
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
36+
$response = Http::asForm()->post($this->endpoint, [
3337
'secret' => $this->secretKey,
3438
'response' => $this->component->gRecaptchaResponse,
3539
'remoteip' => request()->ip(),
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script>
2+
document.addEventListener('livewire:init', () => {
3+
Livewire.directive('recaptcha', ({ el, directive, component, cleanup }) => {
4+
const submitExpression = (() => {
5+
for (const attr of el.attributes) {
6+
if (attr.name.startsWith('wire:submit')) {
7+
return attr.value;
8+
}
9+
}
10+
})();
11+
12+
const onSubmit = (e) => {
13+
e.preventDefault();
14+
e.stopImmediatePropagation();
15+
16+
grecaptcha.enterprise.ready(async () => {
17+
const token = await grecaptcha.enterprise.execute(@json($siteKey), { action: 'submit' });
18+
19+
component.$wire.$set('gRecaptchaResponse', token).then(() => {
20+
Alpine.evaluate(el, "$wire." + submitExpression, { scope: { $event: e } });
21+
});
22+
});
23+
}
24+
25+
el.addEventListener('submit', onSubmit, { capture: true });
26+
});
27+
});
28+
</script>
29+
<script src="https://www.google.com/recaptcha/enterprise.js?render={{ $siteKey }}"></script>

tests/CaptchaTest.php

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace DutchCodingCompany\LivewireRecaptcha\Tests;
44

5+
use DutchCodingCompany\LivewireRecaptcha\Tests\Fixtures\MyCustomAttributeComponent;
56
use DutchCodingCompany\LivewireRecaptcha\Tests\Fixtures\MyTestComponent;
67
use Illuminate\Http\Client\Request;
78
use Illuminate\Support\Facades\Http;
@@ -13,7 +14,7 @@ class CaptchaTest extends TestCase
1314
/**
1415
* @param array{0: bool, 1: array{'success': bool}} $captchaResponse
1516
*/
16-
#[DataProvider('provideCaptchaData')]
17+
#[DataProvider('provideDefaultAttributeData')]
1718
public function testInvalidCaptchaResponse(bool $isValid, array $captchaResponse): void
1819
{
1920
Http::fake([
@@ -45,10 +46,52 @@ public function testInvalidCaptchaResponse(bool $isValid, array $captchaResponse
4546
);
4647
}
4748

49+
/**
50+
* @param array{0: bool, 1: array{'success': bool}} $captchaResponse
51+
*/
52+
#[DataProvider('provideCustomAttributeData')]
53+
public function testCustomAttributeProperties(bool $isValid, array $captchaResponse): void
54+
{
55+
Http::fake([
56+
'https://custom.example.com/verify' => Http::response($captchaResponse),
57+
]);
58+
59+
$testable = Livewire::test(MyCustomAttributeComponent::class)
60+
->set('gRecaptchaResponse', $captcha = 'mygrecaptcharesponse')
61+
->call('save');
62+
63+
if ($isValid) {
64+
$testable->assertHasNoErrors();
65+
} else {
66+
$testable->assertHasErrors([
67+
'gRecaptchaResponse',
68+
]);
69+
}
70+
71+
Http::assertSent(fn (Request $request) => $request->url() === 'https://custom.example.com/verify' &&
72+
$request['secret'] === 'custom-secret-key' &&
73+
$request['response'] === $captcha &&
74+
array_key_exists('remoteip', $request->data())
75+
);
76+
}
77+
78+
/**
79+
* @return array<string, array{0: bool, 1: array{'success': bool}}>
80+
*/
81+
public static function provideCustomAttributeData(): array
82+
{
83+
return [
84+
'valid response' => [true, ['success' => true, 'score' => 0.9]],
85+
'valid response, score at threshold' => [true, ['success' => true, 'score' => 0.7]],
86+
'valid response, score below threshold' => [false, ['success' => true, 'score' => 0.5]],
87+
'invalid response' => [false, ['success' => false]],
88+
];
89+
}
90+
4891
/**
4992
* @return array<string, array{0: bool, 1: array{'success': bool}}>
5093
*/
51-
public static function provideCaptchaData(): array
94+
public static function provideDefaultAttributeData(): array
5295
{
5396
return [
5497
'valid response' => [true, ['success' => true, 'score' => 0.9]],
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace DutchCodingCompany\LivewireRecaptcha\Tests\Fixtures;
4+
5+
use DutchCodingCompany\LivewireRecaptcha\ValidatesRecaptcha;
6+
use Exception;
7+
use Livewire\Component;
8+
9+
class MyCustomAttributeComponent extends Component
10+
{
11+
public string $gRecaptchaResponse;
12+
13+
public function mount(): void
14+
{
15+
//
16+
}
17+
18+
#[ValidatesRecaptcha(
19+
secretKey: 'custom-secret-key',
20+
score: 0.7,
21+
endpoint: 'https://custom.example.com/verify',
22+
)]
23+
public function save(): void
24+
{
25+
//
26+
}
27+
28+
public function render(): string
29+
{
30+
return file_get_contents(__DIR__.'/my-test-component.blade.php') ?: throw new Exception('Failed to load my-test-component.blade.php.');
31+
}
32+
}

0 commit comments

Comments
 (0)