Made user asset handling OOP.
This commit is contained in:
parent
fa0088a3e9
commit
44fc436134
36 changed files with 1198 additions and 1055 deletions
assets/js/misuzu
misuzu.phppublic
src
Comments
Console/Commands
HasRankInterface.phpHttp/Handlers
TwigMisuzu.phpUsers
Assets
StaticUserImageAsset.phpUserAssetException.phpUserAssetScalableInterface.phpUserAvatarAsset.phpUserBackgroundAsset.phpUserImageAsset.phpUserImageAssetInterface.php
User.phpavatar.phpbackground.phpuser_legacy.phptemplates
utility.php
|
@ -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();
|
||||
|
|
35
misuzu.php
35
misuzu.php
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'],
|
||||
]);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -3,5 +3,5 @@ namespace Misuzu;
|
|||
|
||||
interface HasRankInterface {
|
||||
public function getRank(): int;
|
||||
public function HasAuthorityOver(self $other): bool;
|
||||
public function hasAuthorityOver(self $other): bool;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'),
|
||||
];
|
||||
}
|
||||
|
|
30
src/Users/Assets/StaticUserImageAsset.php
Normal file
30
src/Users/Assets/StaticUserImageAsset.php
Normal 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;
|
||||
}
|
||||
}
|
6
src/Users/Assets/UserAssetException.php
Normal file
6
src/Users/Assets/UserAssetException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Misuzu\Users\Assets;
|
||||
|
||||
use Misuzu\Users\UsersException;
|
||||
|
||||
class UserAssetException extends UsersException {}
|
12
src/Users/Assets/UserAssetScalableInterface.php
Normal file
12
src/Users/Assets/UserAssetScalableInterface.php
Normal 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;
|
||||
}
|
99
src/Users/Assets/UserAvatarAsset.php
Normal file
99
src/Users/Assets/UserAvatarAsset.php
Normal 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);
|
||||
}
|
||||
}
|
147
src/Users/Assets/UserBackgroundAsset.php
Normal file
147
src/Users/Assets/UserBackgroundAsset.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
139
src/Users/Assets/UserImageAsset.php
Normal file
139
src/Users/Assets/UserImageAsset.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
9
src/Users/Assets/UserImageAssetInterface.php
Normal file
9
src/Users/Assets/UserImageAssetInterface.php
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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.',
|
||||
],
|
||||
]);
|
|
@ -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>']],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' %}
|
||||
|
||||
|
|
|
@ -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' : '' %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
47
utility.php
47
utility.php
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue