Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
turnstile
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
11 / 11
27
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 is_available
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 has_config
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
 get_client
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_template
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 get_demo_template
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 acp_page
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
9
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 GuzzleHttp\Client;
17use GuzzleHttp\Exception\GuzzleException;
18use phpbb\config\config;
19use phpbb\db\driver\driver_interface;
20use phpbb\language\language;
21use phpbb\log\log_interface;
22use phpbb\request\request_interface;
23use phpbb\template\template;
24use phpbb\user;
25
26class turnstile extends base
27{
28    /** @var string URL to cloudflare turnstile API javascript */
29    private const SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
30
31    /** @var string API endpoint for turnstile verification */
32    private const VERIFY_ENDPOINT = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
33
34    /** @var Client */
35    protected Client $client;
36
37    /** @var language */
38    protected language $language;
39
40    /** @var log_interface */
41    protected log_interface $log;
42
43    /** @var template */
44    protected template $template;
45
46    /** @var string Service name */
47    protected string $service_name = '';
48
49    /** @var array|string[] Supported themes for Turnstile CAPTCHA */
50    protected static array $supported_themes = [
51        'light',
52        'dark',
53        'auto'
54    ];
55
56    /**
57     * Constructor for turnstile captcha plugin
58     *
59     * @param config $config
60     * @param driver_interface $db
61     * @param language $language
62     * @param log_interface $log
63     * @param request_interface $request
64     * @param template $template
65     * @param user $user
66     */
67    public function __construct(config $config, driver_interface $db, language $language, log_interface $log, request_interface $request, template $template, user $user)
68    {
69        parent::__construct($config, $db, $language, $request, $user);
70
71        $this->language = $language;
72        $this->log = $log;
73        $this->template = $template;
74    }
75
76    /**
77     * {@inheritDoc}
78     */
79    public function is_available(): bool
80    {
81        $this->init($this->type);
82
83        return !empty($this->config->offsetGet('captcha_turnstile_sitekey'))
84            && !empty($this->config->offsetGet('captcha_turnstile_secret'));
85    }
86
87    /**
88     * {@inheritDoc}
89     */
90    public function has_config(): bool
91    {
92        return true;
93    }
94
95    /**
96     * {@inheritDoc}
97     */
98    public function get_name(): string
99    {
100        return 'CAPTCHA_TURNSTILE';
101    }
102
103    /**
104     * {@inheritDoc}
105     */
106    public function set_name(string $name): void
107    {
108        $this->service_name = $name;
109    }
110
111    /**
112     * {@inheritDoc}
113     */
114    public function init(confirm_type $type): void
115    {
116        parent::init($type);
117
118        $this->language->add_lang('captcha_turnstile');
119    }
120
121    /**
122     * {@inheritDoc}
123     */
124    public function validate(): bool
125    {
126        if (parent::validate())
127        {
128            return true;
129        }
130
131        $turnstile_response = $this->request->variable('cf-turnstile-response', '');
132        if (!$turnstile_response)
133        {
134            // Return without checking against server without a turnstile response
135            return false;
136        }
137
138        // Retrieve form data for verification
139        $form_data = [
140            'secret'            => $this->config['captcha_turnstile_secret'],
141            'response'            => $turnstile_response,
142            'remoteip'            => $this->user->ip,
143        ];
144
145        // Create guzzle client
146        $client = $this->get_client();
147
148        // Check captcha with turnstile API
149        try
150        {
151            $response = $client->request('POST', self::VERIFY_ENDPOINT, [
152                'form_params' => $form_data,
153            ]);
154        }
155        catch (GuzzleException)
156        {
157            // Something went wrong during the request to Cloudflare, assume captcha was bad
158            $this->solved = false;
159            return false;
160        }
161
162        // Decode the JSON response
163        $result = json_decode($response->getBody(), true);
164
165        // Check if the response indicates success
166        if (isset($result['success']) && $result['success'] === true)
167        {
168            $this->solved = true;
169            $this->confirm_code = $this->code;
170            return true;
171        }
172        else
173        {
174            $this->last_error = $this->language->lang('CAPTCHA_TURNSTILE_INCORRECT');
175            return false;
176        }
177    }
178
179    /**
180     * Get Guzzle client
181     *
182     * @return Client
183     */
184    protected function get_client(): Client
185    {
186        if (!isset($this->client))
187        {
188            $this->client = new Client();
189        }
190
191        return $this->client;
192    }
193
194    /**
195     * {@inheritDoc}
196     */
197    public function get_template(): string
198    {
199        if ($this->is_solved())
200        {
201            return '';
202        }
203
204        $this->template->assign_vars([
205            'S_TURNSTILE_AVAILABLE'        => $this->is_available(),
206            'TURNSTILE_SITEKEY'            => $this->config->offsetGet('captcha_turnstile_sitekey'),
207            'TURNSTILE_THEME'            => $this->config->offsetGet('captcha_turnstile_theme'),
208            'U_TURNSTILE_SCRIPT'        => self::SCRIPT_URL,
209            'CONFIRM_TYPE_REGISTRATION'    => $this->type->value,
210        ]);
211
212        return 'captcha_turnstile.html';
213    }
214
215    /**
216     * {@inheritDoc}
217     */
218    public function get_demo_template(): string
219    {
220        $this->template->assign_vars([
221            'TURNSTILE_THEME'        => $this->config->offsetGet('captcha_turnstile_theme'),
222            'U_TURNSTILE_SCRIPT'    => self::SCRIPT_URL,
223        ]);
224
225        return 'captcha_turnstile_acp_demo.html';
226    }
227
228    /**
229     * {@inheritDoc}
230     */
231    public function acp_page(mixed $id, mixed $module): void
232    {
233        $captcha_vars = [
234            'captcha_turnstile_sitekey'            => 'CAPTCHA_TURNSTILE_SITEKEY',
235            'captcha_turnstile_secret'            => 'CAPTCHA_TURNSTILE_SECRET',
236        ];
237
238        $module->tpl_name = 'captcha_turnstile_acp';
239        $module->page_title = 'ACP_VC_SETTINGS';
240        $form_key = 'acp_captcha';
241        add_form_key($form_key);
242
243        $submit = $this->request->is_set_post('submit');
244
245        if ($submit && check_form_key($form_key))
246        {
247            $captcha_vars = array_keys($captcha_vars);
248            foreach ($captcha_vars as $captcha_var)
249            {
250                $value = $this->request->variable($captcha_var, '');
251                if ($value)
252                {
253                    $this->config->set($captcha_var, $value);
254                }
255            }
256
257            $captcha_theme = $this->request->variable('captcha_turnstile_theme', self::$supported_themes[0]);
258            if (in_array($captcha_theme, self::$supported_themes))
259            {
260                $this->config->set('captcha_turnstile_theme', $captcha_theme);
261            }
262
263            $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONFIG_VISUAL');
264            trigger_error($this->language->lang('CONFIG_UPDATED') . adm_back_link($module->u_action));
265        }
266        else if ($submit)
267        {
268            trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($module->u_action));
269        }
270        else
271        {
272            foreach ($captcha_vars as $captcha_var => $template_var)
273            {
274                $var = $this->request->is_set($captcha_var) ? $this->request->variable($captcha_var, '') : $this->config->offsetGet($captcha_var);
275                $this->template->assign_var($template_var, $var);
276            }
277
278            $this->template->assign_vars(array(
279                'CAPTCHA_PREVIEW'            => $this->get_demo_template(),
280                'CAPTCHA_NAME'                => $this->service_name,
281                'CAPTCHA_TURNSTILE_THEME'    => $this->config->offsetGet('captcha_turnstile_theme'),
282                'CAPTCHA_TURNSTILE_THEMES'    => self::$supported_themes,
283                'U_ACTION'                    => $module->u_action,
284            ));
285        }
286    }
287}