diff --git a/inc/config.php b/inc/config.php index 2e5e9fd9..bfa122b3 100644 --- a/inc/config.php +++ b/inc/config.php @@ -302,6 +302,7 @@ 'raw', 'embed', 'g-recaptcha-response', + 'h-captcha-response', 'cf-turnstile-response', 'spoiler', 'page', @@ -343,10 +344,17 @@ $config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f'; $config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_'; + // Enable hCaptcha. + $config['hcaptcha'] = false; + + // Public and private key pair for using hCaptcha. + $config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0'; + $config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17'; + // Enable Cloudflare's Turnstile captcha. $config['turnstile'] = false; - // Public and private key pair. + // Public and private key pair for turnstile. $config['turnstile_public'] = ''; $config['turnstile_private'] = ''; diff --git a/post.php b/post.php index 4c5aec68..93ca5160 100644 --- a/post.php +++ b/post.php @@ -92,6 +92,44 @@ function check_recaptcha($secret, $response, $remote_ip) return !!$resp['success']; } +function check_hcaptcha($secret, $response, $remote_ip, $public_key) +{ + $data = [ + 'secret' => $secret, + 'response' => $response, + 'sitekey' => $public_key, + ]; + + if ($remote_ip !== null) { + $data['remoteip'] = $remote_ip; + } + + $c = curl_init(); + curl_setopt_array($c, [ + CURLOPT_URL => 'https://api.hcaptcha.com/siteverify', + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => $data, + CURLOPT_RETURNTRANSFER => true, + ]); + $c_ret = curl_exec($c); + if ($c_ret === false) { + $err_no = curl_errno($c); + $err_str = curl_error($c); + + curl_close($c); + error_log("hCaptcha call failed. Curl returned: $err_no ($err_str)"); + return false; + } + curl_close($c); + + $json_ret = json_decode($c_ret, true); + if ($json_ret === null) { + error_log("hCaptcha call failed. Malformed json: $c_ret"); + return false; + } + return $json_ret['success'] === true; +} + function check_turnstile($secret, $response, $remote_ip, $expected_action) { $data = [ @@ -736,6 +774,13 @@ function handle_post() if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], null)) { error($config['error']['captcha']); } + } elseif ($config['hcaptcha']) { + if (!isset($_POST['h-captcha-response'])) { + error($config['error']['bot']); + } + if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], null, $config['hcaptcha_public'])) { + error($config['error']['captcha']); + } } elseif ($config['turnstile']) { if (!isset($_POST['cf-turnstile-response'])) { error($config['error']['bot']); @@ -755,6 +800,13 @@ function handle_post() if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'])) { error($config['error']['captcha']); } + } elseif ($config['hcaptcha']) { + if (!isset($_POST['h-captcha-response'])) { + error($config['error']['bot']); + } + if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) { + error($config['error']['captcha']); + } } elseif ($config['turnstile']) { if (!isset($_POST['cf-turnstile-response'])) { error($config['error']['bot']); diff --git a/templates/captcha_script.html b/templates/captcha_script.html index a511d51e..79b0b230 100644 --- a/templates/captcha_script.html +++ b/templates/captcha_script.html @@ -1,3 +1,6 @@ +{% if config.hcaptcha %} + +{% endif %} {% if config.turnstile %} {% endif %} diff --git a/templates/main.js b/templates/main.js index d8ae450e..18646385 100755 --- a/templates/main.js +++ b/templates/main.js @@ -245,11 +245,39 @@ function getCookie(cookie_name) { {% endraw %} /* BEGIN CAPTCHA REGION */ - -{% if config.turnstile %} +{% if config.hcaptcha or config.turnstile %} // If any captcha // Global captcha object. Assigned by `onCaptchaLoad()`. var captcha_renderer = null; +{% if config.hcaptcha %} // If hcaptcha +function onCaptchaLoadHcaptcha() { + if (captcha_renderer === null) { + let renderer = { + renderOn: (container) => hcaptcha.render(container, { + sitekey: "{{ config.hcaptcha_public }}", + }), + remove: (widgetId) => { /* Not supported */ }, + reset: (widgetId) => hcaptcha.reset(widgetId) + }; + + onCaptchaLoad(renderer); + } +} + +function initCaptchaImpl() { + /* + * hcaptcha is set but the captcha_renderer is null? hcaptcha loaded before main.js could set up the callbacks. + * Init now. + */ + if (hcaptcha && captcha_renderer === null) { + if (active_page === 'index' || active_page === 'catalog' || active_page === 'thread') { + onCaptchaLoadHcaptcha(); + } + } +} +{% endif %} // End if hcaptcha +{% if config.turnstile %} // If turnstile + // Wrapper function to be called from thread.html window.onCaptchaLoadTurnstile_post_reply = function() { onCaptchaLoadTurnstile('post-reply'); @@ -265,7 +293,7 @@ function onCaptchaLoadTurnstile(action) { if (captcha_renderer === null) { let renderer = { renderOn: function(container) { - let widgetId = turnstile.render(container, { + let widgetId = turnstile.render('#' + container, { sitekey: "{{ config.turnstile_public }}", action: action, }); @@ -274,12 +302,8 @@ function onCaptchaLoadTurnstile(action) { } return widgetId; }, - remove: function(widgetId) { - turnstile.remove(widgetId); - }, - reset: function(widgetId) { - turnstile.reset(widgetId); - } + remove: (widgetId) => turnstile.remove(widgetId), + reset: (widgetId) => turnstile.reset(widgetId) }; onCaptchaLoad(renderer); @@ -299,12 +323,12 @@ function initCaptchaImpl() { } } } -{% endif %} +{% endif %} // End if turnstile function onCaptchaLoad(renderer) { captcha_renderer = renderer; - let widgetId = renderer.renderOn('#captcha-container'); + let widgetId = renderer.renderOn('captcha-container'); if (widgetId === null) { console.error('Could not render captcha!'); } @@ -314,23 +338,24 @@ function onCaptchaLoad(renderer) { }); } -{% if config.dynamic_captcha %} +{% if config.dynamic_captcha %} // If dynamic captcha function isDynamicCaptchaEnabled() { let cookie = getCookie('captcha-required'); return cookie === '1'; } -function initDynamicCaptcha() { +function initCaptcha() { if (isDynamicCaptchaEnabled()) { let captcha_hook = document.getElementById('captcha'); captcha_hook.style = ""; initCaptchaImpl(); } } -{% else %} +{% endif %} // End if dynamic captcha +{% else %} // Else if any captcha // No-op for `init()`. -function initDynamicCaptcha() {} -{% endif %} +function initCaptcha() {} +{% endif %} // End if any captcha /* END CAPTCHA REGION */ @@ -500,7 +525,7 @@ var script_settings = function(script_name) { function init() { initStyleChooser(); - initDynamicCaptcha(); + initCaptcha(); {% endraw %} {% if config.allow_delete %} diff --git a/templates/post_form.html b/templates/post_form.html index 6434b7e8..44327915 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -106,7 +106,7 @@ {% endif %} - {% if config.turnstile %} + {% if config.hcaptcha or config.turnstile %} {% if config.dynamic_captcha %}