Fix another user self-delete regression (#7877)

Regression from #7763 
Earlier regression which was fixed before #7626

In addition:
* get rid of `data-toggle` (refactor)
* show invalid login message if deleting account and entered incorrect password instead of redirect to 403
* remove unused reference to `r` parameter
* `forgetOpenCategories()` on login not on any crypto form
This commit is contained in:
Inverle
2025-09-15 22:17:14 +02:00
committed by GitHub
parent 38b7daedf7
commit ddb51c0e95
11 changed files with 111 additions and 118 deletions

View File

@@ -635,13 +635,16 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
$username, FreshRSS_Context::userConf()->passwordHash,
$nonce, $challenge
);
if (!$ok) {
Minz_Request::bad(_t('feedback.auth.login.invalid'), ['c' => 'user', 'a' => 'profile']);
return;
}
} elseif (self::reauthRedirect()) {
return;
}
if ($ok) {
$ok &= self::deleteUser($username);
}
$ok &= self::deleteUser($username);
if ($ok && $self_deletion) {
FreshRSS_Auth::removeAccess();
$redirect_url = ['c' => 'index', 'a' => 'index'];

View File

@@ -11,7 +11,7 @@
</div>
<?php } ?>
<form id="crypto-form" method="post" action="<?= _url('auth', 'login') ?>">
<form class="crypto-form" method="post" action="<?= _url('auth', 'login') ?>">
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
<input type="hidden" name="original_request" value="<?= Minz_Url::serialize(Minz_Request::originalRequest())?>" />
@@ -24,8 +24,8 @@
<div class="form-group">
<label for="passwordPlain"><?= _t('gen.auth.password') ?></label>
<div class="stick">
<input type="password" id="passwordPlain" required="required" />
<button type="button" class="btn toggle-password" data-toggle="passwordPlain"><?= _i('key') ?></button>
<input type="password" id="passwordPlain" class="passwordPlain" required="required" />
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<input type="hidden" id="challenge" name="challenge" />
<noscript><strong><?= _t('gen.js.should_be_activated') ?></strong></noscript>

View File

@@ -6,14 +6,14 @@
<main class="prompt">
<h1><?= _t('gen.auth.reauth.header') ?></h1>
<form id="crypto-form" method="post">
<form class="crypto-form" method="post">
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
<input type="hidden" id="username" value="<?= Minz_User::name() ?>" />
<div class="form-group">
<label for="passwordPlain"><?= _t('gen.auth.password') ?></label>
<div class="stick">
<input type="password" id="passwordPlain" required="required" />
<button type="button" class="btn toggle-password" data-toggle="passwordPlain"><?= _i('key') ?></button>
<input type="password" id="passwordPlain" class="passwordPlain" required="required" />
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<input type="hidden" id="challenge" name="challenge" />
<noscript><strong><?= _t('gen.js.should_be_activated') ?></strong></noscript>
@@ -23,7 +23,7 @@
?>
<p class="help"><?= _i('help') ?> <?= _t('gen.auth.reauth.tip', intval($reauth_time / 60)) ?></p>
<div class="form-group form-group-actions">
<button id="loginButton" type="submit" class="btn btn-important" disabled="disabled">
<button type="submit" class="btn btn-important" disabled="disabled">
<?= _t('gen.auth.login') ?>
</button>
</div>

View File

@@ -50,7 +50,7 @@
<label for="new_user_passwordPlain"><?= _t('gen.auth.password') ?></label>
<div class="stick">
<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" required="required" autocomplete="new-password" pattern=".{7,}" />
<button type="button" class="btn toggle-password" data-toggle="new_user_passwordPlain"><?= _i('key') ?></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<noscript><b><?= _t('gen.js.should_be_activated') ?></b></noscript>
<p class="help"><?= _i('help') ?> <?= _t('gen.auth.password.format') ?></p>

View File

@@ -83,7 +83,7 @@
<div class="group-controls">
<div class="stick">
<input type="password" name="http_pass" id="http_pass" value="<?= $auth['password'] ?>" autocomplete="new-password" />
<button type="button" class="btn toggle-password" data-toggle="http_pass"><?= _i('key') ?></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
</div>
</div>

View File

@@ -231,7 +231,7 @@
<div class="stick w50">
<input type="password" name="http_pass_feed<?= $this->feed->id() ?>" id="http_pass_feed<?= $this->feed->id() ?>" value="<?=
$auth['password'] ?>" autocomplete="new-password" />
<button type="button" class="btn toggle-password" data-toggle="http_pass_feed<?= $this->feed->id() ?>"><?= _i('key') ?></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
</div>
</div>

View File

@@ -277,7 +277,7 @@
<div class="group-controls">
<div class="stick">
<input id="http_pass" name="http_pass" type="password" value="" autocomplete="new-password" />
<button type="button" class="btn toggle-password" data-toggle="http_pass"><?= _i('key') ?></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
</div>
</div>

View File

@@ -61,7 +61,7 @@
<div class="stick">
<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="new-password"
pattern=".{7,}" <?= cryptAvailable() && Minz_User::name() !== $this->username ? '' : 'disabled="disabled" ' ?>/>
<button type="button" class="btn toggle-password" data-toggle="newPasswordPlain"><?= _i('key') ?></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<p class="help"><?= _i('help'); ?> <?= _t('admin.user.password_format') ?></p>
</div>

View File

@@ -71,7 +71,7 @@
<div class="group-controls">
<div class="stick">
<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="new-password" pattern=".{7,}" />
<button type="button" class="btn toggle-password" data-toggle="new_user_passwordPlain"><?= _i('key') ?></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<p class="help"><?= _i('help') ?> <?= _t('admin.user.password_format') ?></p>
<noscript><b><?= _t('gen.js.should_be_activated') ?></b></noscript>

View File

@@ -7,7 +7,7 @@
?>
<main class="post">
<form id="crypto-form" method="post" action="<?= _url('user', 'profile') ?>" data-auto-leave-validation="1">
<form class="crypto-form" method="post" action="<?= _url('user', 'profile') ?>" data-auto-leave-validation="1">
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
<h1><?= _t('conf.profile') ?></h1>
@@ -58,12 +58,12 @@
<details class="form-advanced" data-challenge-if-not-empty="1"<?= $open ? ' open="open"' : ''?>>
<summary class="form-advanced-title"><?= _t('conf.profile.change_password') ?></summary>
<div class="form-group">
<label class="group-name" for="passwordPlain"><?= _t('conf.profile.current_password') ?></label>
<label class="group-name" for="currentPasswordPlain"><?= _t('conf.profile.current_password') ?></label>
<div class="group-controls">
<input type="hidden" id="username" value="<?= Minz_User::name() ?? '' ?>" />
<div class="stick">
<input type="password" id="passwordPlain" />
<button type="button" class="btn toggle-password" data-toggle="passwordPlain"><img class="icon" src="../themes/icons/key.svg" loading="lazy" alt="🔑"></button>
<input type="password" id="currentPasswordPlain" class="passwordPlain" />
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<noscript>
@@ -77,10 +77,10 @@
<div class="group-controls">
<div class="stick">
<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="new-password" pattern=".{7,}" />
<button type="button" class="btn toggle-password" data-toggle="newPasswordPlain"><img class="icon" src="../themes/icons/key.svg" loading="lazy" alt="🔑"></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<p class="help">
<img class="icon" src="../themes/icons/help.svg" loading="lazy" alt=""> <?= _t('conf.profile.password_format') ?>
<?= _i('help') ?> <?= _t('conf.profile.password_format') ?>
</p>
</div>
</div>
@@ -89,7 +89,7 @@
<div class="group-controls">
<div class="stick">
<input type="password" id="confirmPasswordPlain" name="confirmPasswordPlain" autocomplete="new-password" pattern=".{7,}" />
<button type="button" class="btn toggle-password" data-toggle="confirmPasswordPlain"><img class="icon" src="../themes/icons/key.svg" loading="lazy" alt="🔑"></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
</div>
</div>
@@ -120,7 +120,7 @@
placeholder="<?= _t('conf.profile.api.api_not_set') ?>"
<?php } ?>
pattern=".{7,}" <?= cryptAvailable() ? '' : 'disabled="disabled" ' ?>/>
<button type="button" class="btn toggle-password" data-toggle="apiPasswordPlain"><?= _i('key') ?></button>
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<p class="help"><?= _i('help') ?> <?= _t('conf.profile.api.check_link', Minz_Url::display('/api/', 'html', true)) ?></p>
<p class="help"><?= _i('help') ?> <?= _t('conf.profile.api.documentation_link') ?></p>
@@ -146,7 +146,7 @@
<?php if (!FreshRSS_Auth::hasAccess('admin')) { ?>
<h2><?= _t('conf.profile.delete') ?></h2>
<form id="crypto-form" method="post" action="<?= _url('user', 'delete') ?>">
<form class="crypto-form" method="post" action="<?= _url('user', 'delete') ?>">
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
<p class="alert alert-warn"><span class="alert-head"><?= _t('gen.short.attention') ?></span> <?= _t('conf.profile.delete.warn') ?></p>
@@ -155,8 +155,8 @@
<label class="group-name" for="passwordPlain"><?= _t('gen.auth.password') ?></label>
<div class="group-controls">
<div class="stick">
<input type="password" id="passwordPlain" required="required" />
<button type="button" class="btn toggle-password" data-toggle="passwordPlain"><?= _i('key') ?></button>
<input type="password" id="passwordPlain" class="passwordPlain" required="required" />
<button type="button" class="btn toggle-password"><?= _i('key') ?></button>
</div>
<input type="hidden" id="challenge" name="challenge" /><br />
<noscript><strong><?= _t('gen.js.should_be_activated') ?></strong></noscript>
@@ -165,13 +165,6 @@
<div class="form-group form-actions">
<div class="group-controls">
<?php
$redirect_url = urlencode(Minz_Url::display(
['c' => 'user', 'a' => 'profile'],
'php', true
));
?>
<input type="hidden" name="r" value="<?= $redirect_url ?>" />
<input type="hidden" name="username" id="username" value="<?= Minz_User::name() ?>" />
<button type="submit" class="btn btn-attention confirm"><?= _t('gen.action.remove') ?></button>
</div>

View File

@@ -16,114 +16,107 @@ function forgetOpenCategories() {
localStorage.removeItem('FreshRSS_open_categories');
}
function init_crypto_form() {
/* globals bcrypt */
const crypto_form = document.getElementById('crypto-form');
if (!crypto_form) {
return;
}
function init_crypto_forms() {
if (!(window.bcrypt)) {
if (window.console) {
console.log('FreshRSS waiting for bcrypt.js…');
}
setTimeout(init_crypto_form, 100);
setTimeout(init_crypto_forms, 100);
return;
}
forgetOpenCategories();
/* globals bcrypt */
const crypto_forms = document.querySelectorAll('.crypto-form');
crypto_forms.forEach(crypto_form => {
const submit_button = crypto_form.querySelector('[type="submit"]');
if (submit_button) {
submit_button.disabled = false;
}
const submit_button = crypto_form.querySelector('[type="submit"]');
if (submit_button) {
submit_button.disabled = false;
}
crypto_form.onsubmit = function (e) {
let challenge = crypto_form.querySelector('#challenge');
if (!challenge) {
crypto_form.querySelectorAll('[data-challenge-if-not-empty] input[type="password"]').forEach(el => {
if (el.value !== '' && !challenge) {
crypto_form.insertAdjacentHTML('beforeend', '<input type="hidden" id="challenge" name="challenge" />');
challenge = crypto_form.querySelector('#challenge');
}
});
crypto_form.onsubmit = function (e) {
let challenge = crypto_form.querySelector('#challenge');
if (!challenge) {
return true;
}
}
e.preventDefault();
if (!submit_button) {
return false;
}
submit_button.disabled = true;
const req = new XMLHttpRequest();
req.open('GET', './?c=javascript&a=nonce&user=' + document.getElementById('username').value, true);
req.onerror = function () {
openNotification('Communication error!', 'bad');
submit_button.disabled = false;
};
req.onload = function () {
if (req.status == 200) {
const json = xmlHttpRequestJson(req);
if (!json.salt1 || !json.nonce) {
openNotification('Invalid user!', 'bad');
} else {
try {
const strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function');
const s = bcrypt.hashSync(document.getElementById('passwordPlain').value, json.salt1);
const c = bcrypt.hashSync(json.nonce + s, strong ? bcrypt.genSaltSync(4) : poormanSalt());
challenge.value = c;
if (!s || !c) {
openNotification('Crypto error!', 'bad');
} else {
crypto_form.removeEventListener('submit', crypto_form.onsubmit);
crypto_form.submit();
}
} catch (ex) {
openNotification('Crypto exception! ' + ex, 'bad');
crypto_form.querySelectorAll('[data-challenge-if-not-empty] input[type="password"]').forEach(el => {
if (el.value !== '' && !challenge) {
crypto_form.insertAdjacentHTML('beforeend', '<input type="hidden" id="challenge" name="challenge" />');
challenge = crypto_form.querySelector('#challenge');
}
});
if (!challenge) {
return true;
}
} else {
req.onerror();
}
submit_button.disabled = false;
};
req.send();
};
e.preventDefault();
if (!submit_button) {
return false;
}
submit_button.disabled = true;
const req = new XMLHttpRequest();
req.open('GET', './?c=javascript&a=nonce&user=' + crypto_form.querySelector('#username').value, true);
req.onerror = function () {
openNotification('Communication error!', 'bad');
submit_button.disabled = false;
};
req.onload = function () {
if (req.status == 200) {
const json = xmlHttpRequestJson(req);
if (!json.salt1 || !json.nonce) {
openNotification('Invalid user!', 'bad');
} else {
try {
const strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function');
const s = bcrypt.hashSync(crypto_form.querySelector('.passwordPlain').value, json.salt1);
const c = bcrypt.hashSync(json.nonce + s, strong ? bcrypt.genSaltSync(4) : poormanSalt());
challenge.value = c;
if (!s || !c) {
openNotification('Crypto error!', 'bad');
} else {
crypto_form.removeEventListener('submit', crypto_form.onsubmit);
crypto_form.submit();
}
} catch (ex) {
openNotification('Crypto exception! ' + ex, 'bad');
}
}
} else {
req.onerror();
}
submit_button.disabled = false;
};
req.send();
};
});
}
// </crypto form (Web login)>
// <show password>
let timeoutHide;
function showPW_this() {
const id_passwordField = this.getAttribute('data-toggle');
if (this.classList.contains('active')) {
hidePW(id_passwordField);
function togglePW(btn) {
if (btn.classList.contains('active')) {
hidePW(btn);
} else {
showPW(id_passwordField);
showPW(btn);
}
return false;
}
function showPW(id_passwordField) {
const passwordField = document.getElementById(id_passwordField);
function showPW(btn) {
const passwordField = btn.previousElementSibling;
passwordField.setAttribute('type', 'text');
passwordField.nextElementSibling.classList.add('active');
clearTimeout(timeoutHide);
timeoutHide = setTimeout(function () { hidePW(id_passwordField); }, 5000);
btn.classList.add('active');
clearTimeout(btn.timeoutHide);
btn.timeoutHide = setTimeout(function () { hidePW(btn); }, 5000);
return false;
}
function hidePW(id_passwordField) {
clearTimeout(timeoutHide);
const passwordField = document.getElementById(id_passwordField);
function hidePW(btn) {
clearTimeout(btn.timeoutHide);
const passwordField = btn.previousElementSibling;
passwordField.setAttribute('type', 'password');
passwordField.nextElementSibling.classList.remove('active');
return false;
@@ -131,7 +124,7 @@ function hidePW(id_passwordField) {
function init_password_observers(parent) {
parent.querySelectorAll('.toggle-password').forEach(function (btn) {
btn.addEventListener('click', showPW_this);
btn.onclick = () => togglePW(btn);
});
}
// </show password>
@@ -500,8 +493,12 @@ function init_extra_afterDOM() {
setTimeout(init_extra_afterDOM, 50);
return;
}
const loginButton = document.querySelector('#loginButton');
if (loginButton) {
loginButton.addEventListener('click', forgetOpenCategories);
}
if (!['normal', 'global', 'reader'].includes(context.current_view)) {
init_crypto_form();
init_crypto_forms();
init_password_observers(document.body);
init_select_observers();
init_configuration_alert();