Skip to content

Commit 37e9d92

Browse files
norkunaskarser
authored andcommitted
Add SymfonyHttpClient new ReCaptcha request method
1 parent 80a5f78 commit 37e9d92

File tree

9 files changed

+185
-1
lines changed

9 files changed

+185
-1
lines changed

DependencyInjection/KarserRecaptcha3Extension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
88
use Symfony\Component\DependencyInjection\Loader;
99
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
10+
use Symfony\Contracts\HttpClient\HttpClientInterface;
1011

1112
class KarserRecaptcha3Extension extends ConfigurableExtension implements PrependExtensionInterface
1213
{
@@ -17,6 +18,12 @@ public function loadInternal(array $configs, ContainerBuilder $container): void
1718
foreach ($configs as $key => $value) {
1819
$container->setParameter('karser_recaptcha3.'.$key, $value);
1920
}
21+
22+
if (interface_exists(HttpClientInterface::class)) {
23+
$container->setAlias('karser_recaptcha3.google.request_method', 'karser_recaptcha3.request_method.symfony_http_client');
24+
} else {
25+
$container->removeDefinition('karser_recaptcha3.request_method.symfony_http_client');
26+
}
2027
}
2128

2229
public function prepend(ContainerBuilder $container): void

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,11 @@ services:
339339
$requestStack: '@request_stack'
340340
```
341341
342+
### Symfony HttpClient integration
343+
344+
If you have a dependency on `symfony/http-client` in your application then it will be automatically wired
345+
to use via `RequestMethod/SymfonyHttpClient`.
346+
342347
Troubleshooting checklist
343348
-------------------------
344349

RequestMethod/SymfonyHttpClient.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Karser\Recaptcha3Bundle\RequestMethod;
6+
7+
use ReCaptcha\ReCaptcha;
8+
use ReCaptcha\RequestMethod;
9+
use ReCaptcha\RequestParameters;
10+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
11+
use Symfony\Contracts\HttpClient\HttpClientInterface;
12+
13+
final class SymfonyHttpClient implements RequestMethod
14+
{
15+
/**
16+
* @var HttpClientInterface
17+
*/
18+
private $httpClient;
19+
20+
/**
21+
* @var string
22+
*/
23+
private $siteVerifyUrl;
24+
25+
public function __construct(HttpClientInterface $httpClient, ?string $siteVerifyUrl = null)
26+
{
27+
$this->httpClient = $httpClient;
28+
$this->siteVerifyUrl = $siteVerifyUrl ?? ReCaptcha::SITE_VERIFY_URL;
29+
}
30+
31+
public function submit(RequestParameters $params): string
32+
{
33+
$response = $this->httpClient->request('POST', $this->siteVerifyUrl, [
34+
'body' => $params->toArray(),
35+
]);
36+
37+
try {
38+
$statusCode = $response->getStatusCode();
39+
} catch (TransportExceptionInterface $e) {
40+
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
41+
}
42+
43+
if ($statusCode !== 200) {
44+
return '{"success": false, "error-codes": ["'.ReCaptcha::E_BAD_RESPONSE.'"]}';
45+
}
46+
47+
return $response->getContent(false);
48+
}
49+
}

Resources/config/services.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Karser\Recaptcha3Bundle\Form\Recaptcha3Type;
66
use Karser\Recaptcha3Bundle\Services\HostProvider;
77
use Karser\Recaptcha3Bundle\Services\IpResolver;
8+
use Karser\Recaptcha3Bundle\RequestMethod\SymfonyHttpClient;
89
use Karser\Recaptcha3Bundle\Validator\Constraints\Recaptcha3Validator;
910
use ReCaptcha\ReCaptcha;
1011
use ReCaptcha\RequestMethod\Curl;
@@ -57,4 +58,10 @@
5758
]);
5859

5960
$services->set('karser_recaptcha3.google.request_method.curl', Curl::class);
61+
62+
$services->set('karser_recaptcha3.request_method.symfony_http_client', SymfonyHttpClient::class)
63+
->args([
64+
(new ReferenceConfigurator('http_client'))->ignoreOnInvalid(),
65+
new Expression("service('karser_recaptcha3.host_provider').getVerifyUrl()")
66+
]);
6067
};

Tests/FunctionalTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Symfony\Component\Form\Extension\Core\Type\TextType;
1212
use Symfony\Component\Form\FormFactoryInterface;
1313
use Symfony\Component\Form\FormInterface;
14+
use Symfony\Component\HttpKernel\Kernel;
1415
use Symfony\Component\Validator\Constraints\NotBlank;
1516

1617
class FunctionalTest extends TestCase
@@ -213,6 +214,21 @@ public function testFormJavascriptAltHostIsPreserved_ifSet()
213214
self::assertStringContainsString('<script type="text/javascript" src="https://www.recaptcha.net/recaptcha/api.js?render=key&hl=en&onload=recaptchaCallback_form_captcha" async defer nonce=""></script>', $view);
214215
}
215216

217+
public function testUsesSymfonyHttpClient()
218+
{
219+
if (Kernel::VERSION_ID < 50200) {
220+
self::markTestSkipped('skip');
221+
}
222+
223+
$this->bootKernel('http_client.yml');
224+
225+
$form = $this->createContactForm($this->formFactory);
226+
$form->submit(['name' => 'John', 'captcha' => 'token']);
227+
228+
self::assertTrue($form->isSubmitted());
229+
self::assertTrue($form->isValid());
230+
}
231+
216232
private function assertFormHasCaptchaError(FormInterface $form, string $expectedMessage)
217233
{
218234
self::assertTrue($form->isSubmitted());
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RequestMethod;
6+
7+
use Karser\Recaptcha3Bundle\RequestMethod\SymfonyHttpClient;
8+
use PHPUnit\Framework\TestCase;
9+
use ReCaptcha\RequestParameters;
10+
use Symfony\Component\HttpClient\MockHttpClient;
11+
use Symfony\Component\HttpClient\Response\MockResponse;
12+
13+
final class SymfonyHttpClientTest extends TestCase
14+
{
15+
public function testSubmit(): void
16+
{
17+
$httpClient = new MockHttpClient(function (string $method, string $url, array $options) {
18+
self::assertSame('POST', $method);
19+
self::assertSame('https://www.google.com/recaptcha/api/siteverify', $url);
20+
self::assertSame('secret=secret&response=response', $options['body']);
21+
22+
return new MockResponse('RESPONSEBODY');
23+
});
24+
25+
$method = new SymfonyHttpClient($httpClient);
26+
$response = $method->submit(new RequestParameters('secret', 'response'));
27+
28+
self::assertSame('RESPONSEBODY', $response);
29+
}
30+
31+
public function testOverrideSiteVerifyUrl()
32+
{
33+
$httpClient = new MockHttpClient(function (string $method, string $url, array $options) {
34+
self::assertSame('POST', $method);
35+
self::assertSame('http://override/', $url);
36+
self::assertSame('secret=secret&response=response', $options['body']);
37+
38+
return new MockResponse('RESPONSEBODY');
39+
});
40+
41+
$method = new SymfonyHttpClient($httpClient, 'http://override/');
42+
$response = $method->submit(new RequestParameters('secret', 'response'));
43+
44+
self::assertSame('RESPONSEBODY', $response);
45+
}
46+
47+
public function testResponseError()
48+
{
49+
$httpClient = new MockHttpClient(function (string $method, string $url, array $options) {
50+
self::assertSame('POST', $method);
51+
self::assertSame('https://www.google.com/recaptcha/api/siteverify', $url);
52+
self::assertSame('secret=secret&response=response', $options['body']);
53+
54+
return new MockResponse('fail', ['http_code' => 400]);
55+
});
56+
57+
$method = new SymfonyHttpClient($httpClient);
58+
$response = $method->submit(new RequestParameters('secret', 'response'));
59+
60+
self::assertSame('{"success": false, "error-codes": ["bad-response"]}', $response);
61+
}
62+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Karser\Recaptcha3Bundle\Tests\fixtures;
4+
5+
use Symfony\Component\HttpClient\Response\MockResponse;
6+
use Symfony\Contracts\HttpClient\ResponseInterface;
7+
8+
class MockHttpClientCallback
9+
{
10+
public function __invoke(string $method, string $url, array $options = []): ResponseInterface
11+
{
12+
return new MockResponse('{"success": true, "score": 1}');
13+
}
14+
}

Tests/fixtures/config/http_client.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
framework:
2+
secret: ThisIsNotReallyASecretSoPleaseChangeIt
3+
test: true
4+
form: ~
5+
http_client:
6+
mock_response_factory: Karser\Recaptcha3Bundle\Tests\fixtures\MockHttpClientCallback
7+
8+
services:
9+
form.factory.public:
10+
alias: form.factory
11+
public: true
12+
twig.public:
13+
alias: twig
14+
public: true
15+
karser_recaptcha3.google.recaptcha.public:
16+
alias: karser_recaptcha3.google.recaptcha
17+
public: true
18+
Karser\Recaptcha3Bundle\Tests\fixtures\MockHttpClientCallback: ~
19+
20+
karser_recaptcha3:
21+
site_key: 'key'
22+
secret_key: 'secret'
23+
enabled: true

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"twig/twig": "^2.9|^3.0"
4747
},
4848
"require-dev": {
49-
"phpunit/phpunit": "^7|^8|^9"
49+
"phpunit/phpunit": "^7|^8|^9",
50+
"symfony/http-client": "^4.3|^5.0|^6.0"
5051
},
5152
"autoload": {
5253
"psr-4": {

0 commit comments

Comments
 (0)