Passed
Push — 2.x ( ba8268...1f390b )
by Terry
02:18
created

ImageCaptcha::response()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

253
        imagefilledrectangle($this->im, 0, 0, $imgWidth, $imgHeight, /** @scrutinizer ignore-type */ $bgColor);
Loading history...
254
    }
255
256
    /**
257
     * Create a spiral patten.
258
     *
259
     * @param int      $imgWidth  The width of the image.
260
     * @param int      $imgHeight The height of the image.
261
     * @param resource $gridColor The RGB color for the gri of the image.
262
     *
263
     * @return void
264
     */
265
    private function createSpiralPattern(int $imgWidth, int $imgHeight, $gridColor)
266
    {
267
        // Determine angle and position.
268
        $angle = ($this->length >= 6) ?
269
            mt_rand(-($this->length - 6), ($this->length - 6)) :
270
            0;
271
272
        $xAxis = mt_rand(6, (360 / $this->length) - 16);
273
274
        $yAxis = ($angle >= 0) ?
275
            mt_rand($imgHeight, $imgWidth) :
276
            mt_rand(6, $imgHeight);
277
278
        // Create the spiral pattern.
279
        $theta = 1;
280
        $thetac	= 7;
281
        $radius	= 16;
282
        $circles = 20;
283
        $points	= 32;
284
285
        for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) {
286
            $theta += $thetac;
287
            $rad = $radius * ($i / $points);
288
289
            $x = (int) (($rad * cos($theta)) + $xAxis);
290
            $y = (int) (($rad * sin($theta)) + $yAxis);
291
292
            $theta += $thetac;
293
            $rad1 = $radius * (($i + 1) / $points);
294
295
            $x1 = (int) (($rad1 * cos($theta)) + $xAxis);
296
            $y1 = (int) (($rad1 * sin($theta)) + $yAxis);
297
298
            imageline($this->im, $x, $y, $x1, $y1, $gridColor);
0 ignored issues
show
Bug introduced by
$gridColor of type resource is incompatible with the type integer expected by parameter $color of imageline(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

298
            imageline($this->im, $x, $y, $x1, $y1, /** @scrutinizer ignore-type */ $gridColor);
Loading history...
299
            $theta -= $thetac;
300
        }  
301
    }
302
303
    /**
304
     * Write the text into the image canvas.
305
     *
306
     * @param int      $imgWidth  The width of the image.
307
     * @param int      $imgHeight The height of the image.
308
     * @param resource $textColor The RGB color for the grid of the image.
309
     *
310
     * @return void
311
     */
312
    private function writeText(int $imgWidth, int $imgHeight, $textColor)
313
    {
314
        $z = (int) ($imgWidth / ($this->length / 3));
315
        $x = mt_rand(0, $z);
316
        $y = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $y is dead and can be removed.
Loading history...
317
318
        for ($i = 0; $i < $this->length; $i++) {
319
            $y = mt_rand(0 , $imgHeight / 2);
320
            imagestring($this->im, 5, $x, $y, $this->word[$i], $textColor);
0 ignored issues
show
Bug introduced by
$textColor of type resource is incompatible with the type integer expected by parameter $color of imagestring(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

320
            imagestring($this->im, 5, $x, $y, $this->word[$i], /** @scrutinizer ignore-type */ $textColor);
Loading history...
321
            $x += ($this->properties['font_spacing'] * 2);
322
        }
323
    }
324
325
    /**
326
     * Write the text into the image canvas.
327
     *
328
     * @param int      $imgWidth    The width of the image.
329
     * @param int      $imgHeight   The height of the image.
330
     * @param resource $borderColor The RGB color for the border of the image.
331
     *
332
     * @return void
333
     */
334
    private function createBorder(int $imgWidth, int $imgHeight, $borderColor): void
335
    {
336
        // Create the border.
337
        imagerectangle($this->im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
0 ignored issues
show
Bug introduced by
$borderColor of type resource is incompatible with the type integer expected by parameter $color of imagerectangle(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

337
        imagerectangle($this->im, 0, 0, $imgWidth - 1, $imgHeight - 1, /** @scrutinizer ignore-type */ $borderColor);
Loading history...
338
    }
339
340
    /**
341
     * Get the base64 string of the image.
342
     *
343
     * @return string
344
     */
345
    private function getImageBase64Content(): string
346
    {
347
        // Generate image in base64 string.
348
        ob_start ();
349
350
        if (function_exists('imagejpeg')) {
351
            $this->imageType = 'jpeg';
352
            imagejpeg($this->im);
353
354
        // @codeCoverageIgnoreStart
355
356
        } elseif (function_exists('imagepng')) {
357
            $this->imageType = 'png';
358
            imagepng($this->im);
359
        } else {
360
            echo '';
361
        }
362
363
        // @codeCoverageIgnoreEnd
364
365
        $imageContent = ob_get_contents();
366
        ob_end_clean();
367
        imagedestroy($this->im);
368
369
        return base64_encode($imageContent);
370
    }
371
372
    /**
373
     * Create CAPTCHA
374
     *
375
     * @return	string
376
     */
377
    protected function createCaptcha()
378
    {
379
        $imgWidth = $this->properties['img_width'];
380
        $imgHeight = $this->properties['img_height'];
381
382
        $this->createCanvas($imgWidth, $imgHeight);
383
384
        // Assign colors. 
385
        $colors = [];
386
387
        foreach ($this->properties['colors'] as $k => $v) {
388
            // Create image resources for each color.
389
            $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

389
            $colors[$k] = imagecolorallocate(/** @scrutinizer ignore-type */ $this->im, $v[0], $v[1], $v[2]);
Loading history...
390
        }
391
392
        $this->createRandomWords();
393
394
        $this->createBackground(
395
            $imgWidth,
396
            $imgHeight, 
397
            $colors['background']
398
        );
399
400
        $this->createSpiralPattern(
401
            $imgWidth,
402
            $imgHeight,
403
            $colors['grid']
404
        );
405
406
        $this->writeText(
407
            $imgWidth,
408
            $imgHeight,
409
            $colors['text']
410
        );
411
412
        $this->createBorder(
413
            $imgWidth,
414
            $imgHeight,
415
            $colors['border']
416
        );
417
418
        // Save hash to the user sesssion.
419
        $hash = password_hash($this->word, PASSWORD_BCRYPT);
420
        get_session()->set('shieldon_image_captcha_hash', $hash);
421
422
        return $this->getImageBase64Content();
423
    }
424
}
425