diff --git a/public-legacy/auth/password.php b/public-legacy/auth/password.php index a29085b..20282af 100644 --- a/public-legacy/auth/password.php +++ b/public-legacy/auth/password.php @@ -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; } diff --git a/public-legacy/auth/register.php b/public-legacy/auth/register.php index 03e4280..cca5e09 100644 --- a/public-legacy/auth/register.php +++ b/public-legacy/auth/register.php @@ -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; diff --git a/public-legacy/manage/users/user.php b/public-legacy/manage/users/user.php index eff398d..d93e7d0 100644 --- a/public-legacy/manage/users/user.php +++ b/public-legacy/manage/users/user.php @@ -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); } } diff --git a/public-legacy/profile.php b/public-legacy/profile.php index fd997a7..f7b7999 100644 --- a/public-legacy/profile.php +++ b/public-legacy/profile.php @@ -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); } } diff --git a/public-legacy/settings/account.php b/public-legacy/settings/account.php index 1ab6204..4776b30 100644 --- a/public-legacy/settings/account.php +++ b/public-legacy/settings/account.php @@ -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'); diff --git a/src/Users/User.php b/src/Users/User.php deleted file mode 100644 index dbc1fd0..0000000 --- a/src/Users/User.php +++ /dev/null @@ -1,122 +0,0 @@ - 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 ''; - } -} diff --git a/src/Users/Users.php b/src/Users/Users.php index df8c5ab..7060584 100644 --- a/src/Users/Users.php +++ b/src/Users/Users.php @@ -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.', + }; + } }