Made user asset handling OOP.

This commit is contained in:
flash 2020-06-07 20:37:03 +00:00
parent fa0088a3e9
commit 44fc436134
36 changed files with 1198 additions and 1055 deletions

View file

@ -105,10 +105,13 @@ Misuzu.initLoginPage = function() {
if(xhr.readyState !== 4)
return;
avatarElem.src = Misuzu.Urls.format('user-avatar', [
{ name: 'user', value: xhr.responseText.indexOf('<') !== -1 ? '0' : xhr.responseText },
{ name: 'res', value: 100 },
]);
var json = JSON.parse(xhr.responseText);
if(!json)
return;
if(json.name)
usernameElem.value = json.name;
avatarElem.src = json.avatar;
});
xhr.open('GET', Misuzu.Urls.format('auth-resolve-user', [{name: 'username', value: encodeURIComponent(usernameElem.value)}]));
xhr.send();

View file

@ -14,6 +14,10 @@ define('MSZ_ROOT', __DIR__);
define('MSZ_CLI', PHP_SAPI === 'cli');
define('MSZ_DEBUG', is_file(MSZ_ROOT . '/.debug'));
define('MSZ_PHP_MIN_VER', '7.4.0');
define('MSZ_PUBLIC', MSZ_ROOT . '/public');
define('MSZ_SOURCE', MSZ_ROOT . '/src');
define('MSZ_CONFIG', MSZ_ROOT . '/config');
define('MSZ_TEMPLATES', MSZ_ROOT . '/templates');
if(version_compare(PHP_VERSION, MSZ_PHP_MIN_VER, '<'))
die("Misuzu requires <i>at least</i> PHP <b>" . MSZ_PHP_MIN_VER . "</b> to run.\r\n");
@ -41,7 +45,7 @@ set_exception_handler(function(\Throwable $ex) {
echo (string)$ex;
} else {
header('Content-Type: text/html; charset-utf-8');
echo file_get_contents(MSZ_ROOT . '/templates/500.html');
echo file_get_contents(MSZ_TEMPLATES . '/500.html');
}
}
exit;
@ -59,7 +63,7 @@ spl_autoload_register(function(string $className) {
if($parts[0] !== 'Misuzu')
return;
$classPath = MSZ_ROOT . '/src/' . str_replace('\\', '/', $parts[1]) . '.php';
$classPath = MSZ_SOURCE . '/' . str_replace('\\', '/', $parts[1]) . '.php';
if(is_file($classPath))
require_once $classPath;
});
@ -78,11 +82,8 @@ require_once 'src/Forum/poll.php';
require_once 'src/Forum/post.php';
require_once 'src/Forum/topic.php';
require_once 'src/Forum/validate.php';
require_once 'src/Users/avatar.php';
require_once 'src/Users/background.php';
require_once 'src/Users/user_legacy.php';
$dbConfig = parse_ini_file(MSZ_ROOT . '/config/config.ini', true, INI_SCANNER_TYPED);
$dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED);
if(empty($dbConfig)) {
echo 'Database config is missing.';
@ -106,7 +107,8 @@ Mailer::init(Config::get('mail.method', Config::TYPE_STR), [
// replace this with a better storage mechanism
define('MSZ_STORAGE', Config::get('storage.path', Config::TYPE_STR, MSZ_ROOT . '/store'));
mkdirs(MSZ_STORAGE, true);
if(!is_dir(MSZ_STORAGE))
mkdir(MSZ_STORAGE, 0775, true);
if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later
if(realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
@ -133,7 +135,7 @@ if(file_exists(MSZ_ROOT . '/.migrating')) {
http_response_code(503);
if(!isset($_GET['_check'])) {
header('Content-Type: text/html; charset-utf-8');
echo file_get_contents(MSZ_ROOT . '/templates/503.html');
echo file_get_contents(MSZ_TEMPLATES . '/503.html');
}
exit;
}
@ -147,7 +149,8 @@ GeoIP::init(Config::get('geoip.database', Config::TYPE_STR, '/var/lib/GeoIP/GeoL
if(!MSZ_DEBUG) {
$twigCache = sys_get_temp_dir() . '/msz-tpl-cache-' . md5(MSZ_ROOT);
mkdirs($twigCache, true);
if(!is_dir($twigCache))
mkdir($twigCache, 0775, true);
}
Template::init($twigCache ?? null, MSZ_DEBUG);
@ -159,7 +162,7 @@ Template::set('globals', [
'site_twitter' => Config::get('social.twitter', Config::TYPE_STR),
]);
Template::addPath(MSZ_ROOT . '/templates');
Template::addPath(MSZ_TEMPLATES);
if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) {
$authToken = (new AuthToken)
@ -201,10 +204,9 @@ if($authToken->isValid()) {
User::unsetCurrent();
}
if(!UserSession::hasCurrent()) {
setcookie('msz_auth', '', -9001, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
} else {
if(UserSession::hasCurrent()) {
$userInfo->bumpActivity();
$userDisplayInfo = DB::prepare('
SELECT
u.`user_id`, u.`username`,
@ -216,9 +218,10 @@ if($authToken->isValid()) {
') ->bind('user_id', $userInfo->getId())
->fetch();
user_bump_last_active($userInfo->getId());
$userDisplayInfo['perms'] = perms_get_user($userInfo->getId());
} else {
setcookie('msz_auth', '', -9001, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
}
}

View file

@ -17,9 +17,26 @@ if(UserSession::hasCurrent()) {
return;
}
if(!empty($_GET['resolve_user']) && is_string($_GET['resolve_user'])) {
header('Content-Type: text/plain; charset=utf-8');
echo user_id_from_username($_GET['resolve_user']);
if(!empty($_GET['resolve'])) {
header('Content-Type: application/json; charset=utf-8');
try {
// Only works for usernames, this is by design
$userInfo = User::byUsername((string)filter_input(INPUT_GET, 'name', FILTER_SANITIZE_STRING));
} catch(UserNotFoundException $ex) {
echo json_encode([
'id' => 0,
'name' => '',
'avatar' => url('user-avatar', ['res' => 200]),
]);
return;
}
echo json_encode([
'id' => $userInfo->getId(),
'name' => $userInfo->getUsername(),
'avatar' => url('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]),
]);
return;
}
@ -56,7 +73,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
$loginFailedError = "Invalid username or password, {$attemptsRemainingError}.";
try {
$userInfo = User::findForLogin($_POST['login']['username']);
$userInfo = User::byUsernameOrEMailAddress($_POST['login']['username']);
} catch(UserNotFoundException $ex) {
UserLoginAttempt::create(false);
$notices[] = $loginFailedError;
@ -74,9 +91,8 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
break;
}
if($userInfo->passwordNeedsRehash()) {
$userInfo->setPassword($_POST['login']['password']);
}
if($userInfo->passwordNeedsRehash())
$userInfo->setPassword($_POST['login']['password'])->save();
if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userInfo->getId(), $loginPermVal)) {
$notices[] = "Login succeeded, but you're not allowed to browse the site right now.";

View file

@ -71,12 +71,13 @@ while($canResetPassword) {
break;
}
$userInfo->setPassword($passwordNew);
AuditLog::create(AuditLog::PASSWORD_RESET, [], $userInfo);
// disable two factor auth to prevent getting locked out of account entirely
// also disables two factor auth to prevent getting locked out of account entirely
// this behaviour should really be replaced with recovery keys...
user_totp_update($userId, null);
$userInfo->setPassword($passwordNew)
->removeTOTPKey()
->save();
AuditLog::create(AuditLog::PASSWORD_RESET, [], $userInfo);
$tokenInfo->invalidate();

View file

@ -76,13 +76,11 @@ if(CSRF::validateRequest() && $canEdit) {
$userInfo->addRole($role);
}
$setUserInfo = [];
if(!empty($_POST['user']) && is_array($_POST['user'])) {
$setUserInfo['username'] = (string)($_POST['user']['username'] ?? '');
$setUserInfo['email'] = (string)($_POST['user']['email'] ?? '');
$setUserInfo['user_country'] = (string)($_POST['user']['country'] ?? '');
$setUserInfo['user_title'] = (string)($_POST['user']['title'] ?? '');
$setUsername = (string)($_POST['user']['username'] ?? '');
$setEMailAddress = (string)($_POST['user']['email'] ?? '');
$setCountry = (string)($_POST['user']['country'] ?? '');
$setTitle = (string)($_POST['user']['title'] ?? '');
$displayRole = (int)($_POST['user']['display_role'] ?? 0);
@ -90,11 +88,11 @@ if(CSRF::validateRequest() && $canEdit) {
$userInfo->setDisplayRole(UserRole::byId($displayRole));
} catch(UserRoleNotFoundException $ex) {}
$usernameValidation = User::validateUsername($setUserInfo['username']);
$emailValidation = User::validateEMailAddress($setUserInfo['email']);
$countryValidation = strlen($setUserInfo['user_country']) === 2
&& ctype_alpha($setUserInfo['user_country'])
&& ctype_upper($setUserInfo['user_country']);
$usernameValidation = User::validateUsername($setUsername);
$emailValidation = User::validateEMailAddress($setEMailAddress);
$countryValidation = strlen($setCountry) === 2
&& ctype_alpha($setCountry)
&& ctype_upper($setCountry);
if(!empty($usernameValidation))
$notices[] = User::usernameValidationErrorString($usernameValidation);
@ -103,32 +101,37 @@ if(CSRF::validateRequest() && $canEdit) {
$notices[] = $emailValidation === 'in-use'
? 'This e-mail address has already been used!'
: 'This e-mail address is invalid!';
} else
$setUserInfo['email'] = mb_strtolower($setUserInfo['email']);
}
if(!$countryValidation)
$notices[] = 'Country code was invalid.';
if(strlen($setUserInfo['user_title']) < 1)
$setUserInfo['user_title'] = null;
elseif(strlen($setUserInfo['user_title']) > 64)
if(strlen($setTitle) > 64)
$notices[] = 'User title was invalid.';
if(empty($notices))
$userInfo->setUsername((string)($_POST['user']['username'] ?? ''))
->setEMailAddress((string)($_POST['user']['email'] ?? ''))
->setCountry((string)($_POST['user']['country'] ?? ''))
->setTitle((string)($_POST['user']['title'] ?? ''))
->setDisplayRole(UserRole::byId((int)($_POST['user']['display_role'] ?? 0)));
}
if(!empty($_POST['colour']) && is_array($_POST['colour'])) {
$setUserInfo['user_colour'] = null;
$setColour = null;
if(!empty($_POST['colour']['enable'])) {
$userColour = new Colour;
$setColour = new Colour;
try {
$userColour->setHex((string)($_POST['colour']['hex'] ?? ''));
$setColour->setHex((string)($_POST['colour']['hex'] ?? ''));
} catch(\Exception $ex) {
$notices[] = $ex->getMessage();
}
$setUserInfo['user_colour'] = $userColour->getRaw();
}
if(empty($notices))
$userInfo->setColour($setColour);
}
if(!empty($_POST['password']) && is_array($_POST['password'])) {
@ -141,27 +144,12 @@ if(CSRF::validateRequest() && $canEdit) {
elseif(!empty(User::validatePassword($passwordNewValue)))
$notices[] = 'New password is too weak.';
else
$setUserInfo['password'] = User::hashPassword($passwordNewValue);
$userInfo->setPassword($passwordNewValue);
}
}
if(empty($notices) && !empty($setUserInfo)) {
$userUpdate = DB::prepare(sprintf(
'
UPDATE `msz_users`
SET %s
WHERE `user_id` = :set_user_id
',
pdo_prepare_array_update($setUserInfo, true)
));
$userUpdate->bind('set_user_id', $userId);
foreach($setUserInfo as $key => $value)
$userUpdate->bind($key, $value);
if(!$userUpdate->execute())
$notices[] = 'Something went wrong while updating the user.';
}
if(empty($notices))
$userInfo->save();
if($canEditPerms && !empty($_POST['perms']) && is_array($_POST['perms'])) {
$perms = manage_perms_apply($permissions, $_POST['perms']);

View file

@ -21,7 +21,12 @@ $currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
if(!empty($_POST['lookup']) && is_string($_POST['lookup'])) {
url_redirect('manage-users-warnings', ['user' => user_id_from_username($_POST['lookup'])]);
try {
$userId = User::byUsername((string)filter_input(INPUT_POST, 'lookup', FILTER_SANITIZE_STRING))->getId();
} catch(UserNotFoundException $ex) {
$userId = 0;
}
url_redirect('manage-users-warnings', ['user' => $userId]);
return;
}

View file

@ -1,10 +1,17 @@
<?php
namespace Misuzu;
use InvalidArgumentException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\UserSession;
use Misuzu\Users\Assets\UserBackgroundAsset;
use Misuzu\Users\Assets\UserImageAssetException;
use Misuzu\Users\Assets\UserImageAssetInvalidImageException;
use Misuzu\Users\Assets\UserImageAssetInvalidTypeException;
use Misuzu\Users\Assets\UserImageAssetInvalidDimensionsException;
use Misuzu\Users\Assets\UserImageAssetFileTooLargeException;
require_once '../misuzu.php';
@ -53,20 +60,16 @@ if($isEditing) {
Template::set([
'perms' => $perms,
'guidelines' => [
'avatar' => $avatarProps = user_avatar_default_options(),
'background' => $backgroundProps = user_background_default_options(),
],
'background_attachments' => MSZ_USER_BACKGROUND_ATTACHMENTS_NAMES,
'background_attachments' => UserBackgroundAsset::getAttachmentStringOptions(),
]);
if(!empty($_POST) && is_array($_POST)) {
if(!CSRF::validateRequest()) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['csrf'];
$notices[] = 'Couldn\'t verify you, please refresh the page and retry.';
} else {
if(!empty($_POST['profile']) && is_array($_POST['profile'])) {
if(!$perms['edit_profile']) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['profile']['not-allowed'];
$notices[] = 'You\'re not allowed to edit your profile';
} else {
$profileFields = $profileUser->profileFields(false);
@ -74,7 +77,7 @@ if($isEditing) {
if(isset($_POST['profile'][$profileField->field_key])
&& $profileField->field_value !== $_POST['profile'][$profileField->field_key]
&& !$profileField->setFieldValue($_POST['profile'][$profileField->field_key])) {
$notices[] = sprintf(MSZ_TMP_USER_ERROR_STRINGS['profile']['invalid'], $profileField->field_title);
$notices[] = sprintf('%s was formatted incorrectly!', $profileField->field_title);
}
}
}
@ -82,38 +85,48 @@ if($isEditing) {
if(!empty($_POST['about']) && is_array($_POST['about'])) {
if(!$perms['edit_about']) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['about']['not-allowed'];
$notices[] = 'You\'re not allowed to edit your about page.';
} else {
$setAboutError = user_set_about_page(
$profileUser->getId(),
$_POST['about']['text'] ?? '',
(int)($_POST['about']['parser'] ?? Parser::PLAIN)
);
$aboutText = (string)($_POST['about']['text'] ?? '');
$aboutParse = (int)($_POST['about']['parser'] ?? Parser::PLAIN);
$aboutValid = User::validateProfileAbout($aboutParse, $aboutText, strlen($profileUser->getProfileAboutText()) > User::PROFILE_ABOUT_MAX_LENGTH);
if($setAboutError !== MSZ_E_USER_ABOUT_OK) {
$notices[] = sprintf(
MSZ_TMP_USER_ERROR_STRINGS['about'][$setAboutError] ?? MSZ_TMP_USER_ERROR_STRINGS['about']['_'],
MSZ_USER_ABOUT_MAX_LENGTH
);
if($aboutValid === '')
$currentUser->setProfileAboutText($aboutText)->setProfileAboutParser($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;
}
}
}
if(!empty($_POST['signature']) && is_array($_POST['signature'])) {
if(!$perms['edit_signature']) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['signature']['not-allowed'];
$notices[] = 'You\'re not allowed to edit your forum signature.';
} else {
$setSignatureError = user_set_signature(
$profileUser->getId(),
$_POST['signature']['text'] ?? '',
(int)($_POST['signature']['parser'] ?? Parser::PLAIN)
);
$sigText = (string)($_POST['signature']['text'] ?? '');
$sigParse = (int)($_POST['signature']['parser'] ?? Parser::PLAIN);
$sigValid = User::validateForumSignature($sigParse, $sigText);
if($setSignatureError !== MSZ_E_USER_SIGNATURE_OK) {
$notices[] = sprintf(
MSZ_TMP_USER_ERROR_STRINGS['signature'][$setSignatureError] ?? MSZ_TMP_USER_ERROR_STRINGS['signature']['_'],
MSZ_USER_SIGNATURE_MAX_LENGTH
);
if($sigValid === '')
$currentUser->setForumSignatureText($sigText)->setForumSignatureParser($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;
}
}
}
@ -122,68 +135,67 @@ if($isEditing) {
if(!$perms['edit_birthdate']) {
$notices[] = "You aren't allow to change your birthdate.";
} else {
$setBirthdate = user_set_birthdate(
$profileUser->getId(),
(int)($_POST['birthdate']['day'] ?? 0),
(int)($_POST['birthdate']['month'] ?? 0),
(int)($_POST['birthdate']['year'] ?? 0)
);
$birthYear = (int)($_POST['birthdate']['year'] ?? 0);
$birthMonth = (int)($_POST['birthdate']['month'] ?? 0);
$birthDay = (int)($_POST['birthdate']['day'] ?? 0);
$birthValid = User::validateBirthdate($birthYear, $birthMonth, $birthDay);
switch($setBirthdate) {
case MSZ_E_USER_BIRTHDATE_USER:
$notices[] = 'Invalid user specified while setting birthdate?';
break;
case MSZ_E_USER_BIRTHDATE_DATE:
$notices[] = 'The given birthdate is invalid.';
break;
case MSZ_E_USER_BIRTHDATE_FAIL:
$notices[] = 'Failed to set birthdate.';
break;
case MSZ_E_USER_BIRTHDATE_YEAR:
if($birthValid === '')
$currentUser->setBirthdate($birthYear, $birthMonth, $birthDay);
else switch($birthValid) {
case 'year':
$notices[] = 'The given birth year is invalid.';
break;
case MSZ_E_USER_BIRTHDATE_OK:
case 'date':
$notices[] = 'The given birthdate is invalid.';
break;
default:
$notices[] = 'Something unexpected happened while setting your birthdate.';
break;
}
}
}
if(!empty($_FILES['avatar'])) {
$avatarInfo = $profileUser->getAvatarInfo();
if(!empty($_POST['avatar']['delete'])) {
user_avatar_delete($profileUser->getId());
$avatarInfo->delete();
} else {
if(!$perms['edit_avatar']) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['avatar']['not-allowed'];
$notices[] = 'You aren\'t allow to change your avatar.';
} elseif(!empty($_FILES['avatar'])
&& is_array($_FILES['avatar'])
&& !empty($_FILES['avatar']['name']['file'])) {
if($_FILES['avatar']['error']['file'] !== UPLOAD_ERR_OK) {
$notices[] = sprintf(
MSZ_TMP_USER_ERROR_STRINGS['avatar']['upload'][$_FILES['avatar']['error']['file']]
?? MSZ_TMP_USER_ERROR_STRINGS['avatar']['upload']['_'],
$_FILES['avatar']['error']['file'],
byte_symbol($avatarProps['max_size'], true),
$avatarProps['max_width'],
$avatarProps['max_height']
);
switch($_FILES['avatar']['error']['file']) {
case UPLOAD_ERR_NO_FILE:
$notices[] = 'Select a file before hitting upload!';
break;
case UPLOAD_ERR_PARTIAL:
$notices[] = 'The upload was interrupted, please try again!';
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$notices[] = sprintf('Your avatar is not allowed to be larger in file size than %2$s!', byte_symbol($avatarInfo->getMaxBytes(), true));
break;
default:
$notices[] = 'Unable to save your avatar, contact an administator!';
break;
}
} else {
$setAvatar = user_avatar_set_from_path(
$profileUser->getId(),
$_FILES['avatar']['tmp_name']['file'],
$avatarProps
);
if($setAvatar !== MSZ_USER_AVATAR_NO_ERRORS) {
$notices[] = sprintf(
MSZ_TMP_USER_ERROR_STRINGS['avatar']['set'][$setAvatar]
?? MSZ_TMP_USER_ERROR_STRINGS['avatar']['set']['_'],
$setAvatar,
byte_symbol($avatarProps['max_size'], true),
$avatarProps['max_width'],
$avatarProps['max_height']
);
try {
$avatarInfo->setFromPath($_FILES['avatar']['tmp_name']['file']);
} catch(UserImageAssetInvalidImageException $ex) {
$notices[] = 'The file you uploaded was not an image!';
} catch(UserImageAssetInvalidTypeException $ex) {
$notices[] = 'This type of image is not supported, keep to PNG, JPG or GIF!';
} catch(UserImageAssetInvalidDimensionsException $ex) {
$notices[] = sprintf('Your avatar can\'t be larger than %dx%d!', $avatarInfo->getMaxWidth(), $avatarInfo->getMaxHeight());
} catch(UserImageAssetFileTooLargeException $ex) {
$notices[] = sprintf('Your avatar is not allowed to be larger in file size than %2$s!', byte_symbol($avatarInfo->getMaxBytes(), true));
} catch(UserImageAssetException $ex) {
$notices[] = 'Unable to save your avatar, contact an administator!';
}
}
}
@ -191,66 +203,63 @@ if($isEditing) {
}
if(!empty($_FILES['background'])) {
$backgroundInfo = $profileUser->getBackgroundInfo();
if((int)($_POST['background']['attach'] ?? -1) === 0) {
user_background_delete($profileUser->getId());
user_background_set_settings($profileUser->getId(), MSZ_USER_BACKGROUND_ATTACHMENT_NONE);
$backgroundInfo->delete();
} else {
if(!$perms['edit_background']) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['background']['not-allowed'];
} elseif(!empty($_FILES['background'])
&& is_array($_FILES['background'])) {
$notices[] = 'You aren\'t allow to change your background.';
} elseif(!empty($_FILES['background']) && is_array($_FILES['background'])) {
if(!empty($_FILES['background']['name']['file'])) {
if($_FILES['background']['error']['file'] !== UPLOAD_ERR_OK) {
$notices[] = sprintf(
MSZ_TMP_USER_ERROR_STRINGS['background']['upload'][$_FILES['background']['error']['file']]
?? MSZ_TMP_USER_ERROR_STRINGS['background']['upload']['_'],
$_FILES['background']['error']['file'],
byte_symbol($backgroundProps['max_size'], true),
$backgroundProps['max_width'],
$backgroundProps['max_height']
);
} else {
$setBackground = user_background_set_from_path(
$profileUser->getId(),
$_FILES['background']['tmp_name']['file'],
$backgroundProps
);
if($setBackground !== MSZ_USER_BACKGROUND_NO_ERRORS) {
$notices[] = sprintf(
MSZ_TMP_USER_ERROR_STRINGS['background']['set'][$setBackground]
?? MSZ_TMP_USER_ERROR_STRINGS['background']['set']['_'],
$setBackground,
byte_symbol($backgroundProps['max_size'], true),
$backgroundProps['max_width'],
$backgroundProps['max_height']
);
switch($_FILES['background']['error']['file']) {
case UPLOAD_ERR_NO_FILE:
$notices[] = 'Select a file before hitting upload!';
break;
case UPLOAD_ERR_PARTIAL:
$notices[] = 'The upload was interrupted, please try again!';
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$notices[] = sprintf('Your background is not allowed to be larger in file size than %s!', byte_symbol($backgroundProps['max_size'], true));
break;
default:
$notices[] = 'Unable to save your background, contact an administator!';
break;
}
} else {
try {
$backgroundInfo->setFromPath($_FILES['background']['tmp_name']['file']);
} catch(UserImageAssetInvalidImageException $ex) {
$notices[] = 'The file you uploaded was not an image!';
} catch(UserImageAssetInvalidTypeException $ex) {
$notices[] = 'This type of image is not supported, keep to PNG, JPG or GIF!';
} catch(UserImageAssetInvalidDimensionsException $ex) {
$notices[] = sprintf('Your background can\'t be larger than %dx%d!', $backgroundInfo->getMaxWidth(), $backgroundInfo->getMaxHeight());
} catch(UserImageAssetFileTooLargeException $ex) {
$notices[] = sprintf('Your background is not allowed to be larger in file size than %2$s!', byte_symbol($backgroundInfo->getMaxBytes(), true));
} catch(UserImageAssetException $ex) {
$notices[] = 'Unable to save your background, contact an administator!';
}
try {
$backgroundInfo->setAttachmentString($_POST['background']['attach'] ?? '')
->setBlend(!empty($_POST['background']['attr']['blend']))
->setSlide(!empty($_POST['background']['attr']['slide']));
} catch(InvalidArgumentException $ex) {}
}
}
$backgroundSettings = in_array($_POST['background']['attach'] ?? '', MSZ_USER_BACKGROUND_ATTACHMENTS)
? (int)($_POST['background']['attach'])
: MSZ_USER_BACKGROUND_ATTACHMENTS[0];
if(!empty($_POST['background']['attr']['blend'])) {
$backgroundSettings |= MSZ_USER_BACKGROUND_ATTRIBUTE_BLEND;
}
if(!empty($_POST['background']['attr']['slide'])) {
$backgroundSettings |= MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE;
}
user_background_set_settings($profileUser->getId(), $backgroundSettings);
}
}
}
$profileUser->saveProfile();
}
// Unset $isEditing and hope the user doesn't refresh their profile!
if(empty($notices)) {
if(empty($notices))
$isEditing = false;
}
}
}
@ -294,21 +303,6 @@ $profileStats = DB::prepare(sprintf('
WHERE `user_id` = :user_id
', \Misuzu\Users\UserRelation::TYPE_FOLLOW))->bind('user_id', $profileUser->getId())->fetch();
$backgroundPath = sprintf('%s/backgrounds/original/%d.msz', MSZ_STORAGE, $profileUser->getId());
if(is_file($backgroundPath)) {
$backgroundInfo = getimagesize($backgroundPath);
if($backgroundInfo) {
Template::set('site_background', [
'url' => url('user-background', ['user' => $profileUser->getId()]),
'width' => $backgroundInfo[0],
'height' => $backgroundInfo[1],
'settings' => $profileUser->getBackgroundSettings(),
]);
}
}
switch($profileMode) {
default:
echo render_error(404);

View file

@ -21,7 +21,6 @@ $errors = [];
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$isRestricted = $currentUser->hasActiveWarning();
$twoFactorInfo = user_totp_info($currentUserId);
$isVerifiedRequest = CSRF::validateRequest();
if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
@ -47,7 +46,7 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
}
}
if($isVerifiedRequest && isset($_POST['tfa']['enable']) && (bool)$twoFactorInfo['totp_enabled'] !== (bool)$_POST['tfa']['enable']) {
if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $currentUser->hasTOTP() !== (bool)$_POST['tfa']['enable']) {
if((bool)$_POST['tfa']['enable']) {
$tfaKey = TOTP::generateKey();
$tfaIssuer = Config::get('site.name', Config::TYPE_STR, 'Misuzu');
@ -55,7 +54,7 @@ if($isVerifiedRequest && isset($_POST['tfa']['enable']) && (bool)$twoFactorInfo[
'version' => 5,
'outputType' => QRCode::OUTPUT_IMAGE_JPG,
'eccLevel' => QRCode::ECC_L,
])))->render(sprintf('otpauth://totp/%s:%s?%s', $tfaIssuer, $twoFactorInfo['username'], http_build_query([
])))->render(sprintf('otpauth://totp/%s:%s?%s', $tfaIssuer, $currentUser->getUsername(), http_build_query([
'secret' => $tfaKey,
'issuer' => $tfaIssuer,
])));
@ -65,12 +64,10 @@ if($isVerifiedRequest && isset($_POST['tfa']['enable']) && (bool)$twoFactorInfo[
'settings_2fa_image' => $tfaQrcode,
]);
user_totp_update($currentUserId, $tfaKey);
$currentUser->setTOTPKey($tfaKey);
} else {
user_totp_update($currentUserId, null);
$currentUser->removeTOTPKey();
}
$twoFactorInfo['totp_enabled'] = !$twoFactorInfo['totp_enabled'];
}
if($isVerifiedRequest && !empty($_POST['current_password'])) {
@ -104,7 +101,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
$errors[] = 'Unknown e-mail validation error.';
}
} else {
user_email_set($currentUserId, $_POST['email']['new']);
$currentUser->setEMailAddress($_POST['email']['new']);
AuditLog::create(AuditLog::PERSONAL_EMAIL_CHANGE, [
$_POST['email']['new'],
]);
@ -130,9 +127,12 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
}
}
// THIS FUCKING SUCKS AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
$currentUser->save();
Template::render('settings.account', [
'errors' => $errors,
'settings_user' => $currentUser,
'is_restricted' => $isRestricted,
'settings_2fa_enabled' => $twoFactorInfo['totp_enabled'],
]);

View file

@ -1,97 +1,66 @@
<?php
namespace Misuzu;
use Misuzu\Imaging\Image;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\Assets\StaticUserImageAsset;
use Misuzu\Users\Assets\UserAssetScalableInterface;
$userAssetsMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$misuzuBypassLockdown = $userAssetsMode === 'avatar';
$assetMode = (string)filter_input(INPUT_GET, 'm', FILTER_SANITIZE_STRING);
$assetUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
$assetDims = filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT);
$misuzuBypassLockdown = $assetMode === 'avatar';
require_once '../misuzu.php';
try {
$userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
$userExists = true;
} catch(UserNotFoundException $ex) {
$userExists = false;
}
$userId = $userExists ? $userInfo->getId() : 0;
$assetUser = User::byId($assetUserId);
} catch(UserNotFoundException $ex) {}
$canViewImages = !$userExists
|| !$userInfo->isBanned()
|| (
parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_PATH) === url('user-profile')
&& perms_check_user(MSZ_PERMS_USER, User::hasCurrent() ? User::getCurrent()->getId() : 0, MSZ_PERM_USER_MANAGE_USERS)
);
$assetVisible = !isset($assetUser) || !$assetUser->isBanned() || (
parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_PATH) === url('user-profile')
&& User::hasCurrent() && perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS)
);
$isFound = true;
switch($userAssetsMode) {
switch($assetMode) {
case 'avatar':
$isFound = false;
if(!$canViewImages) {
$filename = Config::get('avatar.banned', Config::TYPE_STR, '/images/banned-avatar.png');
if(!$assetVisible) {
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/banned-avatar.png', MSZ_PUBLIC);
break;
}
$filename = Config::get('avatar.default', Config::TYPE_STR, '/images/no-avatar.png');
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
if(!$userExists)
if(!isset($assetUser) || !$assetUser->hasAvatar())
break;
$dimensions = MSZ_USER_AVATAR_RESOLUTION_DEFAULT;
if(isset($_GET['r']) && is_string($_GET['r']) && ctype_digit($_GET['r']))
$dimensions = user_avatar_resolution_closest((int)$_GET['r']);
$avatarFilename = sprintf('%d.msz', $userId);
$avatarOriginal = sprintf('%s/avatars/original/%s', MSZ_STORAGE, $avatarFilename);
if($dimensions === MSZ_USER_AVATAR_RESOLUTION_ORIGINAL) {
$filename = $avatarOriginal;
break;
}
$avatarStorage = sprintf('%1$s/avatars/%2$dx%2$d', MSZ_STORAGE, $dimensions);
$avatarCropped = sprintf('%s/%s', $avatarStorage, $avatarFilename);
$fileDisposition = sprintf('avatar-%d-%2$dx%2$d', $userId, $dimensions);
if(is_file($avatarCropped)) {
$isFound = true;
$filename = $avatarCropped;
} else {
if(is_file($avatarOriginal)) {
$isFound = true;
try {
mkdirs($avatarStorage, true);
$avatarImage = Image::create($avatarOriginal);
$avatarImage->squareCrop($dimensions);
$avatarImage->save($filename = $avatarCropped);
} catch(Exception $ex) {}
}
}
$assetInfo = $assetUser->getAvatarInfo();
break;
case 'background':
if(!$canViewImages && !$userExists)
if(!$assetVisible || !isset($assetUser) || !$assetUser->hasBackground())
break;
$backgroundStorage = sprintf('%s/backgrounds/original', MSZ_STORAGE);
$fileDisposition = sprintf('background-%d', $userId);
$filename = sprintf('%s/%d.msz', $backgroundStorage, $userId);
mkdirs($backgroundStorage, true);
$assetInfo = $assetUser->getBackgroundInfo();
break;
}
if($isFound && (empty($filename) || !is_file($filename))) {
if(!isset($assetInfo) || !$assetInfo->isPresent()) {
http_response_code(404);
return;
}
$contentType = mime_content_type($isFound ? $filename : (MSZ_ROOT . '/public' . $filename));
$contentType = $assetInfo->getMimeType();
$publicPath = $assetInfo->getPublicPath();
$fileName = $assetInfo->getFileName();
header(sprintf('X-Accel-Redirect: %s', str_replace(MSZ_STORAGE, '/msz-storage', $filename)));
if($assetDims > 0 && $assetInfo instanceof UserAssetScalableInterface) {
$assetInfo->ensureScaledExists($assetDims);
$publicPath = $assetInfo->getPublicScaledPath($assetDims);
$fileName = $assetInfo->getScaledFileName($assetDims);
}
header(sprintf('X-Accel-Redirect: %s', $publicPath));
header(sprintf('Content-Type: %s', $contentType));
if(isset($fileDisposition))
header(sprintf('Content-Disposition: inline; filename="%s.%s"', $fileDisposition, explode('/', $contentType)[1]));
header(sprintf('Content-Disposition: inline; filename="%s"', $fileName));

View file

@ -3,6 +3,7 @@ namespace Misuzu\Comments;
use Misuzu\DB;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class CommentsParser {
private const MARKUP_USERNAME = '#\B(?:@{1}(' . User::NAME_REGEX . '))#u';
@ -10,8 +11,11 @@ class CommentsParser {
public static function parseForStorage(string $text): string {
return preg_replace_callback(self::MARKUP_USERNAME, function ($matches) {
return ($userId = user_id_from_username($matches[1])) < 1
? $matches[0] : "@@{$userId}";
try {
return sprintf('@@%d', User::byUsername($matches[1])->getId());
} catch(UserNotFoundException $ex) {
return $matches[0];
}
}, $text);
}

View file

@ -47,7 +47,7 @@ MIG;
$fileName = date('Y_m_d_His_') . trim($name, '_') . '.php';
$filePath = MSZ_ROOT . '/database/' . $fileName;
$namespace = snake_to_camel($name);
$namespace = str_replace('_', '', ucwords($name, '_'));
file_put_contents($filePath, sprintf(self::TEMPLATE, $namespace));

View file

@ -3,5 +3,5 @@ namespace Misuzu;
interface HasRankInterface {
public function getRank(): int;
public function HasAuthorityOver(self $other): bool;
public function hasAuthorityOver(self $other): bool;
}

View file

@ -8,6 +8,7 @@ use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\News\NewsPost;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
final class HomeHandler extends Handler {
@ -59,19 +60,8 @@ final class HomeHandler extends Handler {
$changelog = ChangelogChange::all(new Pagination(10));
$birthdays = UserSession::hasCurrent() ? user_get_birthdays() : [];
$latestUser = DB::query('
SELECT
u.`user_id`, u.`username`, u.`user_created`,
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON r.`role_id` = u.`display_role`
WHERE `user_deleted` IS NULL
ORDER BY u.`user_id` DESC
LIMIT 1
')->fetch();
$birthdays = !UserSession::hasCurrent() ? [] : User::byBirthdate();
$latestUser = !empty($birthdays) ? null : User::byLatest();
$onlineUsers = DB::query('
SELECT

View file

@ -190,7 +190,9 @@ final class SockChatHandler extends Handler {
return;
foreach($bumpInfo as $bumpUser)
user_bump_last_active($bumpUser->id, $bumpUser->ip);
try {
User::byId($bumpUser->id)->bumpActivity($bumpUser->ip);
} catch(UserNotFoundException $ex) {}
}
public function verify(HttpResponse $response, HttpRequest $request): array {
@ -245,7 +247,6 @@ final class SockChatHandler extends Handler {
}
$sessionInfo->bump();
user_bump_last_active($userInfo->getId());
} else {
try {
$token = UserChatToken::byExact($userInfo, $authInfo->token);
@ -259,6 +260,8 @@ final class SockChatHandler extends Handler {
}
}
$userInfo->bumpActivity($authInfo->ip);
$perms = self::PERMS_DEFAULT;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_USERS))

View file

@ -14,7 +14,6 @@ final class TwigMisuzu extends Twig_Extension {
new Twig_Filter('byte_symbol', 'byte_symbol'),
new Twig_Filter('parse_text', fn(string $text, int $parser): string => Parser::instance($parser)->parseText($text)),
new Twig_Filter('perms_check', 'perms_check'),
new Twig_Filter('bg_settings', 'user_background_settings_strings'),
new Twig_Filter('clamp', 'clamp'),
];
}

View file

@ -0,0 +1,30 @@
<?php
namespace Misuzu\Users\Assets;
class StaticUserImageAsset implements UserImageAssetInterface {
private $path = '';
private $filename = '';
private $relativePath = '';
public function __construct(string $path, string $absolutePart = '') {
$this->path = $path;
$this->filename = basename($path);
$this->relativePath = substr($path, strlen($absolutePart));
}
public function isPresent(): bool {
return is_file($this->path);
}
public function getMimeType(): string {
return mime_content_type($this->path);
}
public function getPublicPath(): string {
return $this->relativePath;
}
public function getFileName(): string {
return $this->filename;
}
}

View file

@ -0,0 +1,6 @@
<?php
namespace Misuzu\Users\Assets;
use Misuzu\Users\UsersException;
class UserAssetException extends UsersException {}

View file

@ -0,0 +1,12 @@
<?php
namespace Misuzu\Users\Assets;
interface UserAssetScalableInterface {
public function getScaledRelativePath(int $dims): string;
public function getScaledPath(int $dims): string;
public function isScaledPresent(int $dims): bool;
public function deleteScaled(int $dims): void;
public function ensureScaledExists(int $dims): void;
public function getPublicScaledPath(int $dims): string;
public function getScaledFileName(int $dims): string;
}

View file

@ -0,0 +1,99 @@
<?php
namespace Misuzu\Users\Assets;
use Misuzu\Config;
use Misuzu\Imaging\Image;
use Misuzu\Users\User;
class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterface {
private const FORMAT = 'avatars/%s/%d.msz';
private const DIR_ORIG = 'original';
private const DIR_SIZE = '%1$dx%1$d';
public const DEFAULT_DIMENSION = 200;
public const DIMENSIONS = [
40, 60, 80, 100, 120, 200, 240,
];
private const MAX_RES = 2000;
private const MAX_BYTES = 1048576;
public function getMaxWidth(): int {
return Config::get('avatar.max_res', Config::TYPE_INT, self::MAX_RES);
}
public function getMaxHeight(): int {
return $this->getMaxWidth();
}
public function getMaxBytes(): int {
return Config::get('avatar.max_size', Config::TYPE_INT, self::MAX_BYTES);
}
public function getUrl(): string {
return url('user-avatar', ['user' => $this->getUser()->getId()]);
}
public function getScaledUrl(int $dims): string {
return url('user-avatar', ['user' => $this->getUser()->getId(), 'res' => $dims]);
}
public static function clampDimensions(int $dimensions): int {
$closest = null;
foreach(self::DIMENSIONS as $dims)
if($closest === null || abs($dimensions - $closest) >= abs($dims - $dimensions))
$closest = $dims;
return $closest;
}
public function getFileName(): string {
return sprintf('avatar-%1$d.%2$s', $this->getUser()->getId(), $this->getFileExtension());
}
public function getScaledFileName(int $dims): string {
return sprintf('avatar-%1$d-%3$dx%3$d.%2$s', $this->getUser()->getId(), $this->getFileExtension(), self::clampDimensions($dims));
}
public function getRelativePath(): string {
return sprintf(self::FORMAT, self::DIR_ORIG, $this->getUser()->getId());
}
public function getScaledRelativePath(int $dims): string {
$dims = self::clampDimensions($dims);
return sprintf(self::FORMAT, sprintf(self::DIR_SIZE, $dims), $this->getUser()->getId());
}
public function getScaledPath(int $dims): string {
return $this->getStoragePath() . DIRECTORY_SEPARATOR . $this->getScaledRelativePath($dims);
}
public function isScaledPresent(int $dims): bool {
return is_file($this->getScaledPath($dims));
}
public function deleteScaled(int $dims): void {
if($this->isScaledPresent($dims))
unlink($this->getScaledPath($dims));
}
public function ensureScaledExists(int $dims): void {
if(!$this->isPresent())
return;
$dims = self::clampDimensions($dims);
if($this->isScaledPresent($dims))
return;
$scaledPath = $this->getScaledPath($dims);
$scaledDir = dirname($scaledPath);
if(!is_dir($scaledDir))
mkdir($scaledDir, 0775, true);
$scale = Image::create($this->getPath());
$scale->squareCrop($dims);
$scale->save($scaledPath);
$scale->destroy();
}
public function getPublicScaledPath(int $dims): string {
return self::PUBLIC_STORAGE . '/' . $this->getScaledRelativePath($dims);
}
public function delete(): void {
parent::delete();
foreach(self::DIMENSIONS as $dims)
$this->deleteScaled($dims);
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace Misuzu\Users\Assets;
use InvalidArgumentException;
use Misuzu\Config;
use Misuzu\Users\User;
// attachment and attributes are to be stored in the same byte
// left half is for attributes, right half is for attachments
// this makes for 16 possible attachments and 4 possible attributes
// since attachments are just an incrementing number and attrs are flags
class UserBackgroundAsset extends UserImageAsset {
private const FORMAT = 'backgrounds/original/%d.msz';
private const MAX_WIDTH = 3840;
private const MAX_HEIGHT = 2160;
private const MAX_BYTES = 1048576;
public const ATTACH_NONE = 0x00;
public const ATTACH_COVER = 0x01;
public const ATTACH_STRETCH = 0x02;
public const ATTACH_TILE = 0x03;
public const ATTACH_CONTAIN = 0x04;
public const ATTRIB_BLEND = 0x10;
public const ATTRIB_SLIDE = 0x20;
public const ATTACHMENT_STRINGS = [
self::ATTACH_NONE => '',
self::ATTACH_COVER => 'cover',
self::ATTACH_STRETCH => 'stretch',
self::ATTACH_TILE => 'tile',
self::ATTACH_CONTAIN => 'contain',
];
public const ATTRIBUTE_STRINGS = [
self::ATTRIB_BLEND => 'blend',
self::ATTRIB_SLIDE => 'slide',
];
public static function getAttachmentStringOptions(): array {
return [
self::ATTACH_COVER => 'Cover',
self::ATTACH_STRETCH => 'Stretch',
self::ATTACH_TILE => 'Tile',
self::ATTACH_CONTAIN => 'Contain',
];
}
public function getMaxWidth(): int {
return Config::get('background.max_width', Config::TYPE_INT, self::MAX_WIDTH);
}
public function getMaxHeight(): int {
return Config::get('background.max_height', Config::TYPE_INT, self::MAX_HEIGHT);
}
public function getMaxBytes(): int {
return Config::get('background.max_size', Config::TYPE_INT, self::MAX_BYTES);
}
public function getUrl(): string {
return url('user-background', ['user' => $this->getUser()->getId()]);
}
public function getFileName(): string {
return sprintf('background-%1$d.%2$s', $this->getUser()->getId(), $this->getFileExtension());
}
public function getRelativePath(): string {
return sprintf(self::FORMAT, $this->getUser()->getId());
}
public function getAttachment(): int {
return $this->getUser()->getBackgroundSettings() & 0x0F;
}
public function getAttachmentString(): string {
return self::ATTACHMENT_STRINGS[$this->getAttachment()] ?? '';
}
public function setAttachment(int $attach): self {
$this->getUser()->setBackgroundSettings($this->getAttributes() | ($attach & 0x0F));
return $this;
}
public function setAttachmentString(string $attach): self {
if(!in_array($attach, self::ATTACHMENT_STRINGS))
throw new InvalidArgumentException;
$this->setAttachment(array_flip(self::ATTACHMENT_STRINGS)[$attach]);
return $this;
}
public function getAttributes(): int {
return $this->getUser()->getBackgroundSettings();
}
public function setAttributes(int $attrib): self {
$this->getUser()->setBackgroundSettings($this->getAttachment() | ($attrib & 0xF0));
return $this;
}
public function isBlend(): bool {
return $this->getAttributes() & self::ATTRIB_BLEND;
}
public function setBlend(bool $blend): self {
$this->getUser()->setBackgroundSettings(
$blend
? ($this->getAttributes() | self::ATTRIB_BLEND)
: ($this->getAttributes() & ~self::ATTRIB_BLEND)
);
return $this;
}
public function isSlide(): bool {
return $this->getAttributes() & self::ATTRIB_SLIDE;
}
public function setSlide(bool $slide): self {
$this->getUser()->setBackgroundSettings(
$slide
? ($this->getAttributes() | self::ATTRIB_SLIDE)
: ($this->getAttributes() & ~self::ATTRIB_SLIDE)
);
return $this;
}
public function getClassNames(string $format = '%s'): array {
$names = [];
$attachment = $this->getAttachment();
$attributes = $this->getAttributes();
if(array_key_exists($attachment, self::ATTACHMENT_STRINGS))
$names[] = sprintf($format, self::ATTACHMENT_STRINGS[$attachment]);
foreach(self::ATTRIBUTE_STRINGS as $flag => $name)
if(($attributes & $flag) > 0)
$names[] = sprintf($format, $name);
return $names;
}
public function delete(): void {
parent::delete();
$this->getUser()->setBackgroundSettings(0);
}
public function jsonSerialize() {
return array_merge(parent::jsonSerialize(), [
'attachment' => $this->getAttachmentString(),
'is_blend' => $this->isBlend(),
'is_slide' => $this->isSlide(),
]);
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Misuzu\Users\Assets;
use JsonSerializable;
use Misuzu\Config;
use Misuzu\Users\User;
class UserImageAssetFileCreationFailedException extends UserAssetException {}
class UserImageAssetFileNotFoundException extends UserAssetException {}
class UserImageAssetInvalidImageException extends UserAssetException {}
class UserImageAssetInvalidTypeException extends UserAssetException {}
class UserImageAssetInvalidDimensionsException extends UserAssetException {}
class UserImageAssetFileTooLargeException extends UserAssetException {}
class UserImageAssetMoveFailedException extends UserAssetException {}
abstract class UserImageAsset implements JsonSerializable, UserImageAssetInterface {
public const PUBLIC_STORAGE = '/msz-storage';
public const TYPE_PNG = IMAGETYPE_PNG;
public const TYPE_JPG = IMAGETYPE_JPEG;
public const TYPE_GIF = IMAGETYPE_GIF;
public const TYPES_EXT = [
self::TYPE_PNG => 'png',
self::TYPE_JPG => 'jpg',
self::TYPE_GIF => 'gif',
];
private $user;
public function __construct(User $user) {
$this->user = $user;
}
public function getUser(): User {
return $this->user;
}
public abstract function getMaxWidth(): int;
public abstract function getMaxHeight(): int;
public abstract function getMaxBytes(): int;
public function getAllowedTypes(): array {
return [self::TYPE_PNG, self::TYPE_JPG, self::TYPE_GIF];
}
public function isAllowedType(int $type): bool {
return in_array($type, $this->getAllowedTypes());
}
private function getImageSize(): array {
return $this->isPresent() && ($imageSize = getimagesize($this->getPath())) ? $imageSize : [];
}
public function getWidth(): int {
return $this->getImageSize()[0] ?? -1;
}
public function getHeight(): int {
return $this->getImageSize()[1] ?? -1;
}
public function getIntType(): int {
return $this->getImageSize()[2] ?? -1;
}
public function getMimeType(): string {
return mime_content_type($this->getPath());
}
public function getFileExtension(): string {
return self::TYPES_EXT[$this->getIntType()] ?? 'img';
}
public abstract function getFileName(): string;
public abstract function getRelativePath(): string;
public function isPresent(): bool {
return is_file($this->getPath());
}
public function getPublicPath(): string {
return self::PUBLIC_STORAGE . '/' . $this->getRelativePath();
}
public function delete(): void {
if($this->isPresent())
unlink($this->getPath());
}
public function getStoragePath(): string {
return Config::get('storage.path', Config::TYPE_STR, MSZ_ROOT . DIRECTORY_SEPARATOR . 'store');
}
public function getPath(): string {
return $this->getStoragePath() . DIRECTORY_SEPARATOR . $this->getRelativePath();
}
public function setFromPath(string $path): void {
if(!is_file($path))
throw new UserImageAssetFileNotFoundException;
$imageInfo = getimagesize($path);
if($imageInfo === false || count($imageInfo) < 3 || $imageInfo[0] < 1 || $imageInfo[1] < 1)
throw new UserImageAssetInvalidImageException;
if(!self::isAllowedType($imageInfo[2]))
throw new UserImageAssetInvalidTypeException;
if($imageInfo[0] > $this->getMaxWidth() || $imageInfo[1] > $this->getMaxHeight())
throw new UserImageAssetInvalidDimensionsException;
if(filesize($path) > $this->getMaxBytes())
throw new UserImageAssetFileTooLargeException;
$this->delete();
$targetPath = $this->getPath();
$targetDir = dirname($targetPath);
if(!is_dir($targetDir))
mkdir($targetDir, 0775, true);
if(is_uploaded_file($path) ? move_uploaded_file($path, $targetPath) : copy($path, $targetPath))
throw new UserImageAssetMoveFailedException;
}
public function setFromData(string $data): void {
$file = tempnam(sys_get_temp_dir(), 'msz');
if($file === null || !is_file($file))
throw new UserImageAssetFileCreationFailedException;
chmod($file, 0664);
file_put_contents($file, $data);
self::setFromPath($file);
unlink($file);
}
public function jsonSerialize() {
return [
'is_present' => $this->isPresent(),
'width' => $this->getWidth(),
'height' => $this->getHeight(),
'mime' => $this->getMimeType(),
];
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace Misuzu\Users\Assets;
interface UserImageAssetInterface {
public function isPresent(): bool;
public function getMimeType(): string;
public function getPublicPath(): string;
public function getFileName(): string;
}

View file

@ -1,6 +1,9 @@
<?php
namespace Misuzu\Users;
use DateTime;
use DateTimeZone;
use JsonSerializable;
use Misuzu\Colour;
use Misuzu\DB;
use Misuzu\HasRankInterface;
@ -8,12 +11,24 @@ use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\TOTP;
use Misuzu\Net\IPAddress;
use Misuzu\Parsers\Parser;
use Misuzu\Users\Assets\UserAvatarAsset;
use Misuzu\Users\Assets\UserBackgroundAsset;
class UserException extends UsersException {} // this naming definitely won't lead to confusion down the line!
class UserNotFoundException extends UserException {}
class UserCreationFailedException extends UserException {}
class User implements HasRankInterface {
// Quick note to myself and others about the `display_role` column in the users database and its corresponding methods in this class.
// Never ever EVER use it for ANYTHING other than determining display colours, there's a small chance that it might not be accurate.
// And even if it were, roles properties are aggregated and thus must all be accounted for.
// TODO
// - Search for comments starting with TODO
// - Move background settings and about shit to a separate users_profiles table (should birthdate be profile specific?)
// - Create a users_stats table containing static counts for things like followers, followings, topics, posts, etc.
class User implements HasRankInterface, JsonSerializable {
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
@ -24,6 +39,13 @@ class User implements HasRankInterface {
// Password hashing algorithm
public const PASSWORD_ALGO = PASSWORD_ARGON2ID;
// Maximum length of profile about section
public const PROFILE_ABOUT_MAX_LENGTH = 60000;
public const PROFILE_ABOUT_MAX_LENGTH_OLD = 65535; // Used for malloc's essay
// Maximum length of forum signature
public const FORUM_SIGNATURE_MAX_LENGTH = 2000;
// Order constants for ::all function
public const ORDER_ID = 'id';
public const ORDER_NAME = 'name';
@ -36,28 +58,27 @@ class User implements HasRankInterface {
public const ORDER_FOLLOWERS = 'followers';
// Database fields
// TODO: update all references to use getters and setters and mark all of these as private
public $user_id = -1;
public $username = '';
public $password = '';
public $email = '';
public $register_ip = '::1';
public $last_ip = '::1';
public $user_super = 0;
public $user_country = 'XX';
public $user_colour = null;
public $user_created = null;
public $user_active = null;
public $user_deleted = null;
public $display_role = 1;
public $user_totp_key = null;
public $user_about_content = null;
public $user_about_parser = 0;
public $user_signature_content = null;
public $user_signature_parser = 0;
public $user_birthdate = '';
public $user_background_settings = 0;
public $user_title = null;
private $user_id = -1;
private $username = '';
private $password = '';
private $email = '';
private $register_ip = '::1';
private $last_ip = '::1';
private $user_super = 0;
private $user_country = 'XX';
private $user_colour = null;
private $user_created = null;
private $user_active = null;
private $user_deleted = null;
private $display_role = 1;
private $user_totp_key = null;
private $user_about_content = null;
private $user_about_parser = 0;
private $user_signature_content = null;
private $user_signature_parser = 0;
private $user_birthdate = null;
private $user_background_settings = 0;
private $user_title = null;
private static $localUser = null;
@ -76,28 +97,6 @@ class User implements HasRankInterface {
. ', UNIX_TIMESTAMP(%1$s.`user_active`) AS `user_active`'
. ', UNIX_TIMESTAMP(%1$s.`user_deleted`) AS `user_deleted`';
// Stop using this one and use the one above
private const USER_SELECT = '
SELECT u.`user_id`, u.`username`, u.`password`, u.`email`, u.`user_super`, u.`user_title`,
u.`user_country`, u.`user_colour`, u.`display_role`, u.`user_totp_key`,
u.`user_about_content`, u.`user_about_parser`,
u.`user_signature_content`, u.`user_signature_parser`,
u.`user_birthdate`, u.`user_background_settings`,
INET6_NTOA(u.`register_ip`) AS `register_ip`, INET6_NTOA(u.`last_ip`) AS `last_ip`,
UNIX_TIMESTAMP(u.`user_created`) AS `user_created`, UNIX_TIMESTAMP(u.`user_active`) AS `user_active`,
UNIX_TIMESTAMP(u.`user_deleted`) AS `user_deleted`,
COALESCE(u.`user_title`, r.`role_title`) AS `user_title`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`,
TIMESTAMPDIFF(YEAR, IF(u.`user_birthdate` < \'0001-01-01\', NULL, u.`user_birthdate`), NOW()) AS `user_age`
FROM `msz_users` AS u
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
';
public function __construct() {
//
}
public function getId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
@ -105,43 +104,76 @@ class User implements HasRankInterface {
public function getUsername(): string {
return $this->username;
}
public function setUsername(string $username): self {
$this->username = $username;
return $this;
}
public function getEmailAddress(): string {
return $this->email;
}
public function getRegisterRemoteAddress(): string {
return $this->register_ip;
public function setEmailAddress(string $address): self {
$this->email = mb_strtolower($address);
return $this;
}
public function getRegisterRemoteAddress(): string {
return $this->register_ip ?? '::1';
}
public function getLastRemoteAddress(): string {
return $this->last_ip;
return $this->last_ip ?? '::1';
}
public function isSuper(): bool {
return boolval($this->user_super);
}
public function setSuper(bool $super): self {
$this->user_super = $super ? 1 : 0;
return $this;
}
public function hasCountry(): bool {
return $this->user_country !== 'XX';
}
public function getCountry(): string {
return $this->user_country;
return $this->user_country ?? 'XX';
}
public function setCountry(string $country): self {
$this->user_country = strtoupper(substr($country, 0, 2));
return $this;
}
public function getCountryName(): string {
return get_country_name($this->getCountry());
}
private $userColour = null;
private $realColour = null;
public function getColour(): Colour { // Swaps role colour in if user has no personal colour
// TODO: Check inherit flag and grab role colour instead
return new Colour($this->getColourRaw());
if($this->realColour === null) {
$this->realColour = $this->getUserColour();
if($this->realColour->getInherit())
$this->realColour = $this->getDisplayRole()->getColour();
}
return $this->realColour;
}
public function setColour(?Colour $colour): self {
return $this->setColourRaw($colour === null ? null : $colour->getRaw());
}
public function getUserColour(): Colour { // Only ever gets the user's actual colour
return new Colour($this->getColourRaw());
if($this->userColour === null)
$this->userColour = new Colour($this->getColourRaw());
return $this->userColour;
}
public function getColourRaw(): int {
return $this->user_colour ?? 0x40000000;
}
public function setColourRaw(?int $colour): self {
$this->user_colour = $colour;
$this->userColour = null;
$this->realColour = null;
return $this;
}
public function getCreatedTime(): int {
return $this->user_created === null ? -1 : $this->user_created;
@ -173,39 +205,11 @@ class User implements HasRankInterface {
return $this->getRank() > $other->getRank();
}
public function hasPassword(): bool {
return !empty($this->password);
}
public function checkPassword(string $password): bool {
return $this->hasPassword() && password_verify($password, $this->password);
}
public function passwordNeedsRehash(): bool {
return password_needs_rehash($this->password, self::PASSWORD_ALGO);
}
public function setPassword(string $password): void {
DB::prepare('UPDATE `msz_users` SET `password` = :password WHERE `user_id` = :user_id')
->bind('password', $this->password = self::hashPassword($password))
->bind('user_id', $this->getId())
->execute();
}
public function isDeleted(): bool {
return !empty($this->user_deleted);
}
public function getDisplayRoleId(): int {
return $this->display_role < 1 ? -1 : $this->display_role;
}
public function setDisplayRoleId(int $roleId): self {
$this->display_role = $roleId < 1 ? -1 : $roleId;
// TEMPORARY!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// This DB update statement should be removed when a global update/save/whatever call exists
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `display_role` = :role WHERE `user_id` = :user')
->bind('role', $this->display_role)
->bind('user', $this->user_id)
->execute();
return $this;
}
public function getDisplayRole(): UserRole {
@ -228,6 +232,19 @@ class User implements HasRankInterface {
$this->totp = new TOTP($this->user_totp_key);
return $this->totp;
}
public function getTOTPKey(): string {
return $this->user_totp_key ?? '';
}
public function setTOTPKey(string $key): self {
$this->totp = null;
$this->user_totp_key = $key;
return $this;
}
public function removeTOTPKey(): self {
$this->totp = null;
$this->user_totp_key = null;
return $this;
}
public function getValidTOTPTokens(): array {
if(!$this->hasTOTP())
return [];
@ -239,24 +256,91 @@ class User implements HasRankInterface {
];
}
public function getBackgroundSettings(): int { // Use the below methods instead
public function hasProfileAbout(): bool {
return !empty($this->user_about_content);
}
public function getProfileAboutText(): string {
return $this->user_about_content ?? '';
}
public function setProfileAboutText(string $text): self {
$this->user_about_content = empty($text) ? null : $text;
return $this;
}
public function getProfileAboutParser(): int {
return $this->hasProfileAbout() ? $this->user_about_parser : Parser::BBCODE;
}
public function setProfileAboutParser(int $parser): self {
$this->user_about_parser = $parser;
return $this;
}
public function getProfileAboutParsed(): string {
if(!$this->hasProfileAbout())
return '';
return Parser::instance($this->getProfileAboutParser())
->parseText(htmlspecialchars($this->getProfileAboutText()));
}
public function hasForumSignature(): bool {
return !empty($this->user_signature_content);
}
public function getForumSignatureText(): string {
return $this->user_signature_content ?? '';
}
public function setForumSignatureText(string $text): self {
$this->user_signature_content = empty($text) ? null : $text;
return $this;
}
public function getForumSignatureParser(): int {
return $this->hasForumSignature() ? $this->user_signature_parser : Parser::BBCODE;
}
public function setForumSignatureParser(int $parser): self {
$this->user_signature_parser = $parser;
return $this;
}
public function getForumSignatureParsed(): string {
if(!$this->hasForumSignature())
return '';
return Parser::instance($this->getForumSignatureParser())
->parseText(htmlspecialchars($this->getForumSignatureText()));
}
// Address these through getBackgroundInfo()
public function getBackgroundSettings(): int {
return $this->user_background_settings;
}
public function getBackgroundAttachment(): int {
return $this->user_background_settings & 0x0F;
}
public function getBackgroundBlend(): bool {
return ($this->user_background_settings & MSZ_USER_BACKGROUND_ATTRIBUTE_BLEND) > 0;
}
public function getBackgroundSlide(): bool {
return ($this->user_background_settings & MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE) > 0;
public function setBackgroundSettings(int $settings): self {
$this->user_background_settings = $settings;
return $this;
}
public function hasTitle(): bool {
return !empty($this->user_title);
}
public function getTitle(): string {
return $this->user_title;
return $this->user_title ?? '';
}
public function setTitle(string $title): self {
$this->user_title = empty($title) ? null : $title;
return $this;
}
public function hasBirthdate(): bool {
return $this->user_birthdate !== null;
}
public function getBirthdate(): DateTime {
return new DateTime($this->user_birthdate ?? '0000-01-01', new DateTimeZone('UTC'));
}
public function setBirthdate(int $year, int $month, int $day): self {
$this->user_birthdate = $month < 1 || $day < 1 ? null : sprintf('%04d-%02d-%02d', $year, $month, $day);
return $this;
}
public function hasAge(): bool {
return $this->hasBirthdate() && intval($this->getBirthdate()->format('Y')) > 1900;
}
public function getAge(): int {
if(!$this->hasAge())
return -1;
return intval($this->getBirthdate()->diff(new DateTime('now', new DateTimeZone('UTC')))->format('%y'));
}
public function profileFields(bool $filterEmpty = true): array {
@ -265,6 +349,20 @@ class User implements HasRankInterface {
return ProfileField::user($userId, $filterEmpty);
}
public function bumpActivity(?string $lastRemoteAddress = null): void {
$this->user_active = time();
$this->last_ip = $lastRemoteAddress ?? IPAddress::remote();
DB::prepare(
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
. ' SET `user_active` = FROM_UNIXTIME(:active), `last_ip` = INET6_ATON(:address)'
. ' WHERE `user_id` = :user'
) ->bind('user', $this->user_id)
->bind('active', $this->user_active)
->bind('address', $this->last_ip)
->execute();
}
// TODO: Is this the proper location/implementation for this? (no)
private $commentPermsArray = null;
public function commentPerms(): array {
@ -280,6 +378,118 @@ class User implements HasRankInterface {
return $this->commentPermsArray;
}
/********
* JSON *
********/
public function jsonSerialize() {
return [
'id' => $this->getId(),
'username' => $this->getUsername(),
'country' => $this->getCountry(),
'is_super' => $this->isSuper(),
'rank' => $this->getRank(),
'display_role' => $this->getDisplayRoleId(),
'title' => $this->getTitle(),
'created' => date('c', $this->getCreatedTime()),
'last_active' => ($date = $this->getActiveTime()) < 0 ? null : date('c', $date),
'avatar' => $this->getAvatarInfo(),
'background' => $this->getBackgroundInfo(),
];
}
/************
* PASSWORD *
************/
public static function hashPassword(string $password): string {
return password_hash($password, self::PASSWORD_ALGO);
}
public function hasPassword(): bool {
return !empty($this->password);
}
public function checkPassword(string $password): bool {
return $this->hasPassword() && password_verify($password, $this->password);
}
public function passwordNeedsRehash(): bool {
return password_needs_rehash($this->password, self::PASSWORD_ALGO);
}
public function removePassword(): self {
$this->password = null;
return $this;
}
public function setPassword(string $password): self {
$this->password = self::hashPassword($password);
DB::prepare('UPDATE `msz_users` SET `password` = :password WHERE `user_id` = :user_id')
->bind('password', $this->password)
->bind('user_id', $this->getId())
->execute();
return $this;
}
/************
* DELETING *
************/
private const NUKE_TIMEOUT = 600;
public function getDeletedTime(): int {
return $this->user_deleted === null ? -1 : $this->user_deleted;
}
public function isDeleted(): bool {
return $this->getDeletedTime() >= 0;
}
public function delete(): void {
if($this->isDeleted())
return;
$this->user_deleted = time();
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `user_deleted` = NOW() WHERE `user_id` = :user')
->bind('user', $this->user_id)
->execute();
}
public function restore(): void {
if(!$this->isDeleted())
return;
$this->user_deleted = null;
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `user_deleted` = NULL WHERE `user_id` = :user')
->bind('user', $this->user_id)
->execute();
}
public function canBeNuked(): bool {
return $this->isDeleted() && time() > $this->getDeletedTime() + self::NUKE_TIMEOUT;
}
public function nuke(): void {
if(!$this->canBeNuked())
return;
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `user_id` = :user')
->bind('user', $this->user_id)
->execute();
}
/**********
* ASSETS *
**********/
private $avatarAsset = null;
public function getAvatarInfo(): UserAvatarAsset {
if($this->avatarAsset === null)
$this->avatarAsset = new UserAvatarAsset($this);
return $this->avatarAsset;
}
public function hasAvatar(): bool {
return $this->getAvatarInfo()->isPresent();
}
private $backgroundAsset = null;
public function getBackgroundInfo(): UserBackgroundAsset {
if($this->backgroundAsset === null)
$this->backgroundAsset = new UserBackgroundAsset($this);
return $this->backgroundAsset;
}
public function hasBackground(): bool {
return $this->getBackgroundInfo()->isPresent();
}
/*********
* ROLES *
*********/
@ -408,6 +618,10 @@ class User implements HasRankInterface {
return $this->forumPostCount;
}
/************
* WARNINGS *
************/
private $activeWarning = -1;
public function getActiveWarning(): ?UserWarning {
@ -524,8 +738,80 @@ class User implements HasRankInterface {
return '';
}
public static function hashPassword(string $password): string {
return password_hash($password, self::PASSWORD_ALGO);
public static function validateBirthdate(int $year, int $month, int $day, int $yearRange = 100): string {
if($year > 0) {
if($year < date('Y') - $yearRange || $year > date('Y'))
return 'year';
$checkYear = $year;
} else $checkYear = date('Y');
if(!($day === 0 && $month === 0) && !checkdate($month, $day, $checkYear))
return 'date';
return '';
}
public static function validateProfileAbout(int $parser, string $text, bool $useOld = false): string {
if(!Parser::isValid($parser))
return 'parser';
$length = strlen($text);
if($length > ($useOld ? self::PROFILE_ABOUT_MAX_LENGTH_OLD : 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 '';
}
/*********************
* CREATION + SAVING *
*********************/
public function save(): void {
$save = DB::prepare(
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
. ' SET `username` = :username, `email` = :email, `password` = :password'
. ', `user_super` = :is_super, `user_country` = :country, `user_colour` = :colour, `user_title` = :title'
. ', `display_role` = :display_role, `user_birthdate` = :birthdate, `user_totp_key` = :totp'
. ' WHERE `user_id` = :user'
) ->bind('user', $this->user_id)
->bind('username', $this->username)
->bind('email', $this->email)
->bind('password', $this->password)
->bind('is_super', $this->user_super)
->bind('country', $this->user_country)
->bind('colour', $this->user_colour)
->bind('display_role', $this->display_role)
->bind('birthdate', $this->user_birthdate)
->bind('totp', $this->user_totp_key)
->bind('title', $this->user_title)
->execute();
}
public function saveProfile(): void {
$save = DB::prepare(
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
. ' SET `user_about_content` = :about_content, `user_about_parser` = :about_parser'
. ', `user_signature_content` = :signature_content, `user_signature_parser` = :signature_parser'
. ', `user_background_settings` = :background_settings'
. ' WHERE `user_id` = :user'
) ->bind('user', $this->user_id)
->bind('about_content', $this->user_about_content)
->bind('about_parser', $this->user_about_parser)
->bind('signature_content', $this->user_signature_content)
->bind('signature_parser', $this->user_signature_parser)
->bind('background_settings', $this->user_background_settings)
->execute();
}
public static function create(
@ -551,16 +837,23 @@ class User implements HasRankInterface {
return self::byId($createUser);
}
private static function getMemoizer() {
/************
* FETCHING *
************/
private static function memoizer() {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $userId): ?self {
return self::getMemoizer()->find($userId, function() use ($userId) {
$user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id')
return self::memoizer()->find($userId, function() use ($userId) {
$user = DB::prepare(self::byQueryBase() . ' WHERE `user_id` = :user_id')
->bind('user_id', $userId)
->fetchObject(self::class);
if(!$user)
@ -568,12 +861,25 @@ class User implements HasRankInterface {
return $user;
});
}
public static function byUsername(string $username): ?self {
$username = mb_strtolower($username);
return self::memoizer()->find(function($user) use ($username) {
return mb_strtolower($user->getUsername()) === $username;
}, function() use ($username) {
$user = DB::prepare(self::byQueryBase() . ' WHERE LOWER(`username`) = :username')
->bind('username', $username)
->fetchObject(self::class);
if(!$user)
throw new UserNotFoundException;
return $user;
});
}
public static function byEMailAddress(string $address): ?self {
$address = mb_strtolower($address);
return self::getMemoizer()->find(function($user) use ($address) {
return $user->getEmailAddress() === $address;
return self::memoizer()->find(function($user) use ($address) {
return mb_strtolower($user->getEmailAddress()) === $address;
}, function() use ($address) {
$user = DB::prepare(self::USER_SELECT . 'WHERE LOWER(`email`) = :email')
$user = DB::prepare(self::byQueryBase() . ' WHERE LOWER(`email`) = :email')
->bind('email', $address)
->fetchObject(self::class);
if(!$user)
@ -581,27 +887,31 @@ class User implements HasRankInterface {
return $user;
});
}
public static function findForLogin(string $usernameOrEmail): ?self {
$usernameOrEmailLower = mb_strtolower($usernameOrEmail);
return self::getMemoizer()->find(function($user) use ($usernameOrEmailLower) {
return mb_strtolower($user->getUsername()) === $usernameOrEmailLower
|| mb_strtolower($user->getEmailAddress()) === $usernameOrEmailLower;
}, function() use ($usernameOrEmail) {
$user = DB::prepare(self::USER_SELECT . 'WHERE LOWER(`email`) = LOWER(:email) OR LOWER(`username`) = LOWER(:username)')
->bind('email', $usernameOrEmail)
->bind('username', $usernameOrEmail)
public static function byUsernameOrEMailAddress(string $usernameOrAddress): self {
$usernameOrAddressLower = mb_strtolower($usernameOrAddress);
return self::memoizer()->find(function($user) use ($usernameOrAddressLower) {
return mb_strtolower($user->getUsername()) === $usernameOrAddressLower
|| mb_strtolower($user->getEmailAddress()) === $usernameOrAddressLower;
}, function() use ($usernameOrAddressLower) {
$user = DB::prepare(self::byQueryBase() . ' WHERE LOWER(`email`) = :email OR LOWER(`username`) = :username')
->bind('email', $usernameOrAddressLower)
->bind('username', $usernameOrAddressLower)
->fetchObject(self::class);
if(!$user)
throw new UserNotFoundException;
return $user;
});
}
public static function byLatest(): ?self {
return DB::prepare(self::byQueryBase() . ' WHERE `user_deleted` IS NULL ORDER BY `user_id` DESC LIMIT 1')
->fetchObject(self::class);
}
public static function findForProfile($userIdOrName): ?self {
$userIdOrNameLower = mb_strtolower($userIdOrName);
return self::getMemoizer()->find(function($user) use ($userIdOrNameLower) {
return self::memoizer()->find(function($user) use ($userIdOrNameLower) {
return $user->getId() == $userIdOrNameLower || mb_strtolower($user->getUsername()) === $userIdOrNameLower;
}, function() use ($userIdOrName) {
$user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')
$user = DB::prepare(self::byQueryBase() . ' WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')
->bind('user_id', (int)$userIdOrName)
->bind('username', (string)$userIdOrName)
->fetchObject(self::class);
@ -610,4 +920,10 @@ class User implements HasRankInterface {
return $user;
});
}
public static function byBirthdate(?DateTime $date = null): array {
$date = $date === null ? new DateTime('now', new DateTimeZone('UTC')) : (clone $date)->setTimezone(new DateTimeZone('UTC'));
return DB::prepare(self::byQueryBase() . ' WHERE `user_deleted` IS NULL AND `user_birthdate` LIKE :date')
->bind('date', $date->format('%-m-d'))
->fetchObjects(self::class);
}
}

View file

@ -1,126 +0,0 @@
<?php
define('MSZ_USER_AVATAR_FORMAT', '%d.msz');
define('MSZ_USER_AVATAR_RESOLUTION_DEFAULT', 200);
define('MSZ_USER_AVATAR_RESOLUTION_ORIGINAL', 0);
define('MSZ_USER_AVATAR_RESOLUTIONS', [
MSZ_USER_AVATAR_RESOLUTION_ORIGINAL,
40, 60, 80, 100, 120, 200, 240,
]);
function user_avatar_valid_resolution(int $resolution): bool {
return in_array($resolution, MSZ_USER_AVATAR_RESOLUTIONS, true);
}
function user_avatar_resolution_closest(int $resolution): int {
if($resolution === 0)
return MSZ_USER_AVATAR_RESOLUTION_ORIGINAL;
$closest = null;
foreach(MSZ_USER_AVATAR_RESOLUTIONS as $res) {
if($res === MSZ_USER_AVATAR_RESOLUTION_ORIGINAL)
continue;
if($closest === null || abs($resolution - $closest) >= abs($res - $resolution))
$closest = $res;
}
return $closest;
}
function user_avatar_delete(int $userId): void {
$avatarFileName = sprintf(MSZ_USER_AVATAR_FORMAT, $userId);
$avatarPathFormat = MSZ_STORAGE . '/avatars/%s/%s';
foreach(MSZ_USER_AVATAR_RESOLUTIONS as $res) {
safe_delete(sprintf(
$avatarPathFormat,
$res === MSZ_USER_AVATAR_RESOLUTION_ORIGINAL
? 'original'
: sprintf('%1$dx%1$d', $res),
$avatarFileName
));
}
}
define('MSZ_USER_AVATAR_TYPE_PNG', IMAGETYPE_PNG);
define('MSZ_USER_AVATAR_TYPE_JPG', IMAGETYPE_JPEG);
define('MSZ_USER_AVATAR_TYPE_GIF', IMAGETYPE_GIF);
define('MSZ_USER_AVATAR_TYPES', [
MSZ_USER_AVATAR_TYPE_PNG,
MSZ_USER_AVATAR_TYPE_JPG,
MSZ_USER_AVATAR_TYPE_GIF,
]);
function user_avatar_is_allowed_type(int $type): bool {
return in_array($type, MSZ_USER_AVATAR_TYPES, true);
}
function user_avatar_default_options(): array {
return [
'max_width' => \Misuzu\Config::get('avatar.max_width', \Misuzu\Config::TYPE_INT, 2000),
'max_height' => \Misuzu\Config::get('avatar.max_height', \Misuzu\Config::TYPE_INT, 2000),
'max_size' => \Misuzu\Config::get('avatar.max_height', \Misuzu\Config::TYPE_INT, 500000),
];
}
define('MSZ_USER_AVATAR_NO_ERRORS', 0);
define('MSZ_USER_AVATAR_ERROR_INVALID_IMAGE', 1);
define('MSZ_USER_AVATAR_ERROR_PROHIBITED_TYPE', 2);
define('MSZ_USER_AVATAR_ERROR_DIMENSIONS_TOO_LARGE', 3);
define('MSZ_USER_AVATAR_ERROR_DATA_TOO_LARGE', 4);
define('MSZ_USER_AVATAR_ERROR_TMP_FAILED', 5);
define('MSZ_USER_AVATAR_ERROR_STORE_FAILED', 6);
define('MSZ_USER_AVATAR_ERROR_FILE_NOT_FOUND', 7);
function user_avatar_set_from_path(int $userId, string $path, array $options = []): int {
if(!file_exists($path))
return MSZ_USER_AVATAR_ERROR_FILE_NOT_FOUND;
$options = array_merge(user_avatar_default_options(), $options);
// 0 => width, 1 => height, 2 => type
$imageInfo = getimagesize($path);
if($imageInfo === false
|| count($imageInfo) < 3
|| $imageInfo[0] < 1
|| $imageInfo[1] < 1)
return MSZ_USER_AVATAR_ERROR_INVALID_IMAGE;
if(!user_avatar_is_allowed_type($imageInfo[2]))
return MSZ_USER_AVATAR_ERROR_PROHIBITED_TYPE;
if($imageInfo[0] > $options['max_width']
|| $imageInfo[1] > $options['max_height'])
return MSZ_USER_AVATAR_ERROR_DIMENSIONS_TOO_LARGE;
if(filesize($path) > $options['max_size'])
return MSZ_USER_AVATAR_ERROR_DATA_TOO_LARGE;
user_avatar_delete($userId);
$fileName = sprintf(MSZ_USER_AVATAR_FORMAT, $userId);
$storageDir = MSZ_STORAGE . '/avatars/original';
mkdirs($storageDir, true);
$avatarPath = "{$storageDir}/{$fileName}";
if(!copy($path, $avatarPath))
return MSZ_USER_AVATAR_ERROR_STORE_FAILED;
return MSZ_USER_AVATAR_NO_ERRORS;
}
function user_avatar_set_from_data(int $userId, string $data, array $options = []): int {
$tmp = tempnam(sys_get_temp_dir(), 'msz');
if($tmp === false || !file_exists($tmp))
return MSZ_USER_AVATAR_ERROR_TMP_FAILED;
chmod($tmp, 644);
file_put_contents($tmp, $data);
$result = user_avatar_set_from_path($userId, $tmp, $options);
safe_delete($tmp);
return $result;
}

View file

@ -1,157 +0,0 @@
<?php
define('MSZ_USER_BACKGROUND_FORMAT', '%d.msz');
// attachment and attributes are to be stored in the same byte
// left half is for attributes, right half is for attachments
// this makes for 16 possible attachments and 4 possible attributes
// since attachments are just an incrementing number and attrs are flags
define('MSZ_USER_BACKGROUND_ATTACHMENT_NONE', 0);
define('MSZ_USER_BACKGROUND_ATTACHMENT_COVER', 1);
define('MSZ_USER_BACKGROUND_ATTACHMENT_STRETCH', 2);
define('MSZ_USER_BACKGROUND_ATTACHMENT_TILE', 3);
define('MSZ_USER_BACKGROUND_ATTACHMENT_CONTAIN', 4);
define('MSZ_USER_BACKGROUND_ATTACHMENTS', [
MSZ_USER_BACKGROUND_ATTACHMENT_NONE,
MSZ_USER_BACKGROUND_ATTACHMENT_COVER,
MSZ_USER_BACKGROUND_ATTACHMENT_STRETCH,
MSZ_USER_BACKGROUND_ATTACHMENT_TILE,
MSZ_USER_BACKGROUND_ATTACHMENT_CONTAIN,
]);
define('MSZ_USER_BACKGROUND_ATTACHMENTS_NAMES', [
MSZ_USER_BACKGROUND_ATTACHMENT_COVER => 'cover',
MSZ_USER_BACKGROUND_ATTACHMENT_STRETCH => 'stretch',
MSZ_USER_BACKGROUND_ATTACHMENT_TILE => 'tile',
MSZ_USER_BACKGROUND_ATTACHMENT_CONTAIN => 'contain',
]);
define('MSZ_USER_BACKGROUND_ATTRIBUTE_BLEND', 0x10);
define('MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE', 0x20);
define('MSZ_USER_BACKGROUND_ATTRIBUTES', [
MSZ_USER_BACKGROUND_ATTRIBUTE_BLEND,
MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE,
]);
define('MSZ_USER_BACKGROUND_ATTRIBUTES_NAMES', [
MSZ_USER_BACKGROUND_ATTRIBUTE_BLEND => 'blend',
MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE => 'slide',
]);
function user_background_settings_strings(int $settings, string $format = '%s'): array {
$arr = [];
$attachment = $settings & 0x0F;
if(array_key_exists($attachment, MSZ_USER_BACKGROUND_ATTACHMENTS_NAMES))
$arr[] = sprintf($format, MSZ_USER_BACKGROUND_ATTACHMENTS_NAMES[$attachment]);
foreach(MSZ_USER_BACKGROUND_ATTRIBUTES_NAMES as $flag => $name)
if(($settings & $flag) > 0)
$arr[] = sprintf($format, $name);
return $arr;
}
function user_background_set_settings(int $userId, int $settings): void {
if($userId < 1)
return;
$setAttrs = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `user_background_settings` = :settings
WHERE `user_id` = :user
');
$setAttrs->bind('settings', $settings & 0xFF);
$setAttrs->bind('user', $userId);
$setAttrs->execute();
}
function user_background_delete(int $userId): void {
$backgroundFileName = sprintf(MSZ_USER_BACKGROUND_FORMAT, $userId);
safe_delete(MSZ_STORAGE . '/backgrounds/original/' . $backgroundFileName);
}
define('MSZ_USER_BACKGROUND_TYPE_PNG', IMAGETYPE_PNG);
define('MSZ_USER_BACKGROUND_TYPE_JPG', IMAGETYPE_JPEG);
define('MSZ_USER_BACKGROUND_TYPE_GIF', IMAGETYPE_GIF);
define('MSZ_USER_BACKGROUND_TYPES', [
MSZ_USER_BACKGROUND_TYPE_PNG,
MSZ_USER_BACKGROUND_TYPE_JPG,
MSZ_USER_BACKGROUND_TYPE_GIF,
]);
function user_background_is_allowed_type(int $type): bool {
return in_array($type, MSZ_USER_BACKGROUND_TYPES, true);
}
function user_background_default_options(): array {
return [
'max_width' => \Misuzu\Config::get('background.max_width', \Misuzu\Config::TYPE_INT, 3840),
'max_height' => \Misuzu\Config::get('background.max_height', \Misuzu\Config::TYPE_INT, 2160),
'max_size' => \Misuzu\Config::get('background.max_height', \Misuzu\Config::TYPE_INT, 1000000),
];
}
define('MSZ_USER_BACKGROUND_NO_ERRORS', 0);
define('MSZ_USER_BACKGROUND_ERROR_INVALID_IMAGE', 1);
define('MSZ_USER_BACKGROUND_ERROR_PROHIBITED_TYPE', 2);
define('MSZ_USER_BACKGROUND_ERROR_DIMENSIONS_TOO_LARGE', 3);
define('MSZ_USER_BACKGROUND_ERROR_DATA_TOO_LARGE', 4);
define('MSZ_USER_BACKGROUND_ERROR_TMP_FAILED', 5);
define('MSZ_USER_BACKGROUND_ERROR_STORE_FAILED', 6);
define('MSZ_USER_BACKGROUND_ERROR_FILE_NOT_FOUND', 7);
function user_background_set_from_path(int $userId, string $path, array $options = []): int {
if(!file_exists($path))
return MSZ_USER_BACKGROUND_ERROR_FILE_NOT_FOUND;
$options = array_merge(user_background_default_options(), $options);
// 0 => width, 1 => height, 2 => type
$imageInfo = getimagesize($path);
if($imageInfo === false
|| count($imageInfo) < 3
|| $imageInfo[0] < 1
|| $imageInfo[1] < 1)
return MSZ_USER_BACKGROUND_ERROR_INVALID_IMAGE;
if(!user_background_is_allowed_type($imageInfo[2]))
return MSZ_USER_BACKGROUND_ERROR_PROHIBITED_TYPE;
if($imageInfo[0] > $options['max_width']
|| $imageInfo[1] > $options['max_height'])
return MSZ_USER_BACKGROUND_ERROR_DIMENSIONS_TOO_LARGE;
if(filesize($path) > $options['max_size'])
return MSZ_USER_BACKGROUND_ERROR_DATA_TOO_LARGE;
user_background_delete($userId);
$fileName = sprintf(MSZ_USER_BACKGROUND_FORMAT, $userId);
$storageDir = MSZ_STORAGE . '/backgrounds/original';
mkdirs($storageDir, true);
$backgroundPath = "{$storageDir}/{$fileName}";
if(!copy($path, $backgroundPath))
return MSZ_USER_BACKGROUND_ERROR_STORE_FAILED;
return MSZ_USER_BACKGROUND_NO_ERRORS;
}
function user_background_set_from_data(int $userId, string $data, array $options = []): int {
$tmp = tempnam(sys_get_temp_dir(), 'msz');
if($tmp === false || !file_exists($tmp))
return MSZ_USER_BACKGROUND_ERROR_TMP_FAILED;
chmod($tmp, 644);
file_put_contents($tmp, $data);
$result = user_background_set_from_path($userId, $tmp, $options);
safe_delete($tmp);
return $result;
}

View file

@ -1,261 +0,0 @@
<?php
// Quick note to myself and others about the `display_role` column in the users database.
// Never ever EVER use it for ANYTHING other than determining display colours, there's a small chance that it might not be accurate.
// And even if it were, roles properties are aggregated and thus must all be accounted for.
function user_totp_info(int $userId): array {
if($userId < 1)
return [];
$getTwoFactorInfo = \Misuzu\DB::prepare('
SELECT
`username`, `user_totp_key`,
`user_totp_key` IS NOT NULL AS `totp_enabled`
FROM `msz_users`
WHERE `user_id` = :user_id
');
$getTwoFactorInfo->bind('user_id', $userId);
return $getTwoFactorInfo->fetch();
}
function user_totp_update(int $userId, ?string $key): void {
if($userId < 1)
return;
$key = empty($key) ? null : $key;
$updateTotpKey = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `user_totp_key` = :key
WHERE `user_id` = :user_id
');
$updateTotpKey->bind('user_id', $userId);
$updateTotpKey->bind('key', $key);
$updateTotpKey->execute();
}
function user_email_set(int $userId, string $email): bool {
$updateMail = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `email` = LOWER(:email)
WHERE `user_id` = :user
');
$updateMail->bind('user', $userId);
$updateMail->bind('email', $email);
return $updateMail->execute();
}
function user_id_from_username(string $username): int {
$getId = \Misuzu\DB::prepare('SELECT `user_id` FROM `msz_users` WHERE LOWER(`username`) = LOWER(:username)');
$getId->bind('username', $username);
return (int)$getId->fetchColumn(0, 0);
}
function user_bump_last_active(int $userId, string $ipAddress = null): void {
$bumpUserLast = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `user_active` = NOW(),
`last_ip` = INET6_ATON(:last_ip)
WHERE `user_id` = :user_id
');
$bumpUserLast->bind('last_ip', $ipAddress ?? \Misuzu\Net\IPAddress::remote());
$bumpUserLast->bind('user_id', $userId);
$bumpUserLast->execute();
}
define('MSZ_E_USER_BIRTHDATE_OK', 0);
define('MSZ_E_USER_BIRTHDATE_USER', 1);
define('MSZ_E_USER_BIRTHDATE_DATE', 2);
define('MSZ_E_USER_BIRTHDATE_FAIL', 3);
define('MSZ_E_USER_BIRTHDATE_YEAR', 4);
function user_set_birthdate(int $userId, int $day, int $month, int $year, int $yearRange = 100): int {
if($userId < 1)
return MSZ_E_USER_BIRTHDATE_USER;
$unset = $day === 0 && $month === 0;
if($year === 0) {
$checkYear = date('Y');
} else {
if($year < date('Y') - $yearRange || $year > date('Y'))
return MSZ_E_USER_BIRTHDATE_YEAR;
$checkYear = $year;
}
if(!$unset && !checkdate($month, $day, $checkYear))
return MSZ_E_USER_BIRTHDATE_DATE;
$birthdate = $unset ? null : implode('-', [$year, $month, $day]);
$setBirthdate = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `user_birthdate` = :birthdate
WHERE `user_id` = :user
');
$setBirthdate->bind('birthdate', $birthdate);
$setBirthdate->bind('user', $userId);
return $setBirthdate->execute()
? MSZ_E_USER_BIRTHDATE_OK
: MSZ_E_USER_BIRTHDATE_FAIL;
}
function user_get_birthdays(int $day = 0, int $month = 0) {
$date = ($day < 1 || $month < 1) ? date('%-m-d') : "%-{$month}-{$day}";
$getBirthdays = \Misuzu\DB::prepare('
SELECT `user_id`, `username`, `user_birthdate`,
IF(YEAR(`user_birthdate`) < 1, NULL, YEAR(NOW()) - YEAR(`user_birthdate`)) AS `user_age`
FROM `msz_users`
WHERE `user_deleted` IS NULL
AND `user_birthdate` LIKE :birthdate
');
$getBirthdays->bind('birthdate', $date);
return $getBirthdays->fetchAll();
}
define('MSZ_USER_ABOUT_MAX_LENGTH', 0xFFFF);
define('MSZ_E_USER_ABOUT_OK', 0);
define('MSZ_E_USER_ABOUT_INVALID_USER', 1);
define('MSZ_E_USER_ABOUT_INVALID_PARSER', 2);
define('MSZ_E_USER_ABOUT_TOO_LONG', 3);
define('MSZ_E_USER_ABOUT_UPDATE_FAILED', 4);
function user_set_about_page(int $userId, string $content, int $parser = \Misuzu\Parsers\Parser::PLAIN): int {
if($userId < 1)
return MSZ_E_USER_ABOUT_INVALID_USER;
if(!\Misuzu\Parsers\Parser::isValid($parser))
return MSZ_E_USER_ABOUT_INVALID_PARSER;
$length = strlen($content);
if($length > MSZ_USER_ABOUT_MAX_LENGTH)
return MSZ_E_USER_ABOUT_TOO_LONG;
$setAbout = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `user_about_content` = :content,
`user_about_parser` = :parser
WHERE `user_id` = :user
');
$setAbout->bind('user', $userId);
$setAbout->bind('content', $length < 1 ? null : $content);
$setAbout->bind('parser', $parser);
return $setAbout->execute()
? MSZ_E_USER_ABOUT_OK
: MSZ_E_USER_ABOUT_UPDATE_FAILED;
}
define('MSZ_USER_SIGNATURE_MAX_LENGTH', 2000);
define('MSZ_E_USER_SIGNATURE_OK', 0);
define('MSZ_E_USER_SIGNATURE_INVALID_USER', 1);
define('MSZ_E_USER_SIGNATURE_INVALID_PARSER', 2);
define('MSZ_E_USER_SIGNATURE_TOO_LONG', 3);
define('MSZ_E_USER_SIGNATURE_UPDATE_FAILED', 4);
function user_set_signature(int $userId, string $content, int $parser = \Misuzu\Parsers\Parser::PLAIN): int {
if($userId < 1)
return MSZ_E_USER_SIGNATURE_INVALID_USER;
if(!\Misuzu\Parsers\Parser::isValid($parser))
return MSZ_E_USER_SIGNATURE_INVALID_PARSER;
$length = strlen($content);
if($length > MSZ_USER_SIGNATURE_MAX_LENGTH)
return MSZ_E_USER_SIGNATURE_TOO_LONG;
$setSignature = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `user_signature_content` = :content,
`user_signature_parser` = :parser
WHERE `user_id` = :user
');
$setSignature->bind('user', $userId);
$setSignature->bind('content', $length < 1 ? null : $content);
$setSignature->bind('parser', $parser);
return $setSignature->execute()
? MSZ_E_USER_SIGNATURE_OK
: MSZ_E_USER_SIGNATURE_UPDATE_FAILED;
}
// all the way down here bc of defines, this define is temporary
define('MSZ_TMP_USER_ERROR_STRINGS', [
'csrf' => "Couldn't verify you, please refresh the page and retry.",
'avatar' => [
'not-allowed' => "You aren't allow to change your avatar.",
'upload' => [
'_' => 'Something happened? (UP:%1$d)',
UPLOAD_ERR_OK => '',
UPLOAD_ERR_NO_FILE => 'Select a file before hitting upload!',
UPLOAD_ERR_PARTIAL => 'The upload was interrupted, please try again!',
UPLOAD_ERR_INI_SIZE => 'Your avatar is not allowed to be larger in file size than %2$s!',
UPLOAD_ERR_FORM_SIZE => 'Your avatar is not allowed to be larger in file size than %2$s!',
UPLOAD_ERR_NO_TMP_DIR => 'Unable to save your avatar, contact an administator!',
UPLOAD_ERR_CANT_WRITE => 'Unable to save your avatar, contact an administator!',
],
'set' => [
'_' => 'Something happened? (SET:%1$d)',
MSZ_USER_AVATAR_NO_ERRORS => '',
MSZ_USER_AVATAR_ERROR_INVALID_IMAGE => 'The file you uploaded was not an image!',
MSZ_USER_AVATAR_ERROR_PROHIBITED_TYPE => 'This type of image is not supported, keep to PNG, JPG or GIF!',
MSZ_USER_AVATAR_ERROR_DIMENSIONS_TOO_LARGE => 'Your avatar can\'t be larger than %3$dx%4$d!',
MSZ_USER_AVATAR_ERROR_DATA_TOO_LARGE => 'Your avatar is not allowed to be larger in file size than %2$s!',
MSZ_USER_AVATAR_ERROR_TMP_FAILED => 'Unable to save your avatar, contact an administator!',
MSZ_USER_AVATAR_ERROR_STORE_FAILED => 'Unable to save your avatar, contact an administator!',
MSZ_USER_AVATAR_ERROR_FILE_NOT_FOUND => 'Unable to save your avatar, contact an administator!',
],
],
'background' => [
'not-allowed' => "You aren't allow to change your background.",
'upload' => [
'_' => 'Something happened? (UP:%1$d)',
UPLOAD_ERR_OK => '',
UPLOAD_ERR_NO_FILE => 'Select a file before hitting upload!',
UPLOAD_ERR_PARTIAL => 'The upload was interrupted, please try again!',
UPLOAD_ERR_INI_SIZE => 'Your background is not allowed to be larger in file size than %2$s!',
UPLOAD_ERR_FORM_SIZE => 'Your background is not allowed to be larger in file size than %2$s!',
UPLOAD_ERR_NO_TMP_DIR => 'Unable to save your background, contact an administator!',
UPLOAD_ERR_CANT_WRITE => 'Unable to save your background, contact an administator!',
],
'set' => [
'_' => 'Something happened? (SET:%1$d)',
MSZ_USER_BACKGROUND_NO_ERRORS => '',
MSZ_USER_BACKGROUND_ERROR_INVALID_IMAGE => 'The file you uploaded was not an image!',
MSZ_USER_BACKGROUND_ERROR_PROHIBITED_TYPE => 'This type of image is not supported!',
MSZ_USER_BACKGROUND_ERROR_DIMENSIONS_TOO_LARGE => 'Your background can\'t be larger than %3$dx%4$d!',
MSZ_USER_BACKGROUND_ERROR_DATA_TOO_LARGE => 'Your background is not allowed to be larger in file size than %2$s!',
MSZ_USER_BACKGROUND_ERROR_TMP_FAILED => 'Unable to save your background, contact an administator!',
MSZ_USER_BACKGROUND_ERROR_STORE_FAILED => 'Unable to save your background, contact an administator!',
MSZ_USER_BACKGROUND_ERROR_FILE_NOT_FOUND => 'Unable to save your background, contact an administator!',
],
],
'profile' => [
'_' => 'An unexpected error occurred, contact an administator.',
'not-allowed' => "You're not allowed to edit your profile.",
'invalid' => '%s was formatted incorrectly!',
],
'about' => [
'_' => 'An unexpected error occurred, contact an administator.',
'not-allowed' => "You're not allowed to edit your about page.",
MSZ_E_USER_ABOUT_INVALID_USER => 'The requested user does not exist.',
MSZ_E_USER_ABOUT_INVALID_PARSER => 'The selected parser is invalid.',
MSZ_E_USER_ABOUT_TOO_LONG => 'Please keep the length of your about section below %1$d characters.',
MSZ_E_USER_ABOUT_UPDATE_FAILED => 'Failed to update about section, contact an administator.',
],
'signature' => [
'_' => 'An unexpected error occurred, contact an administator.',
'not-allowed' => "You're not allowed to edit your about page.",
MSZ_E_USER_SIGNATURE_INVALID_USER => 'The requested user does not exist.',
MSZ_E_USER_SIGNATURE_INVALID_PARSER => 'The selected parser is invalid.',
MSZ_E_USER_SIGNATURE_TOO_LONG => 'Please keep the length of your signature below %1$d characters.',
MSZ_E_USER_SIGNATURE_UPDATE_FAILED => 'Failed to update signature, contact an administator.',
],
]);

View file

@ -21,7 +21,7 @@ define('MSZ_URLS', [
'auth-forgot' => ['/auth/password.php'],
'auth-reset' => ['/auth/password.php', ['user' => '<user>']],
'auth-logout' => ['/auth/logout.php', ['csrf' => '{csrf}']],
'auth-resolve-user' => ['/auth/login.php', ['resolve_user' => '<username>']],
'auth-resolve-user' => ['/auth/login.php', ['resolve' => '1', 'name' => '<username>']],
'auth-two-factor' => ['/auth/twofactor.php', ['token' => '<token>']],
'changelog-index' => ['/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>']],

View file

@ -15,7 +15,7 @@
<a href="https://github.com/flashwave/misuzu/tree/{{ git_tag }}" target="_blank" rel="noreferrer noopener" class="footer__link">{{ git_tag }}</a>
{% endif %}
# <a href="https://github.com/flashwave/misuzu/commit/{{ git_commit_hash(true) }}" target="_blank" rel="noreferrer noopener" class="footer__link">{{ git_commit_hash() }}</a>
{% if constant('MSZ_DEBUG') or current_user.user_id|default(0) == 1 %}
{% if constant('MSZ_DEBUG') or current_user2.super|default(false) %}
/ SQL Queries: {{ sql_query_count()|number_format }}
/ Took: {{ startup_time()|number_format(5) }} seconds
/ Load: {{ (startup_time() - startup_time(constant('MSZ_TPL_RENDER')))|number_format(5) }} seconds

View file

@ -50,11 +50,11 @@
] %}
{% set user_menu =
current_user is defined
current_user2 is defined
? [
{
'title': 'Profile',
'url': url('user-profile', {'user': current_user.user_id}),
'url': url('user-profile', {'user': current_user2.id}),
'icon': 'fas fa-user fa-fw',
},
{
@ -139,9 +139,9 @@
{% endif %}
{% endfor %}
{% if current_user is defined %}
<a href="{{ url('user-profile', {'user': current_user.user_id}) }}" class="avatar header__desktop__user__avatar" title="{{ current_user.username }}" style="{{ current_user.user_colour|html_colour }}">
{{ avatar(current_user.user_id, 60, current_user.username) }}
{% if current_user2 is defined %}
<a href="{{ url('user-profile', {'user': current_user2.id}) }}" class="avatar header__desktop__user__avatar" title="{{ current_user.username }}" style="--user-colour: {{ current_user2.colour }}">
{{ avatar(current_user2.id, 60, current_user2.username) }}
</a>
{% else %}
<a href="{{ url('auth-login') }}" class="avatar header__desktop__user__avatar">
@ -162,7 +162,7 @@
</a>
<label class="header__mobile__icon header__mobile__avatar" for="toggle-mobile-header">
{{ avatar(current_user.user_id|default(0), 40, current_user.username|default('Log in')) }}
{{ avatar(current_user2.id|default(0), 40, current_user2.username|default('Log in')) }}
</label>
</div>

View file

@ -89,33 +89,33 @@
{{ container_title('<i class="fas fa-birthday-cake fa-fw"></i> Happy Birthday!') }}
{% for birthday in birthdays %}
<a class="landing__latest" href="{{ url('user-profile', {'user': birthday.user_id}) }}">
<div class="landing__latest__avatar">{{ avatar(birthday.user_id, 50, birthday.username) }}</div>
<a class="landing__latest" style="--user-colour: {{ birthday.colour }}" href="{{ url('user-profile', {'user': birthday.id}) }}">
<div class="landing__latest__avatar">{{ avatar(birthday.id, 50, birthday.username) }}</div>
<div class="landing__latest__content">
<div class="landing__latest__username">
{{ birthday.username }}
</div>
{% if birthday.user_age is not null %}
{% if birthday.hasAge %}
<div class="landing__latest__joined">
Turned {{ birthday.user_age }} today!
Turned {{ birthday.age }} today!
</div>
{% endif %}
</div>
</a>
{% endfor %}
</div>
{% elseif latest_user.user_id|default(0) > 0 %}
{% elseif latest_user is not null %}
<div class="container landing__container">
{{ container_title('<i class="fas fa-user-plus fa-fw"></i> Newest User') }}
<a class="landing__latest" style="{{ latest_user.user_colour|html_colour }}" href="{{ url('user-profile', {'user': latest_user.user_id}) }}">
<div class="landing__latest__avatar">{{ avatar(latest_user.user_id, 50, latest_user.username) }}</div>
<a class="landing__latest" style="--user-colour: {{ latest_user.colour }}" href="{{ url('user-profile', {'user': latest_user.id}) }}">
<div class="landing__latest__avatar">{{ avatar(latest_user.id, 50, latest_user.username) }}</div>
<div class="landing__latest__content">
<div class="landing__latest__username">
{{ latest_user.username }}
</div>
<div class="landing__latest__joined">
Joined <time datetime="{{ latest_user.user_created|date('c') }}" title="{{ latest_user.user_created|date('r') }}">{{ latest_user.user_created|time_diff }}</time>
Joined <time datetime="{{ latest_user.createdTime|date('c') }}" title="{{ latest_user.createdTime|date('r') }}">{{ latest_user.createdTime|time_diff }}</time>
</div>
</div>
</a>

View file

@ -26,7 +26,7 @@
{% endif %}
</head>
<body class="main{% if site_background is defined %} {{ site_background.settings|bg_settings('main--bg-%s')|join(' ') }}{% endif %}"
<body class="main{% if site_background is defined %} {{ site_background.classNames('main--bg-%s')|join(' ') }}{% endif %}"
style="{% if global_accent_colour is defined %}{{ global_accent_colour|html_colour('--accent-colour') }}{% endif %}" id="container">
{% include '_layout/header.twig' %}

View file

@ -8,7 +8,7 @@
<div class="profile__header__avatar">
{% if profile_is_editing and perms.edit_avatar %}
<label class="profile__header__avatar__image profile__header__avatar__image--edit" for="avatar-selection">
{{ avatar(profile_user.user_id, 120, profile_user.username, {'id': 'avatar-preview'}) }}
{{ avatar(profile_user.id, 120, profile_user.username, {'id': 'avatar-preview'}) }}
</label>
<div class="profile__header__avatar__options">
@ -24,27 +24,27 @@
</div>
{% else %}
<div class="profile__header__avatar__image">
{{ avatar(profile_user.user_id|default(0), 120, profile_user.username|default('')) }}
{{ avatar(profile_user.id|default(0), 120, profile_user.username|default('')) }}
</div>
{% endif %}
</div>
<div class="profile__header__details__content">
{% if profile_user is defined %}
<div class="profile__header__username" style="{{ profile_user.user_colour|html_colour }}">
<div class="profile__header__username" style="--user-colour: {{ profile_user.colour }}">
{{ profile_user.username }}
</div>
{% if profile_user.user_title is not empty %}
{% if profile_user.hasTitle %}
<div class="profile__header__title">
{{ profile_user.user_title }}
{{ profile_user.title }}
</div>
{% endif %}
<div class="profile__header__country">
<div class="flag flag--{{ profile_user.country|lower }}"></div>
<div class="profile__header__country__name">
{{ profile_user.countryName }}{% if profile_user.user_age > 0 %}, {{ profile_user.user_age }} year{{ profile_user.user_age != 's' ? 's' : '' }} old{% endif %}
{{ profile_user.countryName }}{% if profile_user.hasAge %},{% set age = profile_user.age %} {{ age }} year{{ age != 's' ? 's' : '' }} old{% endif %}
</div>
</div>
{% else %}
@ -76,21 +76,21 @@
{% if profile_mode is empty %}
{% if profile_is_editing %}
<button class="input__button input__button--save profile__header__action">Save</button>
<a href="{{ url('user-profile', {'user': profile_user.user_id}) }}" class="input__button input__button--destroy profile__header__action">Discard</a>
<a href="{{ url('user-profile', {'user': profile_user.id}) }}" class="input__button input__button--destroy profile__header__action">Discard</a>
<a href="{{ url('settings-index') }}" class="input__button profile__header__action">Settings</a>
{% elseif profile_can_edit %}
<a href="{{ url('user-profile-edit', {'user': profile_user.user_id}) }}" class="input__button profile__header__action">Edit Profile</a>
<a href="{{ url('user-profile-edit', {'user': profile_user.id}) }}" class="input__button profile__header__action">Edit Profile</a>
{% endif %}
{% if current_user is defined and current_user.user_id != profile_user.user_id and not profile_is_editing %}
{% if current_user2 is defined and current_user2.id|default(0) != profile_user.id and not profile_is_editing %}
{% if profile_user.relationString(profile_viewer) != 'following' %}
<a href="{{ url('user-relation-none', {'user': profile_user.user_id}) }}" class="input__button input__button--destroy profile__header__action js-user-relation-action" data-relation-user="{{ profile_user.user_id }}" data-relation-type="0">Unfollow</a>
<a href="{{ url('user-relation-none', {'user': profile_user.id}) }}" class="input__button input__button--destroy profile__header__action js-user-relation-action" data-relation-user="{{ profile_user.id }}" data-relation-type="0">Unfollow</a>
{% else %}
<a href="{{ url('user-relation-follow', {'user': profile_user.user_id}) }}" class="input__button profile__header__action js-user-relation-action" data-relation-user="{{ profile_user.user_id }}" data-relation-type="1">Follow</a>
<a href="{{ url('user-relation-follow', {'user': profile_user.id}) }}" class="input__button profile__header__action js-user-relation-action" data-relation-user="{{ profile_user.id }}" data-relation-type="1">Follow</a>
{% endif %}
{% endif %}
{% else %}
<a href="{{ url('user-profile', {'user': profile_user.user_id}) }}" class="input__button profile__header__action">Return</a>
<a href="{{ url('user-profile', {'user': profile_user.id}) }}" class="input__button profile__header__action">Return</a>
{% endif %}
</div>
{% endif %}
@ -98,7 +98,7 @@
{% if stats is defined %}
<div class="profile__header__stats">
{% for stat in stats %}
{% if stat.value|default(false) %}
{% if stat.value|default(0) > 0 %}
{% set is_date = stat.is_date|default(false) %}
{% set is_url = stat.url is defined %}
{% set active_class = stat.active|default(false) ? ' profile__header__stat--active' : '' %}

View file

@ -48,8 +48,8 @@
{% if perms.edit_avatar %}
<ul class="profile__guidelines__section">
<li class="profile__guidelines__line profile__guidelines__line--header">Avatar</li>
<li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ guidelines.avatar.max_size|byte_symbol(true) }}</span> file size limit.</li>
<li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ guidelines.avatar.max_width }}x{{ guidelines.avatar.max_height }}</span>.</li>
<li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_user.avatarInfo.maxBytes|byte_symbol() }}</span> file size limit.</li>
<li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_user.avatarInfo.maxWidth }}x{{ profile_user.avatarInfo.maxHeight }}</span>.</li>
<li class="profile__guidelines__line">Will be centre cropped and scaled to at most <span class="profile__guidelines__emphasis">240x240</span>.</li>
<li class="profile__guidelines__line">Animated gif images are allowed.</li>
</ul>
@ -58,8 +58,8 @@
{% if perms.edit_background %}
<ul class="profile__guidelines__section">
<li class="profile__guidelines__line profile__guidelines__line--header">Background</li>
<li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ guidelines.background.max_size|byte_symbol(true) }}</span> file size limit.</li>
<li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ guidelines.background.max_width }}x{{ guidelines.background.max_height }}</span>.</li>
<li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_user.backgroundInfo.maxBytes|byte_symbol() }}</span> file size limit.</li>
<li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_user.backgroundInfo.maxWidth }}x{{ profile_user.backgroundInfo.maxHeight }}</span>.</li>
<li class="profile__guidelines__line">Gif images, in general, are only allowed when tiling.</li>
</ul>
{% endif %}
@ -94,11 +94,11 @@
{{ input_checkbox('background[attach]', 'None', true, '', 0, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }}
{% for key, value in background_attachments %}
{{ input_checkbox('background[attach]', value|capitalize, key == profile_user.backgroundAttachment, '', key, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }}
{{ input_checkbox('background[attach]', value, key == profile_user.backgroundInfo.attachment, '', key, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }}
{% endfor %}
{{ input_checkbox('background[attr][blend]', 'Blend', profile_user.backgroundBlend, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'blend\', this.checked)'}) }}
{{ input_checkbox('background[attr][slide]', 'Slide', profile_user.backgroundSlide, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'slide\', this.checked)'}) }}
{{ input_checkbox('background[attr][blend]', 'Blend', profile_user.backgroundInfo.blend, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'blend\', this.checked)'}) }}
{{ input_checkbox('background[attr][slide]', 'Slide', profile_user.backgroundInfo.slide, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'slide\', this.checked)'}) }}
</div>
</div>
{% endif %}
@ -140,22 +140,20 @@
<div class="container profile__container profile__birthdate">
{{ container_title('Birthdate') }}
{% set birthdate = profile_user.user_birthdate|split('-') %}
<div class="profile__birthdate__content">
<div class="profile__birthdate__date">
<label class="profile__birthdate__label">
<div class="profile__birthdate__title">
Day
</div>
{{ input_select('birthdate[day]', ['-']|merge(range(1, 31)), birthdate[2]|default(0)|trim('0', 'left'), '', '', true, 'profile__birthdate__select profile__birthdate__select--day') }}
{{ input_select('birthdate[day]', ['-']|merge(range(1, 31)), profile_user.hasBirthdate ? profile_user.birthdate.format('d') : 0, '', '', true, 'profile__birthdate__select profile__birthdate__select--day') }}
</label>
<label class="profile__birthdate__label">
<div class="profile__birthdate__title">
Month
</div>
{{ input_select('birthdate[month]', ['-']|merge(range(1, 12)), birthdate[1]|default(0)|trim('0', 'left'), '', '', true, 'profile__birthdate__select profile__birthdate__select--month') }}
{{ input_select('birthdate[month]', ['-']|merge(range(1, 12)), profile_user.hasBirthdate ? profile_user.birthdate.format('m') : 0, '', '', true, 'profile__birthdate__select profile__birthdate__select--month') }}
</label>
</div>
@ -164,7 +162,7 @@
<div class="profile__birthdate__title">
Year (may be left empty)
</div>
{{ input_select('birthdate[year]', ['-']|merge(range(null|date('Y'), null|date('Y') - 100)), birthdate[0]|default(0)|trim('0', 'left'), '', '', true, 'profile__birthdate__select profile__birthdate__select--year') }}
{{ input_select('birthdate[year]', ['-']|merge(range(null|date('Y'), null|date('Y') - 100)), profile_user.birthdate.format('Y'), '', '', true, 'profile__birthdate__select profile__birthdate__select--year') }}
</label>
</div>
</div>
@ -175,35 +173,35 @@
{% if profile_user is defined %}
<div class="profile__content__main">
{% if (profile_is_editing and perms.edit_about) or profile_user.user_about_content|length > 0 %}
{% if (profile_is_editing and perms.edit_about) or profile_user.hasProfileAbout %}
<div class="container profile__container profile__about" id="about">
{{ container_title('About ' ~ profile_user.username) }}
{% if profile_is_editing %}
<div class="profile__signature__editor">
{{ input_select('about[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.user_about_parser, '', '', false, 'profile__about__select') }}
<textarea name="about[text]" class="input__textarea profile__about__text" id="about-textarea">{{ profile_user.user_about_content|escape }}</textarea>
{{ input_select('about[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.profileAboutParser, '', '', false, 'profile__about__select') }}
<textarea name="about[text]" class="input__textarea profile__about__text" id="about-textarea">{{ profile_user.profileAboutText }}</textarea>
</div>
{% else %}
<div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_user.user_about_parser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ profile_user.user_about_content|escape|parse_text(profile_user.user_about_parser)|raw }}
<div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_user.profileAboutParser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ profile_user.profileAboutParsed|raw }}
</div>
{% endif %}
</div>
{% endif %}
{% if (profile_is_editing and perms.edit_signature) or profile_user.user_signature_content|length > 0 %}
{% if (profile_is_editing and perms.edit_signature) or profile_user.hasForumSignature %}
<div class="container profile__container profile__signature" id="signature">
{{ container_title('Signature') }}
{% if profile_is_editing %}
<div class="profile__signature__editor">
{{ input_select('signature[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.user_signature_parser, '', '', false, 'profile__signature__select') }}
<textarea name="signature[text]" class="input__textarea profile__signature__text" id="signature-textarea">{{ profile_user.user_signature_content|escape }}</textarea>
{{ input_select('signature[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.forumSignatureParser, '', '', false, 'profile__signature__select') }}
<textarea name="signature[text]" class="input__textarea profile__signature__text" id="signature-textarea">{{ profile_user.forumSignatureText }}</textarea>
</div>
{% else %}
<div class="profile__signature__content{% if profile_is_editing %} profile__signature__content--edit{% elseif profile_user.user_signature_parser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ profile_user.user_signature_content|escape|parse_text(profile_user.user_signature_parser)|raw }}
<div class="profile__signature__content{% if profile_is_editing %} profile__signature__content--edit{% elseif profile_user.forumSignatureParser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ profile_user.forumSignatureParsed|raw }}
</div>
{% endif %}
</div>

View file

@ -3,16 +3,19 @@
{% if profile_user is defined %}
{% set image = url('user-avatar', {'user': profile_user.id, 'res': 200}) %}
{% set manage_link = url('manage-user', {'user': profile_user.id}) %}
{% if profile_user.hasBackground %}
{% set site_background = profile_user.backgroundInfo %}
{% endif %}
{% set stats = [
{
'title': 'Joined',
'is_date': true,
'value': profile_user.user_created,
'value': profile_user.createdTime,
},
{
'title': 'Last seen',
'is_date': true,
'value': profile_user.user_active,
'value': profile_user.activeTime,
},
{
'title': 'Following',

View file

@ -127,7 +127,7 @@
{% endif %}
<div class="settings__two-factor__settings">
{% if settings_2fa_enabled %}
{% if settings_user.hasTOTP %}
<div class="settings__two-factor__settings__status">
<i class="fas fa-lock fa-fw"></i> Two Factor Authentication is enabled!
</div>

View file

@ -50,14 +50,6 @@ function first_paragraph(string $text, string $delimiter = "\n"): string {
return $index === false ? $text : mb_substr($text, 0, $index);
}
function camel_to_snake(string $camel): string {
return trim(mb_strtolower(preg_replace('#([A-Z][a-z]+)#', '$1_', $camel)), '_');
}
function snake_to_camel(string $snake): string {
return str_replace('_', '', ucwords($snake, '_'));
}
function unique_chars(string $input, bool $multibyte = true): int {
$chars = [];
$strlen = $multibyte ? 'mb_strlen' : 'strlen';
@ -87,27 +79,6 @@ function byte_symbol(int $bytes, bool $decimal = false, array $symbols = ['', 'K
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
}
function safe_delete(string $path): void {
$path = realpath($path);
if(empty($path))
return;
if(is_dir($path)) {
rmdir($path);
return;
}
if(is_file($path))
unlink($path);
}
// mkdir but it fails silently
function mkdirs(string $path, bool $recursive = false, int $mode = 0777): bool {
if(file_exists($path))
return true;
return mkdir($path, $mode, $recursive);
}
function get_country_name(string $code): string {
switch(strtolower($code)) {
case 'xx':
@ -124,24 +95,6 @@ function get_country_name(string $code): string {
}
}
function pdo_prepare_array_update(array $keys, bool $useKeys = false, string $format = '%s'): string {
return pdo_prepare_array($keys, $useKeys, sprintf($format, '`%1$s` = :%1$s'));
}
function pdo_prepare_array(array $keys, bool $useKeys = false, string $format = '`%s`'): string {
$parts = [];
if($useKeys) {
$keys = array_keys($keys);
}
foreach($keys as $key) {
$parts[] = sprintf($format, $key);
}
return implode(', ', $parts);
}
// render_error, render_info and render_info_or_json should be redone a bit better
// following a uniform format so there can be a global handler for em