diff --git a/xExtension-Captcha/Controllers/authController.php b/xExtension-Captcha/Controllers/authController.php deleted file mode 100644 index 32ea0b47..00000000 --- a/xExtension-Captcha/Controllers/authController.php +++ /dev/null @@ -1,33 +0,0 @@ -_csp($csp); - - parent::formLoginAction(); - } - - /** - * @throws FreshRSS_Context_Exception - */ - #[\Override] - public function registerAction(): void { - // Checking for valid captcha is not needed here since this isn't a POST action - $csp = CaptchaExtension::loadDependencies(); - if (!empty($csp)) $this->_csp($csp); - - parent::registerAction(); - } -} diff --git a/xExtension-Captcha/Controllers/userController.php b/xExtension-Captcha/Controllers/userController.php deleted file mode 100644 index 4dea0322..00000000 --- a/xExtension-Captcha/Controllers/userController.php +++ /dev/null @@ -1,19 +0,0 @@ -_csp($csp); - - parent::createAction(); - } -} diff --git a/xExtension-Captcha/README.md b/xExtension-Captcha/README.md index 0f1dac79..38d5dfc8 100644 --- a/xExtension-Captcha/README.md +++ b/xExtension-Captcha/README.md @@ -36,6 +36,9 @@ If you are having trouble with logging in after configuring the extension, you c ## Changelog +* 1.0.5 [2026-03-14] + * Ensure controller is hooked properly, even when other extensions are also hooking the same controller, by using new `Minz_HookType::ActionExecute` hook + * Fixed a bug where captcha was being verified when creating a user as admin * 1.0.4 [2026-03-11] * Fixed wrong path in CSP causing reCAPTCHA to not work properly, regression from v1.0.1 [#434](https://github.com/FreshRSS/Extensions/discussions/434) * 1.0.3 [2025-12-17] diff --git a/xExtension-Captcha/extension.php b/xExtension-Captcha/extension.php index 5f0129e8..35340576 100644 --- a/xExtension-Captcha/extension.php +++ b/xExtension-Captcha/extension.php @@ -3,22 +3,18 @@ final class CaptchaExtension extends Minz_Extension { /** @var array{protectedPages:array,captchaProvider:string,provider:array,sendClientIp:bool} $default_config */ - public static array $default_config = [ + private static array $default_config = [ 'protectedPages' => [], 'captchaProvider' => 'none', 'provider' => [], 'sendClientIp' => true, ]; - public static string $recaptcha_v3_js; #[\Override] public function init(): void { $this->registerTranslates(); - $this->registerHook('before_login_btn', [$this, 'captchaWidget']); - $this->registerController('auth'); - $this->registerController('user'); - - self::$recaptcha_v3_js = $this->getFileUrl('recaptcha-v3.js'); + $this->registerHook(Minz_HookType::BeforeLoginBtn, [$this, 'captchaWidget']); + $this->registerHook(Minz_HookType::ActionExecute, [$this, 'handleProtectedPage']); if (Minz_Request::controllerName() === 'extension') { Minz_View::appendScript($this->getFileUrl('captchaConfig.js')); @@ -28,13 +24,32 @@ public function init(): void { /** * @throws FreshRSS_Context_Exception */ - public static function isProtectedPage(): bool { + private function isProtectedPage(): bool { $config = self::getConfig(); $page = Minz_Request::controllerName() . '_' . Minz_Request::actionName(); return in_array($page, $config['protectedPages'], true); } - public static function getClientIp(): string { + /** + * @throws FreshRSS_Context_Exception + * @throws Minz_PermissionDeniedException + */ + public function handleProtectedPage(Minz_ActionController $controller): bool { + if (Minz_Request::is('auth', 'formLogin') || Minz_Request::is('auth', 'register')) { + if (!$this->initCaptcha()) { + return false; + } + $csp = $this->loadDependencies(); + if (!empty($csp)) $controller->_csp($csp); + } elseif (Minz_Request::is('user', 'create') && !FreshRSS_Auth::hasAccess('admin')) { + if (!CaptchaExtension::initCaptcha()) { + return false; + } + } + return true; + } + + private function getClientIp(): string { $ip = FreshRSS_http_Util::checkTrustedIP() ? ($_SERVER['HTTP_X_REAL_IP'] ?? Minz_Request::connectionRemoteAddress()) : Minz_Request::connectionRemoteAddress(); return is_string($ip) ? $ip : ''; @@ -43,9 +58,9 @@ public static function getClientIp(): string { /** * @throws FreshRSS_Context_Exception */ - public function captchaWidget(): string { + private function captchaWidget(): string { $config = self::getConfig(); - if (!self::isProtectedPage()) { + if (!$this->isProtectedPage()) { return ''; } $siteKey = $config['provider']['siteKey'] ?? ''; @@ -61,7 +76,7 @@ public function captchaWidget(): string { /** * @throws Minz_PermissionDeniedException */ - public static function warnLog(string $msg): void { + private function warnLog(string $msg): void { Minz_Log::warning('[Form Captcha] ' . $msg, ADMIN_LOG); } @@ -83,17 +98,17 @@ public static function getConfig(): array { * @throws FreshRSS_Context_Exception * @throws Minz_PermissionDeniedException */ - public static function initCaptcha(): bool { + private function initCaptcha(): bool { $username = Minz_Request::paramString('username'); - $config = CaptchaExtension::getConfig(); + $config = self::getConfig(); $provider = $config['captchaProvider']; if ($provider === 'none') { return true; } - if (Minz_Request::isPost() && CaptchaExtension::isProtectedPage()) { + if (Minz_Request::isPost() && $this->isProtectedPage()) { $ch = curl_init(); if ($ch === false) { Minz_Error::error(500); @@ -132,7 +147,7 @@ public static function initCaptcha(): bool { 'response' => $response_val, ]; if ($config['sendClientIp']) { - $fields['remoteip'] = CaptchaExtension::getClientIp(); + $fields['remoteip'] = $this->getClientIp(); } curl_setopt_array($ch, [ CURLOPT_URL => $siteverify_url, @@ -168,9 +183,9 @@ public static function initCaptcha(): bool { * @throws FreshRSS_Context_Exception * @return array */ - public static function loadDependencies(): array { + private function loadDependencies(): array { $cfg = self::getConfig(); - $provider = self::isProtectedPage() ? $cfg['captchaProvider'] : ''; + $provider = $this->isProtectedPage() ? $cfg['captchaProvider'] : ''; $js_url = match ($provider) { 'turnstile' => 'https://challenges.cloudflare.com/turnstile/v0/api.js', 'recaptcha-v2' => 'https://www.google.com/recaptcha/api.js', @@ -205,7 +220,7 @@ public static function loadDependencies(): array { } Minz_View::appendScript($js_url); if ($provider === 'recaptcha-v3') { - Minz_View::appendScript(self::$recaptcha_v3_js); + Minz_View::appendScript($this->getFileUrl('recaptcha-v3.js')); } return $csp; } diff --git a/xExtension-Captcha/metadata.json b/xExtension-Captcha/metadata.json index cd17a3df..8fa4bd0a 100644 --- a/xExtension-Captcha/metadata.json +++ b/xExtension-Captcha/metadata.json @@ -2,7 +2,7 @@ "name": "Form Captcha", "author": "Inverle", "description": "Protect register/login forms with captcha", - "version": "1.0.4", + "version": "1.0.5", "entrypoint": "Captcha", "type": "system" } diff --git a/xExtension-UnsafeAutologin/Controllers/authController.php b/xExtension-UnsafeAutologin/Controllers/authController.php deleted file mode 100644 index f83240f9..00000000 --- a/xExtension-UnsafeAutologin/Controllers/authController.php +++ /dev/null @@ -1,78 +0,0 @@ - 'index', 'a' => 'index'], true); - return; - } - Minz_Request::forward(['c' => 'auth', 'a' => 'formLogin']); - } - - /** - * @throws FreshRSS_Context_Exception - * @throws Minz_ConfigurationNamespaceException - * @throws Minz_ConfigurationException - * @throws Minz_PermissionDeniedException - */ - #[\Override] - public function loginAction(): void { - if (FreshRSS_Context::systemConf()->auth_type !== 'form') { - parent::loginAction(); - return; - } - - $username = Minz_Request::paramString('u'); - $password = Minz_Request::paramString('p', plaintext: true); - - if ($username === '' || $password === '') { - self::redirectFormLogin(); - return; - } - - if (!FreshRSS_user_Controller::checkUsername($username) || !FreshRSS_user_Controller::userExists($username)) { - Minz_Request::bad( - _t('feedback.auth.login.invalid'), - ['c' => 'index', 'a' => 'index'] - ); - return; - } - - $config = FreshRSS_UserConfiguration::getForUser($username); - - $s = $config->passwordHash ?? ''; - $ok = password_verify($password, $s); - - if ($ok) { - FreshRSS_Context::initUser($username); - FreshRSS_FormAuth::deleteCookie(); - Minz_Session::regenerateID('FreshRSS'); - Minz_Session::_params([ - Minz_User::CURRENT_USER => $username, - 'passwordHash' => $s, - 'lastReauth' => false, - 'csrf' => false, - ]); - FreshRSS_Auth::giveAccess(); - - Minz_Translate::init(FreshRSS_Context::userConf()->language); - - FreshRSS_UserDAO::touch(); - - Minz_Request::good(_t('feedback.auth.login.success'), ['c' => 'index', 'a' => 'index']); - return; - } - - Minz_Log::warning('Unsafe password mismatch for user ' . $username, USERS_PATH . '/' . $username . '/log.txt'); - Minz_Request::bad( - _t('feedback.auth.login.invalid'), - ['c' => 'index', 'a' => 'index'] - ); - } -} diff --git a/xExtension-UnsafeAutologin/README.md b/xExtension-UnsafeAutologin/README.md index 691c7f71..0b315bad 100644 --- a/xExtension-UnsafeAutologin/README.md +++ b/xExtension-UnsafeAutologin/README.md @@ -3,8 +3,11 @@ You can install this extension to bring back unsafe autologin functionality after enabling it. This extension should not be used on public multi-user instances. +It is especially advised to not use this extension, unless only as a last resort, since your password may end up in plaintext in server logs. ## Changelog -* 1.0.0 +* 1.0.1 [2026-03-14] + * Ensure controller is hooked properly, even when other extensions are also hooking the same controller, by using new `Minz_HookType::ActionExecute` hook +* 1.0.0 [2025-11-23] * Initial release diff --git a/xExtension-UnsafeAutologin/extension.php b/xExtension-UnsafeAutologin/extension.php index e50cc531..d8b8bf31 100644 --- a/xExtension-UnsafeAutologin/extension.php +++ b/xExtension-UnsafeAutologin/extension.php @@ -4,6 +4,65 @@ final class UnsafeAutologinExtension extends Minz_Extension { #[\Override] public function init(): void { - $this->registerController('auth'); + $this->registerHook(Minz_HookType::ActionExecute, [$this, 'handleLogin']); + } + + /** + * @throws FreshRSS_Context_Exception + * @throws Minz_ConfigurationNamespaceException + * @throws Minz_ConfigurationException + * @throws Minz_PermissionDeniedException + */ + public function handleLogin(): bool { + if (!Minz_Request::is('auth', 'login') || FreshRSS_Context::systemConf()->auth_type !== 'form') { + return true; + } + + $username = Minz_Request::paramString('u'); + $password = Minz_Request::paramString('p', plaintext: true); + + if ($username === '' || $password === '') { + return true; + } + + if (!FreshRSS_user_Controller::checkUsername($username) || !FreshRSS_user_Controller::userExists($username)) { + Minz_Request::bad( + _t('feedback.auth.login.invalid'), + ['c' => 'index', 'a' => 'index'] + ); + return false; + } + + $config = FreshRSS_UserConfiguration::getForUser($username); + + $s = $config->passwordHash ?? ''; + $ok = password_verify($password, $s); + + if ($ok) { + FreshRSS_Context::initUser($username); + FreshRSS_FormAuth::deleteCookie(); + Minz_Session::regenerateID('FreshRSS'); + Minz_Session::_params([ + Minz_User::CURRENT_USER => $username, + 'passwordHash' => $s, + 'lastReauth' => false, + 'csrf' => false, + ]); + FreshRSS_Auth::giveAccess(); + + Minz_Translate::init(FreshRSS_Context::userConf()->language); + + FreshRSS_UserDAO::touch(); + + Minz_Request::good(_t('feedback.auth.login.success'), ['c' => 'index', 'a' => 'index']); + return false; + } + + Minz_Log::warning('Unsafe password mismatch for user ' . $username, USERS_PATH . '/' . $username . '/log.txt'); + Minz_Request::bad( + _t('feedback.auth.login.invalid'), + ['c' => 'index', 'a' => 'index'] + ); + return false; } } diff --git a/xExtension-UnsafeAutologin/metadata.json b/xExtension-UnsafeAutologin/metadata.json index 2271b98f..a5726b9d 100644 --- a/xExtension-UnsafeAutologin/metadata.json +++ b/xExtension-UnsafeAutologin/metadata.json @@ -2,7 +2,7 @@ "name": "Unsafe Autologin", "author": "Inverle", "description": "Brings back removed unsafe autologin feature from FreshRSS", - "version": "1.0.0", + "version": "1.0.1", "entrypoint": "UnsafeAutologin", "type": "system" }