Moved validation methods into the new Users class.

This commit is contained in:
flash 2023-08-30 23:41:44 +00:00
parent 07a2868159
commit f03c8ebfa5
7 changed files with 219 additions and 202 deletions

View file

@ -64,8 +64,9 @@ while($canResetPassword) {
break;
}
if(User::validatePassword($passwordNew) !== '') {
$notices[] = 'Your password is too weak!';
$passwordValidation = $users->validatePassword($passwordNew);
if($passwordValidation !== '') {
$notices[] = $users->validatePasswordText($passwordValidation);
break;
}

View file

@ -65,21 +65,20 @@ while(!$restricted && !empty($register)) {
break;
}
$usernameValidation = User::validateUsername($register['username']);
$usernameValidation = $users->validateName($register['username']);
if($usernameValidation !== '')
$notices[] = User::usernameValidationErrorString($usernameValidation);
$notices[] = $users->validateNameText($usernameValidation);
$emailValidation = User::validateEMailAddress($register['email']);
$emailValidation = $users->validateEMailAddress($register['email']);
if($emailValidation !== '')
$notices[] = $emailValidation === 'in-use'
? 'This e-mail address has already been used!'
: 'The e-mail address you entered is invalid!';
$notices[] = $users->validateEMailAddressText($emailValidation);
if($register['password_confirm'] !== $register['password'])
$notices[] = 'The given passwords don\'t match.';
if(User::validatePassword($register['password']) !== '')
$notices[] = 'Your password is too weak!';
$passwordValidation = $users->validatePassword($register['password']);
if($passwordValidation !== '')
$notices[] = $users->validatePasswordText($passwordValidation);
if(!empty($notices))
break;
@ -102,6 +101,7 @@ while(!$restricted && !empty($register)) {
$users->addRoles($userInfo, $defaultRoleInfo);
$config->setString('users.newest', $userInfo->getId());
$config->setBoolean('perms.needsRecalc', true);
url_redirect('auth-login-welcome', ['username' => $userInfo->getName()]);
return;

View file

@ -194,9 +194,13 @@ if(CSRF::validateRequest() && $canEdit) {
if(!empty($passwordNewValue)) {
if($passwordNewValue !== $passwordConfirmValue)
$notices[] = 'Confirm password does not match.';
elseif(!empty(User::validatePassword($passwordNewValue)))
$notices[] = 'New password is too weak.';
else
else {
$passwordValidation = $users->validatePassword($passwordNewValue);
if($passwordValidation !== '')
$notices[] = $users->validatePasswordText($passwordValidation);
}
if(empty($notices))
$users->updateUser(userInfo: $userInfo, password: $passwordNewValue);
}
}

View file

@ -144,21 +144,12 @@ if($isEditing) {
} else {
$aboutText = (string)($_POST['about']['text'] ?? '');
$aboutParse = (int)($_POST['about']['parser'] ?? Parser::PLAIN);
$aboutValid = User::validateProfileAbout($aboutParse, $aboutText);
$aboutValid = $users->validateProfileAbout($aboutParse, $aboutText);
if($aboutValid === '')
$users->updateUser($userInfo, aboutContent: $aboutText, aboutParser: $aboutParse);
else switch($aboutValid) {
case 'parser':
$notices[] = 'The selected about section parser is invalid.';
break;
case 'long':
$notices[] = sprintf('Please keep the length of your about section below %d characters.', User::PROFILE_ABOUT_MAX_LENGTH);
break;
default:
$notices[] = 'Failed to update about section, contact an administator.';
break;
}
else
$notices[] = $users->validateProfileAboutText($aboutValid);
}
}
@ -168,21 +159,12 @@ if($isEditing) {
} else {
$sigText = (string)($_POST['signature']['text'] ?? '');
$sigParse = (int)($_POST['signature']['parser'] ?? Parser::PLAIN);
$sigValid = User::validateForumSignature($sigParse, $sigText);
$sigValid = $users->validateForumSignature($sigParse, $sigText);
if($sigValid === '')
$users->updateUser($userInfo, signatureContent: $sigText, signatureParser: $sigParse);
else switch($sigValid) {
case 'parser':
$notices[] = 'The selected forum signature parser is invalid.';
break;
case 'long':
$notices[] = sprintf('Please keep the length of your signature below %d characters.', User::FORUM_SIGNATURE_MAX_LENGTH);
break;
default:
$notices[] = 'Failed to update signature, contact an administator.';
break;
}
else
$notices[] = $users->validateForumSignatureText($sigValid);
}
}
@ -193,21 +175,12 @@ if($isEditing) {
$birthYear = (int)($_POST['birthdate']['year'] ?? 0);
$birthMonth = (int)($_POST['birthdate']['month'] ?? 0);
$birthDay = (int)($_POST['birthdate']['day'] ?? 0);
$birthValid = User::validateBirthdate($birthYear, $birthMonth, $birthDay);
$birthValid = $users->validateBirthdate($birthYear, $birthMonth, $birthDay);
if($birthValid === '')
$users->updateUser($userInfo, birthYear: $birthYear, birthMonth: $birthMonth, birthDay: $birthDay);
else switch($birthValid) {
case 'year':
$notices[] = 'The given birth year is invalid.';
break;
case 'date':
$notices[] = 'The given birthdate is invalid.';
break;
default:
$notices[] = 'Something unexpected happened while setting your birthdate.';
break;
}
else
$notices[] = $users->validateBirthdateText($birthValid);
}
}

View file

@ -80,25 +80,10 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
} elseif($userInfo->getEMailAddress() === mb_strtolower($_POST['email']['confirm'])) {
$errors[] = 'This is already your e-mail address!';
} else {
$checkMail = User::validateEMailAddress($_POST['email']['new'], true);
$checkMail = $users->validateEMailAddress($_POST['email']['new']);
if($checkMail !== '') {
switch($checkMail) {
case 'dns':
$errors[] = 'No valid MX record exists for this domain.';
break;
case 'format':
$errors[] = 'The given e-mail address was incorrectly formatted.';
break;
case 'in-use':
$errors[] = 'This e-mail address is already in use.';
break;
default:
$errors[] = 'Unknown e-mail validation error.';
}
$errors[] = $users->validateEMailAddressText($checkMail);
} else {
$users->updateUser(userInfo: $userInfo, emailAddr: $_POST['email']['new']);
$msz->createAuditLog('PERSONAL_EMAIL_CHANGE', [$_POST['email']['new']]);
@ -111,10 +96,10 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
if(empty($_POST['password']['confirm']) || $_POST['password']['new'] !== $_POST['password']['confirm']) {
$errors[] = 'The new passwords you entered did not match each other.';
} else {
$checkPassword = User::validatePassword($_POST['password']['new']);
$checkPassword = $users->validatePassword($_POST['password']['new']);
if($checkPassword !== '') {
$errors[] = 'The given passwords was too weak.';
$errors[] = $users->validatePasswordText($checkPassword);
} else {
$users->updateUser(userInfo: $userInfo, password: $_POST['password']['new']);
$msz->createAuditLog('PERSONAL_PASSWORD_CHANGE');

View file

@ -1,122 +0,0 @@
<?php
namespace Misuzu\Users;
use Index\XString;
use Misuzu\DateCheck;
use Misuzu\DB;
use Misuzu\Parsers\Parser;
class User {
public const NAME_MIN_LENGTH = 3; // Minimum username length
public const NAME_MAX_LENGTH = 16; // Maximum username length, unless your name is Flappyzor(WorldwideOnline2018through2019through2020)
public const NAME_REGEX = '[A-Za-z0-9-_]+'; // Username character constraint
// Minimum amount of unique characters for passwords
public const PASSWORD_UNIQUE = 6;
// Maximum length of profile about section
public const PROFILE_ABOUT_MAX_LENGTH = 50000;
// Maximum length of forum signature
public const FORUM_SIGNATURE_MAX_LENGTH = 2000;
public static function validateUsername(string $name): string {
if($name !== trim($name))
return 'trim';
if(str_starts_with(mb_strtolower($name), 'flappyzor'))
return 'flapp';
$length = mb_strlen($name);
if($length < self::NAME_MIN_LENGTH)
return 'short';
if($length > self::NAME_MAX_LENGTH)
return 'long';
if(!preg_match('#^' . self::NAME_REGEX . '$#u', $name))
return 'invalid';
$userId = (int)DB::prepare('SELECT user_id FROM msz_users WHERE username = :username')
->bind('username', $name)->fetchColumn();
if($userId > 0)
return 'in-use';
return '';
}
public static function usernameValidationErrorString(string $error): string {
switch($error) {
case 'trim':
return 'Your username may not start or end with spaces!';
case 'short':
return sprintf('Your username is too short, it has to be at least %d characters!', self::NAME_MIN_LENGTH);
case 'long':
return sprintf("Your username is too long, it can't be longer than %d characters!", self::NAME_MAX_LENGTH);
case 'invalid':
return 'Your username contains invalid characters.';
case 'in-use':
return 'This username is already taken!';
case 'flapp':
return 'Your username may not start with Flappyzor!';
case '':
return 'This username is correctly formatted!';
default:
return 'This username is incorrectly formatted.';
}
}
public static function validateEMailAddress(string $address): string {
if(filter_var($address, FILTER_VALIDATE_EMAIL) === false)
return 'format';
if(!checkdnsrr(mb_substr(mb_strstr($address, '@'), 1), 'MX'))
return 'dns';
$userId = (int)DB::prepare('SELECT user_id FROM msz_users WHERE email = :email')
->bind('email', $address)->fetchColumn();
if($userId > 0)
return 'in-use';
return '';
}
public static function validatePassword(string $password): string {
if(XString::countUnique($password) < self::PASSWORD_UNIQUE)
return 'weak';
return '';
}
public static function validateBirthdate(int $year, int $month, int $day, int $yearRange = 100): string {
if($day !== 0 && $month !== 0) {
if($year > 0 && ($year < date('Y') - $yearRange || $year > date('Y')))
return 'year';
if(!DateCheck::isValidDate($year, $month, $day))
return 'date';
}
return '';
}
public static function validateProfileAbout(int $parser, string $text): string {
if(!Parser::isValid($parser))
return 'parser';
$length = strlen($text);
if($length > self::PROFILE_ABOUT_MAX_LENGTH)
return 'long';
return '';
}
public static function validateForumSignature(int $parser, string $text): string {
if(!Parser::isValid($parser))
return 'parser';
$length = strlen($text);
if($length > self::FORUM_SIGNATURE_MAX_LENGTH)
return 'long';
return '';
}
}

View file

@ -4,12 +4,15 @@ namespace Misuzu\Users;
use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\XString;
use Index\Colour\Colour;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Misuzu\DateCheck;
use Misuzu\Pagination;
use Misuzu\Parsers\Parser;
class Users {
private IDbConnection $dbConn;
@ -20,8 +23,15 @@ class Users {
$this->cache = new DbStatementCache($dbConn);
}
private const PASSWORD_ALGO = PASSWORD_ARGON2ID;
private const PASSWORD_OPTS = [];
public const NAME_MIN_LENGTH = 3;
public const NAME_MAX_LENGTH = 16;
public const PASSWORD_ALGO = PASSWORD_ARGON2ID;
public const PASSWORD_OPTS = [];
public const PASSWORD_UNIQUE = 6;
public const PROFILE_ABOUT_MAX_LENGTH = 50000;
public const FORUM_SIGNATURE_MAX_LENGTH = 2000;
public static function passwordHash(string $password): string {
return password_hash($password, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
@ -254,7 +264,10 @@ class Users {
$password = self::passwordHash($password);
// todo: validation
if(self::validateName($name, true) !== '')
throw new InvalidArgumentException('$name is not a valid user name.');
if(self::validateEMailAddress($email, true) !== '')
throw new InvalidArgumentException('$email is not a valid e-mail address.');
$stmt = $this->cache->get('INSERT INTO msz_users (username, password, email, register_ip, last_ip, user_country, display_role) VALUES (?, ?, ?, INET6_ATON(?), INET6_ATON(?), ?, ?)');
$stmt->addParameter(1, $name);
@ -293,16 +306,21 @@ class Users {
if($displayRoleInfo instanceof RoleInfo)
$displayRoleInfo = $displayRoleInfo->getId();
// do sanity checks on values at some point lol
$fields = [];
$values = [];
if($name !== null) {
if(self::validateName($name, true) !== '')
throw new InvalidArgumentException('$name is not valid.');
$fields[] = 'username = ?';
$values[] = $name;
}
if($emailAddr !== null) {
if(self::validateEMailAddress($emailAddr, true) !== '')
throw new InvalidArgumentException('$emailAddr is not valid.');
$fields[] = 'email = ?';
$values[] = $emailAddr;
}
@ -332,27 +350,30 @@ class Users {
$values[] = $totpKey === '' ? null : $totpKey;
}
if($aboutContent !== null) {
if($aboutContent !== null && $aboutParser !== null) {
if(self::validateProfileAbout($aboutParser, $aboutContent) !== '')
throw new InvalidArgumentException('$aboutContent and $aboutParser contain invalid data!');
$fields[] = 'user_about_content = ?';
$values[] = $aboutContent;
}
if($aboutParser !== null) {
$fields[] = 'user_about_parser = ?';
$values[] = $aboutParser;
}
if($signatureContent !== null) {
if($signatureContent !== null && $signatureParser !== null) {
if(self::validateForumSignature($signatureParser, $signatureContent) !== '')
throw new InvalidArgumentException('$signatureContent and $signatureParser contain invalid data!');
$fields[] = 'user_signature_content = ?';
$values[] = $signatureContent;
}
if($signatureParser !== null) {
$fields[] = 'user_signature_parser = ?';
$values[] = $signatureParser;
}
if($birthMonth !== null && $birthDay !== null) {
if(self::validateBirthdate($birthYear, $birthMonth, $birthDay) !== '')
throw new InvalidArgumentException('$birthYear, $birthMonth and $birthDay contain invalid data!');
// lowest leap year MariaDB accepts, used a 'no year' value
if($birthYear < 1004)
$birthYear = 1004;
@ -541,4 +562,159 @@ class Users {
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
public function checkNameInUse(string $name): bool {
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_users WHERE username = ?');
$stmt->addParameter(1, $name);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Was not able to check if name is already in use.');
return $result->getInteger(0) > 0;
}
public function validateName(string $name, bool $skipInUse = false): string {
if($name !== trim($name))
return 'trim';
if(str_starts_with(mb_strtolower($name), 'flappyzor'))
return 'flapp';
$length = mb_strlen($name);
if($length < self::NAME_MIN_LENGTH)
return 'short';
if($length > self::NAME_MAX_LENGTH)
return 'long';
if(!preg_match('#^[A-Za-z0-9-_]+$#u', $name))
return 'invalid';
if(!$skipInUse && $this->checkNameInUse($name))
return 'used';
return '';
}
public static function validateNameText(string $error): string {
return match($error) {
'trim' => 'Your username may not start or end with spaces.',
'short' => sprintf('Your username is too short, it has to be at least %d characters.', self::NAME_MIN_LENGTH),
'long' => sprintf("Your username is too long, it can't be longer than %d characters.", self::NAME_MAX_LENGTH),
'invalid' => 'Your username contains invalid characters.',
'used' => 'That username is already taken.',
'flapp' => 'Your username may not start with Flappyzor.',
'' => 'Your username is correctly formatted, why are you seeing this?',
default => 'This username is incorrectly formatted.',
};
}
public function checkEMailAddressInUse(string $address): bool {
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_users WHERE email = ?');
$stmt->addParameter(1, $address);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Was not able to check if e-mail address is already in use.');
return $result->getInteger(0) > 0;
}
public function validateEMailAddress(string $address, bool $skipInUse = false): string {
if(filter_var($address, FILTER_VALIDATE_EMAIL) === false)
return 'invalid';
if(!checkdnsrr(mb_substr(mb_strstr($address, '@'), 1), 'MX'))
return 'dns';
if(!$skipInUse && $this->checkEMailAddressInUse($address))
return 'used';
return '';
}
public static function validateEMailAddressText(string $error): string {
return match($error) {
'dns' => 'Was unable to find a mail server running on the domain in your e-mail address.',
'' => 'Your e-mail address is correctly formatted, why are you seeing this?',
default => 'Your e-mail address is not correctly formatted.',
};
}
public static function validatePassword(string $password): string {
if(XString::countUnique($password) < self::PASSWORD_UNIQUE)
return 'weak';
return '';
}
public static function validatePasswordText(string $error): string {
return match($error) {
'weak' => sprintf("Your password is too weak, it must contain at least %d unique characters.", self::PASSWORD_UNIQUE),
'' => 'Your password is strong enough, why are you seeing this?',
default => 'Your password is not acceptable.',
};
}
public static function validateBirthdate(?int $year, int $month, int $day, int $yearRange = 100): string {
$year ??= 0;
if($day !== 0 && $month !== 0) {
$currentYear = (int)date('Y');
if($year > 0 && ($year < $currentYear - $yearRange || $year > $currentYear))
return 'year';
if(!DateCheck::isValidDate($year, $month, $day))
return 'date';
}
return '';
}
public static function validateBirthdateText(string $error): string {
return match($error) {
'year' => 'The year in your birthdate is too ridiculous.',
'date' => 'The birthdate you attempted to set is not a valid date.',
'' => 'Your birthdate is fine, why are you seeing this?',
default => 'Your birthdate is not acceptable.',
};
}
public static function validateProfileAbout(int $parser, string $text): string {
if(!Parser::isValid($parser))
return 'parser';
$length = strlen($text);
if($length > self::PROFILE_ABOUT_MAX_LENGTH)
return 'long';
return '';
}
public static function validateProfileAboutText(string $error): string {
return match($error) {
'parser' => 'You attempted to select an invalid parser for your profile about section.',
'long' => sprintf('Please keep the length of your profile about section below %d characters.', self::PROFILE_ABOUT_MAX_LENGTH),
'' => 'Your profile about section is fine, why are you seeing this?',
default => 'Your profile about section is not acceptable.',
};
}
public static function validateForumSignature(int $parser, string $text): string {
if(!Parser::isValid($parser))
return 'parser';
$length = strlen($text);
if($length > self::FORUM_SIGNATURE_MAX_LENGTH)
return 'long';
return '';
}
public static function validateForumSignatureText(string $error): string {
return match($error) {
'parser' => 'You attempted to select an invalid parser for your forum signature.',
'long' => sprintf('Please keep the length of your forum signature below %d characters.', self::FORUM_SIGNATURE_MAX_LENGTH),
'' => 'Your forum signature is fine, why are you seeing this?',
default => 'Your forum signature is not acceptable.',
};
}
}