Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
recaptcha_v3
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 14
1122
0.00% covered (danger)
0.00%
0 / 1
 get_actions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute_demo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_generator_class
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 has_config
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 is_available
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 acp_page
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
90
 get_demo_template
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_template
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 validate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 recaptcha_verify_token
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
 get_login_error_attempts
0.00% covered (danger)
0.00%
0 / 3
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\exception\runtime_exception;
17
18/**
19 * Google reCAPTCHA v3 plugin.
20 */
21class recaptcha_v3 extends captcha_abstract
22{
23    /**
24     * Possible request methods to verify the token.
25     */
26    const CURL            = 'curl';
27    const POST            = 'post';
28    const SOCKET        = 'socket';
29
30    /**
31     * Possible domain names to load the script and verify the token.
32     */
33    const GOOGLE        = 'google.com';
34    const RECAPTCHA        = 'recaptcha.net';
35    const RECAPTCHA_CN    = 'recaptcha.google.cn';
36
37    /** @var string[] List of supported domains */
38    public static $supported_domains = [
39        self::GOOGLE,
40        self::RECAPTCHA,
41        self::RECAPTCHA_CN
42    ];
43
44    /** @var array CAPTCHA types mapped to their action */
45    protected static $actions = [
46        0                => 'default',
47        CONFIRM_REG        => 'register',
48        CONFIRM_LOGIN    => 'login',
49        CONFIRM_POST    => 'post',
50        CONFIRM_REPORT    => 'report',
51    ];
52
53    /**
54     * Get CAPTCHA types mapped to their action.
55     *
56     * @static
57     * @return array
58     */
59    public static function get_actions()
60    {
61        return self::$actions;
62    }
63
64    /**
65     * Execute.
66     *
67     * Not needed by this CAPTCHA plugin.
68     *
69     * @return void
70     */
71    public function execute()
72    {
73    }
74
75    /**
76     * Execute demo.
77     *
78     * Not needed by this CAPTCHA plugin.
79     *
80     * @return void
81     */
82    public function execute_demo()
83    {
84    }
85
86    /**
87     * Get generator class.
88     *
89     * Not needed by this CAPTCHA plugin.
90     *
91     * @throws runtime_exception
92     * @return void
93     */
94    public function get_generator_class()
95    {
96        throw new runtime_exception('NO_GENERATOR_CLASS');
97    }
98
99    /**
100     * Get CAPTCHA plugin name.
101     *
102     * @return string
103     */
104    public function get_name()
105    {
106        return 'CAPTCHA_RECAPTCHA_V3';
107    }
108
109    /**
110     * Indicator that this CAPTCHA plugin requires configuration.
111     *
112     * @return bool
113     */
114    public function has_config()
115    {
116        return true;
117    }
118
119    /**
120     * Initialize this CAPTCHA plugin.
121     *
122     * @param int    $type    The CAPTCHA type
123     * @return void
124     */
125    public function init($type)
126    {
127        /**
128         * @var \phpbb\language\language    $language    Language object
129         */
130        global $language;
131
132        $language->add_lang('captcha_recaptcha');
133
134        parent::init($type);
135    }
136
137    /**
138     * Whether or not this CAPTCHA plugin is available and setup.
139     *
140     * @return bool
141     */
142    public function is_available()
143    {
144        /**
145         * @var \phpbb\config\config        $config        Config object
146         * @var \phpbb\language\language    $language    Language object
147         */
148        global $config, $language;
149
150        $language->add_lang('captcha_recaptcha');
151
152        return ($config->offsetGet('recaptcha_v3_key') ?? false) && ($config->offsetGet('recaptcha_v3_secret') ?? false);
153    }
154
155    /**
156     * Create the ACP page for configuring this CAPTCHA plugin.
157     *
158     * @param string        $id            The ACP module identifier
159     * @param \acp_captcha    $module        The ACP module basename
160     * @return void
161     */
162    public function acp_page($id, $module)
163    {
164        /**
165         * @var \phpbb\config\config        $config        Config object
166         * @var \phpbb\language\language    $language    Language object
167         * @var \phpbb\log\log                $phpbb_log    Log object
168         * @var \phpbb\request\request        $request    Request object
169         * @var \phpbb\template\template    $template    Template object
170         * @var \phpbb\user                    $user        User object
171         */
172        global $config, $language, $phpbb_log, $request, $template, $user;
173
174        $module->tpl_name        = 'captcha_recaptcha_v3_acp';
175        $module->page_title        = 'ACP_VC_SETTINGS';
176        $recaptcha_v3_method    = $request->variable('recaptcha_v3_method', '', true);
177
178        $form_key = 'acp_captcha';
179        add_form_key($form_key);
180
181        if ($request->is_set_post('submit'))
182        {
183            if (!check_form_key($form_key))
184            {
185                trigger_error($language->lang('FORM_INVALID') . adm_back_link($module->u_action), E_USER_WARNING);
186            }
187
188            if (empty($recaptcha_v3_method))
189            {
190                trigger_error($language->lang('EMPTY_RECAPTCHA_V3_REQUEST_METHOD') . adm_back_link($module->u_action), E_USER_WARNING);
191            }
192
193            $recaptcha_domain = $request->variable('recaptcha_v3_domain', '', true);
194            if (in_array($recaptcha_domain, self::$supported_domains))
195            {
196                $config->set('recaptcha_v3_domain', $recaptcha_domain);
197            }
198
199            $config->set('recaptcha_v3_key', $request->variable('recaptcha_v3_key', '', true));
200            $config->set('recaptcha_v3_secret', $request->variable('recaptcha_v3_secret', '', true));
201            $config->set('recaptcha_v3_method', $recaptcha_v3_method);
202
203            foreach (self::$actions as $action)
204            {
205                $config->set("recaptcha_v3_threshold_{$action}", $request->variable("recaptcha_v3_threshold_{$action}", 0.50));
206            }
207
208            $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_CONFIG_VISUAL');
209
210            trigger_error($language->lang('CONFIG_UPDATED') . adm_back_link($module->u_action));
211        }
212
213        foreach (self::$actions as $action)
214        {
215            $template->assign_block_vars('thresholds', [
216                'key'    => "recaptcha_v3_threshold_{$action}",
217                'value'    => $config["recaptcha_v3_threshold_{$action}"] ?? 0.5,
218            ]);
219        }
220
221        $template->assign_vars([
222            'CAPTCHA_NAME'                => $this->get_service_name(),
223            'CAPTCHA_PREVIEW'            => $this->get_demo_template($id),
224
225            'RECAPTCHA_V3_KEY'            => $config['recaptcha_v3_key'] ?? '',
226            'RECAPTCHA_V3_SECRET'        => $config['recaptcha_v3_secret'] ?? '',
227
228            'RECAPTCHA_V3_DOMAIN'        => $config['recaptcha_v3_domain'] ?? self::GOOGLE,
229            'RECAPTCHA_V3_DOMAINS'        => self::$supported_domains,
230
231            'RECAPTCHA_V3_METHOD'        => $config['recaptcha_v3_method'] ?? '',
232            'RECAPTCHA_V3_METHODS'        => [
233                self::POST        => ini_get('allow_url_fopen') && function_exists('file_get_contents'),
234                self::CURL        => extension_loaded('curl') && function_exists('curl_init'),
235                self::SOCKET    => function_exists('fsockopen'),
236            ],
237
238            'U_ACTION'                    => $module->u_action,
239        ]);
240    }
241
242    /**
243     * Create the ACP page for previewing this CAPTCHA plugin.
244     *
245     * @param string    $id        The module identifier
246     * @return bool|string
247     */
248    public function get_demo_template($id)
249    {
250        return $this->get_template();
251    }
252
253    /**
254     * Get the template for this CAPTCHA plugin.
255     *
256     * @return bool|string        False if CAPTCHA is already solved, template file name otherwise
257     */
258    public function get_template()
259    {
260        /**
261         * @var \phpbb\config\config        $config                Config object
262         * @var \phpbb\language\language    $language            Language object
263         * @var \phpbb\template\template    $template            Template object
264         * @var string                        $phpbb_root_path    phpBB root path
265         * @var string                        $phpEx                php File extensions
266         */
267        global $config, $language, $template, $phpbb_root_path, $phpEx;
268
269        if ($this->is_solved())
270        {
271            return false;
272        }
273
274        $contact = phpbb_get_board_contact_link($config, $phpbb_root_path, $phpEx);
275        $explain = $this->type !== CONFIRM_POST ? 'CONFIRM_EXPLAIN' : 'POST_CONFIRM_EXPLAIN';
276
277        $domain = $config['recaptcha_v3_domain'] ?? self::GOOGLE;
278        $render = $config['recaptcha_v3_key'] ?? '';
279
280        $template->assign_vars([
281            'CONFIRM_EXPLAIN'        => $language->lang($explain, '<a href="' . $contact . '">', '</a>'),
282
283            'RECAPTCHA_ACTION'        => self::$actions[$this->type] ?? reset(self::$actions),
284            'RECAPTCHA_KEY'            => $config['recaptcha_v3_key'] ?? '',
285            'U_RECAPTCHA_SCRIPT'    => sprintf('//%1$s/recaptcha/api.js?render=%2$s', $domain, $render),
286
287            'S_CONFIRM_CODE'        => true,
288            'S_RECAPTCHA_AVAILABLE'    => $this->is_available(),
289            'S_TYPE'                => $this->type,
290        ]);
291
292        return 'captcha_recaptcha_v3.html';
293    }
294
295    /**
296     * Validate the user's input.
297     *
298     * @return bool|string
299     */
300    public function validate()
301    {
302        if (!parent::validate())
303        {
304            return false;
305        }
306
307        return $this->recaptcha_verify_token();
308    }
309
310    /**
311     * Validate the token returned by Google reCAPTCHA v3.
312     *
313     * @return bool|string        False on success, string containing the error otherwise
314     */
315    protected function recaptcha_verify_token()
316    {
317        /**
318         * @var \phpbb\config\config        $config        Config object
319         * @var \phpbb\language\language    $language    Language object
320         * @var \phpbb\request\request        $request    Request object
321         * @var \phpbb\user                    $user        User object
322         */
323        global $config, $language, $request, $user;
324
325        $token        = $request->variable('recaptcha_token', '', true);
326        $action        = $request->variable('recaptcha_action', '', true);
327        $action        = in_array($action, self::$actions) ? $action : reset(self::$actions);
328        $threshold    = (double) $config["recaptcha_v3_threshold_{$action}"] ?? 0.5;
329
330        // No token was provided, discard spam submissions
331        if (empty($token))
332        {
333            return $language->lang('RECAPTCHA_INCORRECT');
334        }
335
336        // Create the request method that should be used
337        switch ($config['recaptcha_v3_method'] ?? '')
338        {
339            case self::CURL:
340                $method = new \ReCaptcha\RequestMethod\CurlPost();
341            break;
342
343            case self::SOCKET:
344                $method = new \ReCaptcha\RequestMethod\SocketPost();
345            break;
346
347            case self::POST:
348            default:
349                $method = new \ReCaptcha\RequestMethod\Post();
350            break;
351        }
352
353        // Create the recaptcha instance
354        $recaptcha = new \ReCaptcha\ReCaptcha($config['recaptcha_v3_secret'], $method);
355
356        // Set the expected action and threshold, and verify the token
357        $result = $recaptcha->setExpectedAction($action)
358                            ->setScoreThreshold($threshold)
359                            ->verify($token, $user->ip);
360
361        if ($result->isSuccess())
362        {
363            $this->solved = true;
364
365            return false;
366        }
367
368        return $language->lang('RECAPTCHA_INCORRECT');
369    }
370
371    /**
372     * {@inheritDoc}
373     */
374    public function get_login_error_attempts(): string
375    {
376        global $language;
377
378        $language->add_lang('captcha_recaptcha');
379
380        return 'RECAPTCHA_V3_LOGIN_ERROR_ATTEMPTS';
381    }
382}