1 | <?php |
||||||
2 | /** |
||||||
3 | * @link https://www.yiiframework.com/ |
||||||
4 | * @copyright Copyright (c) 2008 Yii Software LLC |
||||||
5 | * @license https://www.yiiframework.com/license/ |
||||||
6 | */ |
||||||
7 | |||||||
8 | namespace yii\captcha; |
||||||
9 | |||||||
10 | use Yii; |
||||||
11 | use yii\base\Action; |
||||||
12 | use yii\base\InvalidConfigException; |
||||||
13 | use yii\helpers\Url; |
||||||
14 | use yii\web\Response; |
||||||
15 | |||||||
16 | /** |
||||||
17 | * CaptchaAction renders a CAPTCHA image. |
||||||
18 | * |
||||||
19 | * CaptchaAction is used together with [[Captcha]] and [[\yii\captcha\CaptchaValidator]] |
||||||
20 | * to provide the [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA) feature. |
||||||
21 | * |
||||||
22 | * By configuring the properties of CaptchaAction, you may customize the appearance of |
||||||
23 | * the generated CAPTCHA images, such as the font color, the background color, etc. |
||||||
24 | * |
||||||
25 | * Note that CaptchaAction requires either GD2 extension or ImageMagick PHP extension. |
||||||
26 | * |
||||||
27 | * Using CAPTCHA involves the following steps: |
||||||
28 | * |
||||||
29 | * 1. Override [[\yii\web\Controller::actions()]] and register an action of class CaptchaAction with ID 'captcha' |
||||||
30 | * 2. In the form model, declare an attribute to store user-entered verification code, and declare the attribute |
||||||
31 | * to be validated by the 'captcha' validator. |
||||||
32 | * 3. In the controller view, insert a [[Captcha]] widget in the form. |
||||||
33 | * |
||||||
34 | * @property-read string $verifyCode The verification code. |
||||||
35 | * |
||||||
36 | * @author Qiang Xue <[email protected]> |
||||||
37 | * @since 2.0 |
||||||
38 | */ |
||||||
39 | class CaptchaAction extends Action |
||||||
40 | { |
||||||
41 | /** |
||||||
42 | * The name of the GET parameter indicating whether the CAPTCHA image should be regenerated. |
||||||
43 | */ |
||||||
44 | const REFRESH_GET_VAR = 'refresh'; |
||||||
45 | |||||||
46 | /** |
||||||
47 | * @var int how many times should the same CAPTCHA be displayed. Defaults to 3. |
||||||
48 | * A value less than or equal to 0 means the test is unlimited (available since version 1.1.2). |
||||||
49 | */ |
||||||
50 | public $testLimit = 3; |
||||||
51 | /** |
||||||
52 | * @var int the width of the generated CAPTCHA image. Defaults to 120. |
||||||
53 | */ |
||||||
54 | public $width = 120; |
||||||
55 | /** |
||||||
56 | * @var int the height of the generated CAPTCHA image. Defaults to 50. |
||||||
57 | */ |
||||||
58 | public $height = 50; |
||||||
59 | /** |
||||||
60 | * @var int padding around the text. Defaults to 2. |
||||||
61 | */ |
||||||
62 | public $padding = 2; |
||||||
63 | /** |
||||||
64 | * @var int the background color. For example, 0x55FF00. |
||||||
65 | * Defaults to 0xFFFFFF, meaning white color. |
||||||
66 | */ |
||||||
67 | public $backColor = 0xFFFFFF; |
||||||
68 | /** |
||||||
69 | * @var int the font color. For example, 0x55FF00. Defaults to 0x2040A0 (blue color). |
||||||
70 | */ |
||||||
71 | public $foreColor = 0x2040A0; |
||||||
72 | /** |
||||||
73 | * @var bool whether to use transparent background. Defaults to false. |
||||||
74 | */ |
||||||
75 | public $transparent = false; |
||||||
76 | /** |
||||||
77 | * @var int the minimum length for randomly generated word. Defaults to 6. |
||||||
78 | */ |
||||||
79 | public $minLength = 6; |
||||||
80 | /** |
||||||
81 | * @var int the maximum length for randomly generated word. Defaults to 7. |
||||||
82 | */ |
||||||
83 | public $maxLength = 7; |
||||||
84 | /** |
||||||
85 | * @var int the offset between characters. Defaults to -2. You can adjust this property |
||||||
86 | * in order to decrease or increase the readability of the captcha. |
||||||
87 | */ |
||||||
88 | public $offset = -2; |
||||||
89 | /** |
||||||
90 | * @var string the TrueType font file. This can be either a file path or [path alias](guide:concept-aliases). |
||||||
91 | */ |
||||||
92 | public $fontFile = '@yii/captcha/SpicyRice.ttf'; |
||||||
93 | /** |
||||||
94 | * @var string|null the fixed verification code. When this property is set, |
||||||
95 | * [[getVerifyCode()]] will always return the value of this property. |
||||||
96 | * This is mainly used in automated tests where we want to be able to reproduce |
||||||
97 | * the same verification code each time we run the tests. |
||||||
98 | * If not set, it means the verification code will be randomly generated. |
||||||
99 | */ |
||||||
100 | public $fixedVerifyCode; |
||||||
101 | /** |
||||||
102 | * @var string|null the rendering library to use. Currently supported only 'gd' and 'imagick'. |
||||||
103 | * If not set, library will be determined automatically. |
||||||
104 | * @since 2.0.7 |
||||||
105 | */ |
||||||
106 | public $imageLibrary; |
||||||
107 | |||||||
108 | |||||||
109 | /** |
||||||
110 | * Initializes the action. |
||||||
111 | * @throws InvalidConfigException if the font file does not exist. |
||||||
112 | */ |
||||||
113 | public function init() |
||||||
114 | { |
||||||
115 | $this->fontFile = Yii::getAlias($this->fontFile); |
||||||
0 ignored issues
–
show
|
|||||||
116 | if (!is_file($this->fontFile)) { |
||||||
0 ignored issues
–
show
It seems like
$this->fontFile can also be of type false ; however, parameter $filename of is_file() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
117 | throw new InvalidConfigException("The font file does not exist: {$this->fontFile}"); |
||||||
118 | } |
||||||
119 | } |
||||||
120 | |||||||
121 | /** |
||||||
122 | * Runs the action. |
||||||
123 | */ |
||||||
124 | public function run() |
||||||
125 | { |
||||||
126 | if (Yii::$app->request->getQueryParam(self::REFRESH_GET_VAR) !== null) { |
||||||
0 ignored issues
–
show
The method
getQueryParam() does not exist on yii\console\Request . Since you implemented __call , consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
127 | // AJAX request for regenerating code |
||||||
128 | $code = $this->getVerifyCode(true); |
||||||
129 | Yii::$app->response->format = Response::FORMAT_JSON; |
||||||
0 ignored issues
–
show
The property
format does not exist on yii\console\Response . Since you implemented __set , consider adding a @property annotation.
![]() |
|||||||
130 | return [ |
||||||
131 | 'hash1' => $this->generateValidationHash($code), |
||||||
132 | 'hash2' => $this->generateValidationHash(strtolower($code)), |
||||||
133 | // we add a random 'v' parameter so that FireFox can refresh the image |
||||||
134 | // when src attribute of image tag is changed |
||||||
135 | 'url' => Url::to([$this->id, 'v' => uniqid('', true)]), |
||||||
136 | ]; |
||||||
137 | } |
||||||
138 | |||||||
139 | $this->setHttpHeaders(); |
||||||
140 | Yii::$app->response->format = Response::FORMAT_RAW; |
||||||
141 | |||||||
142 | return $this->renderImage($this->getVerifyCode()); |
||||||
143 | } |
||||||
144 | |||||||
145 | /** |
||||||
146 | * Generates a hash code that can be used for client-side validation. |
||||||
147 | * @param string $code the CAPTCHA code |
||||||
148 | * @return string a hash code generated from the CAPTCHA code |
||||||
149 | */ |
||||||
150 | public function generateValidationHash($code) |
||||||
151 | { |
||||||
152 | for ($h = 0, $i = strlen($code) - 1; $i >= 0; --$i) { |
||||||
153 | $h += ord($code[$i]) << $i; |
||||||
154 | } |
||||||
155 | |||||||
156 | return $h; |
||||||
157 | } |
||||||
158 | |||||||
159 | /** |
||||||
160 | * Gets the verification code. |
||||||
161 | * @param bool $regenerate whether the verification code should be regenerated. |
||||||
162 | * @return string the verification code. |
||||||
163 | */ |
||||||
164 | public function getVerifyCode($regenerate = false) |
||||||
165 | { |
||||||
166 | if ($this->fixedVerifyCode !== null) { |
||||||
167 | return $this->fixedVerifyCode; |
||||||
168 | } |
||||||
169 | |||||||
170 | $session = Yii::$app->getSession(); |
||||||
0 ignored issues
–
show
The method
getSession() does not exist on yii\console\Application . Since you implemented __call , consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
171 | $session->open(); |
||||||
172 | $name = $this->getSessionKey(); |
||||||
173 | if ($session[$name] === null || $regenerate) { |
||||||
174 | $session[$name] = $this->generateVerifyCode(); |
||||||
175 | $session[$name . 'count'] = 1; |
||||||
176 | } |
||||||
177 | |||||||
178 | return $session[$name]; |
||||||
179 | } |
||||||
180 | |||||||
181 | /** |
||||||
182 | * Validates the input to see if it matches the generated code. |
||||||
183 | * @param string $input user input |
||||||
184 | * @param bool $caseSensitive whether the comparison should be case-sensitive |
||||||
185 | * @return bool whether the input is valid |
||||||
186 | */ |
||||||
187 | public function validate($input, $caseSensitive) |
||||||
188 | { |
||||||
189 | $code = $this->getVerifyCode(); |
||||||
190 | $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0; |
||||||
191 | $session = Yii::$app->getSession(); |
||||||
192 | $session->open(); |
||||||
193 | $name = $this->getSessionKey() . 'count'; |
||||||
194 | $session[$name] += 1; |
||||||
195 | if ($valid || $session[$name] > $this->testLimit && $this->testLimit > 0) { |
||||||
196 | $this->getVerifyCode(true); |
||||||
197 | } |
||||||
198 | |||||||
199 | return $valid; |
||||||
200 | } |
||||||
201 | |||||||
202 | /** |
||||||
203 | * Generates a new verification code. |
||||||
204 | * @return string the generated verification code |
||||||
205 | */ |
||||||
206 | protected function generateVerifyCode() |
||||||
207 | { |
||||||
208 | if ($this->minLength > $this->maxLength) { |
||||||
209 | $this->maxLength = $this->minLength; |
||||||
210 | } |
||||||
211 | if ($this->minLength < 3) { |
||||||
212 | $this->minLength = 3; |
||||||
213 | } |
||||||
214 | if ($this->maxLength > 20) { |
||||||
215 | $this->maxLength = 20; |
||||||
216 | } |
||||||
217 | |||||||
218 | $length = random_int($this->minLength, $this->maxLength); |
||||||
219 | |||||||
220 | $letters = 'bcdfghjklmnpqrstvwxyz'; |
||||||
221 | $vowels = 'aeiou'; |
||||||
222 | $code = ''; |
||||||
223 | for ($i = 0; $i < $length; ++$i) { |
||||||
224 | if ($i % 2 && random_int(0, 10) > 2 || !($i % 2) && random_int(0, 10) > 9) { |
||||||
0 ignored issues
–
show
|
|||||||
225 | $code .= $vowels[random_int(0, 4)]; |
||||||
226 | } else { |
||||||
227 | $code .= $letters[random_int(0, 20)]; |
||||||
228 | } |
||||||
229 | } |
||||||
230 | |||||||
231 | return $code; |
||||||
232 | } |
||||||
233 | |||||||
234 | /** |
||||||
235 | * Returns the session variable name used to store verification code. |
||||||
236 | * @return string the session variable name |
||||||
237 | */ |
||||||
238 | protected function getSessionKey() |
||||||
239 | { |
||||||
240 | return '__captcha/' . $this->getUniqueId(); |
||||||
241 | } |
||||||
242 | |||||||
243 | /** |
||||||
244 | * Renders the CAPTCHA image. |
||||||
245 | * @param string $code the verification code |
||||||
246 | * @return string image contents |
||||||
247 | * @throws InvalidConfigException if imageLibrary is not supported |
||||||
248 | */ |
||||||
249 | protected function renderImage($code) |
||||||
250 | { |
||||||
251 | if (isset($this->imageLibrary)) { |
||||||
252 | $imageLibrary = $this->imageLibrary; |
||||||
253 | } else { |
||||||
254 | $imageLibrary = Captcha::checkRequirements(); |
||||||
255 | } |
||||||
256 | if ($imageLibrary === 'gd') { |
||||||
257 | return $this->renderImageByGD($code); |
||||||
258 | } elseif ($imageLibrary === 'imagick') { |
||||||
259 | return $this->renderImageByImagick($code); |
||||||
260 | } |
||||||
261 | |||||||
262 | throw new InvalidConfigException("Defined library '{$imageLibrary}' is not supported"); |
||||||
263 | } |
||||||
264 | |||||||
265 | /** |
||||||
266 | * Renders the CAPTCHA image based on the code using GD library. |
||||||
267 | * @param string $code the verification code |
||||||
268 | * @return string image contents in PNG format. |
||||||
269 | */ |
||||||
270 | protected function renderImageByGD($code) |
||||||
271 | { |
||||||
272 | $image = imagecreatetruecolor($this->width, $this->height); |
||||||
273 | |||||||
274 | $backColor = imagecolorallocate( |
||||||
275 | $image, |
||||||
276 | (int) ($this->backColor % 0x1000000 / 0x10000), |
||||||
277 | (int) ($this->backColor % 0x10000 / 0x100), |
||||||
278 | $this->backColor % 0x100 |
||||||
279 | ); |
||||||
280 | imagefilledrectangle($image, 0, 0, $this->width - 1, $this->height - 1, $backColor); |
||||||
281 | imagecolordeallocate($image, $backColor); |
||||||
282 | |||||||
283 | if ($this->transparent) { |
||||||
284 | imagecolortransparent($image, $backColor); |
||||||
285 | } |
||||||
286 | |||||||
287 | $foreColor = imagecolorallocate( |
||||||
288 | $image, |
||||||
289 | (int) ($this->foreColor % 0x1000000 / 0x10000), |
||||||
290 | (int) ($this->foreColor % 0x10000 / 0x100), |
||||||
291 | $this->foreColor % 0x100 |
||||||
292 | ); |
||||||
293 | |||||||
294 | $length = strlen($code); |
||||||
295 | $box = imagettfbbox(30, 0, $this->fontFile, $code); |
||||||
296 | $w = $box[4] - $box[0] + $this->offset * ($length - 1); |
||||||
297 | $h = $box[1] - $box[5]; |
||||||
298 | $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); |
||||||
299 | $x = 10; |
||||||
300 | $y = round($this->height * 27 / 40); |
||||||
301 | for ($i = 0; $i < $length; ++$i) { |
||||||
302 | $fontSize = (int) (random_int(26, 32) * $scale * 0.8); |
||||||
303 | $angle = random_int(-10, 10); |
||||||
304 | $letter = $code[$i]; |
||||||
305 | $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter); |
||||||
0 ignored issues
–
show
$y of type double is incompatible with the type integer expected by parameter $y of imagettftext() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
306 | $x = $box[2] + $this->offset; |
||||||
307 | } |
||||||
308 | |||||||
309 | imagecolordeallocate($image, $foreColor); |
||||||
310 | |||||||
311 | ob_start(); |
||||||
312 | imagepng($image); |
||||||
313 | imagedestroy($image); |
||||||
314 | |||||||
315 | return ob_get_clean(); |
||||||
316 | } |
||||||
317 | |||||||
318 | /** |
||||||
319 | * Renders the CAPTCHA image based on the code using ImageMagick library. |
||||||
320 | * @param string $code the verification code |
||||||
321 | * @return string image contents in PNG format. |
||||||
322 | */ |
||||||
323 | protected function renderImageByImagick($code) |
||||||
324 | { |
||||||
325 | $backColor = $this->transparent ? new \ImagickPixel('transparent') : new \ImagickPixel('#' . str_pad(dechex($this->backColor), 6, 0, STR_PAD_LEFT)); |
||||||
326 | $foreColor = new \ImagickPixel('#' . str_pad(dechex($this->foreColor), 6, 0, STR_PAD_LEFT)); |
||||||
327 | |||||||
328 | $image = new \Imagick(); |
||||||
329 | $image->newImage($this->width, $this->height, $backColor); |
||||||
330 | |||||||
331 | $draw = new \ImagickDraw(); |
||||||
332 | $draw->setFont($this->fontFile); |
||||||
333 | $draw->setFontSize(30); |
||||||
334 | $fontMetrics = $image->queryFontMetrics($draw, $code); |
||||||
335 | |||||||
336 | $length = strlen($code); |
||||||
337 | $w = (int) $fontMetrics['textWidth'] - 8 + $this->offset * ($length - 1); |
||||||
338 | $h = (int) $fontMetrics['textHeight'] - 8; |
||||||
339 | $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); |
||||||
340 | $x = 10; |
||||||
341 | $y = round($this->height * 27 / 40); |
||||||
342 | for ($i = 0; $i < $length; ++$i) { |
||||||
343 | $draw = new \ImagickDraw(); |
||||||
344 | $draw->setFont($this->fontFile); |
||||||
345 | $draw->setFontSize((int) (random_int(26, 32) * $scale * 0.8)); |
||||||
346 | $draw->setFillColor($foreColor); |
||||||
347 | $image->annotateImage($draw, $x, $y, random_int(-10, 10), $code[$i]); |
||||||
348 | $fontMetrics = $image->queryFontMetrics($draw, $code[$i]); |
||||||
349 | $x += (int) $fontMetrics['textWidth'] + $this->offset; |
||||||
350 | } |
||||||
351 | |||||||
352 | $image->setImageFormat('png'); |
||||||
353 | return $image->getImageBlob(); |
||||||
354 | } |
||||||
355 | |||||||
356 | /** |
||||||
357 | * Sets the HTTP headers needed by image response. |
||||||
358 | */ |
||||||
359 | protected function setHttpHeaders() |
||||||
360 | { |
||||||
361 | Yii::$app->getResponse()->getHeaders() |
||||||
0 ignored issues
–
show
The method
getHeaders() does not exist on yii\console\Response . Since you implemented __call , consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
362 | ->set('Pragma', 'public') |
||||||
363 | ->set('Expires', '0') |
||||||
364 | ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') |
||||||
365 | ->set('Content-Transfer-Encoding', 'binary') |
||||||
366 | ->set('Content-type', 'image/png'); |
||||||
367 | } |
||||||
368 | } |
||||||
369 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.