Passed
Push — 2.x ( 96150f...1cf170 )
by Terry
02:22
created

ImageCaptcha   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 133
dl 0
loc 405
rs 9.92
c 1
b 0
f 0
wmc 31

12 Methods

Rating   Name   Duplication   Size   Complexity  
A form() 0 39 3
A createBackground() 0 5 1
A __construct() 0 27 5
A createSpiralPattern() 0 31 4
A createCanvas() 0 9 2
A response() 0 19 4
A createBorder() 0 6 1
A createCaptcha() 0 53 2
A getImageResource() 0 12 2
A getImageBase64Content() 0 27 3
A createRandomWords() 0 12 2
A writeText() 0 12 2
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
        foreach ($defaults['colors'] as $k => $v) {
129
            if (!is_array($this->properties['colors'][$k])) {
130
                $this->properties['colors'][$k] = $defaults['colors'][$k];
131
            }
132
        }
133
    }
134
135
    /**
136
     * Response the result.
137
     *
138
     * @return bool
139
     */
140
    public function response(): bool
141
    {
142
        $postParams = get_request()->getParsedBody();
143
        $sessionCaptchaHash = get_session()->get('shieldon_image_captcha_hash');
144
        
145
        if (empty($postParams['shieldon_image_captcha']) || empty($sessionCaptchaHash)) {
146
            return false;
147
        }
148
149
        $flag = false;
150
151
        if (password_verify($postParams['shieldon_image_captcha'], $sessionCaptchaHash)) {
152
            $flag = true;
153
        }
154
155
        // Prevent detecting POST method on RESTful frameworks.
156
        unset_superglobal('shieldon_image_captcha', 'post');
157
158
        return $flag;
159
    }
160
161
    /**
162
     * Output a required HTML.
163
     *
164
     * @return string
165
     */
166
    public function form(): string
167
    {
168
        // @codeCoverageIgnoreStart
169
        if (!extension_loaded('gd')) {
170
            return '';
171
        }
172
        // @codeCoverageIgnoreEnd
173
174
        $html = '';
175
        $base64image = $this->createCaptcha();
176
        $imgWidth = $this->properties['img_width'];
177
        $imgHeight = $this->properties['img_height'];
178
179
        if (!empty($base64image)) {
180
            $html = '<div style="padding: 0px; overflow: hidden; margin: 10px 0;">';
181
            $html .= '<div style="
182
                border: 1px #dddddd solid;
183
                overflow: hidden;
184
                border-radius: 3px;
185
                display: inline-block;
186
                padding: 5px;
187
                box-shadow: 0px 0px 4px 1px rgba(0,0,0,0.08);">';
188
            $html .= '<div style="margin-bottom: 2px;"><img src="data:image/' . $this->imageType . ';base64,' . $base64image . '" style="width: ' . $imgWidth . '; height: ' . $imgHeight . ';"></div>';
189
            $html .= '<div><input type="text" name="shieldon_image_captcha" style="
190
                width: 100px;
191
                border: 1px solid rgba(27,31,35,.2);
192
                border-radius: 3px;
193
                background-color: #fafafa;
194
                font-size: 14px;
195
                font-weight: bold;
196
                line-height: 20px;
197
                box-shadow: inset 0 1px 2px rgba(27,31,35,.075);
198
                vertical-align: middle;
199
                padding: 6px 12px;;"></div>';
200
            $html .= '</div>';
201
            $html .= '</div>';
202
        }
203
204
        return $html;
205
    }
206
207
    /**
208
     * Create CAPTCHA
209
     *
210
     * @return string
211
     */
212
    protected function createCaptcha()
213
    {
214
        $imgWidth = $this->properties['img_width'];
215
        $imgHeight = $this->properties['img_height'];
216
217
        $this->createCanvas($imgWidth, $imgHeight);
218
219
        $im = $this->getImageResource();
220
221
        // Assign colors. 
222
        $colors = [];
223
224
        foreach ($this->properties['colors'] as $k => $v) {
225
226
            /**
227
             * Create color identifier for each color.
228
             *
229
             * @var int
230
             */
231
            $colors[$k] = imagecolorallocate($im, $v[0], $v[1], $v[2]);
232
        }
233
234
        $this->createRandomWords();
235
236
        $this->createBackground(
237
            $imgWidth,
238
            $imgHeight, 
239
            $colors['background']
240
        );
241
242
        $this->createSpiralPattern(
243
            $imgWidth,
244
            $imgHeight,
245
            $colors['grid']
246
        );
247
248
        $this->writeText(
249
            $imgWidth,
250
            $imgHeight,
251
            $colors['text']
252
        );
253
254
        $this->createBorder(
255
            $imgWidth,
256
            $imgHeight,
257
            $colors['border']
258
        );
259
260
        // Save hash to the user sesssion.
261
        $hash = password_hash($this->word, PASSWORD_BCRYPT);
262
        get_session()->set('shieldon_image_captcha_hash', $hash);
263
264
        return $this->getImageBase64Content();
265
    }
266
267
    /**
268
     * Prepare the random words that want to display to front.
269
     *
270
     * @return void
271
     */
272
    private function createRandomWords()
273
    {
274
        $this->word = '';
275
276
        $poolLength = strlen($this->properties['pool']);
277
        $randMax = $poolLength - 1;
278
279
        for ($i = 0; $i < $this->properties['word_length']; $i++) {
280
            $this->word .= $this->properties['pool'][random_int(0, $randMax)];
281
        }
282
283
        $this->length = strlen($this->word);
284
    }
285
286
    /**
287
     * Create a canvas.
288
     *
289
     * This method initialize the $im.
290
     * 
291
     * @param int $imgWidth  The width of the image.
292
     * @param int $imgHeight The height of the image.
293
     *
294
     * @return void
295
     */
296
    private function createCanvas(int $imgWidth, int $imgHeight)
297
    {
298
        if (function_exists('imagecreatetruecolor')) {
299
            $this->im = imagecreatetruecolor($imgWidth, $imgHeight);
300
    
301
            // @codeCoverageIgnoreStart
302
303
        } else {
304
            $this->im = imagecreate($imgWidth, $imgHeight);
305
        }
306
307
        // @codeCoverageIgnoreEnd
308
    }
309
310
    /**
311
     * Create the background.
312
     * 
313
     * @param int $imgWidth  The width of the image.
314
     * @param int $imgHeight The height of the image.
315
     * @param int $bgColor   The RGB color for the background of the image.
316
     *
317
     * @return void
318
     */
319
    private function createBackground(int $imgWidth, int $imgHeight, $bgColor)
320
    {
321
        $im = $this->getImageResource();
322
323
        imagefilledrectangle($im, 0, 0, $imgWidth, $imgHeight, $bgColor);
324
    }
325
326
    /**
327
     * Create a spiral patten.
328
     *
329
     * @param int $imgWidth  The width of the image.
330
     * @param int $imgHeight The height of the image.
331
     * @param int $gridColor The RGB color for the gri of the image.
332
     *
333
     * @return void
334
     */
335
    private function createSpiralPattern(int $imgWidth, int $imgHeight, $gridColor)
336
    {
337
        $im = $this->getImageResource();
338
339
        // Determine angle and position.
340
        $angle = ($this->length >= 6) ? mt_rand(-($this->length - 6), ($this->length - 6)) : 0;
341
        $xAxis = mt_rand(6, (360 / $this->length) - 16);
342
        $yAxis = ($angle >= 0) ? mt_rand($imgHeight, $imgWidth) : mt_rand(6, $imgHeight);
343
344
        // Create the spiral pattern.
345
        $theta   = 1;
346
        $thetac  = 7;
347
        $radius  = 16;
348
        $circles = 20;
349
        $points  = 32;
350
351
        for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) {
352
            $theta += $thetac;
353
            $rad = $radius * ($i / $points);
354
355
            $x = (int) (($rad * cos($theta)) + $xAxis);
356
            $y = (int) (($rad * sin($theta)) + $yAxis);
357
358
            $theta += $thetac;
359
            $rad1 = $radius * (($i + 1) / $points);
360
361
            $x1 = (int) (($rad1 * cos($theta)) + $xAxis);
362
            $y1 = (int) (($rad1 * sin($theta)) + $yAxis);
363
364
            imageline($im, $x, $y, $x1, $y1, $gridColor);
365
            $theta -= $thetac;
366
        }  
367
    }
368
369
    /**
370
     * Write the text into the image canvas.
371
     *
372
     * @param int $imgWidth  The width of the image.
373
     * @param int $imgHeight The height of the image.
374
     * @param int $textColor The RGB color for the grid of the image.
375
     *
376
     * @return void
377
     */
378
    private function writeText(int $imgWidth, int $imgHeight, $textColor)
379
    {
380
        $im = $this->getImageResource();
381
382
        $z = (int) ($imgWidth / ($this->length / 3));
383
        $x = mt_rand(0, $z);
384
        // $y = 0;
385
386
        for ($i = 0; $i < $this->length; $i++) {
387
            $y = mt_rand(0, $imgHeight / 2);
388
            imagestring($im, 5, $x, $y, $this->word[$i], $textColor);
389
            $x += ($this->properties['font_spacing'] * 2);
390
        }
391
    }
392
393
    /**
394
     * Write the text into the image canvas.
395
     *
396
     * @param int $imgWidth    The width of the image.
397
     * @param int $imgHeight   The height of the image.
398
     * @param int $borderColor The RGB color for the border of the image.
399
     *
400
     * @return void
401
     */
402
    private function createBorder(int $imgWidth, int $imgHeight, $borderColor): void
403
    {
404
        $im = $this->getImageResource();
405
406
        // Create the border.
407
        imagerectangle($im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
408
    }
409
410
    /**
411
     * Get the base64 string of the image.
412
     *
413
     * @return string
414
     */
415
    private function getImageBase64Content(): string
416
    {
417
        $im = $this->getImageResource();
418
419
        // Generate image in base64 string.
420
        ob_start();
421
422
        if (function_exists('imagejpeg')) {
423
            $this->imageType = 'jpeg';
424
            imagejpeg($im);
425
426
            // @codeCoverageIgnoreStart
427
428
        } elseif (function_exists('imagepng')) {
429
            $this->imageType = 'png';
430
            imagepng($im);
431
        } else {
432
            echo '';
433
        }
434
435
        // @codeCoverageIgnoreEnd
436
437
        $imageContent = ob_get_contents();
438
        ob_end_clean();
439
        imagedestroy($im);
440
441
        return base64_encode($imageContent);
442
    }
443
444
    /**
445
     * Get image resource.
446
     *
447
     * @return resource
448
     */
449
    private function getImageResource()
450
    {
451
        if (!is_resource($this->im)) {
452
453
            // @codeCoverageIgnoreStart
454
            throw new RuntimeException(
455
                'Cannot create image resource.'
456
            );
457
            // @codeCoverageIgnoreEnd
458
        }
459
460
        return $this->im;
461
    }
462
}
463