├── .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 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/yii-captcha/v/stable.png)](https://packagist.org/packages/yiisoft/yii-captcha) 16 | [![Total Downloads](https://poser.pugx.org/yiisoft/yii-captcha/downloads.png)](https://packagist.org/packages/yiisoft/yii-captcha) 17 | [![Build Status](https://travis-ci.com/yiisoft/yii-captcha.svg?branch=master)](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 | * 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 | --------------------------------------------------------------------------------