Test Failed
Push — 2.x ( 25acc4...469edd )
by Terry
20:00
created

ImageCaptcha::response()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.9666
cc 4
nc 3
nop 0
crap 4
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/' . $this->imageType . ';base64,' . $base64image . '" style="width: ' . $imgWidth . '; height: ' . $imgHeight . ';"></div>';
194 4
            $html .= '<div><input type="text" name="shieldon_image_captcha" style="
195
                width: 100px;
196
                border: 1px solid rgba(27,31,35,.2);
197
                border-radius: 3px;
198
                background-color: #fafafa;
199
                font-size: 14px;
200
                font-weight: bold;
201
                line-height: 20px;
202
                box-shadow: inset 0 1px 2px rgba(27,31,35,.075);
203
                vertical-align: middle;
204 4
                padding: 6px 12px;;"></div>';
205 4
            $html .= '</div>';
206 4
            $html .= '</div>';
207
        }
208
209 4
        return $html;
210
    }
211
212
    /**
213
     * Create CAPTCHA
214
     *
215
     * @return string
216
     */
217 4
    protected function createCaptcha()
218
    {
219 4
        $imgWidth = $this->properties['img_width'];
220 4
        $imgHeight = $this->properties['img_height'];
221
222 4
        $this->createCanvas($imgWidth, $imgHeight);
223
224 4
        $im = $this->getImageResource();
225
226
        // Assign colors. 
227 4
        $colors = [];
228
229 4
        foreach ($this->properties['colors'] as $k => $v) {
230
231
            /**
232
             * Create color identifier for each color.
233
             *
234
             * @var int
235
             */
236 4
            $colors[$k] = imagecolorallocate($im, $v[0], $v[1], $v[2]);
237
        }
238
239 4
        $this->createRandomWords();
240
241 4
        $this->createBackground(
242 4
            $imgWidth,
243 4
            $imgHeight, 
244 4
            $colors['background']
245 4
        );
246
247 4
        $this->createSpiralPattern(
248 4
            $imgWidth,
249 4
            $imgHeight,
250 4
            $colors['grid']
251 4
        );
252
253 4
        $this->writeText(
254 4
            $imgWidth,
255 4
            $imgHeight,
256 4
            $colors['text']
257 4
        );
258
259 4
        $this->createBorder(
260 4
            $imgWidth,
261 4
            $imgHeight,
262 4
            $colors['border']
263 4
        );
264
265
        // Save hash to the user sesssion.
266 4
        $hash = password_hash($this->word, PASSWORD_BCRYPT);
267
268 4
        get_session_instance()->set('shieldon_image_captcha_hash', $hash);
269 4
        get_session_instance()->save();
270
271 4
        return $this->getImageBase64Content();
272
    }
273
274
    /**
275
     * Prepare the random words that want to display to front.
276
     *
277
     * @return void
278
     */
279 4
    private function createRandomWords()
280
    {
281 4
        $this->word = '';
282
283 4
        $poolLength = strlen($this->properties['pool']);
284 4
        $randMax = $poolLength - 1;
285
286 4
        for ($i = 0; $i < $this->properties['word_length']; $i++) {
287 4
            $this->word .= $this->properties['pool'][random_int(0, $randMax)];
288
        }
289
290 4
        $this->length = strlen($this->word);
291
    }
292
293
    /**
294
     * Create a canvas.
295
     *
296
     * This method initialize the $im.
297
     * 
298
     * @param int $imgWidth  The width of the image.
299
     * @param int $imgHeight The height of the image.
300
     *
301
     * @return void
302
     */
303 4
    private function createCanvas(int $imgWidth, int $imgHeight)
304
    {
305 4
        if (function_exists('imagecreatetruecolor')) {
306 4
            $this->im = imagecreatetruecolor($imgWidth, $imgHeight);
0 ignored issues
show
Documentation Bug introduced by
It seems like imagecreatetruecolor($imgWidth, $imgHeight) can also be of type GdImage. However, the property $im is declared as type boolean|null|resource. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
307
    
308
            // @codeCoverageIgnoreStart
309
310
        } else {
311
            $this->im = imagecreate($imgWidth, $imgHeight);
312
        }
313
314
        // @codeCoverageIgnoreEnd
315
    }
316
317
    /**
318
     * Create the background.
319
     * 
320
     * @param int $imgWidth  The width of the image.
321
     * @param int $imgHeight The height of the image.
322
     * @param int $bgColor   The RGB color for the background of the image.
323
     *
324
     * @return void
325
     */
326 4
    private function createBackground(int $imgWidth, int $imgHeight, $bgColor)
327
    {
328 4
        $im = $this->getImageResource();
329
330 4
        imagefilledrectangle($im, 0, 0, $imgWidth, $imgHeight, $bgColor);
331
    }
332
333
    /**
334
     * Create a spiral patten.
335
     *
336
     * @param int $imgWidth  The width of the image.
337
     * @param int $imgHeight The height of the image.
338
     * @param int $gridColor The RGB color for the gri of the image.
339
     *
340
     * @return void
341
     */
342 4
    private function createSpiralPattern(int $imgWidth, int $imgHeight, $gridColor)
343
    {
344 4
        $im = $this->getImageResource();
345
346
        // Determine angle and position.
347 4
        $angle = ($this->length >= 6) ? mt_rand(-($this->length - 6), ($this->length - 6)) : 0;
348 4
        $xAxis = mt_rand(6, (360 / $this->length) - 16);
349 4
        $yAxis = ($angle >= 0) ? mt_rand($imgHeight, $imgWidth) : mt_rand(6, $imgHeight);
350
351
        // Create the spiral pattern.
352 4
        $theta   = 1;
353 4
        $thetac  = 7;
354 4
        $radius  = 16;
355 4
        $circles = 20;
356 4
        $points  = 32;
357
358 4
        for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) {
359 4
            $theta += $thetac;
360 4
            $rad = $radius * ($i / $points);
361
362 4
            $x = (int) (($rad * cos($theta)) + $xAxis);
363 4
            $y = (int) (($rad * sin($theta)) + $yAxis);
364
365 4
            $theta += $thetac;
366 4
            $rad1 = $radius * (($i + 1) / $points);
367
368 4
            $x1 = (int) (($rad1 * cos($theta)) + $xAxis);
369 4
            $y1 = (int) (($rad1 * sin($theta)) + $yAxis);
370
371 4
            imageline($im, $x, $y, $x1, $y1, $gridColor);
372 4
            $theta -= $thetac;
373
        }  
374
    }
375
376
    /**
377
     * Write the text into the image canvas.
378
     *
379
     * @param int $imgWidth  The width of the image.
380
     * @param int $imgHeight The height of the image.
381
     * @param int $textColor The RGB color for the grid of the image.
382
     *
383
     * @return void
384
     */
385 4
    private function writeText(int $imgWidth, int $imgHeight, $textColor)
386
    {
387 4
        $im = $this->getImageResource();
388
389 4
        $z = (int) ($imgWidth / ($this->length / 3));
390 4
        $x = mt_rand(0, $z);
391
        // $y = 0;
392
393 4
        for ($i = 0; $i < $this->length; $i++) {
394 4
            $y = mt_rand(0, $imgHeight / 2);
395 4
            imagestring($im, 5, $x, $y, $this->word[$i], $textColor);
396 4
            $x += ($this->properties['font_spacing'] * 2);
397
        }
398
    }
399
400
    /**
401
     * Write the text into the image canvas.
402
     *
403
     * @param int $imgWidth    The width of the image.
404
     * @param int $imgHeight   The height of the image.
405
     * @param int $borderColor The RGB color for the border of the image.
406
     *
407
     * @return void
408
     */
409 4
    private function createBorder(int $imgWidth, int $imgHeight, $borderColor): void
410
    {
411 4
        $im = $this->getImageResource();
412
413
        // Create the border.
414 4
        imagerectangle($im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
415
    }
416
417
    /**
418
     * Get the base64 string of the image.
419
     *
420
     * @return string
421
     */
422 4
    private function getImageBase64Content(): string
423
    {
424 4
        $im = $this->getImageResource();
425
426
        // Generate image in base64 string.
427 4
        ob_start();
428
429 4
        if (function_exists('imagejpeg')) {
430 4
            $this->imageType = 'jpeg';
431 4
            imagejpeg($im);
432
433
            // @codeCoverageIgnoreStart
434
435
        } elseif (function_exists('imagepng')) {
436
            $this->imageType = 'png';
437
            imagepng($im);
438
        } else {
439
            echo '';
440
        }
441
442
        // @codeCoverageIgnoreEnd
443
444 4
        $imageContent = ob_get_contents();
445 4
        ob_end_clean();
446 4
        imagedestroy($im);
447
448 4
        return base64_encode($imageContent);
449
    }
450
451
    /**
452
     * Get image resource.
453
     *
454
     * @return resource|GdImage
455
     */
456 4
    private function getImageResource()
457
    {
458 4
        if (version_compare(phpversion(), '8.0.0', '>=')) {
459
            if (!$this->im instanceof GdImage)  {
460
                // @codeCoverageIgnoreStart
461
                throw new RuntimeException(
462
                    'Cannot create image resource.'
463
                );
464
                // @codeCoverageIgnoreEnd
465
            }
466
        } else {
467 4
            if (!is_resource($this->im))  {
468
469
                // @codeCoverageIgnoreStart
470
                throw new RuntimeException(
471
                    'Cannot create image resource.'
472
                );
473
                // @codeCoverageIgnoreEnd
474
            }
475
        }
476
477 4
        return $this->im;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->im also could return the type boolean which is incompatible with the documented return type GdImage|resource.
Loading history...
478
    }
479
}
480