From 2c392dd826fdef407e3b75b21e54439f58a9ef9d Mon Sep 17 00:00:00 2001 From: AI Date: Sun, 28 Dec 2025 14:07:36 +0000 Subject: [PATCH 1/3] feat: add Wordcha question-based captcha from contributte/wordcha Merge source code, tests, and documentation from the contributte/wordcha repository with namespace updated from Contributte\Wordcha to Contributte\Forms\Wordcha. --- .docs/README.md | 68 +++++++++++++ .docs/wordcha.png | Bin 0 -> 1715 bytes src/Wordcha/DI/FormBinder.php | 20 ++++ src/Wordcha/DI/WordchaExtension.php | 94 +++++++++++++++++ src/Wordcha/DataSource/DataSource.php | 10 ++ src/Wordcha/DataSource/NumericDataSource.php | 40 ++++++++ src/Wordcha/DataSource/Pair.php | 28 +++++ src/Wordcha/DataSource/QuestionDataSource.php | 37 +++++++ src/Wordcha/Exception/LogicalException.php | 8 ++ src/Wordcha/Exception/RuntimeException.php | 8 ++ src/Wordcha/Factory.php | 15 +++ src/Wordcha/Form/WordchaContainer.php | 79 ++++++++++++++ src/Wordcha/Generator/Generator.php | 12 +++ src/Wordcha/Generator/Security.php | 28 +++++ src/Wordcha/Generator/WordchaGenerator.php | 42 ++++++++ src/Wordcha/Validator/Validator.php | 10 ++ src/Wordcha/Validator/WordchaValidator.php | 24 +++++ src/Wordcha/WordchaFactory.php | 37 +++++++ src/Wordcha/WordchaUniqueFactory.php | 32 ++++++ tests/Cases/Wordcha/DI/FormBinder.phpt | 46 +++++++++ .../Wordcha/DataSource/NumericDataSource.phpt | 65 ++++++++++++ .../Cases/Wordcha/Forms/WordchaContainer.phpt | 96 ++++++++++++++++++ 22 files changed, 799 insertions(+) create mode 100644 .docs/wordcha.png create mode 100644 src/Wordcha/DI/FormBinder.php create mode 100644 src/Wordcha/DI/WordchaExtension.php create mode 100644 src/Wordcha/DataSource/DataSource.php create mode 100644 src/Wordcha/DataSource/NumericDataSource.php create mode 100644 src/Wordcha/DataSource/Pair.php create mode 100644 src/Wordcha/DataSource/QuestionDataSource.php create mode 100644 src/Wordcha/Exception/LogicalException.php create mode 100644 src/Wordcha/Exception/RuntimeException.php create mode 100644 src/Wordcha/Factory.php create mode 100644 src/Wordcha/Form/WordchaContainer.php create mode 100644 src/Wordcha/Generator/Generator.php create mode 100644 src/Wordcha/Generator/Security.php create mode 100644 src/Wordcha/Generator/WordchaGenerator.php create mode 100644 src/Wordcha/Validator/Validator.php create mode 100644 src/Wordcha/Validator/WordchaValidator.php create mode 100644 src/Wordcha/WordchaFactory.php create mode 100644 src/Wordcha/WordchaUniqueFactory.php create mode 100644 tests/Cases/Wordcha/DI/FormBinder.phpt create mode 100644 tests/Cases/Wordcha/DataSource/NumericDataSource.phpt create mode 100644 tests/Cases/Wordcha/Forms/WordchaContainer.phpt diff --git a/.docs/README.md b/.docs/README.md index 9fb1440..7bede1b 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -8,6 +8,7 @@ - [StandaloneFormFactoryExtension](#standalone-form-factory) (Nette\Forms\Form) - Controls - [Date/time inputs](#date-time-inputs) (DateTimeInput, DateInput, TimeInput) +- [Wordcha](#wordcha) (Question-based captcha) ## Setup @@ -346,3 +347,70 @@ $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 ``` + +## Wordcha + +Question-based captcha for Nette Framework / Forms. + +### Wordcha Setup + +Register extension + +```yaml +extensions: + wordcha: Contributte\Forms\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) diff --git a/.docs/wordcha.png b/.docs/wordcha.png new file mode 100644 index 0000000000000000000000000000000000000000..2312908d0139f2b732fca745a68dfbcd13acaab5 GIT binary patch literal 1715 zcmb7_Yd9N*8png`jF#$6gs~cRIbA1~$~aBjf@Ur)GM6BAPv#Ot93m=;KBA*q%-pR> zo2c8W3x*_=R>ZB+xD<_0=2G1v?i5+hp7Y^+IUmmddEWQ;y#EjH$NxX)hP(4gRgfwG z062-fhVYbgPCnOF6y>{scjZ?(loITbC>0fz>ACB(@>84W=u7m9jUXlk6T$&ld~8hk zmkFVS@Nj$rE|w@!>VyLT$DNP}J5+MTAEH24FRU(4%8U3XR_pxbM}`)Cpo|gZNqnHW zZN^JSfpU@S3x3e@AWC=E`&Vtz%dWwun%{ttpg6C1fp+fHvC|3)yc6Lk$aCpxhVgx; zRZg(j$Iux|ooZdUf1xoDl3tG_VQjl7p$c+yI0577YmYc#8w8qB0+c9&lm!nxPJQ*C_F(L$xr2bXq(z-yf9?f?y%1O0Prh$^joK)P)Ea%M=`-E<{2 z@yLu+wYu{SVSc@=ii|}26FT}J;&abwSIBgw&U5Fjje&*d+duvO3AmRP{a%XmcgQw{ z>E7;sJ2bvboks-=>m_`*FJ0PE1;C%5-O#BbCi5OVajY6Y7}^o{TSRI>P`-~+_48BE zKd4pqR>0=CJ{DlGg~dbeHO!CXFovkApH}wRU--pStvh>bV>L`Vr1QdC!W;I7@Myj; z<>6-9?y%wEfUou?1Nh>8<4rZQtzwrPg?jtMHXbQ#W2Wr+3U)Y8a?ZTBxVNI-#InwH zQR#}~gtM#hP$W+Y08fr#EuoK;Tj(YsG-g!XNDr_axU%xurxQ`t!=uku)*hRwg+{YX zzdE#kz4z|qfMIV~!<2x*Y#yCzA@=M6b?63i-hUro)*rN@lv0QAnv5!=E3jA zF}CT&ZsDd;KPf9Gi=5R*eSguo{Avm;aq$6QbOQ14&}mv1>k>BvePa+ zAfa6v+|T1DK}{nb-64w&9)9}eHPvZX%{{z*txQ&iz?!vQ&Eo;@%*o;xws~9opMULV zd5qI%8!RXrxW!0c7)UDA;^w3;6lm?m`+kV))I(%D*%5JP|Au|4&#{}|E9w`|QpD-A z<`bFABPLY!yo~?CJ=vDv6Ae8Ui*=Q>o;44IfZT{gA`WN51E)g|bYk#$cO-HSrW(;Y zEvPBkg<4x#nRegG;`5O+4uHf19U}urkL3J}oHepyZ59rd1xEmtYTk)W9Q0?;$fB;_ zO$rS>mkrPL*8~EA%M$&NRLVr@A`di0oGZLNoL+x~*1D&lYVX|0+rwAEuPL)!riu3A4 zGC`chcs{jnYobjgJ*I*llo6LH}M!|v|T%W&r>L(JNx$*HIZ!=boKk|B4d-@!Zjy-Ctk6m z)$>$cs7{W`S_j_KtC*#~GA1baGlu7KqL!rX3*bP19npw)#{+{>S?U zxe6$0Y*Da{>c0)TfI3p+)Fa!}l$#K1>-P3`be81ncwNA5myE5~1$B-4J462yrT+`< aGQbhqZfs!f+I@NL0FaLEh(`OMjK2WWnM`f~ literal 0 HcmV?d00001 diff --git a/src/Wordcha/DI/FormBinder.php b/src/Wordcha/DI/FormBinder.php new file mode 100644 index 0000000..80c4417 --- /dev/null +++ b/src/Wordcha/DI/FormBinder.php @@ -0,0 +1,20 @@ + $container[$name] = new WordchaContainer($factory) // @phpcs:ignore + ); + } + +} diff --git a/src/Wordcha/DI/WordchaExtension.php b/src/Wordcha/DI/WordchaExtension.php new file mode 100644 index 0000000..b08f1e0 --- /dev/null +++ b/src/Wordcha/DI/WordchaExtension.php @@ -0,0 +1,94 @@ +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'), + ] + ); + } + } + +} diff --git a/src/Wordcha/DataSource/DataSource.php b/src/Wordcha/DataSource/DataSource.php new file mode 100644 index 0000000..963f88e --- /dev/null +++ b/src/Wordcha/DataSource/DataSource.php @@ -0,0 +1,10 @@ + $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); + } + +} diff --git a/src/Wordcha/DataSource/Pair.php b/src/Wordcha/DataSource/Pair.php new file mode 100644 index 0000000..4780077 --- /dev/null +++ b/src/Wordcha/DataSource/Pair.php @@ -0,0 +1,28 @@ +question = $question; + $this->answer = $answer; + } + + public function getQuestion(): string + { + return $this->question; + } + + public function getAnswer(): string + { + return $this->answer; + } + +} diff --git a/src/Wordcha/DataSource/QuestionDataSource.php b/src/Wordcha/DataSource/QuestionDataSource.php new file mode 100644 index 0000000..1387b82 --- /dev/null +++ b/src/Wordcha/DataSource/QuestionDataSource.php @@ -0,0 +1,37 @@ + Pairs of question:answer */ + private array $questions; + + /** + * @param array $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); + } + +} diff --git a/src/Wordcha/Exception/LogicalException.php b/src/Wordcha/Exception/LogicalException.php new file mode 100644 index 0000000..98e70dd --- /dev/null +++ b/src/Wordcha/Exception/LogicalException.php @@ -0,0 +1,8 @@ +validator = $factory->createValidator(); + $this->generator = $factory->createGenerator(); + + $security = $this->generator->generate(); + + $textInput = new TextInput($security->getQuestion()); + $textInput->setRequired(true); + + $hiddenField = new HiddenField($security->getHash()); + + $this['question'] = $textInput; + $this['hash'] = $hiddenField; + } + + public function getQuestion(): TextInput + { + $control = $this->getComponent('question'); + 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 $answer */ + $answer = $form->getHttpData(Form::DataLine, $this->getQuestion()->getHtmlName()); + + $answer = Strings::lower($answer); + + return $this->validator->validate($answer, $hash); + } + + public function getValidator(): Validator + { + return $this->validator; + } + + public function getGenerator(): Generator + { + return $this->generator; + } + +} diff --git a/src/Wordcha/Generator/Generator.php b/src/Wordcha/Generator/Generator.php new file mode 100644 index 0000000..19e096f --- /dev/null +++ b/src/Wordcha/Generator/Generator.php @@ -0,0 +1,12 @@ +question = $question; + $this->hash = $hash; + } + + public function getQuestion(): string + { + return $this->question; + } + + public function getHash(): string + { + return $this->hash; + } + +} diff --git a/src/Wordcha/Generator/WordchaGenerator.php b/src/Wordcha/Generator/WordchaGenerator.php new file mode 100644 index 0000000..0158971 --- /dev/null +++ b/src/Wordcha/Generator/WordchaGenerator.php @@ -0,0 +1,42 @@ +dataSource = $dataSource; + } + + public function setUniqueKey(string $uniqueKey): void + { + $this->uniqueKey = $uniqueKey; + } + + public function generate(): Security + { + $pair = $this->dataSource->get(); + $hash = $this->hash($pair->getAnswer()); + $question = $pair->getQuestion(); + + return new Security($question, $hash); + } + + public function hash(string $answer): string + { + if ($this->uniqueKey !== null) { + $answer .= $this->uniqueKey; + } + + return sha1(strtolower($answer)); + } + +} diff --git a/src/Wordcha/Validator/Validator.php b/src/Wordcha/Validator/Validator.php new file mode 100644 index 0000000..4f9f9f6 --- /dev/null +++ b/src/Wordcha/Validator/Validator.php @@ -0,0 +1,10 @@ +generator = $generator; + } + + public function validate(string $answer, string $hash): bool + { + $answerHash = $this->generator->hash($answer); + + return $hash === $answerHash; + } + +} diff --git a/src/Wordcha/WordchaFactory.php b/src/Wordcha/WordchaFactory.php new file mode 100644 index 0000000..36cfd75 --- /dev/null +++ b/src/Wordcha/WordchaFactory.php @@ -0,0 +1,37 @@ +dataSource = $dataSource; + } + + /** + * @return WordchaValidator + */ + public function createValidator(): Validator + { + return new WordchaValidator($this->createGenerator()); + } + + /** + * @return WordchaGenerator + */ + public function createGenerator(): Generator + { + return new WordchaGenerator($this->dataSource); + } + +} diff --git a/src/Wordcha/WordchaUniqueFactory.php b/src/Wordcha/WordchaUniqueFactory.php new file mode 100644 index 0000000..b16e6c6 --- /dev/null +++ b/src/Wordcha/WordchaUniqueFactory.php @@ -0,0 +1,32 @@ +uniqueKey = $uniqueKey; + } + + /** + * @return WordchaGenerator + */ + public function createGenerator(): Generator + { + $generator = parent::createGenerator(); + $generator->setUniqueKey($this->uniqueKey); + + return $generator; + } + +} diff --git a/tests/Cases/Wordcha/DI/FormBinder.phpt b/tests/Cases/Wordcha/DI/FormBinder.phpt new file mode 100644 index 0000000..74050ec --- /dev/null +++ b/tests/Cases/Wordcha/DI/FormBinder.phpt @@ -0,0 +1,46 @@ +shouldReceive('generate') + ->andReturn(new Security('..', $hash)) + ->getMock() + ->shouldReceive('hash') + ->andReturn($hash) + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createGenerator') + ->andReturn($generator) + ->getMock(); + + FormBinder::bind($factory); + + $form = new Form(); + $captcha = $form->addWordcha(); + + Assert::type(WordchaContainer::class, $captcha); + Assert::type(TextInput::class, $captcha['question']); + Assert::type(HiddenField::class, $captcha['hash']); + + Assert::equal($hash, $captcha['hash']->getValue()); +}); diff --git a/tests/Cases/Wordcha/DataSource/NumericDataSource.phpt b/tests/Cases/Wordcha/DataSource/NumericDataSource.phpt new file mode 100644 index 0000000..652efa0 --- /dev/null +++ b/tests/Cases/Wordcha/DataSource/NumericDataSource.phpt @@ -0,0 +1,65 @@ +get(); + + Assert::type('string', $pair->getQuestion()); + Assert::type('string', $pair->getAnswer()); + Assert::match('~^\d+ \+ \d+$~', $pair->getQuestion()); + + // Answer should be between 0 and 20 (0+0 to 10+10) + $answer = (int) $pair->getAnswer(); + Assert::true($answer >= 0 && $answer <= 20); +}); + +// Test custom min/max values +Toolkit::test(function (): void { + $dataSource = new NumericDataSource(5, 15); + $pair = $dataSource->get(); + + Assert::type('string', $pair->getQuestion()); + Assert::type('string', $pair->getAnswer()); + Assert::match('~^\d+ \+ \d+$~', $pair->getQuestion()); + + // Answer should be between 10 and 30 (5+5 to 15+15) + $answer = (int) $pair->getAnswer(); + Assert::true($answer >= 10 && $answer <= 30); +}); + +// Test min equals max +Toolkit::test(function (): void { + $dataSource = new NumericDataSource(5, 5); + $pair = $dataSource->get(); + + Assert::equal('5 + 5', $pair->getQuestion()); + Assert::equal('10', $pair->getAnswer()); +}); + +// Test min greater than max throws exception +Toolkit::test(function (): void { + Assert::exception(function (): void { + new NumericDataSource(10, 5); + }, LogicalException::class, 'Min (10) must be less than or equal to max (5)'); +}); + +// Test negative numbers +Toolkit::test(function (): void { + $dataSource = new NumericDataSource(-5, 5); + $pair = $dataSource->get(); + + Assert::type('string', $pair->getQuestion()); + Assert::type('string', $pair->getAnswer()); + + // Answer should be between -10 and 10 (-5+-5 to 5+5) + $answer = (int) $pair->getAnswer(); + Assert::true($answer >= -10 && $answer <= 10); +}); diff --git a/tests/Cases/Wordcha/Forms/WordchaContainer.phpt b/tests/Cases/Wordcha/Forms/WordchaContainer.phpt new file mode 100644 index 0000000..fa5c509 --- /dev/null +++ b/tests/Cases/Wordcha/Forms/WordchaContainer.phpt @@ -0,0 +1,96 @@ +shouldReceive('generate') + ->andReturn(new Security('..', $hash)) + ->getMock() + ->shouldReceive('hash') + ->andReturn($hash) + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createGenerator') + ->andReturn($generator) + ->getMock(); + + $captcha = new WordchaContainer($factory); + Assert::type(WordchaContainer::class, $captcha); + Assert::type(TextInput::class, $captcha['question']); + Assert::type(HiddenField::class, $captcha['hash']); + + Assert::equal($hash, $captcha['hash']->getValue()); +}); + +Toolkit::test(function (): void { + $hash = '12345'; + $validator = Mockery::mock(Validator::class) + ->shouldReceive('validate') + ->andReturn(true) + ->getMock(); + + $generator = Mockery::mock(Generator::class) + ->shouldReceive('generate') + ->andReturn(new Security('..', $hash)) + ->getMock() + ->shouldReceive('hash') + ->andReturn($hash) + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createGenerator') + ->andReturn($generator) + ->getMock(); + + $captcha = new WordchaContainer($factory); + $validator = $captcha->getValidator(); + + Assert::true($validator->validate('foo', 'bar')); +}); + +Toolkit::test(function (): void { + $hash = '12345'; + + $validator = Mockery::mock(Validator::class) + ->shouldReceive('validate') + ->andReturn(false) + ->getMock(); + + $generator = Mockery::mock(Generator::class) + ->shouldReceive('generate') + ->andReturn(new Security('..', $hash)) + ->getMock() + ->shouldReceive('hash') + ->andReturn($hash) + ->getMock(); + + $factory = Mockery::mock(Factory::class) + ->shouldReceive('createValidator') + ->andReturn($validator) + ->shouldReceive('createGenerator') + ->andReturn($generator) + ->getMock(); + + $captcha = new WordchaContainer($factory); + $validator = $captcha->getValidator(); + + Assert::false($validator->validate('foo', 'bar')); +}); From 41e391c9fb350e225f82ec0b4f8ec34837fad8e4 Mon Sep 17 00:00:00 2001 From: AI Date: Sun, 28 Dec 2025 14:22:02 +0000 Subject: [PATCH 2/3] refactor: move Wordcha to src/Captcha/Wordcha Move Wordcha source files to src/Captcha/Wordcha and update namespaces from Contributte\Forms\Wordcha to Contributte\Forms\Captcha\Wordcha. Update documentation with a Captcha section containing Wordcha. --- .docs/README.md | 21 +++++++++++-------- src/{ => Captcha}/Wordcha/DI/FormBinder.php | 6 +++--- .../Wordcha/DI/WordchaExtension.php | 14 ++++++------- .../Wordcha/DataSource/DataSource.php | 2 +- .../Wordcha/DataSource/NumericDataSource.php | 4 ++-- src/{ => Captcha}/Wordcha/DataSource/Pair.php | 2 +- .../Wordcha/DataSource/QuestionDataSource.php | 4 ++-- .../Wordcha/Exception/LogicalException.php | 2 +- .../Wordcha/Exception/RuntimeException.php | 2 +- src/Captcha/Wordcha/Factory.php | 15 +++++++++++++ .../Wordcha/Form/WordchaContainer.php | 8 +++---- .../Wordcha/Generator/Generator.php | 2 +- .../Wordcha/Generator/Security.php | 2 +- .../Wordcha/Generator/WordchaGenerator.php | 4 ++-- .../Wordcha/Validator/Validator.php | 2 +- .../Wordcha/Validator/WordchaValidator.php | 4 ++-- src/{ => Captcha}/Wordcha/WordchaFactory.php | 12 +++++------ .../Wordcha/WordchaUniqueFactory.php | 8 +++---- src/Wordcha/Factory.php | 15 ------------- tests/Cases/Wordcha/DI/FormBinder.phpt | 12 +++++------ .../Wordcha/DataSource/NumericDataSource.phpt | 4 ++-- .../Cases/Wordcha/Forms/WordchaContainer.phpt | 10 ++++----- 22 files changed, 79 insertions(+), 76 deletions(-) rename src/{ => Captcha}/Wordcha/DI/FormBinder.php (70%) rename src/{ => Captcha}/Wordcha/DI/WordchaExtension.php (83%) rename src/{ => Captcha}/Wordcha/DataSource/DataSource.php (62%) rename src/{ => Captcha}/Wordcha/DataSource/NumericDataSource.php (85%) rename src/{ => Captcha}/Wordcha/DataSource/Pair.php (86%) rename src/{ => Captcha}/Wordcha/DataSource/QuestionDataSource.php (83%) rename src/{ => Captcha}/Wordcha/Exception/LogicalException.php (61%) rename src/{ => Captcha}/Wordcha/Exception/RuntimeException.php (61%) create mode 100644 src/Captcha/Wordcha/Factory.php rename src/{ => Captcha}/Wordcha/Form/WordchaContainer.php (87%) rename src/{ => Captcha}/Wordcha/Generator/Generator.php (72%) rename src/{ => Captcha}/Wordcha/Generator/Security.php (86%) rename src/{ => Captcha}/Wordcha/Generator/WordchaGenerator.php (85%) rename src/{ => Captcha}/Wordcha/Validator/Validator.php (69%) rename src/{ => Captcha}/Wordcha/Validator/WordchaValidator.php (75%) rename src/{ => Captcha}/Wordcha/WordchaFactory.php (58%) rename src/{ => Captcha}/Wordcha/WordchaUniqueFactory.php (67%) delete mode 100644 src/Wordcha/Factory.php diff --git a/.docs/README.md b/.docs/README.md index 7bede1b..a4babf6 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -8,7 +8,8 @@ - [StandaloneFormFactoryExtension](#standalone-form-factory) (Nette\Forms\Form) - Controls - [Date/time inputs](#date-time-inputs) (DateTimeInput, DateInput, TimeInput) -- [Wordcha](#wordcha) (Question-based captcha) +- Captcha + - [Wordcha](#wordcha) (Question-based captcha) ## Setup @@ -348,31 +349,33 @@ $control->setValueInTzAs(DateTime::class); // value in server default timezone a $control->setValueInTzAs(DateTime::class, new DateTimeZone('Americe/New_York')); // value in given timezone as \DateTime ``` -## Wordcha +## Captcha + +### Wordcha Question-based captcha for Nette Framework / Forms. -### Wordcha Setup +#### Wordcha Setup Register extension ```yaml extensions: - wordcha: Contributte\Forms\Wordcha\DI\WordchaExtension + wordcha: Contributte\Forms\Captcha\Wordcha\DI\WordchaExtension ``` -### Wordcha Configuration +#### Wordcha Configuration At the beginning you should pick the right datasource. -#### Numeric datasource +##### Numeric datasource ```yaml wordcha: datasource: numeric ``` -#### Question datasource +##### Question datasource ```yaml wordcha: @@ -382,7 +385,7 @@ wordcha: "Question b?": "b" ``` -### Wordcha Form Usage +#### Wordcha Form Usage ```php use Nette\Application\UI\Form; @@ -411,6 +414,6 @@ protected function createComponentForm() } ``` -### Wordcha Example +#### Wordcha Example ![captcha](wordcha.png) diff --git a/src/Wordcha/DI/FormBinder.php b/src/Captcha/Wordcha/DI/FormBinder.php similarity index 70% rename from src/Wordcha/DI/FormBinder.php rename to src/Captcha/Wordcha/DI/FormBinder.php index 80c4417..15c0ed3 100644 --- a/src/Wordcha/DI/FormBinder.php +++ b/src/Captcha/Wordcha/DI/FormBinder.php @@ -1,9 +1,9 @@ Date: Sun, 28 Dec 2025 14:47:37 +0000 Subject: [PATCH 3/3] style: sort use statements alphabetically in Wordcha tests --- tests/Cases/Wordcha/DI/FormBinder.phpt | 2 +- tests/Cases/Wordcha/DataSource/NumericDataSource.phpt | 2 +- tests/Cases/Wordcha/Forms/WordchaContainer.phpt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Cases/Wordcha/DI/FormBinder.phpt b/tests/Cases/Wordcha/DI/FormBinder.phpt index 5ea8b79..ede26fc 100644 --- a/tests/Cases/Wordcha/DI/FormBinder.phpt +++ b/tests/Cases/Wordcha/DI/FormBinder.phpt @@ -1,12 +1,12 @@