Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions xExtension-Captcha/Controllers/authController.php

This file was deleted.

19 changes: 0 additions & 19 deletions xExtension-Captcha/Controllers/userController.php

This file was deleted.

3 changes: 3 additions & 0 deletions xExtension-Captcha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
53 changes: 34 additions & 19 deletions xExtension-Captcha/extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,18 @@

final class CaptchaExtension extends Minz_Extension {
/** @var array{protectedPages:array<string,string>,captchaProvider:string,provider:array<string,string>,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'));
Expand All @@ -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 : '';
Expand All @@ -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'] ?? '';
Expand All @@ -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);
}

Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -168,9 +183,9 @@ public static function initCaptcha(): bool {
* @throws FreshRSS_Context_Exception
* @return array<string,string>
*/
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',
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion xExtension-Captcha/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
78 changes: 0 additions & 78 deletions xExtension-UnsafeAutologin/Controllers/authController.php

This file was deleted.

5 changes: 4 additions & 1 deletion xExtension-UnsafeAutologin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
61 changes: 60 additions & 1 deletion xExtension-UnsafeAutologin/extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion xExtension-UnsafeAutologin/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}