1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of the Shieldon package. |
4
|
|
|
* |
5
|
|
|
* (c) Terry L. <[email protected]> |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
* |
10
|
|
|
* php version 7.1.0 |
11
|
|
|
* |
12
|
|
|
* @category Web-security |
13
|
|
|
* @package Shieldon |
14
|
|
|
* @author Terry Lin <[email protected]> |
15
|
|
|
* @copyright 2019 terrylinooo |
16
|
|
|
* @license https://github.com/terrylinooo/shieldon/blob/2.x/LICENSE MIT |
17
|
|
|
* @link https://github.com/terrylinooo/shieldon |
18
|
|
|
* @see https://shieldon.io |
19
|
|
|
*/ |
20
|
|
|
|
21
|
|
|
declare(strict_types=1); |
22
|
|
|
|
23
|
|
|
namespace Shieldon\Firewall\Captcha; |
24
|
|
|
|
25
|
|
|
use RuntimeException; |
26
|
|
|
use GdImage; // PHP 8 |
27
|
|
|
use Shieldon\Firewall\Captcha\CaptchaProvider; |
28
|
|
|
|
29
|
|
|
use function Shieldon\Firewall\get_request; |
30
|
|
|
use function Shieldon\Firewall\get_session_instance; |
31
|
|
|
use function Shieldon\Firewall\unset_superglobal; |
32
|
|
|
use function base64_encode; |
33
|
|
|
use function cos; |
34
|
|
|
use function function_exists; |
35
|
|
|
use function imagecolorallocate; |
36
|
|
|
use function imagecreate; |
37
|
|
|
use function imagecreatetruecolor; |
38
|
|
|
use function imagedestroy; |
39
|
|
|
use function imagefilledrectangle; |
40
|
|
|
use function imagejpeg; |
41
|
|
|
use function imageline; |
42
|
|
|
use function imagepng; |
43
|
|
|
use function imagerectangle; |
44
|
|
|
use function imagestring; |
45
|
|
|
use function mt_rand; |
46
|
|
|
use function ob_end_clean; |
47
|
|
|
use function ob_get_contents; |
48
|
|
|
use function password_hash; |
49
|
|
|
use function password_verify; |
50
|
|
|
use function random_int; |
51
|
|
|
use function sin; |
52
|
|
|
use function strlen; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Simple Image Captcha. |
56
|
|
|
*/ |
57
|
|
|
class ImageCaptcha extends CaptchaProvider |
58
|
|
|
{ |
59
|
|
|
/** |
60
|
|
|
* Settings. |
61
|
|
|
* |
62
|
|
|
* @var array |
63
|
|
|
*/ |
64
|
|
|
protected $properties = []; |
65
|
|
|
|
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Image type. |
69
|
|
|
* |
70
|
|
|
* @var string |
71
|
|
|
*/ |
72
|
|
|
protected $imageType = ''; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Word. |
76
|
|
|
* |
77
|
|
|
* @var string |
78
|
|
|
*/ |
79
|
|
|
protected $word = ''; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Image resource. |
83
|
|
|
* Throw exception the the value is not resource. |
84
|
|
|
* |
85
|
|
|
* @var resource|null|bool |
86
|
|
|
*/ |
87
|
|
|
private $im; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* The length of the word. |
91
|
|
|
* |
92
|
|
|
* @var int |
93
|
|
|
*/ |
94
|
|
|
protected $length = 4; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Constructor. |
98
|
|
|
* |
99
|
|
|
* It will implement default configuration settings here. |
100
|
|
|
* |
101
|
|
|
* @param array $config The settings for creating Captcha. |
102
|
|
|
* |
103
|
|
|
* @return void |
104
|
|
|
*/ |
105
|
28 |
|
public function __construct(array $config = []) |
106
|
|
|
{ |
107
|
28 |
|
$defaults = [ |
108
|
28 |
|
'img_width' => 250, |
109
|
28 |
|
'img_height' => 50, |
110
|
28 |
|
'word_length' => 8, |
111
|
28 |
|
'font_spacing' => 10, |
112
|
28 |
|
'pool' => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', |
113
|
28 |
|
'colors' => [ |
114
|
28 |
|
'background' => [255, 255, 255], |
115
|
28 |
|
'border' => [153, 200, 255], |
116
|
28 |
|
'text' => [51, 153, 255], |
117
|
28 |
|
'grid' => [153, 200, 255], |
118
|
28 |
|
], |
119
|
28 |
|
]; |
120
|
|
|
|
121
|
28 |
|
foreach ($defaults as $k => $v) { |
122
|
28 |
|
if (isset($config[$k])) { |
123
|
21 |
|
$this->properties[$k] = $config[$k]; |
124
|
|
|
} else { |
125
|
28 |
|
$this->properties[$k] = $defaults[$k]; |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
|
129
|
28 |
|
if (!is_array($this->properties['colors'])) { |
130
|
1 |
|
$this->properties['colors'] = $defaults['colors']; |
131
|
|
|
} |
132
|
|
|
|
133
|
28 |
|
foreach ($defaults['colors'] as $k => $v) { |
134
|
28 |
|
if (!is_array($this->properties['colors'][$k])) { |
135
|
28 |
|
$this->properties['colors'][$k] = $defaults['colors'][$k]; |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Response the result. |
142
|
|
|
* |
143
|
|
|
* @return bool |
144
|
|
|
*/ |
145
|
3 |
|
public function response(): bool |
146
|
|
|
{ |
147
|
3 |
|
$postParams = get_request()->getParsedBody(); |
148
|
3 |
|
$sessionCaptchaHash = get_session_instance()->get('shieldon_image_captcha_hash'); |
149
|
|
|
|
150
|
3 |
|
if (empty($postParams['shieldon_image_captcha']) || empty($sessionCaptchaHash)) { |
151
|
2 |
|
return false; |
152
|
|
|
} |
153
|
|
|
|
154
|
1 |
|
$flag = false; |
155
|
|
|
|
156
|
1 |
|
if (password_verify($postParams['shieldon_image_captcha'], $sessionCaptchaHash)) { |
157
|
1 |
|
$flag = true; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
// Prevent detecting POST method on RESTful frameworks. |
161
|
1 |
|
unset_superglobal('shieldon_image_captcha', 'post'); |
162
|
|
|
|
163
|
1 |
|
return $flag; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Output a required HTML. |
168
|
|
|
* |
169
|
|
|
* @return string |
170
|
|
|
*/ |
171
|
4 |
|
public function form(): string |
172
|
|
|
{ |
173
|
|
|
// @codeCoverageIgnoreStart |
174
|
|
|
if (!extension_loaded('gd')) { |
175
|
|
|
return ''; |
176
|
|
|
} |
177
|
|
|
// @codeCoverageIgnoreEnd |
178
|
|
|
|
179
|
4 |
|
$html = ''; |
180
|
4 |
|
$base64image = $this->createCaptcha(); |
181
|
4 |
|
$imgWidth = $this->properties['img_width']; |
182
|
4 |
|
$imgHeight = $this->properties['img_height']; |
183
|
|
|
|
184
|
4 |
|
if (!empty($base64image)) { |
185
|
4 |
|
$html = '<div style="padding: 0px; overflow: hidden; margin: 10px 0;">'; |
186
|
4 |
|
$html .= '<div style=" |
187
|
|
|
border: 1px #dddddd solid; |
188
|
|
|
overflow: hidden; |
189
|
|
|
border-radius: 3px; |
190
|
|
|
display: inline-block; |
191
|
|
|
padding: 5px; |
192
|
4 |
|
box-shadow: 0px 0px 4px 1px rgba(0,0,0,0.08);">'; |
193
|
4 |
|
$html .= '<div style="margin-bottom: 2px;"><img src="data:image/' . |
194
|
4 |
|
$this->imageType . ';base64,' . |
195
|
|
|
$base64image . '" style="width: ' . |
196
|
|
|
$imgWidth . '; height: ' . |
197
|
|
|
$imgHeight . ';"></div>'; |
198
|
|
|
$html .= '<div><input type="text" name="shieldon_image_captcha" style=" |
199
|
|
|
width: 100px; |
200
|
|
|
border: 1px solid rgba(27,31,35,.2); |
201
|
|
|
border-radius: 3px; |
202
|
|
|
background-color: #fafafa; |
203
|
|
|
font-size: 14px; |
204
|
4 |
|
font-weight: bold; |
205
|
4 |
|
line-height: 20px; |
206
|
4 |
|
box-shadow: inset 0 1px 2px rgba(27,31,35,.075); |
207
|
|
|
vertical-align: middle; |
208
|
|
|
padding: 6px 12px;;"></div>'; |
209
|
4 |
|
$html .= '</div>'; |
210
|
|
|
$html .= '</div>'; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
return $html; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
4 |
|
* Create CAPTCHA |
218
|
|
|
* |
219
|
4 |
|
* @return string |
220
|
4 |
|
*/ |
221
|
|
|
protected function createCaptcha() |
222
|
4 |
|
{ |
223
|
|
|
$imgWidth = $this->properties['img_width']; |
224
|
4 |
|
$imgHeight = $this->properties['img_height']; |
225
|
|
|
|
226
|
|
|
$this->createCanvas($imgWidth, $imgHeight); |
227
|
4 |
|
|
228
|
|
|
$im = $this->getImageResource(); |
229
|
4 |
|
|
230
|
|
|
// Assign colors. |
231
|
|
|
$colors = []; |
232
|
|
|
|
233
|
|
|
foreach ($this->properties['colors'] as $k => $v) { |
234
|
|
|
|
235
|
|
|
/** |
236
|
4 |
|
* Create color identifier for each color. |
237
|
|
|
* |
238
|
|
|
* @var int |
239
|
4 |
|
*/ |
240
|
|
|
$colors[$k] = imagecolorallocate($im, $v[0], $v[1], $v[2]); |
241
|
4 |
|
} |
242
|
4 |
|
|
243
|
4 |
|
$this->createRandomWords(); |
244
|
4 |
|
|
245
|
4 |
|
$this->createBackground( |
246
|
|
|
$imgWidth, |
247
|
4 |
|
$imgHeight, |
248
|
4 |
|
$colors['background'] |
249
|
4 |
|
); |
250
|
4 |
|
|
251
|
4 |
|
$this->createSpiralPattern( |
252
|
|
|
$imgWidth, |
253
|
4 |
|
$imgHeight, |
254
|
4 |
|
$colors['grid'] |
255
|
4 |
|
); |
256
|
4 |
|
|
257
|
4 |
|
$this->writeText( |
258
|
|
|
$imgWidth, |
259
|
4 |
|
$imgHeight, |
260
|
4 |
|
$colors['text'] |
261
|
4 |
|
); |
262
|
4 |
|
|
263
|
4 |
|
$this->createBorder( |
264
|
|
|
$imgWidth, |
265
|
|
|
$imgHeight, |
266
|
4 |
|
$colors['border'] |
267
|
|
|
); |
268
|
4 |
|
|
269
|
4 |
|
// Save hash to the user sesssion. |
270
|
|
|
$hash = password_hash($this->word, PASSWORD_BCRYPT); |
271
|
4 |
|
|
272
|
|
|
get_session_instance()->set('shieldon_image_captcha_hash', $hash); |
273
|
|
|
get_session_instance()->save(); |
274
|
|
|
|
275
|
|
|
return $this->getImageBase64Content(); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
4 |
|
* Prepare the random words that want to display to front. |
280
|
|
|
* |
281
|
4 |
|
* @return void |
282
|
|
|
*/ |
283
|
4 |
|
private function createRandomWords() |
284
|
4 |
|
{ |
285
|
|
|
$this->word = ''; |
286
|
4 |
|
|
287
|
4 |
|
$poolLength = strlen($this->properties['pool']); |
288
|
|
|
$randMax = $poolLength - 1; |
289
|
|
|
|
290
|
4 |
|
for ($i = 0; $i < $this->properties['word_length']; $i++) { |
291
|
|
|
$this->word .= $this->properties['pool'][random_int(0, $randMax)]; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
$this->length = strlen($this->word); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Create a canvas. |
299
|
|
|
* |
300
|
|
|
* This method initialize the $im. |
301
|
|
|
* |
302
|
|
|
* @param int $imgWidth The width of the image. |
303
|
4 |
|
* @param int $imgHeight The height of the image. |
304
|
|
|
* |
305
|
4 |
|
* @return void |
306
|
4 |
|
*/ |
307
|
|
|
private function createCanvas(int $imgWidth, int $imgHeight) |
308
|
|
|
{ |
309
|
|
|
if (function_exists('imagecreatetruecolor')) { |
310
|
|
|
$this->im = imagecreatetruecolor($imgWidth, $imgHeight); |
|
|
|
|
311
|
|
|
|
312
|
|
|
// @codeCoverageIgnoreStart |
313
|
|
|
} else { |
314
|
|
|
$this->im = imagecreate($imgWidth, $imgHeight); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
// @codeCoverageIgnoreEnd |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Create the background. |
322
|
|
|
* |
323
|
|
|
* @param int $imgWidth The width of the image. |
324
|
|
|
* @param int $imgHeight The height of the image. |
325
|
|
|
* @param int $bgColor The RGB color for the background of the image. |
326
|
4 |
|
* |
327
|
|
|
* @return void |
328
|
4 |
|
*/ |
329
|
|
|
private function createBackground(int $imgWidth, int $imgHeight, $bgColor) |
330
|
4 |
|
{ |
331
|
|
|
$im = $this->getImageResource(); |
332
|
|
|
|
333
|
|
|
imagefilledrectangle($im, 0, 0, $imgWidth, $imgHeight, $bgColor); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Create a spiral patten. |
338
|
|
|
* |
339
|
|
|
* @param int $imgWidth The width of the image. |
340
|
|
|
* @param int $imgHeight The height of the image. |
341
|
|
|
* @param int $gridColor The RGB color for the gri of the image. |
342
|
4 |
|
* |
343
|
|
|
* @return void |
344
|
4 |
|
*/ |
345
|
|
|
private function createSpiralPattern(int $imgWidth, int $imgHeight, $gridColor) |
346
|
|
|
{ |
347
|
4 |
|
$im = $this->getImageResource(); |
348
|
4 |
|
|
349
|
4 |
|
// Determine angle and position. |
350
|
|
|
$angle = ($this->length >= 6) ? mt_rand(-($this->length - 6), ($this->length - 6)) : 0; |
351
|
|
|
$xAxis = mt_rand(6, (360 / $this->length) - 16); |
352
|
4 |
|
$yAxis = ($angle >= 0) ? mt_rand($imgHeight, $imgWidth) : mt_rand(6, $imgHeight); |
353
|
4 |
|
|
354
|
4 |
|
// Create the spiral pattern. |
355
|
4 |
|
$theta = 1; |
356
|
4 |
|
$thetac = 7; |
357
|
|
|
$radius = 16; |
358
|
4 |
|
$circles = 20; |
359
|
4 |
|
$points = 32; |
360
|
4 |
|
|
361
|
|
|
for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) { |
362
|
4 |
|
$theta += $thetac; |
363
|
4 |
|
$rad = $radius * ($i / $points); |
364
|
|
|
|
365
|
4 |
|
$x = (int) (($rad * cos($theta)) + $xAxis); |
366
|
4 |
|
$y = (int) (($rad * sin($theta)) + $yAxis); |
367
|
|
|
|
368
|
4 |
|
$theta += $thetac; |
369
|
4 |
|
$rad1 = $radius * (($i + 1) / $points); |
370
|
|
|
|
371
|
4 |
|
$x1 = (int) (($rad1 * cos($theta)) + $xAxis); |
372
|
4 |
|
$y1 = (int) (($rad1 * sin($theta)) + $yAxis); |
373
|
|
|
|
374
|
|
|
imageline($im, $x, $y, $x1, $y1, $gridColor); |
375
|
|
|
$theta -= $thetac; |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* Write the text into the image canvas. |
381
|
|
|
* |
382
|
|
|
* @param int $imgWidth The width of the image. |
383
|
|
|
* @param int $imgHeight The height of the image. |
384
|
|
|
* @param int $textColor The RGB color for the grid of the image. |
385
|
4 |
|
* |
386
|
|
|
* @return void |
387
|
4 |
|
*/ |
388
|
|
|
private function writeText(int $imgWidth, int $imgHeight, $textColor) |
389
|
4 |
|
{ |
390
|
4 |
|
$im = $this->getImageResource(); |
391
|
|
|
|
392
|
|
|
$z = (int) ($imgWidth / ($this->length / 3)); |
393
|
4 |
|
$x = mt_rand(0, $z); |
394
|
4 |
|
// $y = 0; |
395
|
4 |
|
|
396
|
4 |
|
for ($i = 0; $i < $this->length; $i++) { |
397
|
|
|
$y = mt_rand(0, $imgHeight / 2); |
398
|
|
|
imagestring($im, 5, $x, $y, $this->word[$i], $textColor); |
399
|
|
|
$x += ($this->properties['font_spacing'] * 2); |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Write the text into the image canvas. |
405
|
|
|
* |
406
|
|
|
* @param int $imgWidth The width of the image. |
407
|
|
|
* @param int $imgHeight The height of the image. |
408
|
|
|
* @param int $borderColor The RGB color for the border of the image. |
409
|
4 |
|
* |
410
|
|
|
* @return void |
411
|
4 |
|
*/ |
412
|
|
|
private function createBorder(int $imgWidth, int $imgHeight, $borderColor): void |
413
|
|
|
{ |
414
|
4 |
|
$im = $this->getImageResource(); |
415
|
|
|
|
416
|
|
|
// Create the border. |
417
|
|
|
imagerectangle($im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor); |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
/** |
421
|
|
|
* Get the base64 string of the image. |
422
|
4 |
|
* |
423
|
|
|
* @return string |
424
|
4 |
|
*/ |
425
|
|
|
private function getImageBase64Content(): string |
426
|
|
|
{ |
427
|
4 |
|
$im = $this->getImageResource(); |
428
|
|
|
|
429
|
4 |
|
// Generate image in base64 string. |
430
|
4 |
|
ob_start(); |
431
|
4 |
|
|
432
|
|
|
if (function_exists('imagejpeg')) { |
433
|
|
|
$this->imageType = 'jpeg'; |
434
|
|
|
imagejpeg($im); |
435
|
|
|
|
436
|
|
|
// @codeCoverageIgnoreStart |
437
|
|
|
} elseif (function_exists('imagepng')) { |
438
|
|
|
$this->imageType = 'png'; |
439
|
|
|
imagepng($im); |
440
|
|
|
} else { |
441
|
|
|
echo ''; |
442
|
|
|
} |
443
|
|
|
|
444
|
4 |
|
// @codeCoverageIgnoreEnd |
445
|
4 |
|
|
446
|
4 |
|
$imageContent = ob_get_contents(); |
447
|
|
|
ob_end_clean(); |
448
|
4 |
|
imagedestroy($im); |
449
|
|
|
|
450
|
|
|
return base64_encode($imageContent); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
/** |
454
|
|
|
* Get image resource. |
455
|
|
|
* |
456
|
4 |
|
* @return resource|GdImage |
457
|
|
|
*/ |
458
|
4 |
|
private function getImageResource() |
459
|
|
|
{ |
460
|
|
|
if (version_compare(phpversion(), '8.0.0', '>=')) { |
461
|
|
|
if (!$this->im instanceof GdImage) { |
462
|
|
|
// @codeCoverageIgnoreStart |
463
|
|
|
throw new RuntimeException( |
464
|
|
|
'Cannot create image resource.' |
465
|
|
|
); |
466
|
|
|
// @codeCoverageIgnoreEnd |
467
|
4 |
|
} |
468
|
|
|
} else { |
469
|
|
|
if (!is_resource($this->im)) { |
470
|
|
|
// @codeCoverageIgnoreStart |
471
|
|
|
throw new RuntimeException( |
472
|
|
|
'Cannot create image resource.' |
473
|
|
|
); |
474
|
|
|
// @codeCoverageIgnoreEnd |
475
|
|
|
} |
476
|
|
|
} |
477
|
4 |
|
|
478
|
|
|
return $this->im; |
|
|
|
|
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
|
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.