Passed
Push — 2.x ( 147c36...984b77 )
by Terry
01:58
created

ImageCaptcha::checkImType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 6
rs 10
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 Shieldon\Firewall\Captcha\CaptchaProvider;
27
28
use function Shieldon\Firewall\get_request;
29
use function Shieldon\Firewall\get_session;
30
use function Shieldon\Firewall\unset_superglobal;
31
use function base64_encode;
32
use function cos;
33
use function function_exists;
34
use function imagecolorallocate;
35
use function imagecreate;
36
use function imagecreatetruecolor;
37
use function imagedestroy;
38
use function imagefilledrectangle;
39
use function imagejpeg;
40
use function imageline;
41
use function imagepng;
42
use function imagerectangle;
43
use function imagestring;
44
use function mt_rand;
45
use function ob_end_clean;
46
use function ob_get_contents;
47
use function password_hash;
48
use function password_verify;
49
use function random_int;
50
use function sin;
51
use function strlen;
52
53
/**
54
 * Simple Image Captcha.
55
 */
56
class ImageCaptcha extends CaptchaProvider
57
{
58
    /**
59
     * Settings.
60
     *
61
     * @var array
62
     */
63
    protected $properties = [];
64
65
66
    /**
67
     * Image type.
68
     *
69
     * @var string
70
     */
71
    protected $imageType = '';
72
73
    /**
74
     * Word.
75
     *
76
     * @var string
77
     */
78
    protected $word = '';
79
80
    /**
81
     * Image resource.
82
     * Throw exception the the value is not resource.
83
     *
84
     * @var resource|null|bool
85
     */
86
    private $im;
87
88
    /**
89
     * The length of the word.
90
     *
91
     * @var int
92
     */
93
    protected $length = 4;
94
95
    /**
96
     * Constructor.
97
     *
98
     * It will implement default configuration settings here.
99
     *
100
     * @param array $config The settings for creating Captcha.
101
     *
102
     * @return void
103
     */
104
    public function __construct(array $config = [])
105
    {
106
        $defaults = [
107
            'img_width'    => 250,
108
            'img_height'   => 50,
109
            'word_length'  => 8,
110
            'font_spacing' => 10,
111
            'pool'         => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
112
            'colors'       => [
113
                'background' => [255, 255, 255],
114
                'border'     => [153, 200, 255],
115
                'text'       => [51,  153, 255],
116
                'grid'       => [153, 200, 255]
117
            ]
118
        ];
119
120
        foreach ($defaults as $k => $v) {
121
            if (isset($config[$k])) {
122
                $this->properties[$k] = $config[$k];
123
            } else {
124
                $this->properties[$k] = $defaults[$k];
125
            }
126
        }
127
128
        if (!is_array($this->properties['colors'])) {
129
            $this->properties['colors'] = $defaults['colors'];
130
        }
131
132
        foreach ($defaults['colors'] as $k => $v) {
133
            if (!is_array($this->properties['colors'][$k])) {
134
                $this->properties['colors'][$k] = $defaults['colors'][$k];
135
            }
136
        }
137
    }
138
139
    /**
140
     * Response the result.
141
     *
142
     * @return bool
143
     */
144
    public function response(): bool
145
    {
146
        $postParams = get_request()->getParsedBody();
147
        $sessionCaptchaHash = get_session()->get('shieldon_image_captcha_hash');
148
        
149
        if (empty($postParams['shieldon_image_captcha']) || empty($sessionCaptchaHash)) {
150
            return false;
151
        }
152
153
        $flag = false;
154
155
        if (password_verify($postParams['shieldon_image_captcha'], $sessionCaptchaHash)) {
156
            $flag = true;
157
        }
158
159
        // Prevent detecting POST method on RESTful frameworks.
160
        unset_superglobal('shieldon_image_captcha', 'post');
161
162
        return $flag;
163
    }
164
165
    /**
166
     * Output a required HTML.
167
     *
168
     * @return string
169
     */
170
    public function form(): string
171
    {
172
        // @codeCoverageIgnoreStart
173
        if (!extension_loaded('gd')) {
174
            return '';
175
        }
176
        // @codeCoverageIgnoreEnd
177
178
        $html = '';
179
        $base64image = $this->createCaptcha();
180
        $imgWidth = $this->properties['img_width'];
181
        $imgHeight = $this->properties['img_height'];
182
183
        if (!empty($base64image)) {
184
            $html = '<div style="padding: 0px; overflow: hidden; margin: 10px 0;">';
185
            $html .= '<div style="
186
                border: 1px #dddddd solid;
187
                overflow: hidden;
188
                border-radius: 3px;
189
                display: inline-block;
190
                padding: 5px;
191
                box-shadow: 0px 0px 4px 1px rgba(0,0,0,0.08);">';
192
            $html .= '<div style="margin-bottom: 2px;"><img src="data:image/' . $this->imageType . ';base64,' . $base64image . '" style="width: ' . $imgWidth . '; height: ' . $imgHeight . ';"></div>';
193
            $html .= '<div><input type="text" name="shieldon_image_captcha" style="
194
                width: 100px;
195
                border: 1px solid rgba(27,31,35,.2);
196
                border-radius: 3px;
197
                background-color: #fafafa;
198
                font-size: 14px;
199
                font-weight: bold;
200
                line-height: 20px;
201
                box-shadow: inset 0 1px 2px rgba(27,31,35,.075);
202
                vertical-align: middle;
203
                padding: 6px 12px;;"></div>';
204
            $html .= '</div>';
205
            $html .= '</div>';
206
        }
207
208
        return $html;
209
    }
210
211
    /**
212
     * Create CAPTCHA
213
     *
214
     * @return string
215
     */
216
    protected function createCaptcha()
217
    {
218
        $imgWidth = $this->properties['img_width'];
219
        $imgHeight = $this->properties['img_height'];
220
221
        $this->createCanvas($imgWidth, $imgHeight);
222
223
        // Assign colors. 
224
        $colors = [];
225
226
        foreach ($this->properties['colors'] as $k => $v) {
227
228
            /**
229
             * Create color identifier for each color.
230
             *
231
             * @var int
232
             */
233
            $colors[$k] = imagecolorallocate($this->im, $v[0], $v[1], $v[2]);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type false; however, parameter $image of imagecolorallocate() does only seem to accept resource, 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 ignore-type  annotation

233
            $colors[$k] = imagecolorallocate(/** @scrutinizer ignore-type */ $this->im, $v[0], $v[1], $v[2]);
Loading history...
234
        }
235
236
        $this->createRandomWords();
237
238
        $this->createBackground(
239
            $imgWidth,
240
            $imgHeight, 
241
            $colors['background']
242
        );
243
244
        $this->createSpiralPattern(
245
            $imgWidth,
246
            $imgHeight,
247
            $colors['grid']
248
        );
249
250
        $this->writeText(
251
            $imgWidth,
252
            $imgHeight,
253
            $colors['text']
254
        );
255
256
        $this->createBorder(
257
            $imgWidth,
258
            $imgHeight,
259
            $colors['border']
260
        );
261
262
        // Save hash to the user sesssion.
263
        $hash = password_hash($this->word, PASSWORD_BCRYPT);
264
        get_session()->set('shieldon_image_captcha_hash', $hash);
265
266
        return $this->getImageBase64Content();
267
    }
268
269
    /**
270
     * Prepare the random words that want to display to front.
271
     *
272
     * @return void
273
     */
274
    private function createRandomWords()
275
    {
276
        $this->word = '';
277
278
        $poolLength = strlen($this->properties['pool']);
279
        $randMax = $poolLength - 1;
280
281
        for ($i = 0; $i < $this->properties['word_length']; $i++) {
282
            $this->word .= $this->properties['pool'][random_int(0, $randMax)];
283
        }
284
285
        $this->length = strlen($this->word);
286
    }
287
288
    /**
289
     * Create a canvas.
290
     *
291
     * This method initialize the $im.
292
     * 
293
     * @param int $imgWidth  The width of the image.
294
     * @param int $imgHeight The height of the image.
295
     *
296
     * @return void
297
     */
298
    private function createCanvas(int $imgWidth, int $imgHeight)
299
    {
300
        if (function_exists('imagecreatetruecolor')) {
301
            $this->im = imagecreatetruecolor($imgWidth, $imgHeight);
302
    
303
            // @codeCoverageIgnoreStart
304
305
        } else {
306
            $this->im = imagecreate($imgWidth, $imgHeight);
307
        }
308
309
        // @codeCoverageIgnoreEnd
310
    }
311
312
    /**
313
     * Create the background.
314
     * 
315
     * @param int $imgWidth  The width of the image.
316
     * @param int $imgHeight The height of the image.
317
     * @param int $bgColor   The RGB color for the background of the image.
318
     *
319
     * @return void
320
     */
321
    private function createBackground(int $imgWidth, int $imgHeight, $bgColor)
322
    {
323
        $this->checkImType();
324
325
        imagefilledrectangle($this->im, 0, 0, $imgWidth, $imgHeight, $bgColor);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type boolean; however, parameter $image of imagefilledrectangle() does only seem to accept resource, 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 ignore-type  annotation

325
        imagefilledrectangle(/** @scrutinizer ignore-type */ $this->im, 0, 0, $imgWidth, $imgHeight, $bgColor);
Loading history...
326
    }
327
328
    /**
329
     * Create a spiral patten.
330
     *
331
     * @param int $imgWidth  The width of the image.
332
     * @param int $imgHeight The height of the image.
333
     * @param int $gridColor The RGB color for the gri of the image.
334
     *
335
     * @return void
336
     */
337
    private function createSpiralPattern(int $imgWidth, int $imgHeight, $gridColor)
338
    {
339
        $this->checkImType();
340
341
        // Determine angle and position.
342
        $angle = ($this->length >= 6) ? mt_rand(-($this->length - 6), ($this->length - 6)) : 0;
343
        $xAxis = mt_rand(6, (360 / $this->length) - 16);
344
        $yAxis = ($angle >= 0) ? mt_rand($imgHeight, $imgWidth) : mt_rand(6, $imgHeight);
345
346
        // Create the spiral pattern.
347
        $theta   = 1;
348
        $thetac  = 7;
349
        $radius  = 16;
350
        $circles = 20;
351
        $points  = 32;
352
353
        for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) {
354
            $theta += $thetac;
355
            $rad = $radius * ($i / $points);
356
357
            $x = (int) (($rad * cos($theta)) + $xAxis);
358
            $y = (int) (($rad * sin($theta)) + $yAxis);
359
360
            $theta += $thetac;
361
            $rad1 = $radius * (($i + 1) / $points);
362
363
            $x1 = (int) (($rad1 * cos($theta)) + $xAxis);
364
            $y1 = (int) (($rad1 * sin($theta)) + $yAxis);
365
366
            imageline($this->im, $x, $y, $x1, $y1, $gridColor);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type boolean; however, parameter $image of imageline() does only seem to accept resource, 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 ignore-type  annotation

366
            imageline(/** @scrutinizer ignore-type */ $this->im, $x, $y, $x1, $y1, $gridColor);
Loading history...
367
            $theta -= $thetac;
368
        }  
369
    }
370
371
    /**
372
     * Write the text into the image canvas.
373
     *
374
     * @param int $imgWidth  The width of the image.
375
     * @param int $imgHeight The height of the image.
376
     * @param int $textColor The RGB color for the grid of the image.
377
     *
378
     * @return void
379
     */
380
    private function writeText(int $imgWidth, int $imgHeight, $textColor)
381
    {
382
        $this->checkImType();
383
384
        $z = (int) ($imgWidth / ($this->length / 3));
385
        $x = mt_rand(0, $z);
386
        // $y = 0;
387
388
        for ($i = 0; $i < $this->length; $i++) {
389
            $y = mt_rand(0, $imgHeight / 2);
390
            imagestring($this->im, 5, $x, $y, $this->word[$i], $textColor);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type boolean; however, parameter $image of imagestring() does only seem to accept resource, 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 ignore-type  annotation

390
            imagestring(/** @scrutinizer ignore-type */ $this->im, 5, $x, $y, $this->word[$i], $textColor);
Loading history...
391
            $x += ($this->properties['font_spacing'] * 2);
392
        }
393
    }
394
395
    /**
396
     * Write the text into the image canvas.
397
     *
398
     * @param int $imgWidth    The width of the image.
399
     * @param int $imgHeight   The height of the image.
400
     * @param int $borderColor The RGB color for the border of the image.
401
     *
402
     * @return void
403
     */
404
    private function createBorder(int $imgWidth, int $imgHeight, $borderColor): void
405
    {
406
        $this->checkImType();
407
408
        // Create the border.
409
        imagerectangle($this->im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type boolean; however, parameter $image of imagerectangle() does only seem to accept resource, 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 ignore-type  annotation

409
        imagerectangle(/** @scrutinizer ignore-type */ $this->im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
Loading history...
410
    }
411
412
    /**
413
     * Get the base64 string of the image.
414
     *
415
     * @return string
416
     */
417
    private function getImageBase64Content(): string
418
    {
419
        $this->checkImType();
420
421
        // Generate image in base64 string.
422
        ob_start();
423
424
        if (function_exists('imagejpeg')) {
425
            $this->imageType = 'jpeg';
426
            imagejpeg($this->im);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type boolean; however, parameter $image of imagejpeg() does only seem to accept resource, 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 ignore-type  annotation

426
            imagejpeg(/** @scrutinizer ignore-type */ $this->im);
Loading history...
427
428
            // @codeCoverageIgnoreStart
429
430
        } elseif (function_exists('imagepng')) {
431
            $this->imageType = 'png';
432
            imagepng($this->im);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type boolean; however, parameter $image of imagepng() does only seem to accept resource, 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 ignore-type  annotation

432
            imagepng(/** @scrutinizer ignore-type */ $this->im);
Loading history...
433
        } else {
434
            echo '';
435
        }
436
437
        // @codeCoverageIgnoreEnd
438
439
        $imageContent = ob_get_contents();
440
        ob_end_clean();
441
        imagedestroy($this->im);
0 ignored issues
show
Bug introduced by
It seems like $this->im can also be of type boolean; however, parameter $image of imagedestroy() does only seem to accept resource, 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 ignore-type  annotation

441
        imagedestroy(/** @scrutinizer ignore-type */ $this->im);
Loading history...
442
443
        return base64_encode($imageContent);
444
    }
445
446
    /**
447
     * Throw exception if the image can not be created.
448
     *
449
     * @return void
450
     */
451
    private function checkImType(): void
452
    {
453
        if (!is_resource($this->im)) {
454
            // @codeCoverageIgnoreStart
455
            throw new RuntimeException(
456
                'Cannot create image resource.'
457
            );
458
            // @codeCoverageIgnoreEnd
459
        }
460
    }
461
}
462