Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (success)
75.00%
54 / 72
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Captcha
75.00% covered (success)
75.00%
54 / 72
57.14% covered (warning)
57.14%
4 / 7
54.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
9.36
 createNewToken
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLabel
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
7.04
 setValidator
45.83% covered (warning)
45.83%
11 / 24
0.00% covered (danger)
0.00%
0 / 1
25.89
 generateEquation
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 evaluateEquation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Form\Element\Input;
15
16/**
17 * Form CAPTCHA element class
18 *
19 * @category   Pop
20 * @package    Pop\Form
21 * @author     Nick Sagona, III <dev@nolainteractive.com>
22 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
23 * @license    http://www.popphp.org/license     New BSD License
24 * @version    4.0.0
25 */
26
27class Captcha extends Text
28{
29
30    /**
31     * Current token data
32     * @var array
33     */
34    protected array $token = [];
35
36    /**
37     * Constructor
38     *
39     * Instantiate the captcha input form element
40     *
41     * @param  string  $name
42     * @param  ?string $value
43     * @param  ?string $captcha
44     * @param  ?string $answer
45     * @param  int     $expire
46     * @param  ?string $indent
47     */
48    public function __construct(
49        string $name, ?string $value = null, ?string $captcha = null, ?string $answer = null, int $expire = 300, ?string $indent = null
50    )
51    {
52        // Start a session.
53        if (session_id() == '') {
54            session_start();
55        }
56
57        // If token does not exist, create one
58        if (!isset($_SESSION['pop_captcha']) || (isset($_GET['captcha']) && ((int)$_GET['captcha'] == 1))) {
59            $this->createNewToken($captcha, $answer, $expire);
60        // Else, retrieve existing token
61        } else {
62            $this->token = unserialize($_SESSION['pop_captcha']);
63
64            // Check to see if the token has expired
65            if ($this->token['expire'] > 0) {
66                if (($this->token['expire'] + $this->token['start']) < time()) {
67                    $this->createNewToken($captcha, $value, $expire);
68                }
69            }
70        }
71
72        parent::__construct($name, strtoupper((string)$value), $indent);
73        $this->setRequired(true);
74        $this->setValidator();
75    }
76
77    /**
78     * Set the token of the CAPTCHA form element
79     *
80     * @param  ?string $captcha
81     * @param  ?string $answer
82     * @param  int     $expire
83     * @return Captcha
84     */
85    public function createNewToken(?string $captcha = null, ?string $answer = null, int $expire = 300): Captcha
86    {
87        if (($captcha === null) || ($answer === null)) {
88            $captcha = $this->generateEquation();
89            $answer  = $this->evaluateEquation($captcha);
90        }
91
92        $this->token = [
93            'captcha' => $captcha,
94            'answer'  => $answer,
95            'expire'  => (int)$expire,
96            'start'   => time()
97        ];
98        $_SESSION['pop_captcha'] = serialize($this->token);
99        return $this;
100    }
101
102    /**
103     * Get token
104     *
105     * @return array
106     */
107    public function getToken(): array
108    {
109        return $this->token;
110    }
111
112    /**
113     * Set the label of the captcha form element
114     *
115     * @param  string $label
116     * @return Captcha
117     */
118    public function setLabel(string $label): Captcha
119    {
120        parent::setLabel($label);
121
122        if (isset($this->token['captcha'])) {
123            if ((!str_contains($this->token['captcha'], '<img')) &&
124                ((str_contains($this->token['captcha'], ' + ')) ||
125                 (str_contains($this->token['captcha'], ' - ')) ||
126                 (str_contains($this->token['captcha'], ' * ')) ||
127                 (str_contains($this->token['captcha'], ' / ')))) {
128                $this->label = $this->label . '(' .
129                    str_replace([' * ', ' / '], [' &#215; ', ' &#247; '], $this->token['captcha'] .')');
130            } else {
131                $this->label = $this->label . $this->token['captcha'];
132            }
133        }
134
135        return $this;
136    }
137
138    /**
139     * Set the validator
140     *
141     * @throws Exception
142     * @return void
143     */
144    protected function setValidator(): void
145    {
146        $this->validators = [];
147
148        // Get query data
149        if (!isset($_SERVER['REQUEST_METHOD'])) {
150            throw new Exception('Error: The server request method is not set.');
151        }
152
153        $queryData = [];
154        switch ($_SERVER['REQUEST_METHOD']) {
155            case 'GET':
156                $queryData = $_GET;
157                break;
158
159            case 'POST':
160                $queryData = $_POST;
161                break;
162
163            default:
164                $input = fopen('php://input', 'r');
165                $qData = null;
166                while ($data = fread($input, 1024)) {
167                    $qData .= $data;
168                }
169
170                parse_str($qData, $queryData);
171        }
172
173        // If there is query data, set validator to check against the token value
174        if (count($queryData) > 0) {
175            if (isset($queryData[$this->name])) {
176                $this->addValidator(function($value){
177                    $token = $this->getToken();
178                    if (isset($token['answer']) && (strtoupper($token['answer']) == strtoupper($value))) {
179                        return null;
180                    } else {
181                        return 'The answer is incorrect.';
182                    }
183                });
184            }
185        }
186    }
187
188    /**
189     * Randomly generate a simple, basic equation
190     *
191     * @return string
192     */
193    protected function generateEquation(): string
194    {
195        $ops = [' + ', ' - ', ' * ', ' / '];
196        $equation = null;
197
198        $rand1 = rand(1, 10);
199        $rand2 = rand(1, 10);
200        $op    = $ops[rand(0, 3)];
201
202        // If the operator is division, keep the equation very simple, with no remainder
203        if ($op == ' / ') {
204            $mod = ($rand2 > $rand1) ? $rand2 % $rand1 : $rand1 % $rand2;
205            while ($mod != 0) {
206                $rand1 = rand(1, 10);
207                $rand2 = rand(1, 10);
208                $mod   = ($rand2 > $rand1) ? $rand2 % $rand1 : $rand1 % $rand2;
209            }
210        }
211
212        $equation = ($rand2 > $rand1) ? $rand2 . $op . $rand1 : $rand1 . $op . $rand2;
213
214        return $equation;
215    }
216
217    /**
218     * Evaluate equation
219     *
220     * @param  string $equation
221     * @return int
222     */
223    protected function evaluateEquation(string $equation): int
224    {
225        return eval("return ($equation);");
226    }
227
228}