1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <contact@terryl.in>
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 <contact@terryl.in>
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_instance;
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 3
    public function __construct(array $config = [])
105
    {
106
        $defaults = [
107 3
            '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 3
        foreach ($defaults as $k => $v) {
121 3
            if (isset($config[$k])) {
122 3
                $this->properties[$k] = $config[$k];
123
            } else {
124 3
                $this->properties[$k] = $defaults[$k];
125
            }
126
        }
127

128 3
        if (!is_array($this->properties['colors'])) {
129 3
            $this->properties['colors'] = $defaults['colors'];
130
        }
131

132 3
        foreach ($defaults['colors'] as $k => $v) {
133 3
            if (!is_array($this->properties['colors'][$k])) {
134 3
                $this->properties['colors'][$k] = $defaults['colors'][$k];
135
            }
136
        }
137
    }
138

139
    /**
140
     * Response the result.
141
     *
142
     * @return bool
143
     */
144 3
    public function response(): bool
145
    {
146 3
        $postParams = get_request()->getParsedBody();
147 3
        $sessionCaptchaHash = get_session_instance()->get('shieldon_image_captcha_hash');
148

149 3
        if (empty($postParams['shieldon_image_captcha']) || empty($sessionCaptchaHash)) {
150 3
            return false;
151
        }
152

153 3
        $flag = false;
154

155 3
        if (password_verify($postParams['shieldon_image_captcha'], $sessionCaptchaHash)) {
156 3
            $flag = true;
157
        }
158

159
        // Prevent detecting POST method on RESTful frameworks.
160 3
        unset_superglobal('shieldon_image_captcha', 'post');
161

162 3
        return $flag;
163
    }
164

165
    /**
166
     * Output a required HTML.
167
     *
168
     * @return string
169
     */
170 3
    public function form(): string
171
    {
172
        // @codeCoverageIgnoreStart
173
        if (!extension_loaded('gd')) {
174
            return '';
175
        }
176
        // @codeCoverageIgnoreEnd
177

178 3
        $html = '';
179 3
        $base64image = $this->createCaptcha();
180 3
        $imgWidth = $this->properties['img_width'];
181 3
        $imgHeight = $this->properties['img_height'];
182

183 3
        if (!empty($base64image)) {
184 3
            $html = '<div style="padding: 0px; overflow: hidden; margin: 10px 0;">';
185 3
            $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 3
            $html .= '<div style="margin-bottom: 2px;"><img src="data:image/' . $this->imageType . ';base64,' . $base64image . '" style="width: ' . $imgWidth . '; height: ' . $imgHeight . ';"></div>';
193 3
            $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 3
            $html .= '</div>';
205 3
            $html .= '</div>';
206
        }
207

208 3
        return $html;
209
    }
210

211
    /**
212
     * Create CAPTCHA
213
     *
214
     * @return string
215
     */
216 3
    protected function createCaptcha()
217
    {
218 3
        $imgWidth = $this->properties['img_width'];
219 3
        $imgHeight = $this->properties['img_height'];
220

221 3
        $this->createCanvas($imgWidth, $imgHeight);
222

223 3
        $im = $this->getImageResource();
224

225
        // Assign colors. 
226 3
        $colors = [];
227

228 3
        foreach ($this->properties['colors'] as $k => $v) {
229

230
            /**
231
             * Create color identifier for each color.
232
             *
233
             * @var int
234
             */
235 3
            $colors[$k] = imagecolorallocate($im, $v[0], $v[1], $v[2]);
236
        }
237

238 3
        $this->createRandomWords();
239

240 3
        $this->createBackground(
241 3
            $imgWidth,
242 2
            $imgHeight, 
243 3
            $colors['background']
244
        );
245

246 3
        $this->createSpiralPattern(
247 3
            $imgWidth,
248 2
            $imgHeight,
249 3
            $colors['grid']
250
        );
251

252 3
        $this->writeText(
253 3
            $imgWidth,
254 2
            $imgHeight,
255 3
            $colors['text']
256
        );
257

258 3
        $this->createBorder(
259 3
            $imgWidth,
260 2
            $imgHeight,
261 3
            $colors['border']
262
        );
263

264
        // Save hash to the user sesssion.
265 3
        $hash = password_hash($this->word, PASSWORD_BCRYPT);
266

267 3
        get_session_instance()->set('shieldon_image_captcha_hash', $hash);
268 3
        get_session_instance()->save();
269

270 3
        return $this->getImageBase64Content();
271
    }
272

273
    /**
274
     * Prepare the random words that want to display to front.
275
     *
276
     * @return void
277
     */
278 3
    private function createRandomWords()
279
    {
280 3
        $this->word = '';
281

282 3
        $poolLength = strlen($this->properties['pool']);
283 3
        $randMax = $poolLength - 1;
284

285 3
        for ($i = 0; $i < $this->properties['word_length']; $i++) {
286 3
            $this->word .= $this->properties['pool'][random_int(0, $randMax)];
287
        }
288

289 3
        $this->length = strlen($this->word);
290
    }
291

292
    /**
293
     * Create a canvas.
294
     *
295
     * This method initialize the $im.
296
     * 
297
     * @param int $imgWidth  The width of the image.
298
     * @param int $imgHeight The height of the image.
299
     *
300
     * @return void
301
     */
302 3
    private function createCanvas(int $imgWidth, int $imgHeight)
303
    {
304 3
        if (function_exists('imagecreatetruecolor')) {
305 3
            $this->im = imagecreatetruecolor($imgWidth, $imgHeight);
306
    
307
            // @codeCoverageIgnoreStart
308

309
        } else {
310
            $this->im = imagecreate($imgWidth, $imgHeight);
311
        }
312

313
        // @codeCoverageIgnoreEnd
314
    }
315

316
    /**
317
     * Create the background.
318
     * 
319
     * @param int $imgWidth  The width of the image.
320
     * @param int $imgHeight The height of the image.
321
     * @param int $bgColor   The RGB color for the background of the image.
322
     *
323
     * @return void
324
     */
325 3
    private function createBackground(int $imgWidth, int $imgHeight, $bgColor)
326
    {
327 3
        $im = $this->getImageResource();
328

329 3
        imagefilledrectangle($im, 0, 0, $imgWidth, $imgHeight, $bgColor);
330
    }
331

332
    /**
333
     * Create a spiral patten.
334
     *
335
     * @param int $imgWidth  The width of the image.
336
     * @param int $imgHeight The height of the image.
337
     * @param int $gridColor The RGB color for the gri of the image.
338
     *
339
     * @return void
340
     */
341 3
    private function createSpiralPattern(int $imgWidth, int $imgHeight, $gridColor)
342
    {
343 3
        $im = $this->getImageResource();
344

345
        // Determine angle and position.
346 3
        $angle = ($this->length >= 6) ? mt_rand(-($this->length - 6), ($this->length - 6)) : 0;
347 3
        $xAxis = mt_rand(6, (360 / $this->length) - 16);
348 3
        $yAxis = ($angle >= 0) ? mt_rand($imgHeight, $imgWidth) : mt_rand(6, $imgHeight);
349

350
        // Create the spiral pattern.
351 3
        $theta   = 1;
352 3
        $thetac  = 7;
353 3
        $radius  = 16;
354 3
        $circles = 20;
355 3
        $points  = 32;
356

357 3
        for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) {
358 3
            $theta += $thetac;
359 3
            $rad = $radius * ($i / $points);
360

361 3
            $x = (int) (($rad * cos($theta)) + $xAxis);
362 3
            $y = (int) (($rad * sin($theta)) + $yAxis);
363

364 3
            $theta += $thetac;
365 3
            $rad1 = $radius * (($i + 1) / $points);
366

367 3
            $x1 = (int) (($rad1 * cos($theta)) + $xAxis);
368 3
            $y1 = (int) (($rad1 * sin($theta)) + $yAxis);
369

370 3
            imageline($im, $x, $y, $x1, $y1, $gridColor);
371 3
            $theta -= $thetac;
372
        }  
373
    }
374

375
    /**
376
     * Write the text into the image canvas.
377
     *
378
     * @param int $imgWidth  The width of the image.
379
     * @param int $imgHeight The height of the image.
380
     * @param int $textColor The RGB color for the grid of the image.
381
     *
382
     * @return void
383
     */
384 3
    private function writeText(int $imgWidth, int $imgHeight, $textColor)
385
    {
386 3
        $im = $this->getImageResource();
387

388 3
        $z = (int) ($imgWidth / ($this->length / 3));
389 3
        $x = mt_rand(0, $z);
390
        // $y = 0;
391

392 3
        for ($i = 0; $i < $this->length; $i++) {
393 3
            $y = mt_rand(0, $imgHeight / 2);
394 3
            imagestring($im, 5, $x, $y, $this->word[$i], $textColor);
395 3
            $x += ($this->properties['font_spacing'] * 2);
396
        }
397
    }
398

399
    /**
400
     * Write the text into the image canvas.
401
     *
402
     * @param int $imgWidth    The width of the image.
403
     * @param int $imgHeight   The height of the image.
404
     * @param int $borderColor The RGB color for the border of the image.
405
     *
406
     * @return void
407
     */
408 3
    private function createBorder(int $imgWidth, int $imgHeight, $borderColor): void
409
    {
410 3
        $im = $this->getImageResource();
411

412
        // Create the border.
413 3
        imagerectangle($im, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
414
    }
415

416
    /**
417
     * Get the base64 string of the image.
418
     *
419
     * @return string
420
     */
421 3
    private function getImageBase64Content(): string
422
    {
423 3
        $im = $this->getImageResource();
424

425
        // Generate image in base64 string.
426 3
        ob_start();
427

428 3
        if (function_exists('imagejpeg')) {
429 3
            $this->imageType = 'jpeg';
430 3
            imagejpeg($im);
431

432
            // @codeCoverageIgnoreStart
433

434
        } elseif (function_exists('imagepng')) {
435
            $this->imageType = 'png';
436
            imagepng($im);
437
        } else {
438
            echo '';
439
        }
440

441
        // @codeCoverageIgnoreEnd
442

443 3
        $imageContent = ob_get_contents();
444 3
        ob_end_clean();
445 3
        imagedestroy($im);
446

447 3
        return base64_encode($imageContent);
448
    }
449

450
    /**
451
     * Get image resource.
452
     *
453
     * @return resource
454
     */
455 3
    private function getImageResource()
456
    {
457 3
        if (!is_resource($this->im)) {
458

459
            // @codeCoverageIgnoreStart
460
            throw new RuntimeException(
461
                'Cannot create image resource.'
462
            );
463
            // @codeCoverageIgnoreEnd
464
        }
465

466 3
        return $this->im;
467
    }
468
}

Read our documentation on viewing source code .

Loading