ImageCaptcha::createCanvas()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 8
ccs 0
cts 0
cp 0
rs 10
cc 2
nc 2
nop 2
crap 6
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);
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...
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;
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...
479
    }
480
}
481