Passed
Push — 2.x ( c97871...a1962c )
by Terry
02:15
created

ImageCaptcha::__construct()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 31
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
c 1
b 0
f 0
nc 18
nop 1
dl 0
loc 31
rs 8.9617
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
        $im = $this->getImageResource();
224
225
        // Assign colors. 
226
        $colors = [];
227
228
        foreach ($this->properties['colors'] as $k => $v) {
229
230
            /**
231
             * Create color identifier for each color.
232
             *
233
             * @var int
234
             */
235
            $colors[$k] = imagecolorallocate($im, $v[0], $v[1], $v[2]);
236
        }
237
238
        $this->createRandomWords();
239
240
        $this->createBackground(
241
            $imgWidth,
242
            $imgHeight, 
243
            $colors['background']
244
        );
245
246
        $this->createSpiralPattern(
247
            $imgWidth,
248
            $imgHeight,
249
            $colors['grid']
250
        );
251
252
        $this->writeText(
253
            $imgWidth,
254
            $imgHeight,
255
            $colors['text']
256
        );
257
258
        $this->createBorder(
259
            $imgWidth,
260
            $imgHeight,
261
            $colors['border']
262
        );
263
264
        // Save hash to the user sesssion.
265
        $hash = password_hash($this->word, PASSWORD_BCRYPT);
266
        get_session()->set('shieldon_image_captcha_hash', $hash);
267
268
        return $this->getImageBase64Content();
269
    }
270
271
    /**
272
     * Prepare the random words that want to display to front.
273
     *
274
     * @return void
275
     */
276
    private function createRandomWords()
277
    {
278
        $this->word = '';
279
280
        $poolLength = strlen($this->properties['pool']);
281
        $randMax = $poolLength - 1;
282
283
        for ($i = 0; $i < $this->properties['word_length']; $i++) {
284
            $this->word .= $this->properties['pool'][random_int(0, $randMax)];
285
        }
286
287
        $this->length = strlen($this->word);
288
    }
289
290
    /**
291
     * Create a canvas.
292
     *
293
     * This method initialize the $im.
294
     * 
295
     * @param int $imgWidth  The width of the image.
296
     * @param int $imgHeight The height of the image.
297
     *
298
     * @return void
299
     */
300
    private function createCanvas(int $imgWidth, int $imgHeight)
301
    {
302
        if (function_exists('imagecreatetruecolor')) {
303
            $this->im = imagecreatetruecolor($imgWidth, $imgHeight);
304
    
305
            // @codeCoverageIgnoreStart
306
307
        } else {
308
            $this->im = imagecreate($imgWidth, $imgHeight);
309
        }
310
311
        // @codeCoverageIgnoreEnd
312
    }
313
314
    /**
315
     * Create the background.
316
     * 
317
     * @param int $imgWidth  The width of the image.
318
     * @param int $imgHeight The height of the image.
319
     * @param int $bgColor   The RGB color for the background of the image.
320
     *
321
     * @return void
322
     */
323
    private function createBackground(int $imgWidth, int $imgHeight, $bgColor)
324
    {
325
        $im = $this->getImageResource();
326
327
        imagefilledrectangle($im, 0, 0, $imgWidth, $imgHeight, $bgColor);
328
    }
329
330
    /**
331
     * Create a spiral patten.
332
     *
333
     * @param int $imgWidth  The width of the image.
334
     * @param int $imgHeight The height of the image.
335
     * @param int $gridColor The RGB color for the gri of the image.
336
     *
337
     * @return void
338
     */
339
    private function createSpiralPattern(int $imgWidth, int $imgHeight, $gridColor)
340
    {
341
        $im = $this->getImageResource();
342
343
        // Determine angle and position.
344
        $angle = ($this->length >= 6) ? mt_rand(-($this->length - 6), ($this->length - 6)) : 0;
345
        $xAxis = mt_rand(6, (360 / $this->length) - 16);
346
        $yAxis = ($angle >= 0) ? mt_rand($imgHeight, $imgWidth) : mt_rand(6, $imgHeight);
347
348
        // Create the spiral pattern.
349
        $theta   = 1;
350
        $thetac  = 7;
351
        $radius  = 16;
352
        $circles = 20;
353
        $points  = 32;
354
355
        for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) {
356
            $theta += $thetac;
357
            $rad = $radius * ($i / $points);
358
359
            $x = (int) (($rad * cos($theta)) + $xAxis);
360
            $y = (int) (($rad * sin($theta)) + $yAxis);
361
362
            $theta += $thetac;
363
            $rad1 = $radius * (($i + 1) / $points);
364
365
            $x1 = (int) (($rad1 * cos($theta)) + $xAxis);
366
            $y1 = (int) (($rad1 * sin($theta)) + $yAxis);
367
368
            imageline($im, $x, $y, $x1, $y1, $gridColor);
369
            $theta -= $thetac;
370
        }  
371
    }
372
373
    /**
374
     * Write the text into the image canvas.
375
     *
376
     * @param int $imgWidth  The width of the image.
377
     * @param int $imgHeight The height of the image.
378
     * @param int $textColor The RGB color for the grid of the image.
379
     *
380
     * @return void
381
     */
382
    private function writeText(int $imgWidth, int $imgHeight, $textColor)
383
    {
384
        $im = $this->getImageResource();
385
386
        $z = (int) ($imgWidth / ($this->length / 3));
387
        $x = mt_rand(0, $z);
388
        // $y = 0;
389
390
        for ($i = 0; $i < $this->length; $i++) {
391
            $y = mt_rand(0, $imgHeight / 2);
392
            imagestring($im, 5, $x, $y, $this->word[$i], $textColor);
393
            $x += ($this->properties['font_spacing'] * 2);
394
        }
395
    }
396
397
    /**
398
     * Write the text into the image canvas.
399
     *
400
     * @param int $imgWidth    The width of the image.
401
     * @param int $imgHeight   The height of the image.
402
     * @param int $borderColor The RGB color for the border of the image.
403
     *
404
     * @return void
405
     */
406
    private function createBorder(int $imgWidth, int $imgHeight, $borderColor): void
407
    {
408
        $im = $this->getImageResource();
409
410
        // Create the border.
411
        imagerectangle($im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
412
    }
413
414
    /**
415
     * Get the base64 string of the image.
416
     *
417
     * @return string
418
     */
419
    private function getImageBase64Content(): string
420
    {
421
        $im = $this->getImageResource();
422
423
        // Generate image in base64 string.
424
        ob_start();
425
426
        if (function_exists('imagejpeg')) {
427
            $this->imageType = 'jpeg';
428
            imagejpeg($im);
429
430
            // @codeCoverageIgnoreStart
431
432
        } elseif (function_exists('imagepng')) {
433
            $this->imageType = 'png';
434
            imagepng($im);
435
        } else {
436
            echo '';
437
        }
438
439
        // @codeCoverageIgnoreEnd
440
441
        $imageContent = ob_get_contents();
442
        ob_end_clean();
443
        imagedestroy($im);
444
445
        return base64_encode($imageContent);
446
    }
447
448
    /**
449
     * Get image resource.
450
     *
451
     * @return resource
452
     */
453
    private function getImageResource()
454
    {
455
        if (!is_resource($this->im)) {
456
457
            // @codeCoverageIgnoreStart
458
            throw new RuntimeException(
459
                'Cannot create image resource.'
460
            );
461
            // @codeCoverageIgnoreEnd
462
        }
463
464
        return $this->im;
465
    }
466
}
467