Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.61% covered (success)
98.61%
71 / 72
92.31% covered (success)
92.31%
12 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
base
98.61% covered (success)
98.61%
71 / 72
92.31% covered (success)
92.31%
12 / 13
22
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 validate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 reset
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_attempt_count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 load_confirm_data
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 generate_confirm_data
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 increment_attempts
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_hidden_fields
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 is_solved
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 garbage_collect
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 acp_page
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\captcha\plugins;
15
16use phpbb\config\config;
17use phpbb\db\driver\driver_interface;
18use phpbb\language\language;
19use phpbb\request\request_interface;
20use phpbb\user;
21
22abstract class base implements plugin_interface
23{
24    /** @var config */
25    protected config $config;
26
27    /** @var driver_interface */
28    protected driver_interface $db;
29
30    /** @var language */
31    protected language $language;
32
33    /** @var request_interface */
34    protected request_interface $request;
35
36    /** @var user */
37    protected user $user;
38
39    /** @var int Attempts at solving the CAPTCHA */
40    protected int $attempts = 0;
41
42    /** @var string Stored random CAPTCHA code */
43    protected string $code = '';
44
45    /** @var bool Resolved state of captcha */
46    protected bool $solved = false;
47
48    /** @var string User supplied confirm code */
49    protected string $confirm_code = '';
50
51    /** @var string Confirm id hash */
52    protected string $confirm_id = '';
53
54    /** @var confirm_type Confirmation type */
55    protected confirm_type $type = confirm_type::UNDEFINED;
56
57    /** @var string Last error message */
58    protected string $last_error = '';
59
60    /**
61     * Constructor for abstract captcha base class
62     *
63     * @param config $config
64     * @param driver_interface $db
65     * @param language $language
66     * @param request_interface $request
67     * @param user $user
68     */
69    public function __construct(config $config, driver_interface $db, language $language, request_interface $request, user $user)
70    {
71        $this->config = $config;
72        $this->db = $db;
73        $this->language = $language;
74        $this->request = $request;
75        $this->user = $user;
76    }
77
78    /**
79     * {@inheritDoc}
80     */
81    public function init(confirm_type $type): void
82    {
83        $this->confirm_id = $this->request->variable('confirm_id', '');
84        $this->confirm_code = $this->request->variable('confirm_code', '');
85        $this->type = $type;
86
87        if (empty($this->confirm_id) || !$this->load_confirm_data())
88        {
89            // we have no confirm ID, better get ready to display something
90            $this->generate_confirm_data();
91        }
92    }
93
94    /**
95     * {@inheritDoc}
96     */
97    public function validate(): bool
98    {
99        if ($this->confirm_id && hash_equals($this->code, $this->confirm_code))
100        {
101            $this->solved = true;
102            return true;
103        }
104
105        $this->increment_attempts();
106        $this->last_error = $this->language->lang('CONFIRM_CODE_WRONG');
107        return false;
108    }
109
110    /**
111     * {@inheritDoc}
112     */
113    public function reset(): void
114    {
115        $sql = 'DELETE FROM ' . CONFIRM_TABLE . "
116            WHERE session_id = '" . $this->db->sql_escape($this->user->session_id) . "'
117                AND confirm_type = " . $this->type->value;
118        $this->db->sql_query($sql);
119
120        $this->generate_confirm_data();
121    }
122
123    /**
124     * {@inheritDoc}
125     */
126    public function get_attempt_count(): int
127    {
128        return $this->attempts;
129    }
130
131    /**
132     * Look up attempts from confirm table
133     */
134    protected function load_confirm_data(): bool
135    {
136        $sql = 'SELECT code, attempts
137            FROM ' . CONFIRM_TABLE . "
138            WHERE confirm_id = '" . $this->db->sql_escape($this->confirm_id) . "'
139                AND session_id = '" . $this->db->sql_escape($this->user->session_id) . "'
140                AND confirm_type = " . $this->type->value;
141        $result = $this->db->sql_query($sql);
142        $row = $this->db->sql_fetchrow($result);
143        $this->db->sql_freeresult($result);
144
145        if ($row)
146        {
147            $this->attempts = $row['attempts'];
148            $this->code = $row['code'];
149
150            return true;
151        }
152
153        return false;
154    }
155
156    /**
157     * Generate confirm data for tracking attempts
158     *
159     * @return void
160     */
161    protected function generate_confirm_data(): void
162    {
163        $this->code = gen_rand_string_friendly(CAPTCHA_MAX_CHARS);
164        $this->confirm_id = md5(unique_id());
165        $this->attempts = 0;
166
167        $sql = 'INSERT INTO ' . CONFIRM_TABLE . ' ' . $this->db->sql_build_array('INSERT', array(
168                    'confirm_id'    => $this->confirm_id,
169                    'session_id'    => (string) $this->user->session_id,
170                    'confirm_type'    => $this->type->value,
171                    'code'            => $this->code,
172            ));
173        $this->db->sql_query($sql);
174    }
175
176    /**
177     * Increment number of attempts for confirm ID and session
178     *
179     * @return void
180     */
181    protected function increment_attempts(): void
182    {
183        $sql = 'UPDATE ' . CONFIRM_TABLE . "
184                SET attempts = attempts + 1
185                WHERE confirm_id = '{$this->db->sql_escape($this->confirm_id)}'
186                    AND session_id = '{$this->db->sql_escape($this->user->session_id)}'";
187        $this->db->sql_query($sql);
188
189        $this->attempts++;
190    }
191
192    /**
193     * {@inheritDoc}
194     */
195    public function get_hidden_fields(): array
196    {
197        return [
198            'confirm_id'    => $this->confirm_id,
199            'confirm_code'    => $this->solved === true ? $this->confirm_code : '',
200        ];
201    }
202
203    /**
204     * {@inheritDoc}
205     */
206    public function is_solved(): bool
207    {
208        return $this->solved;
209    }
210
211    /**
212     * {@inheritDoc}
213     */
214    public function get_error(): string
215    {
216        return $this->last_error;
217    }
218
219    /**
220     * @inheritDoc
221     */
222    public function garbage_collect(confirm_type $confirm_type = confirm_type::UNDEFINED): void
223    {
224        $sql = 'SELECT DISTINCT c.session_id
225            FROM ' . CONFIRM_TABLE . ' c
226            LEFT JOIN ' . SESSIONS_TABLE . ' s ON (c.session_id = s.session_id)
227            WHERE s.session_id IS NULL' .
228            ((empty($confirm_type)) ? '' : ' AND c.confirm_type = ' . $confirm_type->value);
229        $result = $this->db->sql_query($sql);
230
231        if ($row = $this->db->sql_fetchrow($result))
232        {
233            $sql_in = [];
234            do
235            {
236                $sql_in[] = (string) $row['session_id'];
237            }
238            while ($row = $this->db->sql_fetchrow($result));
239
240            if (count($sql_in))
241            {
242                $sql = 'DELETE FROM ' . CONFIRM_TABLE . '
243                    WHERE ' . $this->db->sql_in_set('session_id', $sql_in);
244                $this->db->sql_query($sql);
245            }
246        }
247        $this->db->sql_freeresult($result);
248    }
249
250    /**
251     * {@inheritDoc}
252     */
253    public function acp_page(mixed $id, mixed $module): void
254    {
255    }
256}