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
71 changes: 71 additions & 0 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
- [StandaloneFormFactoryExtension](#standalone-form-factory) (Nette\Forms\Form)
- Controls
- [Date/time inputs](#date-time-inputs) (DateTimeInput, DateInput, TimeInput)
- Captcha
- [Wordcha](#wordcha) (Question-based captcha)

## Setup

Expand Down Expand Up @@ -346,3 +348,72 @@ $control->setValueAs(DateTime::class); // value in input timezone as \DateTime
$control->setValueInTzAs(DateTime::class); // value in server default timezone as \DateTime
$control->setValueInTzAs(DateTime::class, new DateTimeZone('Americe/New_York')); // value in given timezone as \DateTime
```

## Captcha

### Wordcha

Question-based captcha for Nette Framework / Forms.

#### Wordcha Setup

Register extension

```yaml
extensions:
wordcha: Contributte\Forms\Captcha\Wordcha\DI\WordchaExtension
```

#### Wordcha Configuration

At the beginning you should pick the right datasource.

##### Numeric datasource

```yaml
wordcha:
datasource: numeric
```

##### Question datasource

```yaml
wordcha:
datasource: questions
questions:
"Question a?": "a"
"Question b?": "b"
```

#### Wordcha Form Usage

```php
use Nette\Application\UI\Form;

protected function createComponentForm()
{
$form = new Form();

$form->addWordcha('wordcha')
->getQuestion()
->setRequired('Please answer antispam question');

$form->addSubmit('send');

$form->onValidate[] = function (Form $form) {
if ($form['wordcha']->verify() !== TRUE) {
$form->addError('Are you robot?');
}
};

$form->onSuccess[] = function (Form $form) {
dump($form['wordcha']);
};

return $form;
}
```

#### Wordcha Example

![captcha](wordcha.png)
Binary file added .docs/wordcha.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/Captcha/Wordcha/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\Wordcha\DI;

use Contributte\Forms\Captcha\Wordcha\Factory;
use Contributte\Forms\Captcha\Wordcha\Form\WordchaContainer;
use Nette\Forms\Container;

final class FormBinder
{

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

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

namespace Contributte\Forms\Captcha\Wordcha\DI;

use Contributte\Forms\Captcha\Wordcha\DataSource\DataSource;
use Contributte\Forms\Captcha\Wordcha\DataSource\NumericDataSource;
use Contributte\Forms\Captcha\Wordcha\DataSource\QuestionDataSource;
use Contributte\Forms\Captcha\Wordcha\Factory;
use Contributte\Forms\Captcha\Wordcha\WordchaFactory;
use Contributte\Forms\Captcha\Wordcha\WordchaUniqueFactory;
use Nette\DI\CompilerExtension;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Nette\Schema\Expect;
use Nette\Schema\Schema;
use Nette\Utils\AssertionException;
use stdClass;

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

public const DATASOURCE_NUMERIC = 'numeric';
public const DATASOURCE_QUESTIONS = 'questions';

public const DATASOURCES = [
self::DATASOURCE_NUMERIC,
self::DATASOURCE_QUESTIONS,
];

private bool $debugMode;

public function __construct(bool $debugMode = false)
{
$this->debugMode = $debugMode;
}

public function getConfigSchema(): Schema
{
return Expect::structure([
'auto' => Expect::bool()->default(true),
'datasource' => Expect::anyOf(...self::DATASOURCES)->default(self::DATASOURCE_NUMERIC),
'questions' => Expect::listOf('string'),
]);
}

/**
* Register services
*
* @throws AssertionException
*/
public function loadConfiguration(): void
{
$builder = $this->getContainerBuilder();

// Add datasource
$dataSource = $builder->addDefinition($this->prefix('dataSource'))
->setType(DataSource::class);

if ($this->config->datasource === self::DATASOURCE_NUMERIC) {
$dataSource->setFactory(NumericDataSource::class);
} elseif ($this->config->datasource === self::DATASOURCE_QUESTIONS) {
$dataSource->setFactory(QuestionDataSource::class, [$this->config->questions]);
}

// Add factory
$factory = $builder->addDefinition($this->prefix('factory'))
->setType(Factory::class);
if ($this->debugMode) {
$factory->setFactory(WordchaFactory::class, [$dataSource]);
} else {
$uniqueKey = sha1(random_bytes(10) . microtime(true));
$factory->setFactory(WordchaUniqueFactory::class, [$dataSource, $uniqueKey]);
}
}

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'),
]
);
}
}

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

namespace Contributte\Forms\Captcha\Wordcha\DataSource;

interface DataSource
{

public function get(): Pair;

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

namespace Contributte\Forms\Captcha\Wordcha\DataSource;

use Contributte\Forms\Captcha\Wordcha\Exception\LogicalException;

class NumericDataSource implements DataSource
{

private int $min;

private int $max;

public function __construct(int $min = 0, int $max = 10)
{
if ($min > $max) {
throw new LogicalException(sprintf('Min (%d) must be less than or equal to max (%d)', $min, $max));
}

$this->min = $min;
$this->max = $max;
}

public function get(): Pair
{
$numberA = $this->generateNumber();
$numberB = $this->generateNumber();

$question = sprintf('%s + %s', $numberA, $numberB);
$answer = $numberA + $numberB;

return new Pair($question, (string) $answer);
}

private function generateNumber(): int
{
return random_int($this->min, $this->max);
}

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

namespace Contributte\Forms\Captcha\Wordcha\DataSource;

class Pair
{

private string $question;

private string $answer;

public function __construct(string $question, string $answer)
{
$this->question = $question;
$this->answer = $answer;
}

public function getQuestion(): string
{
return $this->question;
}

public function getAnswer(): string
{
return $this->answer;
}

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

namespace Contributte\Forms\Captcha\Wordcha\DataSource;

use Contributte\Forms\Captcha\Wordcha\Exception\LogicalException;
use Exception;

class QuestionDataSource implements DataSource
{

/** @var array<string, string> Pairs of question:answer */
private array $questions;

/**
* @param array<string, string> $questions
*/
public function __construct(array $questions)
{
$this->questions = $questions;
}

/**
* @throws Exception
*/
public function get(): Pair
{
if ($this->questions === []) {
throw new LogicalException('Questions are empty');
}

$question = array_rand($this->questions);
$answer = $this->questions[$question];

return new Pair($question, $answer);
}

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

namespace Contributte\Forms\Captcha\Wordcha\Exception;

class LogicalException extends \LogicException
{

}
8 changes: 8 additions & 0 deletions src/Captcha/Wordcha/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\Wordcha\Exception;

class RuntimeException extends \RuntimeException
{

}
15 changes: 15 additions & 0 deletions src/Captcha/Wordcha/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\Wordcha;

use Contributte\Forms\Captcha\Wordcha\Generator\Generator;
use Contributte\Forms\Captcha\Wordcha\Validator\Validator;

interface Factory
{

public function createValidator(): Validator;

public function createGenerator(): Generator;

}
Loading