Replaced validation system

This commit is contained in:
Sergio Brighenti 2020-04-04 19:29:00 +02:00
parent f215dc70e8
commit 5606224339
11 changed files with 249 additions and 293 deletions

View file

@ -0,0 +1,68 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Web\Session;
use App\Web\ValidationHelper;
use Psr\Http\Message\ServerRequestInterface as Request;
abstract class AuthController extends Controller
{
protected function checkRecaptcha(ValidationHelper $validator, Request $request)
{
$validator->callIf($this->getSetting('recaptcha_enabled') === 'on', function (Session $session) use (&$request) {
$recaptcha = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret='.$this->getSetting('recaptcha_secret_key').'&response='.param($request, 'recaptcha_token')));
if ($recaptcha->success && $recaptcha->score < 0.5) {
$session->alert(lang('recaptcha_failed'), 'danger');
return false;
}
return true;
});
return $validator;
}
/**
* @return bool|false|resource
*/
public function ldapConnect()
{
if (!extension_loaded('ldap')) {
$this->logger->error('The LDAP extension is not loaded.');
return false;
}
$server = ldap_connect($this->config['ldap']['host'], $this->config['ldap']['port']);
if ($server) {
ldap_set_option($server, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($server, LDAP_OPT_REFERRALS, 0);
ldap_set_option($server, LDAP_OPT_NETWORK_TIMEOUT, 10);
}
return $server;
}
/**
* @param string $username
* @return string
*/
protected function getLdapRdn(string $username)
{
$bindString = 'uid='.addslashes($username);
if ($this->config['ldap']['user_domain'] !== null) {
$bindString .= ','.$this->config['ldap']['user_domain'];
}
if ($this->config['ldap']['base_domain'] !== null) {
$bindString .= ','.$this->config['ldap']['base_domain'];
}
return $bindString;
}
}

View file

@ -2,13 +2,12 @@
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Database\Queries\UserQuery;
use App\Web\ValidationChecker;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class LoginController extends Controller
class LoginController extends AuthController
{
/**
* @param Response $response
@ -41,13 +40,11 @@ class LoginController extends Controller
*/
public function login(Request $request, Response $response): Response
{
if ($this->getSetting('recaptcha_enabled') === 'on') {
$recaptcha = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret='.$this->getSetting('recaptcha_secret_key').'&response='.param($request, 'recaptcha_token')));
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class);
if ($recaptcha->success && $recaptcha->score < 0.5) {
$this->session->alert(lang('recaptcha_failed'), 'danger');
return redirect($response, route('login'));
}
if ($this->checkRecaptcha($validator, $request)->fails()) {
return redirect($response, route('login'));
}
$username = param($request, 'username');
@ -57,30 +54,21 @@ class LoginController extends Controller
$user = $this->ldapLogin($request, $username, param($request, 'password'), $user);
}
$validator = ValidationChecker::make()
->rules([
'login' => $user && password_verify(param($request, 'password'), $user->password),
'maintenance' => !isset($this->config['maintenance']) || !$this->config['maintenance'] || $user->is_admin ?? false,
'user_active' => $user->active ?? false,
])
->onFail(function ($rule) {
$alerts = [
'login' => lang('bad_login'),
'maintenance' => lang('maintenance_in_progress'),
'user_active' => lang('account_disabled'),
];
$validator
->alertIf(!$user || !password_verify(param($request, 'password'), $user->password), 'bad_login')
->alertIf(isset($this->config['maintenance']) && $this->config['maintenance'] && !($user->is_admin ?? true), 'maintenance_in_progress', 'info')
->alertIf(!($user->active ?? false), 'account_disabled');
$this->session->alert($alerts[$rule], $rule === 'maintenance' ? 'info' : 'danger');
});
if ($validator->fails()) {
return redirect($response, route('login'));
}
$this->session->set('logged', true);
$this->session->set('user_id', $user->id);
$this->session->set('username', $user->username);
$this->session->set('admin', $user->is_admin);
$this->session->set('copy_raw', $user->copy_raw);
$this->session->set('logged', true)
->set('user_id', $user->id)
->set('username', $user->username)
->set('admin', $user->is_admin)
->set('copy_raw', $user->copy_raw);
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
$this->session->alert(lang('welcome', [$user->username]), 'info');
@ -159,22 +147,4 @@ class LoginController extends Controller
return $dbUser;
}
/**
* @param string $username
* @return string
*/
private function getLdapRdn(string $username)
{
$bindString = 'uid='.addslashes($username);
if ($this->config['ldap']['user_domain'] !== null) {
$bindString .= ','.$this->config['ldap']['user_domain'];
}
if ($this->config['ldap']['base_domain'] !== null) {
$bindString .= ','.$this->config['ldap']['base_domain'];
}
return $bindString;
}
}

View file

@ -5,7 +5,7 @@ namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Web\Mail;
use App\Web\ValidationChecker;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
@ -105,19 +105,10 @@ class PasswordRecoveryController extends Controller
throw new HttpNotFoundException($request);
}
$validator = ValidationChecker::make()
->rules([
'password.required' => !empty(param($request, 'password')),
'password.match' => param($request, 'password') === param($request, 'password_repeat'),
])
->onFail(function ($rule) {
$alerts = [
'password.required' => lang('password_required'),
'password.match' => lang('password_match'),
];
$this->session->alert($alerts[$rule], 'danger');
});
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(empty(param($request, 'password')), 'password_required')
->alertIf(param($request, 'password') !== param($request, 'password_repeat'), 'password_match');
if ($validator->fails()) {
return redirect($response, route('recover.password', ['resetToken' => $resetToken]));

View file

@ -63,7 +63,7 @@ class RegisterController extends Controller
}
}
$validator = $this->getUserCreateValidator($request);
$validator = $this->getUserCreateValidator($request)->alertIf(empty(param($request, 'password')), 'password_required');
if ($validator->fails()) {
return redirect($response, route('register.show'));

View file

@ -6,7 +6,7 @@ use App\Database\DB;
use App\Database\Queries\UserQuery;
use App\Web\Lang;
use App\Web\Session;
use App\Web\ValidationChecker;
use App\Web\ValidationHelper;
use App\Web\View;
use DI\Container;
use DI\DependencyException;
@ -71,15 +71,12 @@ abstract class Controller
$this->session->set('current_disk_quota', humanFileSize($current));
if ($this->getSetting('quota_enabled', 'off') === 'on') {
if ($max < 0) {
$this->session->set('max_disk_quota', '∞');
$this->session->set('percent_disk_quota', null);
$this->session->set('max_disk_quota', '∞')->set('percent_disk_quota', null);
} else {
$this->session->set('max_disk_quota', humanFileSize($max));
$this->session->set('percent_disk_quota', round(($current * 100) / $max));
$this->session->set('max_disk_quota', humanFileSize($max))->set('percent_disk_quota', round(($current * 100) / $max));
}
} else {
$this->session->set('max_disk_quota', null);
$this->session->set('percent_disk_quota', null);
$this->session->set('max_disk_quota', null)->set('percent_disk_quota', null);
}
}
@ -143,49 +140,14 @@ abstract class Controller
/**
* @param Request $request
* @return ValidationChecker
* @return ValidationHelper
*/
public function getUserCreateValidator(Request $request)
{
return ValidationChecker::make()
->rules([
'email.required' => filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL) !== false,
'username.required' => !empty(param($request, 'username')),
'password.required' => !empty(param($request, 'password')),
'email.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ?', param($request, 'email'))->fetch()->count == 0,
'username.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', param($request, 'username'))->fetch()->count == 0,
])
->onFail(function ($rule) {
$alerts = [
'email.required' => lang('email_required'),
'username.required' => lang('username_required'),
'password.required' => lang('password_required'),
'email.unique' => lang('email_taken'),
'username.unique' => lang('username_taken'),
];
$this->session->alert($alerts[$rule], 'danger');
});
}
/**
* @return bool|false|resource
*/
public function ldapConnect()
{
if (!extension_loaded('ldap')) {
$this->logger->error('The LDAP extension is not loaded.');
return false;
}
$server = ldap_connect($this->config['ldap']['host'], $this->config['ldap']['port']);
if ($server) {
ldap_set_option($server, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($server, LDAP_OPT_REFERRALS, 0);
ldap_set_option($server, LDAP_OPT_NETWORK_TIMEOUT, 10);
}
return $server;
return make(ValidationHelper::class)
->alertIf(empty(param($request, 'username')), 'username_required')
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ?', param($request, 'email'))->fetch()->count != 0, 'email_taken')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', param($request, 'username'))->fetch()->count != 0, 'username_taken');
}
}

View file

@ -4,7 +4,7 @@
namespace App\Controllers;
use App\Database\Queries\UserQuery;
use App\Web\ValidationChecker;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@ -25,7 +25,7 @@ class ProfileController extends Controller
return view()->render($response, 'user/edit.twig', [
'profile' => true,
'user' => $user,
'user' => $user,
]);
}
@ -40,19 +40,10 @@ class ProfileController extends Controller
{
$user = make(UserQuery::class)->get($request, $id, true);
$validator = ValidationChecker::make()
->rules([
'email.required' => filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL),
'email.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count == 0,
])
->onFail(function ($rule) {
$alerts = [
'email.required' => lang('email_required'),
'email.unique' => lang('email_taken'),
];
$this->session->alert($alerts[$rule], 'danger');
});
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count != 0, 'email_taken');
if ($validator->fails()) {
return redirect($response, route('profile'));
@ -75,8 +66,7 @@ class ProfileController extends Controller
]);
}
$this->session->set('copy_raw', param($request, 'copy_raw') !== null ? 1 : 0);
$this->session->alert(lang('profile_updated'), 'success');
$this->session->set('copy_raw', param($request, 'copy_raw') !== null ? 1 : 0)->alert(lang('profile_updated'), 'success');
$this->logger->info('User '.$this->session->get('username')." updated profile of $user->id.");
return redirect($response, route('profile'));

View file

@ -4,7 +4,7 @@
namespace App\Controllers;
use App\Database\Queries\TagQuery;
use App\Web\ValidationChecker;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpBadRequestException;
@ -12,7 +12,6 @@ use Slim\Exception\HttpNotFoundException;
class TagController extends Controller
{
const PER_MEDIA_LIMIT = 10;
/**
* @param Request $request
@ -22,7 +21,7 @@ class TagController extends Controller
*/
public function addTag(Request $request, Response $response): Response
{
$validator = $this->validateTag($request);
$validator = $this->validateTag($request)->failIf(empty(param($request, 'tag')));
if ($validator->fails()) {
throw new HttpBadRequestException($request);
@ -48,7 +47,7 @@ class TagController extends Controller
*/
public function removeTag(Request $request, Response $response): Response
{
$validator = $this->validateTag($request)->removeRule('tag.notEmpty');
$validator = $this->validateTag($request);
if ($validator->fails()) {
throw new HttpBadRequestException($request);
@ -65,14 +64,15 @@ class TagController extends Controller
return $response;
}
/**
* @param Request $request
* @return ValidationHelper
*/
protected function validateTag(Request $request)
{
return ValidationChecker::make()
->rules([
'tag.notEmpty' => !empty(param($request, 'tag')),
'mediaId.notEmpty' => !empty(param($request, 'mediaId')),
'media.exists' => $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `id` = ?', param($request, 'mediaId'))->fetch()->count > 0,
'sameUserOrAdmin' => $this->session->get('admin', false) || $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', param($request, 'mediaId'))->fetch()->user_id === $this->session->get('user_id'),
]);
return make(ValidationHelper::class)
->failIf(empty(param($request, 'mediaId')))
->failIf($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `id` = ?', param($request, 'mediaId'))->fetch()->count == 0)
->failIf($this->session->get('admin', false) || $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', param($request, 'mediaId'))->fetch()->user_id !== $this->session->get('user_id'));
}
}

View file

@ -4,7 +4,7 @@ namespace App\Controllers;
use App\Database\Queries\UserQuery;
use App\Web\Mail;
use App\Web\ValidationChecker;
use App\Web\ValidationHelper;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@ -69,26 +69,26 @@ class UserController extends Controller
*/
public function store(Request $request, Response $response): Response
{
$validator = $this->getUserCreateValidator($request);
$hasPassword = $validator->removeRule('password.required');
$maxUserQuota = -1;
$validator = $this->getUserCreateValidator($request)
->callIf($this->getSetting('quota_enabled') === 'on', function ($session) use (&$maxUserQuota, &$request) {
$maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true));
if (!preg_match('/([0-9]+[K|M|G|T])|(\-1)/i', $maxUserQuota)) {
$session->alert(lang('invalid_quota', 'danger'));
return false;
}
if ($maxUserQuota !== '-1') {
$maxUserQuota = stringToBytes($maxUserQuota);
}
return true;
});
if ($validator->fails()) {
return redirect($response, route('user.create'));
}
$maxUserQuota = -1;
if ($this->getSetting('quota_enabled') === 'on') {
$maxUserQuotaStr = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota', -1), 0, true));
if (!preg_match('/([0-9]+[K|M|G|T])|(\-1)/i', $maxUserQuotaStr)) {
$this->session->alert(lang('invalid_quota', 'danger'));
return redirect($response, route('user.create'));
}
if ($maxUserQuotaStr !== '-1') {
$maxUserQuota = stringToBytes($maxUserQuotaStr);
}
}
make(UserQuery::class)->create(
param($request, 'email'),
param($request, 'username'),
@ -102,7 +102,16 @@ class UserController extends Controller
);
if (param($request, 'send_notification') !== null) {
$this->sendCreateNotification($hasPassword, $request);
$resetToken = null;
if (!empty(param($request, 'password'))) {
$resetToken = bin2hex(random_bytes(16));
$this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [
$resetToken,
$this->database->getPdo()->lastInsertId(),
]);
}
$this->sendCreateNotification($request, $resetToken);
}
$this->session->alert(lang('user_created', [param($request, 'username')]), 'success');
@ -143,44 +152,33 @@ class UserController extends Controller
public function update(Request $request, Response $response, int $id): Response
{
$user = make(UserQuery::class)->get($request, $id);
$user->max_disk_quota = -1;
$validator = ValidationChecker::make()
->rules([
'email.required' => filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL),
'username.required' => !empty(param($request, 'username')),
'email.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count == 0,
'username.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [param($request, 'username'), $user->username])->fetch()->count == 0,
'demote' => !($user->id === $this->session->get('user_id') && param($request, 'is_admin') === null),
])
->onFail(function ($rule) {
$alerts = [
'email.required' => lang('email_required'),
'username.required' => lang('username_required'),
'email.unique' => lang('email_taken'),
'username.unique' => lang('username_taken'),
'demote' => lang('cannot_demote'),
];
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf(empty(param($request, 'username')), 'username_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count != 0, 'email_taken')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [param($request, 'username'), $user->username])->fetch()->count != 0, 'username_taken')
->alertIf($user->id === $this->session->get('user_id') && param($request, 'is_admin') === null, 'cannot_demote')
->callIf($this->getSetting('quota_enabled') === 'on', function ($session) use (&$user, &$request) {
$maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true));
if (!preg_match('/([0-9]+[K|M|G|T])|(\-1)/i', $maxUserQuota)) {
$session->alert(lang('invalid_quota', 'danger'));
return false;
}
$this->session->alert($alerts[$rule], 'danger');
if ($maxUserQuota !== '-1') {
$user->max_disk_quota = stringToBytes($maxUserQuota);
}
return true;
});
if ($validator->fails()) {
return redirect($response, route('user.edit', ['id' => $id]));
}
$user->max_disk_quota = -1;
if ($this->getSetting('quota_enabled') === 'on') {
$maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true));
if (!preg_match('/([0-9]+[K|M|G|T])|(\-1)/i', $maxUserQuota)) {
$this->session->alert(lang('invalid_quota', 'danger'));
return redirect($response, route('user.create'));
}
if ($maxUserQuota !== '-1') {
$user->max_disk_quota = stringToBytes($maxUserQuota);
}
}
make(UserQuery::class)->update(
$user->id,
param($request, 'email'),
@ -281,13 +279,12 @@ class UserController extends Controller
}
/**
* @param $hasPassword
* @param $request
* @throws \Exception
* @param null $resetToken
*/
private function sendCreateNotification($hasPassword, $request)
private function sendCreateNotification($request, $resetToken = null)
{
if ($hasPassword) {
if (empty(param($request, 'password'))) {
$message = lang('mail.new_account_text_with_pw', [
param($request, 'username'),
$this->config['app_name'],
@ -297,13 +294,6 @@ class UserController extends Controller
route('login.show'),
]);
} else {
$resetToken = bin2hex(random_bytes(16));
$this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [
$resetToken,
$this->database->getPdo()->lastInsertId(),
]);
$message = lang('mail.new_account_text_with_reset', [
param($request, 'username'),
$this->config['app_name'],

View file

@ -9,8 +9,8 @@ class Session
/**
* Session constructor.
*
* @param string $name
* @param string $path
* @param string $name
* @param string $path
*
* @throws Exception
*/
@ -34,10 +34,10 @@ class Session
}
$started = @session_start([
'name' => $name,
'save_path' => $path,
'name' => $name,
'save_path' => $path,
'cookie_httponly' => true,
'gc_probability' => 25,
'gc_probability' => 25,
'cookie_samesite' => 'Lax', // works only for php >= 7.3
]);
@ -68,9 +68,10 @@ class Session
/**
* Clear all session stored values.
*/
public function clear(): void
public function clear(): Session
{
$_SESSION = [];
return $this;
}
/**
@ -99,7 +100,7 @@ class Session
* Returned a value given a key.
*
* @param $key
* @param null $default
* @param null $default
*
* @return mixed
*/
@ -113,21 +114,25 @@ class Session
*
* @param $key
* @param $value
* @return Session
*/
public function set($key, $value): void
public function set($key, $value): Session
{
$_SESSION[$key] = $value;
return $this;
}
/**
* Set a flash alert.
*
* @param $message
* @param string $type
* @param string $type
* @return Session
*/
public function alert($message, string $type = 'info'): void
public function alert($message, string $type = 'info'): Session
{
$_SESSION['_flash'][] = [$type => $message];
return $this;
}
/**

View file

@ -1,88 +0,0 @@
<?php
namespace App\Web;
class ValidationChecker
{
protected $rules = [];
protected $failClosure;
protected $lastRule;
/**
* @return ValidationChecker
*/
public static function make()
{
return new self();
}
/**
* @param array $rules
* @return $this
*/
public function rules(array $rules)
{
$this->rules = $rules;
return $this;
}
/**
* @param callable $closure
* @return $this
*/
public function onFail(callable $closure)
{
$this->failClosure = $closure;
return $this;
}
/**
* @return bool
*/
public function fails()
{
foreach ($this->rules as $rule => $condition) {
if (!$condition) {
$this->lastRule = $rule;
if (is_callable($this->failClosure)) {
($this->failClosure)($rule);
}
return true;
}
}
return false;
}
/**
* @param string $key
* @return ValidationChecker
*/
public function removeRule(string $key)
{
$this->rules[$key];
unset($this->rules[$key]);
return $this;
}
/**
* @param string $key
* @param $condition
* @return ValidationChecker
*/
public function addRule(string $key, $condition)
{
$this->rules[$key] = $condition;
return $this;
}
/**
* @return mixed
*/
public function getLastRule()
{
return $this->lastRule;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Web;
class ValidationHelper
{
/**
* @var Session
*/
protected $session;
/**
* @var bool
*/
protected $failed;
/**
* Validator constructor.
* @param Session $session
*/
public function __construct(Session $session)
{
$this->session = $session;
$this->failed = false;
}
public function alertIf(bool $condition, string $alert, string $type = 'danger')
{
if (!$this->failed && $condition) {
$this->failed = true;
$this->session->alert(lang($alert), $type);
}
return $this;
}
public function failIf(bool $condition)
{
if (!$this->failed && $condition) {
$this->failed = true;
}
return $this;
}
public function callIf(bool $condition, callable $closure)
{
if (!$this->failed && $condition) {
do {
$result = $closure($this->session);
if (is_callable($result)) {
$closure = $result;
}
} while (!is_bool($result));
$this->failed = !$result;
}
return $this;
}
public function fails()
{
return $this->failed;
}
}