diff --git a/inc/context.php b/inc/context.php index f65c3b0d..e6604835 100644 --- a/inc/context.php +++ b/inc/context.php @@ -3,6 +3,9 @@ namespace Vichan; use RuntimeException; use Vichan\Driver\{HttpDriver, HttpDrivers, Log, LogDrivers}; +use Vichan\Service\HCaptchaQuery; +use Vichan\Service\ReCaptchaQuery; +use Vichan\Service\RemoteCaptchaQuery; defined('TINYBOARD') or exit; @@ -53,6 +56,17 @@ function build_context(array $config): Context { HttpDriver::class => function($c) { $config = $c->get('config'); return HttpDrivers::getHttpDriver($config['upload_by_url_timeout'], $config['max_filesize']); + }, + RemoteCaptchaQuery::class => function($c) { + $config = $c->get('config'); + $http = $c->get(HttpDriver::class); + if ($config['recaptcha']) { + return new ReCaptchaQuery($http, $config['recaptcha_private']); + } elseif ($config['hcaptcha']) { + return new HCaptchaQuery($http, $config['hcaptcha_private'], $config['hcaptcha_public']); + } else { + throw new RuntimeException('No remote captcha service available'); + } } ]); } diff --git a/inc/service/captcha-queries.php b/inc/service/captcha-queries.php index d7966501..cbd344a1 100644 --- a/inc/service/captcha-queries.php +++ b/inc/service/captcha-queries.php @@ -6,68 +6,107 @@ use Vichan\Driver\HttpDriver; defined('TINYBOARD') or exit; -class RemoteCaptchaQuery { +class ReCaptchaQuery implements RemoteCaptchaQuery { private HttpDriver $http; private string $secret; - private string $endpoint; - /** - * Creates a new CaptchaRemoteQueries instance using the google recaptcha service. + * Creates a new ReCaptchaQuery using the google recaptcha service. * * @param HttpDriver $http The http client. * @param string $secret Server side secret. - * @return CaptchaRemoteQueries A new captcha query instance. + * @return ReCaptchaQuery A new ReCaptchaQuery query instance. */ - public static function withRecaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery { - return new self($http, $secret, 'https://www.google.com/recaptcha/api/siteverify'); - } - - /** - * Creates a new CaptchaRemoteQueries instance using the hcaptcha service. - * - * @param HttpDriver $http The http client. - * @param string $secret Server side secret. - * @return CaptchaRemoteQueries A new captcha query instance. - */ - public static function withHCaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery { - return new self($http, $secret, 'https://hcaptcha.com/siteverify'); - } - - private function __construct(HttpDriver $http, string $secret, string $endpoint) { + public function __construct(HttpDriver $http, string $secret) { $this->http = $http; $this->secret = $secret; - $this->endpoint = $endpoint; } - /** - * Checks if the user at the remote ip passed the captcha. - * - * @param string $response User provided response. - * @param string $remote_ip User ip. - * @return bool Returns true if the user passed the captcha. - * @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer. - */ - public function verify(string $response, string $remote_ip): bool { - $data = array( - 'secret' => $this->secret, - 'response' => $response, - 'remoteip' => $remote_ip - ); + public function responseField(): string { + return 'g-recaptcha-response'; + } - $ret = $this->http->requestGet($this->endpoint, $data); + public function verify(string $response, ?string $remote_ip): bool { + $data = [ + 'secret' => $this->secret, + 'response' => $response + ]; + + if ($remote_ip !== null) { + $data['remoteip'] = $remote_ip; + } + + $ret = $this->http->requestGet('https://www.google.com/recaptcha/api/siteverify', $data); $resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR); return isset($resp['success']) && $resp['success']; } } +class HCaptchaQuery implements RemoteCaptchaQuery { + private HttpDriver $http; + private string $secret; + private string $sitekey; + + /** + * Creates a new HCaptchaQuery using the hCaptcha service. + * + * @param HttpDriver $http The http client. + * @param string $secret Server side secret. + * @return HCaptchaQuery A new hCaptcha query instance. + */ + public function __construct(HttpDriver $http, string $secret, string $sitekey) { + $this->http = $http; + $this->secret = $secret; + $this->sitekey = $sitekey; + } + + public function responseField(): string { + return 'h-captcha-response'; + } + + public function verify(string $response, ?string $remote_ip): bool { + $data = [ + 'secret' => $this->secret, + 'response' => $response, + 'sitekey' => $this->sitekey + ]; + + if ($remote_ip !== null) { + $data['remoteip'] = $remote_ip; + } + + $ret = $this->http->requestGet('https://hcaptcha.com/siteverify', $data); + $resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR); + + return isset($resp['success']) && $resp['success']; + } +} + +interface RemoteCaptchaQuery { + /** + * Name of the response field in the form data expected by the implementation. + * + * @return string The name of the field. + */ + public function responseField(): string; + + /** + * Checks if the user at the remote ip passed the captcha. + * + * @param string $response User provided response. + * @param ?string $remote_ip User ip. Leave to null to only check the response value. + * @return bool Returns true if the user passed the captcha. + * @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer. + */ + public function verify(string $response, ?string $remote_ip): bool; +} + class NativeCaptchaQuery { private HttpDriver $http; private string $domain; private string $provider_check; - /** * @param HttpDriver $http The http client. * @param string $domain The server's domain. @@ -89,12 +128,12 @@ class NativeCaptchaQuery { * @throws RuntimeException Throws on IO errors. */ public function verify(string $extra, string $user_text, string $user_cookie): bool { - $data = array( + $data = [ 'mode' => 'check', 'text' => $user_text, 'extra' => $extra, 'cookie' => $user_cookie - ); + ]; $ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data); return $ret === '1'; diff --git a/post.php b/post.php index a7f7e88b..fd78d749 100644 --- a/post.php +++ b/post.php @@ -648,29 +648,19 @@ if (isset($_POST['delete'])) { } } // Remote 3rd party captchas. - else if (!$config['dynamic_captcha'] || $config['dynamic_captcha'] === $_SERVER['REMOTE_ADDR']) { - // recaptcha - if ($config['recaptcha']) { - if (!isset($_POST['g-recaptcha-response'])) { - error($config['error']['bot']); - } - $response = $_POST['g-recaptcha-response']; - $query = RemoteCaptchaQuery::withRecaptcha($context->get(HttpDriver::class), $config['recaptcha_private']); - } - // hCaptcha - elseif ($config['hcaptcha']) { - if (!isset($_POST['h-captcha-response'])) { - error($config['error']['bot']); - } - $response = $_POST['h-captcha-response']; - $query = RemoteCaptchaQuery::withHCaptcha($context->get(HttpDriver::class), $config['hcaptcha_private']); - } + elseif (($config['recaptcha'] || $config['hcaptcha']) + && (!$config['dynamic_captcha'] || $config['dynamic_captcha'] === $_SERVER['REMOTE_ADDR'])) { + $query = $content->get(RemoteCaptchaQuery::class); + $field = $query->responseField(); - if (isset($query, $response)) { - $success = $query->verify($response, $_SERVER['REMOTE_ADDR']); - if (!$success) { - error($config['error']['captcha']); - } + if (!isset($_POST[$field])) { + error($config['error']['bot']); + } + $response = $_POST[$field]; + + $success = $query->verify($response, $_SERVER['REMOTE_ADDR']); + if (!$success) { + error($config['error']['captcha']); } } } catch (RuntimeException $e) {