From 95153233eb2c580a1f0387a5b70b84b6b3accc64 Mon Sep 17 00:00:00 2001 From: Lorenzo Yario Date: Sat, 30 Mar 2024 22:46:35 -0700 Subject: [PATCH] Revert "Revert "Revert "Share REST call code and separate components via dependency injection""" --- composer.json | 4 +- inc/config.php | 2 - inc/context.php | 28 ----- inc/driver/http-driver.php | 151 ----------------------- inc/service/captcha-queries.php | 102 ---------------- post.php | 210 ++++++++++++++++---------------- 6 files changed, 104 insertions(+), 393 deletions(-) delete mode 100644 inc/context.php delete mode 100644 inc/driver/http-driver.php delete mode 100644 inc/service/captcha-queries.php diff --git a/composer.json b/composer.json index e1951679..ec4a090d 100644 --- a/composer.json +++ b/composer.json @@ -32,9 +32,7 @@ "inc/mod/auth.php", "inc/lock.php", "inc/queue.php", - "inc/functions.php", - "inc/driver/http-driver.php", - "inc/service/captcha-queries.php" + "inc/functions.php" ] }, "license": "Tinyboard + vichan", diff --git a/inc/config.php b/inc/config.php index 23c31990..9140201c 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1231,8 +1231,6 @@ $config['error']['captcha'] = _('You seem to have mistyped the verification.'); $config['error']['flag_undefined'] = _('The flag %s is undefined, your PHP version is too old!'); $config['error']['flag_wrongtype'] = _('defined_flags_accumulate(): The flag %s is of the wrong type!'); - $config['error']['remote_io_error'] = _('IO error while interacting with a remote service.'); - $config['error']['local_io_error'] = _('IO error while interacting with a local resource or service.'); // Moderator errors diff --git a/inc/context.php b/inc/context.php deleted file mode 100644 index 5e540bab..00000000 --- a/inc/context.php +++ /dev/null @@ -1,28 +0,0 @@ -config = $config; - } - - public function getHttpDriver(): HttpDriver { - if (is_null($this->http)) { - $this->http = HttpDrivers::getHttpDriver($this->config['upload_by_url_timeout'], $this->config['max_filesize']); - } - return $this->http; - } -} diff --git a/inc/driver/http-driver.php b/inc/driver/http-driver.php deleted file mode 100644 index cfbedfad..00000000 --- a/inc/driver/http-driver.php +++ /dev/null @@ -1,151 +0,0 @@ -inner); - curl_setopt_array($this->inner, array( - CURLOPT_URL => $url, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_USERAGENT => $this->user_agent, - CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, - )); - } - - private function setSizeLimit(): void { - // Adapted from: https://stackoverflow.com/a/17642638 - curl_setopt($this->inner, CURLOPT_NOPROGRESS, false); - - if (PHP_MAJOR_VERSION >= 8 && PHP_MINOR_VERSION >= 2) { - curl_setopt($this->inner, CURLOPT_XFERINFOFUNCTION, function($res, $next_dl, $dl, $next_up, $up) { - return (int)($dl <= $this->max_file_size); - }); - } else { - curl_setopt($this->inner, CURLOPT_PROGRESSFUNCTION, function($res, $next_dl, $dl, $next_up, $up) { - return (int)($dl <= $this->max_file_size); - }); - } - } - - function __construct($timeout, $user_agent, $max_file_size) { - $this->inner = curl_init(); - $this->timeout = $timeout; - $this->user_agent = $user_agent; - $this->max_file_size = $max_file_size; - } - - function __destruct() { - curl_close($this->inner); - } - - /** - * Execute a GET request. - * - * @param string $endpoint Uri endpoint. - * @param ?array $data Optional GET parameters. - * @param int $timeout Optional request timeout in seconds. Use the default timeout if 0. - * @return string Returns the body of the response. - * @throws RuntimeException Throws on IO error. - */ - public function requestGet(string $endpoint, ?array $data, int $timeout = 0): string { - if (!empty($data)) { - $endpoint .= '?' . http_build_query($data); - } - if ($timeout == 0) { - $timeout = $this->timeout; - } - - $this->resetTowards($endpoint, $timeout); - curl_setopt($this->inner, CURLOPT_RETURNTRANSFER, true); - $ret = curl_exec($this->inner); - - if ($ret === false) { - throw new \RuntimeException(curl_error($this->inner)); - } - return $ret; - } - - /** - * Execute a POST request. - * - * @param string $endpoint Uri endpoint. - * @param ?array $data Optional POST parameters. - * @param int $timeout Optional request timeout in seconds. Use the default timeout if 0. - * @return string Returns the body of the response. - * @throws RuntimeException Throws on IO error. - */ - public function requestPost(string $endpoint, ?array $data, int $timeout = 0): string { - if ($timeout == 0) { - $timeout = $this->timeout; - } - - $this->resetTowards($endpoint, $timeout); - curl_setopt($this->inner, CURLOPT_POST, true); - if (!empty($data)) { - curl_setopt($this->inner, CURLOPT_POSTFIELDS, http_build_query($data)); - } - curl_setopt($this->inner, CURLOPT_RETURNTRANSFER, true); - $ret = curl_exec($this->inner); - - if ($ret === false) { - throw new \RuntimeException(curl_error($this->inner)); - } - return $ret; - } - - /** - * Download the url's target with curl. - * - * @param string $url Url to the file to download. - * @param ?array $data Optional GET parameters. - * @param resource $fd File descriptor to save the content to. - * @param int $timeout Optional request timeout in seconds. Use the default timeout if 0. - * @return bool Returns true on success, false if the file was too large. - * @throws RuntimeException Throws on IO error. - */ - public function requestGetInto(string $endpoint, ?array $data, mixed $fd, int $timeout = 0): bool { - if (!empty($data)) { - $endpoint .= '?' . http_build_query($data); - } - if ($timeout == 0) { - $timeout = $this->timeout; - } - - $this->resetTowards($endpoint, $timeout); - curl_setopt($this->inner, CURLOPT_FAILONERROR, true); - curl_setopt($this->inner, CURLOPT_FOLLOWLOCATION, false); - curl_setopt($this->inner, CURLOPT_FILE, $fd); - curl_setopt($this->inner, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - $this->setSizeLimit(); - $ret = curl_exec($this->inner); - - if ($ret === false) { - if (curl_errno($this->inner) === CURLE_ABORTED_BY_CALLBACK) { - return false; - } - - throw new \RuntimeException(curl_error($this->inner)); - } - return true; - } -} diff --git a/inc/service/captcha-queries.php b/inc/service/captcha-queries.php deleted file mode 100644 index d7966501..00000000 --- a/inc/service/captcha-queries.php +++ /dev/null @@ -1,102 +0,0 @@ -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 - ); - - $ret = $this->http->requestGet($this->endpoint, $data); - $resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR); - - return isset($resp['success']) && $resp['success']; - } -} - -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. - * @param string $provider_check Path to the endpoint. - */ - function __construct(HttpDriver $http, string $domain, string $provider_check) { - $this->http = $http; - $this->domain = $domain; - $this->provider_check = $provider_check; - } - - /** - * Checks if the user at the remote ip passed the native vichan captcha. - * - * @param string $extra Extra http parameters. - * @param string $user_text Remote user's text input. - * @param string $user_cookie Remote user cookie. - * @return bool Returns true if the user passed the check. - * @throws RuntimeException Throws on IO errors. - */ - public function verify(string $extra, string $user_text, string $user_cookie): bool { - $data = array( - '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 d0e30bdc..e22171ea 100644 --- a/post.php +++ b/post.php @@ -5,10 +5,6 @@ require_once 'inc/bootstrap.php'; -use Vichan\AppContext; -use Vichan\Driver\HttpDriver; -use Vichan\Service\{RemoteCaptchaQuery, NativeCaptchaQuery}; - /** * Utility functions */ @@ -65,27 +61,54 @@ function strip_symbols($input) { } } +/** + * Download the url's target with curl. + * + * @param string $url Url to the file to download. + * @param int $timeout Request timeout in seconds. + * @param File $fd File descriptor to save the content to. + * @return null|string Returns a string on error. + */ +function download_file_into($url, $timeout, $fd) { + $err = null; + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_FAILONERROR, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($curl, CURLOPT_TIMEOUT, $timeout); + curl_setopt($curl, CURLOPT_USERAGENT, 'Tinyboard'); + curl_setopt($curl, CURLOPT_FILE, $fd); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + + if (curl_exec($curl) === false) { + $err = curl_error($curl); + } + + curl_close($curl); + return $err; +} + /** * Download a remote file from the given url. * The file is deleted at shutdown. * - * @param HttpDriver $http The http client. * @param string $file_url The url to download the file from. * @param int $request_timeout Timeout to retrieve the file. * @param array $extra_extensions Allowed file extensions. * @param string $tmp_dir Temporary directory to save the file into. * @param array $error_array An array with error codes, used to create exceptions on failure. - * @return array|false Returns an array describing the file on success, or false if the file was too large - * @throws InvalidArgumentException|RuntimeException Throws on invalid arguments and IO errors. + * @return array Returns an array describing the file on success. + * @throws Exception on error. */ -function download_file_from_url(HttpDriver $http, $file_url, $request_timeout, $allowed_extensions, $tmp_dir, &$error_array) { +function download_file_from_url($file_url, $request_timeout, $allowed_extensions, $tmp_dir, &$error_array) { if (!preg_match('@^https?://@', $file_url)) { throw new InvalidArgumentException($error_array['invalidimg']); } - $param_idx = mb_strpos($file_url, '?'); - if ($param_idx !== false) { - $url_without_params = mb_substr($file_url, 0, $param_idx); + if (mb_strpos($file_url, '?') !== false) { + $url_without_params = mb_substr($file_url, 0, mb_strpos($file_url, '?')); } else { $url_without_params = $file_url; } @@ -105,13 +128,10 @@ function download_file_from_url(HttpDriver $http, $file_url, $request_timeout, $ $fd = fopen($tmp_file, 'w'); - try { - $success = $http->requestGetInto($url_without_params, null, $fd, $request_timeout); - if (!$success) { - return false; - } - } finally { - fclose($fd); + $dl_err = download_file_into($fd, $request_timeout, $fd); + fclose($fd); + if ($dl_err !== null) { + throw new Exception($error_array['nomove'] . '
Curl says: ' . $dl_err); } return array( @@ -145,7 +165,6 @@ function ocr_image(array $config, string $img_path): string { return trim($ret); } - /** * Trim an image's EXIF metadata * @@ -171,7 +190,6 @@ function strip_image_metadata(string $img_path): int { */ $dropped_post = false; -$context = new AppContext($config); // Is it a post coming from NNTP? Let's extract it and pretend it's a normal post. if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) { @@ -469,32 +487,22 @@ if (isset($_POST['delete'])) { if (count($report) > $config['report_limit']) error($config['error']['toomanyreports']); + if ($config['report_captcha'] && !isset($_POST['captcha_text'], $_POST['captcha_cookie'])) { + error($config['error']['bot']); + } if ($config['report_captcha']) { - if (!isset($_POST['captcha_text'], $_POST['captcha_cookie'])) { - error($config['error']['bot']); - } + $ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([ + 'mode' => 'check', + 'text' => $_POST['captcha_text'], + 'extra' => $config['captcha']['extra'], + 'cookie' => $_POST['captcha_cookie'] + ])); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $resp = curl_exec($ch); - try { - $query = new NativeCaptchaQuery( - $context->getHttpDriver(), - $config['domain'], - $config['captcha']['provider_check'] - ); - $success = $query->verify( - $config['captcha']['extra'], - $_POST['captcha_text'], - $_POST['captcha_cookie'] - ); - - if (!$success) { - error($config['error']['captcha']); - } - } catch (RuntimeException $e) { - if ($config['syslog']) { - _syslog(LOG_ERR, "Native captcha IO exception: {$e->getMessage()}"); - } - error($config['error']['local_io_error']); + if ($resp !== '1') { + error($config['error']['captcha']); } } @@ -584,60 +592,62 @@ if (isset($_POST['delete'])) { // Check if banned checkBan($board['uri']); - // Check for CAPTCHA right after opening the board so the "return" link is in there. - try { - // With our custom captcha provider - if ($config['captcha']['enabled'] || ($post['op'] && $config['new_thread_capt'])) { - $query = new NativeCaptchaQuery($context->getHttpDriver(), $config['domain'], $config['captcha']['provider_check']); - $success = $query->verify($config['captcha']['extra'], $_POST['captcha_text'], $_POST['captcha_cookie']); + // Check for CAPTCHA right after opening the board so the "return" link is in there + if ($config['recaptcha']) { + if (!isset($_POST['g-recaptcha-response'])) + error($config['error']['bot']); - if (!$success) { - error( - $config['error']['captcha'] - . '' - ); - } - } - // Remote 3rd party captchas. - else { - // recaptcha - if ($config['recaptcha']) { - if (!isset($_POST['g-recaptcha-response'])) { - error($config['error']['bot']); - } - $response = $_POST['g-recaptcha-response']; - $query = RemoteCaptchaQuery::withRecaptcha($context->getHttpDriver(), $config['recaptcha_private']); - } - // hCaptcha - elseif ($config['hcaptcha']) { - if (!isset($_POST['h-captcha-response'])) { - error($config['error']['bot']); - } - $response = $_POST['g-recaptcha-response']; - $query = RemoteCaptchaQuery::withHCaptcha($context->getHttpDriver(), $config['hcaptcha_private']); - } + // Check what reCAPTCHA has to say... + $resp = json_decode(file_get_contents(sprintf('https://www.recaptcha.net/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s', + $config['recaptcha_private'], + urlencode($_POST['g-recaptcha-response']), + $_SERVER['REMOTE_ADDR'])), true); - $success = $query->verify($response, $_SERVER['REMOTE_ADDR']); - if (!$success) { - error($config['error']['captcha']); - } + if (!$resp['success']) { + error($config['error']['captcha']); } - } catch (RuntimeException $e) { - if ($config['syslog']) { - _syslog(LOG_ERR, "Captcha IO exception: {$e->getMessage()}"); - } - error($config['error']['remote_io_error']); - } catch (JsonException $e) { - if ($config['syslog']) { - _syslog(LOG_ERR, "Bad JSON reply to captcha: {$e->getMessage()}"); - } - error($config['error']['remote_io_error']); } + // hCaptcha + if ($config['hcaptcha']) { + if (!isset($_POST['h-captcha-response'])) { + error($config['error']['bot']); + } + $data = array( + 'secret' => $config['hcaptcha_private'], + 'response' => $_POST['h-captcha-response'], + 'remoteip' => $_SERVER['REMOTE_ADDR'] + ); + + $hcaptchaverify = curl_init(); + curl_setopt($hcaptchaverify, CURLOPT_URL, "https://hcaptcha.com/siteverify"); + curl_setopt($hcaptchaverify, CURLOPT_POST, true); + curl_setopt($hcaptchaverify, CURLOPT_POSTFIELDS, http_build_query($data)); + curl_setopt($hcaptchaverify, CURLOPT_RETURNTRANSFER, true); + $hcaptcharesponse = curl_exec($hcaptchaverify); + + $resp = json_decode($hcaptcharesponse, true); // Decoding $hcaptcharesponse instead of $response + + if (!$resp['success']) { + error($config['error']['captcha']); + } + } + // Same, but now with our custom captcha provider + if (($config['captcha']['enabled']) || (($post['op']) && ($config['new_thread_capt'])) ) { + $ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([ + 'mode' => 'check', + 'text' => $_POST['captcha_text'], + 'extra' => $config['captcha']['extra'], + 'cookie' => $_POST['captcha_cookie'] + ])); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $resp = curl_exec($ch); + + if ($resp !== '1') { + error($config['error']['captcha'] . + ''); + } + } if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) || (!$post['op'] && $_POST['post'] == $config['button_reply']))) @@ -741,21 +751,7 @@ if (isset($_POST['delete'])) { } try { - $ret = download_file_from_url( - $context->getHttpDriver(), - $_POST['file_url'], - $config['upload_by_url_timeout'], - $allowed_extensions, - $config['tmp'], - $config['error'] - ); - if ($ret === false) { - error(sprintf3($config['error']['filesize'], array( - 'filesz' => 'more than that', - 'maxsz' => number_format($config['max_filesize']) - ))); - } - $_FILES['file'] = $ret; + $_FILES['file'] = download_file_from_url($_POST['file_url'], $config['upload_by_url_timeout'], $allowed_extensions, $config['tmp'], $config['error']); } catch (Exception $e) { error($e->getMessage()); }