From 5c5ebbfd9d8621847d333022ab29c2a823f336c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Dec 2025 15:00:47 +0000 Subject: [PATCH] Introduce captcha: seznamcaptcha from contributte/seznamcaptcha - Add SeznamCaptcha integration following Wordcha pattern - Backend client for Seznam captcha API - Provider interface with SeznamProvider implementation - Validator interface with SeznamValidator implementation - SeznamCaptchaContainer form component - FormBinder for Nette Forms integration - SeznamCaptchaExtension for DI container - Documentation and tests --- .docs/README.md | 54 ++++++++ .docs/seznam-captcha.png | Bin 0 -> 7254 bytes src/Captcha/Seznam/Backend/Client.php | 34 ++++++ src/Captcha/Seznam/Backend/HttpClient.php | 74 +++++++++++ src/Captcha/Seznam/DI/FormBinder.php | 20 +++ .../Seznam/DI/SeznamCaptchaExtension.php | 54 ++++++++ .../Seznam/Exception/RuntimeException.php | 8 ++ src/Captcha/Seznam/Factory.php | 15 +++ .../Seznam/Form/SeznamCaptchaContainer.php | 115 ++++++++++++++++++ src/Captcha/Seznam/Provider/Provider.php | 14 +++ .../Seznam/Provider/SeznamProvider.php | 35 ++++++ src/Captcha/Seznam/SeznamFactory.php | 31 +++++ .../Seznam/Validator/SeznamValidator.php | 22 ++++ src/Captcha/Seznam/Validator/Validator.php | 10 ++ tests/Cases/Seznam/DI/FormBinder.phpt | 48 ++++++++ .../Seznam/Form/SeznamCaptchaContainer.phpt | 97 +++++++++++++++ 16 files changed, 631 insertions(+) create mode 100644 .docs/seznam-captcha.png create mode 100644 src/Captcha/Seznam/Backend/Client.php create mode 100644 src/Captcha/Seznam/Backend/HttpClient.php create mode 100644 src/Captcha/Seznam/DI/FormBinder.php create mode 100644 src/Captcha/Seznam/DI/SeznamCaptchaExtension.php create mode 100644 src/Captcha/Seznam/Exception/RuntimeException.php create mode 100644 src/Captcha/Seznam/Factory.php create mode 100644 src/Captcha/Seznam/Form/SeznamCaptchaContainer.php create mode 100644 src/Captcha/Seznam/Provider/Provider.php create mode 100644 src/Captcha/Seznam/Provider/SeznamProvider.php create mode 100644 src/Captcha/Seznam/SeznamFactory.php create mode 100644 src/Captcha/Seznam/Validator/SeznamValidator.php create mode 100644 src/Captcha/Seznam/Validator/Validator.php create mode 100644 tests/Cases/Seznam/DI/FormBinder.phpt create mode 100644 tests/Cases/Seznam/Form/SeznamCaptchaContainer.phpt diff --git a/.docs/README.md b/.docs/README.md index a4babf6..ef21097 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -10,6 +10,7 @@ - [Date/time inputs](#date-time-inputs) (DateTimeInput, DateInput, TimeInput) - Captcha - [Wordcha](#wordcha) (Question-based captcha) + - [Seznam Captcha](#seznam-captcha) (Image-based captcha) ## Setup @@ -417,3 +418,56 @@ protected function createComponentForm() #### Wordcha Example ![captcha](wordcha.png) + +### Seznam Captcha + +Image-based captcha using [Seznam.cz Captcha](https://captcha.seznam.cz) service. + +#### Seznam Captcha Setup + +Register extension + +```yaml +extensions: + seznamCaptcha: Contributte\Forms\Captcha\Seznam\DI\SeznamCaptchaExtension +``` + +#### Seznam Captcha Configuration + +```yaml +seznamCaptcha: + auto: true # Automatically bind addSeznamCaptcha to forms +``` + +#### Seznam Captcha Form Usage + +```php +use Nette\Application\UI\Form; + +protected function createComponentForm() +{ + $form = new Form(); + + $form->addSeznamCaptcha('captcha') + ->getCode() + ->setRequired('Please enter the captcha code'); + + $form->addSubmit('send'); + + $form->onValidate[] = function (Form $form) { + if ($form['captcha']->verify() !== TRUE) { + $form->addError('Are you robot?'); + } + }; + + $form->onSuccess[] = function (Form $form) { + dump($form['captcha']); + }; + + return $form; +} +``` + +#### Seznam Captcha Example + +![captcha](seznam-captcha.png) diff --git a/.docs/seznam-captcha.png b/.docs/seznam-captcha.png new file mode 100644 index 0000000000000000000000000000000000000000..670d0575ecf78a984627fdcccb8ae445b08291eb GIT binary patch literal 7254 zcmZvBcQ{;M)b1e!38M2ON<*K(9#KC60Du4{C#?=1PeBRA#RR`4g~Kku z!vkk2m?kbR?)<9C3iy-AMMm32!`{rr-O$Mt(6DfIaWQo=4j91$0GcD1w4|oT+@9Vi zZPNF1eykWIHq4^|9dEtmvXQHnMWfXu%lRb_D9o+Fw_q!?}2gUmf4R6jElTY*Qi{Iw&1zLJtZ% z{}wb~I&MLW1*(x@^2I+kk{F~dmeAt`)jmBP=TKWAsRoUgGZCnslX8NWNE zZPxc&ZNqj;9nF12ZbP48zyKQ%Ftb)gKIXT`?GhAC$$ z5xT?kmDAV0@NjTQo#DX}dV70!?PjYi6O;Y69E8Bn%$wSb$1M{&DR6eJsmh!X~V{Hcd5Bi4SD)!+PUgyQIVRE*`F!oP*=D) za&8Vj^bxf1%VBuBr*FOY-DZ^8|22u26tUg(DC~90ZPRwc(XrwxzT+% zeJY%kbtd?HZ$1>9gQd+e&6$r*)MKQ8LXzkyG9Vy;o4mu3eXhyVIf2GzrmS_

ATX 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 literal 0 HcmV?d00001 diff --git a/src/Captcha/Seznam/Backend/Client.php b/src/Captcha/Seznam/Backend/Client.php new file mode 100644 index 0000000..e97ce99 --- /dev/null +++ b/src/Captcha/Seznam/Backend/Client.php @@ -0,0 +1,34 @@ +serverHostname = $hostname; + $this->serverPort = $port; + } + + abstract public function create(): string; + + abstract public function getImage(string $hash): string; + + abstract public function check(string $hash, string $code): bool; + + public function setProxy(string $hostname, int $port): void + { + $this->proxyHostname = $hostname; + $this->proxyPort = $port; + } + +} diff --git a/src/Captcha/Seznam/Backend/HttpClient.php b/src/Captcha/Seznam/Backend/HttpClient.php new file mode 100644 index 0000000..416b57e --- /dev/null +++ b/src/Captcha/Seznam/Backend/HttpClient.php @@ -0,0 +1,74 @@ +call('captcha.create'); + + if ($result['status'] !== 200 || $result['data'] === false) { + throw new RuntimeException(sprintf('Captcha create failed: %s', print_r($result, true))); + } + + return $result['data']; + } + + public function getImage(string $hash): string + { + return sprintf( + 'https://%s:%d/%s?%s', + $this->serverHostname, + $this->serverPort, + 'captcha.getImage', + http_build_query(['hash' => $hash]) + ); + } + + public function check(string $hash, string $code): bool + { + $result = $this->call('captcha.check', ['hash' => $hash, 'code' => $code]); + + if (!in_array($result['status'], [200, 402, 403, 404], true)) { + throw new RuntimeException(sprintf('Captcha check failed: %s', print_r($result, true))); + } + + return $result['status'] === 200; + } + + /** + * @param array $params + * @return array{status: int, data: string|false} + */ + protected function call(string $methodName, array $params = []): array + { + $url = sprintf('https://%s:%d/%s?%s', $this->serverHostname, $this->serverPort, $methodName, http_build_query($params)); + $ch = curl_init($url); + + if ($ch === false) { + throw new RuntimeException('Failed to initialize curl'); + } + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + if ($this->proxyHostname !== null) { + curl_setopt($ch, CURLOPT_PROXY, $this->proxyHostname); + curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxyPort); + } + + /** @var string|false $response */ + $response = curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + + return [ + 'status' => $info['http_code'], + 'data' => $response, + ]; + } + +} diff --git a/src/Captcha/Seznam/DI/FormBinder.php b/src/Captcha/Seznam/DI/FormBinder.php new file mode 100644 index 0000000..9158d03 --- /dev/null +++ b/src/Captcha/Seznam/DI/FormBinder.php @@ -0,0 +1,20 @@ + $container[$name] = new SeznamCaptchaContainer($factory) // @phpcs:ignore + ); + } + +} diff --git a/src/Captcha/Seznam/DI/SeznamCaptchaExtension.php b/src/Captcha/Seznam/DI/SeznamCaptchaExtension.php new file mode 100644 index 0000000..dd8d434 --- /dev/null +++ b/src/Captcha/Seznam/DI/SeznamCaptchaExtension.php @@ -0,0 +1,54 @@ + Expect::bool()->default(true), + ]); + } + + public function loadConfiguration(): void + { + $builder = $this->getContainerBuilder(); + + $client = $builder->addDefinition($this->prefix('client')) + ->setFactory(HttpClient::class, ['captcha.seznam.cz', 443]); + + $builder->addDefinition($this->prefix('factory')) + ->setType(Factory::class) + ->setFactory(SeznamFactory::class, [$client]); + } + + public function afterCompile(ClassType $class): void + { + if ($this->config->auto === true) { + $method = $class->getMethod('initialize'); + $method->addBody( + '?::bind($this->getService(?));', + [ + new Literal(FormBinder::class), + $this->prefix('factory'), + ] + ); + } + } + +} diff --git a/src/Captcha/Seznam/Exception/RuntimeException.php b/src/Captcha/Seznam/Exception/RuntimeException.php new file mode 100644 index 0000000..a63a0b8 --- /dev/null +++ b/src/Captcha/Seznam/Exception/RuntimeException.php @@ -0,0 +1,8 @@ +provider = $factory->createProvider(); + $this->validator = $factory->createValidator(); + + $imageControl = new class ('Captcha') extends BaseControl { + + private string $imageUrl = ''; + + public function __construct(string $label) + { + parent::__construct($label); + + $this->control = Html::el('img'); + $this->control->addClass('captcha-image seznam-captcha-image'); + } + + public function setImageUrl(string $url): void + { + $this->imageUrl = $url; + } + + public function getControl(): Html + { + $img = parent::getControl(); + assert($img instanceof Html); + + $img->addAttributes(['src' => $this->imageUrl]); + + return $img; + } + + }; + $imageControl->setImageUrl($this->provider->getImage()); + + $codeInput = new TextInput('Code', 5); + $codeInput->getControlPrototype()->addClass('captcha-input seznam-captcha-input'); + + $hashField = new HiddenField($this->provider->getHash()); + + $this['image'] = $imageControl; + $this['code'] = $codeInput; + $this['hash'] = $hashField; + } + + public function getImage(): BaseControl + { + $control = $this->getComponent('image'); + assert($control instanceof BaseControl); + + return $control; + } + + public function getCode(): TextInput + { + $control = $this->getComponent('code'); + assert($control instanceof TextInput); + + return $control; + } + + public function getHash(): HiddenField + { + $control = $this->getComponent('hash'); + assert($control instanceof HiddenField); + + return $control; + } + + public function verify(): bool + { + /** @var Form $form */ + $form = $this->getForm(); + + /** @var string $hash */ + $hash = $form->getHttpData(Form::DataLine, $this->getHash()->getHtmlName()); + + /** @var string $code */ + $code = $form->getHttpData(Form::DataLine, $this->getCode()->getHtmlName()); + + return $this->validator->validate($code, $hash); + } + + public function getValidator(): Validator + { + return $this->validator; + } + + public function getProvider(): Provider + { + return $this->provider; + } + +} diff --git a/src/Captcha/Seznam/Provider/Provider.php b/src/Captcha/Seznam/Provider/Provider.php new file mode 100644 index 0000000..a0b820e --- /dev/null +++ b/src/Captcha/Seznam/Provider/Provider.php @@ -0,0 +1,14 @@ +client = $client; + $this->hash = $client->create(); + } + + public function getHash(): string + { + return $this->hash; + } + + public function getImage(): string + { + return $this->client->getImage($this->hash); + } + + public function check(string $code, string $hash): bool + { + return $this->client->check($hash, $code); + } + +} diff --git a/src/Captcha/Seznam/SeznamFactory.php b/src/Captcha/Seznam/SeznamFactory.php new file mode 100644 index 0000000..ee32fe7 --- /dev/null +++ b/src/Captcha/Seznam/SeznamFactory.php @@ -0,0 +1,31 @@ +client = $client; + } + + public function createValidator(): Validator + { + return new SeznamValidator($this->createProvider()); + } + + public function createProvider(): Provider + { + return new SeznamProvider($this->client); + } + +} diff --git a/src/Captcha/Seznam/Validator/SeznamValidator.php b/src/Captcha/Seznam/Validator/SeznamValidator.php new file mode 100644 index 0000000..cd13b86 --- /dev/null +++ b/src/Captcha/Seznam/Validator/SeznamValidator.php @@ -0,0 +1,22 @@ +provider = $provider; + } + + public function validate(string $code, string $hash): bool + { + return $this->provider->check($code, $hash); + } + +} diff --git a/src/Captcha/Seznam/Validator/Validator.php b/src/Captcha/Seznam/Validator/Validator.php new file mode 100644 index 0000000..433ff60 --- /dev/null +++ b/src/Captcha/Seznam/Validator/Validator.php @@ -0,0 +1,10 @@ +shouldReceive('getHash') + ->andReturn($hash) + ->shouldReceive('getImage') + ->andReturn($imageUrl) + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createProvider') + ->andReturn($provider) + ->getMock(); + + FormBinder::bind($factory); + + $form = new Form(); + $captcha = $form->addSeznamCaptcha(); + + Assert::type(SeznamCaptchaContainer::class, $captcha); + Assert::type(BaseControl::class, $captcha['image']); + Assert::type(TextInput::class, $captcha['code']); + Assert::type(HiddenField::class, $captcha['hash']); + + Assert::equal($hash, $captcha['hash']->getValue()); +}); diff --git a/tests/Cases/Seznam/Form/SeznamCaptchaContainer.phpt b/tests/Cases/Seznam/Form/SeznamCaptchaContainer.phpt new file mode 100644 index 0000000..f8f0997 --- /dev/null +++ b/tests/Cases/Seznam/Form/SeznamCaptchaContainer.phpt @@ -0,0 +1,97 @@ +shouldReceive('getHash') + ->andReturn($hash) + ->shouldReceive('getImage') + ->andReturn($imageUrl) + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createProvider') + ->andReturn($provider) + ->getMock(); + + $captcha = new SeznamCaptchaContainer($factory); + + $form = new Form(); + $form['captcha'] = $captcha; + + Assert::type(BaseControl::class, $captcha['image']); + Assert::type(TextInput::class, $captcha['code']); + Assert::type(HiddenField::class, $captcha['hash']); + + Assert::equal($hash, $captcha->getHash()->getValue()); + Assert::contains($imageUrl, (string) $captcha->getImage()->getControl()); +}); + +Toolkit::test(function (): void { + $validator = Mockery::mock(Validator::class) + ->shouldReceive('validate') + ->andReturn(true) + ->getMock(); + + $provider = Mockery::mock(Provider::class) + ->shouldReceive('getHash') + ->andReturn('hash') + ->shouldReceive('getImage') + ->andReturn('image') + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createProvider') + ->andReturn($provider) + ->getMock(); + + $captcha = new SeznamCaptchaContainer($factory); + + Assert::true($captcha->getValidator()->validate('foo', 'bar')); +}); + +Toolkit::test(function (): void { + $validator = Mockery::mock(Validator::class) + ->shouldReceive('validate') + ->andReturn(false) + ->getMock(); + + $provider = Mockery::mock(Provider::class) + ->shouldReceive('getHash') + ->andReturn('hash') + ->shouldReceive('getImage') + ->andReturn('image') + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createProvider') + ->andReturn($provider) + ->getMock(); + + $captcha = new SeznamCaptchaContainer($factory); + + Assert::false($captcha->getValidator()->validate('foo', 'bar')); +});