Skip to content

Commit 5c5ebbf

Browse files
committed
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
1 parent 9584a27 commit 5c5ebbf

File tree

16 files changed

+631
-0
lines changed

16 files changed

+631
-0
lines changed

.docs/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [Date/time inputs](#date-time-inputs) (DateTimeInput, DateInput, TimeInput)
1111
- Captcha
1212
- [Wordcha](#wordcha) (Question-based captcha)
13+
- [Seznam Captcha](#seznam-captcha) (Image-based captcha)
1314

1415
## Setup
1516

@@ -417,3 +418,56 @@ protected function createComponentForm()
417418
#### Wordcha Example
418419

419420
![captcha](wordcha.png)
421+
422+
### Seznam Captcha
423+
424+
Image-based captcha using [Seznam.cz Captcha](https://captcha.seznam.cz) service.
425+
426+
#### Seznam Captcha Setup
427+
428+
Register extension
429+
430+
```yaml
431+
extensions:
432+
seznamCaptcha: Contributte\Forms\Captcha\Seznam\DI\SeznamCaptchaExtension
433+
```
434+
435+
#### Seznam Captcha Configuration
436+
437+
```yaml
438+
seznamCaptcha:
439+
auto: true # Automatically bind addSeznamCaptcha to forms
440+
```
441+
442+
#### Seznam Captcha Form Usage
443+
444+
```php
445+
use Nette\Application\UI\Form;
446+
447+
protected function createComponentForm()
448+
{
449+
$form = new Form();
450+
451+
$form->addSeznamCaptcha('captcha')
452+
->getCode()
453+
->setRequired('Please enter the captcha code');
454+
455+
$form->addSubmit('send');
456+
457+
$form->onValidate[] = function (Form $form) {
458+
if ($form['captcha']->verify() !== TRUE) {
459+
$form->addError('Are you robot?');
460+
}
461+
};
462+
463+
$form->onSuccess[] = function (Form $form) {
464+
dump($form['captcha']);
465+
};
466+
467+
return $form;
468+
}
469+
```
470+
471+
#### Seznam Captcha Example
472+
473+
![captcha](seznam-captcha.png)

.docs/seznam-captcha.png

7.08 KB
Loading
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Forms\Captcha\Seznam\Backend;
4+
5+
abstract class Client
6+
{
7+
8+
protected string $serverHostname;
9+
10+
protected int $serverPort;
11+
12+
protected ?string $proxyHostname = null;
13+
14+
protected ?int $proxyPort = null;
15+
16+
public function __construct(string $hostname, int $port)
17+
{
18+
$this->serverHostname = $hostname;
19+
$this->serverPort = $port;
20+
}
21+
22+
abstract public function create(): string;
23+
24+
abstract public function getImage(string $hash): string;
25+
26+
abstract public function check(string $hash, string $code): bool;
27+
28+
public function setProxy(string $hostname, int $port): void
29+
{
30+
$this->proxyHostname = $hostname;
31+
$this->proxyPort = $port;
32+
}
33+
34+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Forms\Captcha\Seznam\Backend;
4+
5+
use Contributte\Forms\Captcha\Seznam\Exception\RuntimeException;
6+
7+
class HttpClient extends Client
8+
{
9+
10+
public function create(): string
11+
{
12+
$result = $this->call('captcha.create');
13+
14+
if ($result['status'] !== 200 || $result['data'] === false) {
15+
throw new RuntimeException(sprintf('Captcha create failed: %s', print_r($result, true)));
16+
}
17+
18+
return $result['data'];
19+
}
20+
21+
public function getImage(string $hash): string
22+
{
23+
return sprintf(
24+
'https://%s:%d/%s?%s',
25+
$this->serverHostname,
26+
$this->serverPort,
27+
'captcha.getImage',
28+
http_build_query(['hash' => $hash])
29+
);
30+
}
31+
32+
public function check(string $hash, string $code): bool
33+
{
34+
$result = $this->call('captcha.check', ['hash' => $hash, 'code' => $code]);
35+
36+
if (!in_array($result['status'], [200, 402, 403, 404], true)) {
37+
throw new RuntimeException(sprintf('Captcha check failed: %s', print_r($result, true)));
38+
}
39+
40+
return $result['status'] === 200;
41+
}
42+
43+
/**
44+
* @param array<string, mixed> $params
45+
* @return array{status: int, data: string|false}
46+
*/
47+
protected function call(string $methodName, array $params = []): array
48+
{
49+
$url = sprintf('https://%s:%d/%s?%s', $this->serverHostname, $this->serverPort, $methodName, http_build_query($params));
50+
$ch = curl_init($url);
51+
52+
if ($ch === false) {
53+
throw new RuntimeException('Failed to initialize curl');
54+
}
55+
56+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
57+
58+
if ($this->proxyHostname !== null) {
59+
curl_setopt($ch, CURLOPT_PROXY, $this->proxyHostname);
60+
curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxyPort);
61+
}
62+
63+
/** @var string|false $response */
64+
$response = curl_exec($ch);
65+
$info = curl_getinfo($ch);
66+
curl_close($ch);
67+
68+
return [
69+
'status' => $info['http_code'],
70+
'data' => $response,
71+
];
72+
}
73+
74+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Forms\Captcha\Seznam\DI;
4+
5+
use Contributte\Forms\Captcha\Seznam\Factory;
6+
use Contributte\Forms\Captcha\Seznam\Form\SeznamCaptchaContainer;
7+
use Nette\Forms\Container;
8+
9+
final class FormBinder
10+
{
11+
12+
public static function bind(Factory $factory): void
13+
{
14+
Container::extensionMethod(
15+
'addSeznamCaptcha',
16+
fn (Container $container, string $name = 'captcha'): SeznamCaptchaContainer => $container[$name] = new SeznamCaptchaContainer($factory) // @phpcs:ignore
17+
);
18+
}
19+
20+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Forms\Captcha\Seznam\DI;
4+
5+
use Contributte\Forms\Captcha\Seznam\Backend\HttpClient;
6+
use Contributte\Forms\Captcha\Seznam\Factory;
7+
use Contributte\Forms\Captcha\Seznam\SeznamFactory;
8+
use Nette\DI\CompilerExtension;
9+
use Nette\PhpGenerator\ClassType;
10+
use Nette\PhpGenerator\Literal;
11+
use Nette\Schema\Expect;
12+
use Nette\Schema\Schema;
13+
use stdClass;
14+
15+
/**
16+
* @property-read stdClass $config
17+
*/
18+
final class SeznamCaptchaExtension extends CompilerExtension
19+
{
20+
21+
public function getConfigSchema(): Schema
22+
{
23+
return Expect::structure([
24+
'auto' => Expect::bool()->default(true),
25+
]);
26+
}
27+
28+
public function loadConfiguration(): void
29+
{
30+
$builder = $this->getContainerBuilder();
31+
32+
$client = $builder->addDefinition($this->prefix('client'))
33+
->setFactory(HttpClient::class, ['captcha.seznam.cz', 443]);
34+
35+
$builder->addDefinition($this->prefix('factory'))
36+
->setType(Factory::class)
37+
->setFactory(SeznamFactory::class, [$client]);
38+
}
39+
40+
public function afterCompile(ClassType $class): void
41+
{
42+
if ($this->config->auto === true) {
43+
$method = $class->getMethod('initialize');
44+
$method->addBody(
45+
'?::bind($this->getService(?));',
46+
[
47+
new Literal(FormBinder::class),
48+
$this->prefix('factory'),
49+
]
50+
);
51+
}
52+
}
53+
54+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Forms\Captcha\Seznam\Exception;
4+
5+
class RuntimeException extends \RuntimeException
6+
{
7+
8+
}

src/Captcha/Seznam/Factory.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Forms\Captcha\Seznam;
4+
5+
use Contributte\Forms\Captcha\Seznam\Provider\Provider;
6+
use Contributte\Forms\Captcha\Seznam\Validator\Validator;
7+
8+
interface Factory
9+
{
10+
11+
public function createValidator(): Validator;
12+
13+
public function createProvider(): Provider;
14+
15+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Forms\Captcha\Seznam\Form;
4+
5+
use Contributte\Forms\Captcha\Seznam\Factory;
6+
use Contributte\Forms\Captcha\Seznam\Provider\Provider;
7+
use Contributte\Forms\Captcha\Seznam\Validator\Validator;
8+
use Nette\Forms\Container;
9+
use Nette\Forms\Controls\BaseControl;
10+
use Nette\Forms\Controls\HiddenField;
11+
use Nette\Forms\Controls\TextInput;
12+
use Nette\Forms\Form;
13+
use Nette\Utils\Html;
14+
15+
class SeznamCaptchaContainer extends Container
16+
{
17+
18+
private Validator $validator;
19+
20+
private Provider $provider;
21+
22+
public function __construct(Factory $factory)
23+
{
24+
$this->provider = $factory->createProvider();
25+
$this->validator = $factory->createValidator();
26+
27+
$imageControl = new class ('Captcha') extends BaseControl {
28+
29+
private string $imageUrl = '';
30+
31+
public function __construct(string $label)
32+
{
33+
parent::__construct($label);
34+
35+
$this->control = Html::el('img');
36+
$this->control->addClass('captcha-image seznam-captcha-image');
37+
}
38+
39+
public function setImageUrl(string $url): void
40+
{
41+
$this->imageUrl = $url;
42+
}
43+
44+
public function getControl(): Html
45+
{
46+
$img = parent::getControl();
47+
assert($img instanceof Html);
48+
49+
$img->addAttributes(['src' => $this->imageUrl]);
50+
51+
return $img;
52+
}
53+
54+
};
55+
$imageControl->setImageUrl($this->provider->getImage());
56+
57+
$codeInput = new TextInput('Code', 5);
58+
$codeInput->getControlPrototype()->addClass('captcha-input seznam-captcha-input');
59+
60+
$hashField = new HiddenField($this->provider->getHash());
61+
62+
$this['image'] = $imageControl;
63+
$this['code'] = $codeInput;
64+
$this['hash'] = $hashField;
65+
}
66+
67+
public function getImage(): BaseControl
68+
{
69+
$control = $this->getComponent('image');
70+
assert($control instanceof BaseControl);
71+
72+
return $control;
73+
}
74+
75+
public function getCode(): TextInput
76+
{
77+
$control = $this->getComponent('code');
78+
assert($control instanceof TextInput);
79+
80+
return $control;
81+
}
82+
83+
public function getHash(): HiddenField
84+
{
85+
$control = $this->getComponent('hash');
86+
assert($control instanceof HiddenField);
87+
88+
return $control;
89+
}
90+
91+
public function verify(): bool
92+
{
93+
/** @var Form $form */
94+
$form = $this->getForm();
95+
96+
/** @var string $hash */
97+
$hash = $form->getHttpData(Form::DataLine, $this->getHash()->getHtmlName());
98+
99+
/** @var string $code */
100+
$code = $form->getHttpData(Form::DataLine, $this->getCode()->getHtmlName());
101+
102+
return $this->validator->validate($code, $hash);
103+
}
104+
105+
public function getValidator(): Validator
106+
{
107+
return $this->validator;
108+
}
109+
110+
public function getProvider(): Provider
111+
{
112+
return $this->provider;
113+
}
114+
115+
}

0 commit comments

Comments
 (0)