Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Binary file added .docs/seznam-captcha.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions src/Captcha/Seznam/Backend/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace Contributte\Forms\Captcha\Seznam\Backend;

abstract class Client
{

protected string $serverHostname;

protected int $serverPort;

protected ?string $proxyHostname = null;

protected ?int $proxyPort = null;

public function __construct(string $hostname, int $port)
{
$this->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;
}

}
74 changes: 74 additions & 0 deletions src/Captcha/Seznam/Backend/HttpClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);

namespace Contributte\Forms\Captcha\Seznam\Backend;

use Contributte\Forms\Captcha\Seznam\Exception\RuntimeException;

class HttpClient extends Client
{

public function create(): string
{
$result = $this->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<string, mixed> $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,
];
}

}
20 changes: 20 additions & 0 deletions src/Captcha/Seznam/DI/FormBinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace Contributte\Forms\Captcha\Seznam\DI;

use Contributte\Forms\Captcha\Seznam\Factory;
use Contributte\Forms\Captcha\Seznam\Form\SeznamCaptchaContainer;
use Nette\Forms\Container;

final class FormBinder
{

public static function bind(Factory $factory): void
{
Container::extensionMethod(
'addSeznamCaptcha',
fn (Container $container, string $name = 'captcha'): SeznamCaptchaContainer => $container[$name] = new SeznamCaptchaContainer($factory) // @phpcs:ignore
);
}

}
54 changes: 54 additions & 0 deletions src/Captcha/Seznam/DI/SeznamCaptchaExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace Contributte\Forms\Captcha\Seznam\DI;

use Contributte\Forms\Captcha\Seznam\Backend\HttpClient;
use Contributte\Forms\Captcha\Seznam\Factory;
use Contributte\Forms\Captcha\Seznam\SeznamFactory;
use Nette\DI\CompilerExtension;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Nette\Schema\Expect;
use Nette\Schema\Schema;
use stdClass;

/**
* @property-read stdClass $config
*/
final class SeznamCaptchaExtension extends CompilerExtension
{

public function getConfigSchema(): Schema
{
return Expect::structure([
'auto' => 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'),
]
);
}
}

}
8 changes: 8 additions & 0 deletions src/Captcha/Seznam/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace Contributte\Forms\Captcha\Seznam\Exception;

class RuntimeException extends \RuntimeException
{

}
15 changes: 15 additions & 0 deletions src/Captcha/Seznam/Factory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types = 1);

namespace Contributte\Forms\Captcha\Seznam;

use Contributte\Forms\Captcha\Seznam\Provider\Provider;
use Contributte\Forms\Captcha\Seznam\Validator\Validator;

interface Factory
{

public function createValidator(): Validator;

public function createProvider(): Provider;

}
115 changes: 115 additions & 0 deletions src/Captcha/Seznam/Form/SeznamCaptchaContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php declare(strict_types = 1);

namespace Contributte\Forms\Captcha\Seznam\Form;

use Contributte\Forms\Captcha\Seznam\Factory;
use Contributte\Forms\Captcha\Seznam\Provider\Provider;
use Contributte\Forms\Captcha\Seznam\Validator\Validator;
use Nette\Forms\Container;
use Nette\Forms\Controls\BaseControl;
use Nette\Forms\Controls\HiddenField;
use Nette\Forms\Controls\TextInput;
use Nette\Forms\Form;
use Nette\Utils\Html;

class SeznamCaptchaContainer extends Container
{

private Validator $validator;

private Provider $provider;

public function __construct(Factory $factory)
{
$this->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;
}

}
Loading