├── .phpunit-watcher.yml
├── .styleci.yml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
└── src
├── Captcha.php
├── CaptchaAction.php
├── CaptchaAsset.php
├── CaptchaValidator.php
├── Driver.php
├── DriverInterface.php
├── GdDriver.php
├── ImagickDriver.php
├── SpicyRice.md
├── SpicyRice.ttf
├── VerifyCodeGeneratorTrait.php
└── assets
└── yii.captcha.js
/.phpunit-watcher.yml:
--------------------------------------------------------------------------------
1 | watch:
2 | directories:
3 | - src
4 | - tests
5 | fileMask: '*.php'
6 | notifications:
7 | passingTests: false
8 | failingTests: false
9 | phpunit:
10 | binaryPath: vendor/bin/phpunit
11 | timeout: 180
12 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr12
2 | risky: true
3 |
4 | version: 8
5 |
6 | finder:
7 | exclude:
8 | - docs
9 | - vendor
10 | - resources
11 | - views
12 | - public
13 | - templates
14 | not-name:
15 | - UnionCar.php
16 | - TimerUnionTypes.php
17 | - schema1.php
18 |
19 | enabled:
20 | - alpha_ordered_traits
21 | - array_indentation
22 | - array_push
23 | - combine_consecutive_issets
24 | - combine_consecutive_unsets
25 | - combine_nested_dirname
26 | - declare_strict_types
27 | - dir_constant
28 | - fully_qualified_strict_types
29 | - function_to_constant
30 | - hash_to_slash_comment
31 | - is_null
32 | - logical_operators
33 | - magic_constant_casing
34 | - magic_method_casing
35 | - method_separation
36 | - modernize_types_casting
37 | - native_function_casing
38 | - native_function_type_declaration_casing
39 | - no_alias_functions
40 | - no_empty_comment
41 | - no_empty_phpdoc
42 | - no_empty_statement
43 | - no_extra_block_blank_lines
44 | - no_short_bool_cast
45 | - no_superfluous_elseif
46 | - no_unneeded_control_parentheses
47 | - no_unneeded_curly_braces
48 | - no_unneeded_final_method
49 | - no_unset_cast
50 | - no_unused_imports
51 | - no_unused_lambda_imports
52 | - no_useless_else
53 | - no_useless_return
54 | - normalize_index_brace
55 | - php_unit_dedicate_assert
56 | - php_unit_dedicate_assert_internal_type
57 | - php_unit_expectation
58 | - php_unit_mock
59 | - php_unit_mock_short_will_return
60 | - php_unit_namespaced
61 | - php_unit_no_expectation_annotation
62 | - phpdoc_no_empty_return
63 | - phpdoc_no_useless_inheritdoc
64 | - phpdoc_order
65 | - phpdoc_property
66 | - phpdoc_scalar
67 | - phpdoc_separation
68 | - phpdoc_singular_inheritdoc
69 | - phpdoc_trim
70 | - phpdoc_trim_consecutive_blank_line_separation
71 | - phpdoc_type_to_var
72 | - phpdoc_types
73 | - phpdoc_types_order
74 | - print_to_echo
75 | - regular_callable_call
76 | - return_assignment
77 | - self_accessor
78 | - self_static_accessor
79 | - set_type_to_cast
80 | - short_array_syntax
81 | - short_list_syntax
82 | - simplified_if_return
83 | - single_quote
84 | - standardize_not_equals
85 | - ternary_to_null_coalescing
86 | - trailing_comma_in_multiline_array
87 | - unalign_double_arrow
88 | - unalign_equals
89 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | env:
4 | global:
5 | - DEFAULT_COMPOSER_FLAGS="--prefer-dist --no-interaction --no-progress --optimize-autoloader"
6 | - TASK_STATIC_ANALYSIS=0
7 | - TASK_TESTS_COVERAGE=0
8 |
9 | matrix:
10 | include:
11 | - php: "7.4"
12 | env:
13 | - TASK_STATIC_ANALYSIS=0 # set to 1 to enable static analysis
14 | - TASK_TESTS_COVERAGE=1
15 |
16 | # faster builds on new travis setup not using sudo
17 | sudo: false
18 |
19 | # cache vendor dirs
20 | cache:
21 | directories:
22 | - $HOME/.composer/cache
23 |
24 | before_install:
25 | - phpenv config-rm xdebug.ini || echo "xdebug is not installed"
26 |
27 | install:
28 | - travis_retry composer self-update && composer --version
29 | - export PATH="$HOME/.composer/vendor/bin:$PATH"
30 | - travis_retry composer install $DEFAULT_COMPOSER_FLAGS
31 | - |
32 | if [ $TASK_STATIC_ANALYSIS == 1 ]; then
33 | pecl install ast
34 | fi
35 |
36 | before_script:
37 | - php --version
38 | - composer --version
39 | # enable code coverage
40 | - |
41 | if [ $TASK_TESTS_COVERAGE == 1 ]; then
42 | PHPUNIT_COVERAGE_FLAG="--coverage-clover=coverage.clover"
43 | fi
44 |
45 | script:
46 | - phpdbg -qrr vendor/bin/phpunit --verbose $PHPUNIT_COVERAGE_FLAG
47 | - |
48 | if [ $TASK_STATIC_ANALYSIS == 1 ]; then
49 | composer phan
50 | fi
51 | - |
52 | if [ $TASK_STATIC_ANALYSIS == 1 ]; then
53 | cat analysis.txt
54 | fi
55 |
56 | after_script:
57 | - |
58 | if [ $TASK_TESTS_COVERAGE == 1 ]; then
59 | travis_retry wget https://scrutinizer-ci.com/ocular.phar
60 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover
61 | fi
62 |
63 |
64 | notifications:
65 | slack:
66 | -
67 | rooms:
68 | -
69 | secure: bMwbLe0dn8b9IEoZ7yEP/Ag3AzWo9U0leZDcmdyFmOjVgK8m/GgJzFJ837b4qTuS6h3WtICwMcsi2s0rvaXFbk0ibzUs31Atx8A0MRxXlCUku2vWZCjqq83MVUVedMd6vS7L40AVd0hwPGiAmdx1Qy8up6rI5fuXj/mAlfH92P+DGNbG5ovukNkGNHwCSJeV/DvRyF5PbOjISgnuCx9UWYCwc2ieV757z/ijuIlznHb7tUC1TbI8bz1tC/fjZjsaN9h0muUD+TyD/wXLMfQpk3cy8uTXW63h/WACTPFWmJvLdKcCMGV2sBsbs2akAEnzwfP+7Rzy/4x3iw8Q3zx6LD3zpw5+StjmJ0eIypIEhutysWSVermQAFAPgHXapZNhGotMjMKuSshBgyUYhXahM+fjr/XygatpvyM+xRrNLbD/i3hg1t/P9h3y8R5TKB4xMrSn5hC3wU/zssf3gJYSn2qZO7yva9cy9oANj/OPNogmVnajSaytSzv2YkVbSh+Gkp3olXHsUkd6ZOoJBRq5zH7ShM71/wuJJ7cXhaMgw+4J1ApDFRwINcijNAzfvoBy1i63wiu/nihznGHcmdPAV143s/qkcrnoS/wtGKNzJy4WBYJ9G4HFGbEq9uNJvQnFLMJFjMdoKswLLJTzQcU7xzOuyO2tdtVBmpjlKDKUMOg=
70 | on_success: always
71 | on_failure: never
72 | on_pull_requests: false
73 | -
74 | rooms:
75 | -
76 | secure: AamS+67y+rvB/qX7o0G7bdzHhaflgX85kab6a8rfL4UCgwIC9LrDgvXGS75hTcE0jl28aG+qvjC/4C+xp0MWu9E/jN7u39zexuYKXk7qhXBgfFs9BoTZM0iuMVhrxW+jN/S/FoD+g3hNxSVR6R8ku76Zf6YKqeHoT7IlDfsP2He8uxeX2zKd/hKOfkNW6DPCSAj2ncQkZKbWivKfai7275vtpH7CUO+GyC7FtasHYuAGYe5u6imvlcSUsGby2l293dxyKwDLrVIeGPE793CZ7wfMoU18ASvAwhTKSo09YMJ+kvoCTIrzkKa7WAF9svw8+oMto0JtjFOZaGW8rJtfHCZL3DguIl/3pwrjMrnKDVC73VfdE2af4nGCVfWbT859IDCbpbwC+s97QVJ2ZIrpybGu9nEBc9/BOg22g3uppE3+gQp775cCsInhwgji+npB0WMleH1d6J2dEv7iKOIKZGzGas4fq6pUeV+vUrLsLpgxroMG4cTTCkM04MnVd0NP63ocBSUr1Jo/qn7cXNTIxU4tbFPn5p5UqfWV5SK59az26RiBGmRKtIcisN8W0GWqoKTP2XoW1ZE7NdtHw/CntY3Z7bft92Mb9hBLgcgj+wqwPqQx3IDaeBQd4HMiRylmFVZ0Vk66Zh9k2pYgVgsWtZ0o/KyqHCUI/LcLY4oNIuo=
77 | on_success: never
78 | on_failure: always
79 | on_pull_requests: false
80 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Framework Captcha widget Extension Change Log
2 |
3 | 1.0.0 under development
4 | -----------------------
5 |
6 | - Initial release.
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Framework Captcha widget Extension
6 |
7 |
8 |
9 | This extension provides the CAPTCHA for the [Yii framework](http://www.yiiframework.com).
10 |
11 | For license information check the [LICENSE](LICENSE.md)-file.
12 |
13 | Documentation is at [docs/guide/README.md](docs/guide/README.md).
14 |
15 | [](https://packagist.org/packages/yiisoft/yii-captcha)
16 | [](https://packagist.org/packages/yiisoft/yii-captcha)
17 | [](https://travis-ci.com/yiisoft/yii-captcha)
18 |
19 |
20 | Installation
21 | ------------
22 |
23 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
24 |
25 | ```
26 | php composer.phar require --prefer-dist yiisoft/yii-captcha
27 | ```
28 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/yii-captcha",
3 | "type": "library",
4 | "description": "Yii Framework CAPTCHA Extension",
5 | "keywords": [
6 | "yii",
7 | "captcha",
8 | "turing test"
9 | ],
10 | "license": "BSD-3-Clause",
11 | "support": {
12 | "source": "https://github.com/yiisoft/yii-captcha",
13 | "issues": "https://github.com/yiisoft/yii-captcha/issues",
14 | "forum": "https://www.yiiframework.com/forum/",
15 | "wiki": "https://www.yiiframework.com/wiki/",
16 | "irc": "irc://irc.freenode.net/yii",
17 | "chat": "https://t.me/yii3en"
18 | },
19 | "minimum-stability": "dev",
20 | "prefer-stable": true,
21 | "require": {
22 | "php": "^7.4|^8.0",
23 | "ext-json": "*",
24 | "yiisoft/view": "^4.0"
25 | },
26 | "require-dev": {
27 | "phan/phan": "^4.0",
28 | "phpunit/phpunit": "^9.4",
29 | "roave/infection-static-analysis-plugin": "^1.5",
30 | "spatie/phpunit-watcher": "^1.23",
31 | "vimeo/psalm": "^4.2",
32 | "yiisoft/di": "^1.0",
33 | "yiisoft/event-dispatcher": "^1.0"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "Yiisoft\\Yii\\Captcha\\": "src"
38 | }
39 | },
40 | "autoload-dev": {
41 | "psr-4": {
42 | "Yiisoft\\Yii\\Captcha\\Tests\\": "tests"
43 | }
44 | },
45 | "extra": {
46 | "branch-alias": {
47 | "dev-master": "3.0.x-dev"
48 | }
49 | },
50 | "config": {
51 | "sort-packages": true,
52 | "allow-plugins": {
53 | "infection/extension-installer": true,
54 | "composer/package-versions-deprecated": true
55 | }
56 | },
57 | "scripts": {
58 | "test": "phpunit --testdox --no-interaction",
59 | "test-watch": "phpunit-watcher watch"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Captcha.php:
--------------------------------------------------------------------------------
1 | $model,
38 | * 'attribute' => 'captcha',
39 | * ]);
40 | * ```
41 | *
42 | * The following example will use the name property instead:
43 | *
44 | * ```php
45 | * echo Captcha::widget([
46 | * 'name' => 'captcha',
47 | * ]);
48 | * ```
49 | *
50 | * You can also use this widget in an [[\yii\widgets\ActiveForm|ActiveForm]] using the [[\yii\widgets\ActiveField::widget()|widget()]]
51 | * method, for example like this:
52 | *
53 | * ```php
54 | * = $form
55 | * ->field($model, 'captcha')
56 | * ->widget(\Yiisoft\Yii\Captcha\Captcha::class, [
57 | * // configure additional widget properties here
58 | * ]) ?>
59 | * ```
60 | */
61 | class Captcha extends InputWidget
62 | {
63 | /**
64 | * @var array|string the route of the action that generates the CAPTCHA images.
65 | * The action represented by this route must be an action of [[CaptchaAction]].
66 | * Please refer to [[\yii\helpers\Url::toRoute()]] for acceptable formats.
67 | */
68 | public $captchaAction = '/site/captcha';
69 | /**
70 | * @var array HTML attributes to be applied to the CAPTCHA image tag.
71 | *
72 | * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
73 | */
74 | public $imageOptions = [];
75 | /**
76 | * @var string the template for arranging the CAPTCHA image tag and the text input tag.
77 | * In this template, the token `{image}` will be replaced with the actual image tag,
78 | * while `{input}` will be replaced with the text input tag.
79 | */
80 | public $template = '{image} {input}';
81 | /**
82 | * @var array the HTML attributes for the input tag.
83 | *
84 | * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
85 | */
86 | public $options = ['class' => 'form-control'];
87 |
88 | /**
89 | * Initializes the widget.
90 | */
91 | public function init(): void
92 | {
93 | parent::init();
94 |
95 | if (!isset($this->imageOptions['id'])) {
96 | $this->imageOptions['id'] = $this->options['id'] . '-image';
97 | }
98 | }
99 |
100 | /**
101 | * Renders the widget.
102 | */
103 | public function run()
104 | {
105 | $this->registerClientScript();
106 | $input = $this->renderInputHtml('text');
107 | $route = $this->captchaAction;
108 | if (is_array($route)) {
109 | $route['v'] = uniqid('', true);
110 | } else {
111 | $route = [$route, 'v' => uniqid('', true)];
112 | }
113 | $image = Html::img($route, $this->imageOptions);
114 | return strtr($this->template, [
115 | '{input}' => $input,
116 | '{image}' => $image,
117 | ]);
118 | }
119 |
120 | /**
121 | * Registers the needed JavaScript.
122 | */
123 | public function registerClientScript()
124 | {
125 | $options = $this->getClientOptions();
126 | $options = empty($options) ? '' : Json::htmlEncode($options);
127 | $id = $this->imageOptions['id'];
128 | $view = $this->getView();
129 | CaptchaAsset::register($view);
130 | $view->registerJs("(new YiiCaptcha(document.getElementById('$id'))).init($options);");
131 | }
132 |
133 | /**
134 | * Returns the options for the captcha JS widget.
135 | *
136 | * @return array the options
137 | */
138 | protected function getClientOptions(): array
139 | {
140 | $route = $this->captchaAction;
141 | if (is_array($route)) {
142 | $route[CaptchaAction::REFRESH_GET_VAR] = 1;
143 | } else {
144 | $route = [$route, CaptchaAction::REFRESH_GET_VAR => 1];
145 | }
146 |
147 | return [
148 | 'refreshUrl' => Url::toRoute($route),
149 | 'hashKey' => 'yii-captcha-' . str_replace(trim($route[0], '/'), '/', '-'),
150 | ];
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/CaptchaAction.php:
--------------------------------------------------------------------------------
1 | \Yiisoft\Yii\Captcha\ImagickDriver::class,
64 | * // 'backColor' => 0xFFFFFF,
65 | * // 'foreColor' => 0x2040A0,
66 | * ]
67 | * ```
68 | *
69 | * After the action object is created, if you want to change this property, you should assign it
70 | * with a [[DriverInterface]] object only.
71 | */
72 | public $driver;
73 |
74 | /**
75 | * Runs the action.
76 | */
77 | public function run()
78 | {
79 | if ($this->driver === null || (is_array($this->driver) && !isset($this->driver['class']))) {
80 | $this->driver['class'] = GdDriver::class;
81 | }
82 |
83 | $this->driver = $this->app->ensureObject($this->driver, DriverInterface::class);
84 |
85 | if ($this->app->request->getQueryParam(self::REFRESH_GET_VAR) !== null) {
86 | // AJAX request for regenerating code
87 | $code = $this->getVerifyCode(true);
88 | $this->app->response->format = Response::FORMAT_JSON;
89 | return [
90 | 'hash1' => $this->generateValidationHash($code),
91 | 'hash2' => $this->generateValidationHash(strtolower($code)),
92 | // we add a random 'v' parameter so that FireFox can refresh the image
93 | // when src attribute of image tag is changed
94 | 'url' => Url::to([$this->id, 'v' => uniqid()]),
95 | ];
96 | }
97 |
98 | $this->setHttpHeaders();
99 | $this->app->response->format = Response::FORMAT_RAW;
100 |
101 | return $this->driver->renderImage($this->getVerifyCode());
102 | }
103 |
104 | /**
105 | * Generates a hash code that can be used for client-side validation.
106 | *
107 | * @param string $code the CAPTCHA code
108 | *
109 | * @return string a hash code generated from the CAPTCHA code
110 | */
111 | public function generateValidationHash(string $code): string
112 | {
113 | for ($h = 0, $i = strlen($code) - 1; $i >= 0; --$i) {
114 | $h += ord($code[$i]);
115 | }
116 |
117 | return $h;
118 | }
119 |
120 | /**
121 | * Gets the verification code.
122 | *
123 | * @param bool $regenerate whether the verification code should be regenerated.
124 | *
125 | * @return string the verification code.
126 | */
127 | public function getVerifyCode(bool $regenerate = false): string
128 | {
129 | if ($this->fixedVerifyCode !== null) {
130 | return $this->fixedVerifyCode;
131 | }
132 |
133 | $session = $this->app->getSession();
134 | $session->open();
135 | $name = $this->getSessionKey();
136 | if ($regenerate || $session->get($name) === null) {
137 | $session->set($name, $this->driver->generateVerifyCode());
138 | $session->set($name . 'count', 1);
139 | }
140 |
141 | return $session->get($name);
142 | }
143 |
144 | /**
145 | * Validates the input to see if it matches the generated code.
146 | *
147 | * @param string $input user input
148 | * @param bool $caseSensitive whether the comparison should be case-sensitive
149 | *
150 | * @return bool whether the input is valid
151 | */
152 | public function validate(string $input, bool $caseSensitive): bool
153 | {
154 | $code = $this->getVerifyCode();
155 | $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0;
156 | $session = $this
157 | ->getApp()
158 | ->getSession();
159 | $session->open();
160 | $name = $this->getSessionKey() . 'count';
161 | $session[$name] += 1;
162 | if ($valid || $session[$name] > $this->testLimit && $this->testLimit > 0) {
163 | $this->getVerifyCode(true);
164 | }
165 |
166 | return $valid;
167 | }
168 |
169 | /**
170 | * Returns the session variable name used to store verification code.
171 | *
172 | * @return string the session variable name
173 | */
174 | protected function getSessionKey(): string
175 | {
176 | return '__captcha/' . $this->getUniqueId();
177 | }
178 |
179 | /**
180 | * Sets the HTTP headers needed by image response.
181 | */
182 | protected function setHttpHeaders(): void
183 | {
184 | $response = $this
185 | ->getApp()
186 | ->getResponse();
187 | $response->setHeader('Pragma', 'public');
188 | $response->setHeader('Expires', '0');
189 | $response->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0');
190 | $response->setHeader('Content-Transfer-Encoding', 'binary');
191 | $response->setHeader('Content-type', $this->driver->getImageMimeType());
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/CaptchaAsset.php:
--------------------------------------------------------------------------------
1 | message === null) {
49 | $this->message = Yii::getApp()->t('yii', 'The verification code is incorrect.');
50 | }
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | protected function validateValue($value)
57 | {
58 | $captcha = $this->createCaptchaAction();
59 | $valid = !is_array($value) && $captcha->validate($value, $this->caseSensitive);
60 |
61 | return $valid ? null : [$this->message, []];
62 | }
63 |
64 | /**
65 | * Creates the CAPTCHA action object from the route specified by [[captchaAction]].
66 | *
67 | * @throws InvalidConfigException
68 | *
69 | * @return \Yiisoft\Yii\Captcha\CaptchaAction the action object
70 | */
71 | public function createCaptchaAction()
72 | {
73 | $ca = Yii::getApp()->createController($this->captchaAction);
74 | if ($ca !== false) {
75 | /* @var $controller \yii\base\Controller */
76 | [$controller, $actionID] = $ca;
77 | /** @var $action CaptchaAction */
78 | $action = $controller->createAction($actionID);
79 | if ($action !== null) {
80 | return $action;
81 | }
82 | }
83 | throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction);
84 | }
85 |
86 | /**
87 | * {@inheritdoc}
88 | */
89 | public function clientValidateAttribute($model, $attribute, $view)
90 | {
91 | ValidationAsset::register($view);
92 | $options = $this->getClientOptions($model, $attribute);
93 |
94 | return 'yii.validation.captcha(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');';
95 | }
96 |
97 | /**
98 | * {@inheritdoc}
99 | */
100 | public function getClientOptions($model, $attribute)
101 | {
102 | $captcha = $this->createCaptchaAction();
103 | $code = $captcha->getVerifyCode(false);
104 | $hash = $captcha->generateValidationHash($this->caseSensitive ? $code : strtolower($code));
105 | $options = [
106 | 'hash' => $hash,
107 | 'hashKey' => 'yiiCaptcha/' . $captcha->getUniqueId(),
108 | 'caseSensitive' => $this->caseSensitive,
109 | 'message' => Yii::getApp()
110 | ->getI18n()
111 | ->format($this->message, [
112 | 'attribute' => $model->getAttributeLabel($attribute),
113 | ], Yii::getApp()->language),
114 | ];
115 | if ($this->skipOnEmpty) {
116 | $options['skipOnEmpty'] = 1;
117 | }
118 |
119 | return $options;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/Driver.php:
--------------------------------------------------------------------------------
1 | app = $app;
76 | }
77 |
78 | /**
79 | * @return string|null
80 | */
81 | public function getFontFile(): string
82 | {
83 | if ($this->fontFile === null) {
84 | $this->setFontFile('@Yiisoft/Yii/Captcha/SpicyRice.ttf');
85 | }
86 |
87 | return $this->fontFile;
88 | }
89 |
90 | /**
91 | * @param string $fontFile
92 | *
93 | * @return Driver
94 | */
95 | public function setFontFile(string $fontFile): self
96 | {
97 | $this->fontFile = $this->app->getAlias($fontFile);
98 |
99 | return $this;
100 | }
101 |
102 | /**
103 | * {@inheritdoc}
104 | */
105 | public function getImageMimeType(): string
106 | {
107 | $file = $this->getFontFile();
108 |
109 | if (!is_file($file)) {
110 | throw new InvalidConfigException("The font file does not exist: {$file}");
111 | }
112 |
113 | return 'image/png';
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/DriverInterface.php:
--------------------------------------------------------------------------------
1 | width, $this->height);
46 |
47 | $backColor = imagecolorallocate(
48 | $image,
49 | (int) ($this->backColor % 0x1000000 / 0x10000),
50 | (int) ($this->backColor % 0x10000 / 0x100),
51 | $this->backColor % 0x100
52 | );
53 | imagefilledrectangle($image, 0, 0, $this->width - 1, $this->height - 1, $backColor);
54 | imagecolordeallocate($image, $backColor);
55 |
56 | if ($this->transparent) {
57 | imagecolortransparent($image, $backColor);
58 | }
59 |
60 | $foreColor = imagecolorallocate(
61 | $image,
62 | (int) ($this->foreColor % 0x1000000 / 0x10000),
63 | (int) ($this->foreColor % 0x10000 / 0x100),
64 | $this->foreColor % 0x100
65 | );
66 |
67 | $length = strlen($code);
68 | $box = imagettfbbox(30, 0, $this->getFontFile(), $code);
69 | $w = $box[4] - $box[0] + $this->offset * ($length - 1);
70 | $h = $box[1] - $box[5];
71 | $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h);
72 | $x = 10;
73 | $y = round($this->height * 27 / 40);
74 | for ($i = 0; $i < $length; ++$i) {
75 | $fontSize = (int) (random_int(26, 32) * $scale * 0.8);
76 | $angle = random_int(-10, 10);
77 | $letter = $code[$i];
78 | $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter);
79 | $x = $box[2] + $this->offset;
80 | }
81 |
82 | imagecolordeallocate($image, $foreColor);
83 |
84 | ob_start();
85 | imagepng($image);
86 | imagedestroy($image);
87 |
88 | return ob_get_clean();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/ImagickDriver.php:
--------------------------------------------------------------------------------
1 | transparent ? new ImagickPixel('transparent') : new ImagickPixel('#' . str_pad(dechex($this->backColor), 6, 0, STR_PAD_LEFT));
48 | $foreColor = new ImagickPixel('#' . str_pad(dechex($this->foreColor), 6, 0, STR_PAD_LEFT));
49 |
50 | $image = new Imagick();
51 | $image->newImage($this->width, $this->height, $backColor);
52 |
53 | $draw = new \ImagickDraw();
54 | $draw->setFont($this->getFontFile());
55 | $draw->setFontSize(30);
56 | $fontMetrics = $image->queryFontMetrics($draw, $code);
57 |
58 | $length = \strlen($code);
59 | $w = (int) $fontMetrics['textWidth'] - 8 + $this->offset * ($length - 1);
60 | $h = (int) $fontMetrics['textHeight'] - 8;
61 | $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h);
62 | $x = 10;
63 | $y = round($this->height * 27 / 40);
64 | for ($i = 0; $i < $length; ++$i) {
65 | $draw = new \ImagickDraw();
66 | $draw->setFont($this->fontFile);
67 | $draw->setFontSize((int) (random_int(26, 32) * $scale * 0.8));
68 | $draw->setFillColor($foreColor);
69 | $image->annotateImage($draw, $x, $y, random_int(-10, 10), $code[$i]);
70 | $fontMetrics = $image->queryFontMetrics($draw, $code[$i]);
71 | $x += (int) $fontMetrics['textWidth'] + $this->offset;
72 | }
73 |
74 | $image->setImageFormat('png');
75 | return $image->getImageBlob();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/SpicyRice.md:
--------------------------------------------------------------------------------
1 | ## Spicy Rice font
2 |
3 | * **Author:** Brian J. Bonislawsky, Astigmatic (AOETI, Astigmatic One Eye Typographic Institute)
4 | * **License:** SIL Open Font License (OFL), version 1.1, [notes and FAQ](http://scripts.sil.org/OFL)
5 |
6 | ## Links
7 |
8 | * [Astigmatic](http://www.astigmatic.com/)
9 | * [Google WebFonts](http://www.google.com/webfonts/specimen/Spicy+Rice)
10 | * [fontsquirrel.com](http://www.fontsquirrel.com/fonts/spicy-rice)
11 | * [fontspace.com](http://www.fontspace.com/astigmatic-one-eye-typographic-institute/spicy-rice)
12 |
--------------------------------------------------------------------------------
/src/SpicyRice.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yiisoft/yii-captcha/19d539f6535d655aa5b883e0989e90eb624a033e/src/SpicyRice.ttf
--------------------------------------------------------------------------------
/src/VerifyCodeGeneratorTrait.php:
--------------------------------------------------------------------------------
1 | minLength > $this->maxLength) {
38 | $this->maxLength = $this->minLength;
39 | }
40 | if ($this->minLength < 3) {
41 | $this->minLength = 3;
42 | }
43 | if ($this->maxLength > 20) {
44 | $this->maxLength = 20;
45 | }
46 | $length = random_int($this->minLength, $this->maxLength);
47 |
48 | $letters = 'bcdfghjklmnpqrstvwxyz';
49 | $vowels = 'aeiou';
50 | $code = '';
51 | for ($i = 0; $i < $length; ++$i) {
52 | if (($i % 2 && random_int(0, 10) > 2) || (!($i % 2) && random_int(0, 10) > 9)) {
53 | $code .= $vowels[random_int(0, 4)];
54 | } else {
55 | $code .= $letters[random_int(0, 20)];
56 | }
57 | }
58 |
59 | return $code;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/assets/yii.captcha.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Yii Captcha widget.
3 | *
4 | * This is the JavaScript widget used by the Yiisoft\Yii\Captcha\Captcha widget.
5 | *
6 | * @link http://www.yiiframework.com/
7 | * @copyright Copyright (c) 2008 Yii Software LLC
8 | * @license http://www.yiiframework.com/license/
9 | */
10 | function YiiCaptcha(el) {
11 | var refreshUrl,
12 | hashKey;
13 |
14 | this.init = function (options) {
15 | if (options.hasOwnProperty('refreshUrl')) {
16 | refreshUrl = options.refreshUrl;
17 | }
18 |
19 | if (options.hasOwnProperty('hashKey')) {
20 | hashKey = options.hashKey;
21 | }
22 |
23 | el.addEventListener('click', this.refresh);
24 | };
25 |
26 | this.refresh = function () {
27 | var request = new XMLHttpRequest();
28 | request.onreadystatechange = function (e) {
29 | try {
30 | if (request.readyState === XMLHttpRequest.DONE) {
31 | if (request.status === 200) {
32 | var data = JSON.parse(request.responseText);
33 | el.setAttribute('src', data.url);
34 | document.getElementsByTagName('body')[0].setAttribute('data-' + hashKey, [data.hash1, data.hash2]);
35 | } else {
36 | alert('There was a problem refreshing captcha.');
37 | }
38 | }
39 | } catch (e) {
40 | alert('There was a problem refreshing captcha: ' + e);
41 | }
42 | };
43 | request.open('GET', refreshUrl);
44 | request.setRequestHeader('Cache-Control', 'no-cache');
45 | request.setRequestHeader('Content-Type', 'application/json');
46 | request.send();
47 | };
48 |
49 | this.destroy = function () {
50 | el.removeEventListener('click', this.refresh);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------