A bunch of housekeeping.

This commit is contained in:
flash 2018-05-27 02:20:35 +02:00
parent 489a347c22
commit fca5b423a4
19 changed files with 555 additions and 343 deletions

View file

@ -0,0 +1,11 @@
.avatar {
flex-shrink: 0;
background-color: #333;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
display: block;
border: 1px solid #444;
max-height: 200px;
max-width: 200px;
}

View file

@ -9,7 +9,19 @@
&__content {
display: flex;
align-items: flex-start;
}
}
&__info {
display: flex;
flex-grow: 1;
flex-shrink: 1;
}
&__avatar {
width: 46px;
height: 46px;
flex-grow: 0;
flex-shrink: 0;
}
}

View file

@ -19,6 +19,7 @@ body {
color: #fff;
}
@import "classes/avatar";
@import "classes/button";
@import "classes/container";
@import "classes/footer";

View file

@ -9,6 +9,10 @@ require_once __DIR__ . '/src/Forum/post.php';
require_once __DIR__ . '/src/Forum/topic.php';
require_once __DIR__ . '/src/Forum/validate.php';
require_once __DIR__ . '/src/Users/login_attempt.php';
require_once __DIR__ . '/src/Users/profile.php';
require_once __DIR__ . '/src/Users/role.php';
require_once __DIR__ . '/src/Users/session.php';
require_once __DIR__ . '/src/Users/user.php';
require_once __DIR__ . '/src/Users/validation.php';
$app = new Application(

View file

@ -10,31 +10,28 @@ $db = Database::connection();
$config = $app->getConfig();
$templating = $app->getTemplating();
$username_validation_errors = [
$usernameValidationErrors = [
'trim' => 'Your username may not start or end with spaces!',
'short' => "Your username is too short, it has to be at least " . MSZ_USERNAME_MIN_LENGTH . " characters!",
'long' => "Your username is too long, it can't be longer than " . MSZ_USERNAME_MAX_LENGTH . " characters!",
'short' => sprintf('Your username is too short, it has to be at least %d characters!', MSZ_USERNAME_MIN_LENGTH),
'long' => sprintf("Your username is too long, it can't be longer than %d characters!", MSZ_USERNAME_MAX_LENGTH),
'double-spaces' => "Your username can't contain double spaces.",
'invalid' => 'Your username contains invalid characters.',
'spacing' => 'Please use either underscores or spaces, not both!',
'in-use' => 'This username is already taken!',
];
$mode = $_GET['m'] ?? 'login';
$prevent_registration = $config->get('Auth', 'prevent_registration', 'bool', false);
$templating->var('auth_mode', $mode);
$authMode = $_GET['m'] ?? 'login';
$preventRegistration = $config->get('Auth', 'prevent_registration', 'bool', false);
$templating->addPath('auth', __DIR__ . '/../views/auth');
$templating->var('prevent_registration', $prevent_registration);
if (!empty($_REQUEST['username'])) {
$templating->var('auth_username', $_REQUEST['username']);
}
$templating->vars([
'prevent_registration' => $preventRegistration,
'auth_mode' => $authMode,
'auth_username' => $_REQUEST['username'] ?? '',
'auth_email' => $_REQUEST['email'] ?? '',
]);
if (!empty($_REQUEST['email'])) {
$templating->var('auth_email', $_REQUEST['email']);
}
switch ($mode) {
switch ($authMode) {
case 'logout':
if (!$app->hasActiveSession()) {
header('Location: /');
@ -44,12 +41,7 @@ switch ($mode) {
if (isset($_GET['s']) && tmp_csrf_verify($_GET['s'])) {
set_cookie_m('uid', '', -3600);
set_cookie_m('sid', '', -3600);
$deleteSession = $db->prepare('
DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $app->getSessionId());
$deleteSession->execute();
user_session_delete($app->getSessionId());
header('Location: /');
return;
}
@ -63,35 +55,24 @@ switch ($mode) {
break;
}
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$auth_login_error = '';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$authLoginError = '';
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ipAddress = IPAddress::remote()->getString();
if (!isset($_POST['username'], $_POST['password'])) {
$auth_login_error = "You didn't fill all the forms!";
$authLoginError = "You didn't fill all the forms!";
break;
}
$fetchRemainingAttempts = $db->prepare('
SELECT 5 - COUNT(`attempt_id`)
FROM `msz_login_attempts`
WHERE `was_successful` = false
AND `created_at` > NOW() - INTERVAL 1 HOUR
AND `attempt_ip` = INET6_ATON(:remote_ip)
');
$fetchRemainingAttempts->bindValue('remote_ip', $ipAddress);
$remainingAttempts = $fetchRemainingAttempts->execute()
? (int)$fetchRemainingAttempts->fetchColumn()
: 0;
$remainingAttempts = user_login_attempts_remaining($ipAddress);
if ($remainingAttempts < 1) {
$auth_login_error = 'Too many failed login attempts, try again later.';
$authLoginError = 'Too many failed login attempts, try again later.';
break;
}
$remainingAttempts -= 1;
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
@ -106,44 +87,29 @@ switch ($mode) {
$userData = $getUser->execute() ? $getUser->fetch() : [];
$userId = (int)($userData['user_id'] ?? 0);
$auth_error_str = "Invalid username or password, {$remainingAttempts} attempt(s) remaining.";
$loginFailedError = sprintf(
"Invalid username or password, %d attempt%s remaining.",
$remainingAttempts - 1,
$remainingAttempts === 2 ? '' : 's'
);
if ($userId < 1) {
user_login_attempt_record(false, null, $ipAddress, $user_agent);
$auth_login_error = $auth_error_str;
user_login_attempt_record(false, null, $ipAddress, $userAgent);
$authLoginError = $loginFailedError;
break;
}
if (!password_verify($password, $userData['password'])) {
user_login_attempt_record(false, $userId, $ipAddress, $user_agent);
$auth_login_error = $auth_error_str;
user_login_attempt_record(false, $userId, $ipAddress, $userAgent);
$authLoginError = $loginFailedError;
break;
}
user_login_attempt_record(true, $userId, $ipAddress, $user_agent);
user_login_attempt_record(true, $userId, $ipAddress, $userAgent);
$sessionKey = user_session_create($userId, $ipAddress, $userAgent);
$sessionKey = bin2hex(random_bytes(32));
$createSession = $db->prepare('
INSERT INTO `msz_sessions`
(
`user_id`, `session_ip`, `session_country`,
`user_agent`, `session_key`, `created_at`, `expires_on`
)
VALUES
(
:user_id, INET6_ATON(:session_ip), :session_country,
:user_agent, :session_key, NOW(), NOW() + INTERVAL 1 MONTH
)
');
$createSession->bindValue('user_id', $userId);
$createSession->bindValue('session_ip', $ipAddress);
$createSession->bindValue('session_country', get_country_code($ipAddress));
$createSession->bindValue('user_agent', $user_agent);
$createSession->bindValue('session_key', $sessionKey);
if (!$createSession->execute()) {
$auth_login_error = 'Unable to create new session, contact an administrator.';
if ($sessionKey === '') {
$authLoginError = 'Unable to create new session, contact an administrator ASAP.';
break;
}
@ -152,30 +118,21 @@ switch ($mode) {
set_cookie_m('uid', $userId, $cookieLife);
set_cookie_m('sid', $sessionKey, $cookieLife);
// Temporary key generation for chat login.
// Should eventually be replaced with a callback login system.
// Also uses different cookies since $httponly is required to be false for these.
$chatKey = bin2hex(random_bytes(16));
if (strpos($_SERVER['HTTP_HOST'], 'flashii.net') !== false) {
$chatKey = user_generate_chat_key($userId);
$setChatKey = $db->prepare('
UPDATE `msz_users`
SET `user_chat_key` = :user_chat_key
WHERE `user_id` = :user_id
');
$setChatKey->bindValue('user_chat_key', $chatKey);
$setChatKey->bindValue('user_id', $userId);
if ($setChatKey->execute()) {
if ($chatKey !== '') {
setcookie('msz_tmp_id', $userId, $cookieLife, '/', '.flashii.net');
setcookie('msz_tmp_key', $chatKey, $cookieLife, '/', '.flashii.net');
}
}
header('Location: /');
return;
}
if (!empty($auth_login_error)) {
$templating->var('auth_login_error', $auth_login_error);
if (!empty($authLoginError)) {
$templating->var('auth_login_error', $authLoginError);
}
echo $templating->render('auth');
@ -186,16 +143,16 @@ switch ($mode) {
header('Location: /');
}
$auth_register_error = '';
$authRegistrationError = '';
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($prevent_registration) {
$auth_register_error = 'Registration is not allowed on this instance.';
if ($preventRegistration) {
$authRegistrationError = 'Registration is not allowed on this instance.';
break;
}
if (!isset($_POST['username'], $_POST['password'], $_POST['email'])) {
$auth_register_error = "You didn't fill all the forms!";
$authRegistrationError = "You didn't fill all the forms!";
break;
}
@ -203,65 +160,45 @@ switch ($mode) {
$password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? '';
$username_validate = user_validate_username($username, true);
if ($username_validate !== '') {
$auth_register_error = $username_validation_errors[$username_validate];
$usernameValidation = user_validate_username($username, true);
if ($usernameValidation !== '') {
$authRegistrationError = $usernameValidationErrors[$usernameValidation];
break;
}
$email_validate = user_validate_email($email, true);
if ($email_validate !== '') {
$auth_register_error = $email_validate === 'in-use'
$emailValidation = user_validate_email($email, true);
if ($emailValidation !== '') {
$authRegistrationError = $emailValidation === 'in-use'
? 'This e-mail address has already been used!'
: 'The e-mail address you entered is invalid!';
break;
}
if (user_validate_password($password) !== '') {
$auth_register_error = 'Your password is too weak!';
$authRegistrationError = 'Your password is too weak!';
break;
}
$ipAddress = IPAddress::remote()->getString();
$createUser = $db->prepare('
INSERT INTO `msz_users`
(
`username`, `password`, `email`, `register_ip`,
`last_ip`, `user_country`, `created_at`, `display_role`
)
VALUES
(
:username, :password, :email, INET6_ATON(:register_ip),
INET6_ATON(:last_ip), :user_country, NOW(), 1
)
');
$createUser->bindValue('username', $username);
$createUser->bindValue('password', password_hash($password, PASSWORD_ARGON2I));
$createUser->bindValue('email', $email);
$createUser->bindValue('register_ip', $ipAddress);
$createUser->bindValue('last_ip', $ipAddress);
$createUser->bindValue('user_country', get_country_code($ipAddress));
$createUser = user_create(
$username,
$password,
$email,
IPAddress::remote()->getString()
);
if (!$createUser->execute()) {
$auth_register_error = 'Something happened?';
if ($createUser < 1) {
$authRegistrationError = 'Something happened?';
break;
}
$addRole = $db->prepare('
INSERT INTO `msz_user_roles`
(`user_id`, `role_id`)
VALUES
(:user_id, 1)
');
$addRole->bindValue('user_id', $db->lastInsertId());
$addRole->execute();
user_role_add($createUser, MSZ_ROLE_MAIN);
$templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
break;
}
if (!empty($auth_register_error)) {
$templating->var('auth_register_error', $auth_register_error);
if (!empty($authRegistrationError)) {
$templating->var('auth_register_error', $authRegistrationError);
}
echo $templating->render('@auth.auth');

View file

@ -21,4 +21,6 @@ $featuredNews = Database::connection()
//var_dump(Database::connection()->query('SHOW SESSION STATUS LIKE "Questions"')->fetch()['Value']);
echo $app->getTemplating()->render('home.landing', compact('featuredNews'));
echo $app->getTemplating()->render('home.landing', [
'featured_news' => $featuredNews,
]);

View file

@ -6,9 +6,8 @@ require_once __DIR__ . '/../../misuzu.php';
$db = Database::connection();
$templating = $app->getTemplating();
$is_post_request = $_SERVER['REQUEST_METHOD'] === 'POST';
$isPostRequest = $_SERVER['REQUEST_METHOD'] === 'POST';
$queryQffset = (int)($_GET['o'] ?? 0);
$page_id = (int)($_GET['p'] ?? 1);
switch ($_GET['v'] ?? null) {
case 'listing':
@ -42,9 +41,9 @@ switch ($_GET['v'] ?? null) {
break;
case 'view':
$user_id = $_GET['u'] ?? null;
$userId = $_GET['u'] ?? null;
if ($user_id === null || ($user_id = (int)$user_id) < 1) {
if ($userId === null || ($userId = (int)$userId) < 1) {
echo 'no';
break;
}
@ -60,7 +59,7 @@ switch ($_GET['v'] ?? null) {
ON u.`display_role` = r.`role_id`
WHERE `user_id` = :user_id
');
$getUser->bindValue('user_id', $user_id);
$getUser->bindValue('user_id', $userId);
$getUser->execute();
$manageUser = $getUser->execute() ? $getUser->fetch() : [];
@ -106,9 +105,9 @@ switch ($_GET['v'] ?? null) {
break;
case 'role':
$role_id = $_GET['r'] ?? null;
$roleId = $_GET['r'] ?? null;
if ($is_post_request) {
if ($isPostRequest) {
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
echo 'csrf err';
break;
@ -119,27 +118,27 @@ switch ($_GET['v'] ?? null) {
break;
}
$role_name = $_POST['role']['name'] ?? '';
$role_name_length = strlen($role_name);
$roleName = $_POST['role']['name'] ?? '';
$roleNameLength = strlen($roleName);
if ($role_name_length < 1 || $role_name_length > 255) {
if ($roleNameLength < 1 || $roleNameLength > 255) {
echo 'invalid name length';
break;
}
$role_secret = !empty($_POST['role']['secret']);
$roleSecret = !empty($_POST['role']['secret']);
$role_hierarchy = (int)($_POST['role']['hierarchy'] ?? -1);
$roleHierarchy = (int)($_POST['role']['hierarchy'] ?? -1);
if ($role_hierarchy < 1 || $role_hierarchy > 100) {
if ($roleHierarchy < 1 || $roleHierarchy > 100) {
echo 'Invalid hierarchy value.';
break;
}
$role_colour = colour_create();
$roleColour = colour_create();
if (!empty($_POST['role']['colour']['inherit'])) {
colour_set_inherit($role_colour);
colour_set_inherit($roleColour);
} else {
foreach (['red', 'green', 'blue'] as $key) {
$value = (int)($_POST['role']['colour'][$key] ?? -1);
@ -150,18 +149,18 @@ switch ($_GET['v'] ?? null) {
break 2;
}
$func($role_colour, $value);
$func($roleColour, $value);
}
}
$role_description = $_POST['role']['description'] ?? '';
$roleDescription = $_POST['role']['description'] ?? '';
if (strlen($role_description) > 1000) {
if (strlen($roleDescription) > 1000) {
echo 'description is too long';
break;
}
if ($role_id < 1) {
if ($roleId < 1) {
$updateRole = $db->prepare('
INSERT INTO `msz_roles`
(`role_name`, `role_hierarchy`, `role_secret`, `role_colour`, `role_description`, `created_at`)
@ -178,26 +177,26 @@ switch ($_GET['v'] ?? null) {
`role_description` = :role_description
WHERE `role_id` = :role_id
');
$updateRole->bindValue('role_id', $role_id);
$updateRole->bindValue('role_id', $roleId);
}
$updateRole->bindValue('role_name', $role_name);
$updateRole->bindValue('role_hierarchy', $role_hierarchy);
$updateRole->bindValue('role_secret', $role_secret ? 1 : 0);
$updateRole->bindValue('role_colour', $role_colour);
$updateRole->bindValue('role_description', $role_description);
$updateRole->bindValue('role_name', $roleName);
$updateRole->bindValue('role_hierarchy', $roleHierarchy);
$updateRole->bindValue('role_secret', $roleSecret ? 1 : 0);
$updateRole->bindValue('role_colour', $roleColour);
$updateRole->bindValue('role_description', $roleDescription);
$updateRole->execute();
if ($role_id < 1) {
$role_id = (int)$db->lastInsertId();
if ($roleId < 1) {
$roleId = (int)$db->lastInsertId();
}
header("Location: ?v=role&r={$role_id}");
header("Location: ?v=role&r={$roleId}");
break;
}
if ($role_id !== null) {
if ($role_id < 1) {
if ($roleId !== null) {
if ($roleId < 1) {
echo 'no';
break;
}
@ -207,15 +206,15 @@ switch ($_GET['v'] ?? null) {
FROM `msz_roles`
WHERE `role_id` = :role_id
');
$getEditRole->bindValue('role_id', $role_id);
$edit_role = $getEditRole->execute() ? $getEditRole->fetch() : [];
$getEditRole->bindValue('role_id', $roleId);
$editRole = $getEditRole->execute() ? $getEditRole->fetch() : [];
if (!$edit_role) {
if (!$editRole) {
echo 'invalid role';
break;
}
$templating->vars(compact('edit_role'));
$templating->vars(['edit_role' => $editRole]);
}
echo $templating->render('@manage.users.roles_create');

View file

@ -6,14 +6,17 @@ require_once __DIR__ . '/../misuzu.php';
$db = Database::connection();
$templating = $app->getTemplating();
$category_id = isset($_GET['c']) ? (int)$_GET['c'] : null;
$post_id = isset($_GET['p']) ? (int)$_GET['p'] : (isset($_GET['n']) ? (int)$_GET['n'] : null);
$posts_offset = (int)($_GET['o'] ?? 0);
$posts_take = 5;
$categoryId = isset($_GET['c']) ? (int)$_GET['c'] : null;
$postId = isset($_GET['p']) ? (int)$_GET['p'] : (isset($_GET['n']) ? (int)$_GET['n'] : null);
$postsOffset = (int)($_GET['o'] ?? 0);
$postsTake = 5;
$templating->vars(compact('posts_offset', 'posts_take'));
$templating->vars([
'posts_offset' => $postsOffset,
'posts_take' => $postsTake,
]);
if ($post_id !== null) {
if ($postId !== null) {
$getPost = $db->prepare('
SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`,
@ -29,7 +32,7 @@ if ($post_id !== null) {
ON u.`display_role` = r.`role_id`
WHERE `post_id` = :post_id
');
$getPost->bindValue(':post_id', $post_id, PDO::PARAM_INT);
$getPost->bindValue(':post_id', $postId, PDO::PARAM_INT);
$post = $getPost->execute() ? $getPost->fetch() : false;
if ($post === false) {
@ -41,7 +44,7 @@ if ($post_id !== null) {
return;
}
if ($category_id !== null) {
if ($categoryId !== null) {
$getCategory = $db->prepare('
SELECT
c.`category_id`, c.`category_name`, c.`category_description`,
@ -52,10 +55,10 @@ if ($category_id !== null) {
WHERE c.`category_id` = :category_id
GROUP BY c.`category_id`
');
$getCategory->bindValue(':category_id', $category_id, PDO::PARAM_INT);
$getCategory->bindValue(':category_id', $categoryId, PDO::PARAM_INT);
$category = $getCategory->execute() ? $getCategory->fetch() : false;
if ($category === false || $posts_offset < 0 || $posts_offset >= $category['posts_count']) {
if ($category === false || $postsOffset < 0 || $postsOffset >= $category['posts_count']) {
echo render_error(404);
return;
}
@ -77,8 +80,8 @@ if ($category_id !== null) {
ORDER BY `created_at` DESC
LIMIT :offset, :take
');
$getPosts->bindValue('offset', $posts_offset);
$getPosts->bindValue('take', $posts_take);
$getPosts->bindValue('offset', $postsOffset);
$getPosts->bindValue('take', $postsTake);
$getPosts->bindValue('category_id', $category['category_id'], PDO::PARAM_INT);
$posts = $getPosts->execute() ? $getPosts->fetchAll() : false;
@ -121,7 +124,7 @@ $postsCount = (int)$db->query('
$templating->var('posts_count', $postsCount);
if ($posts_offset < 0 || $posts_offset >= $postsCount) {
if ($postsOffset < 0 || $postsOffset >= $postsCount) {
echo render_error(404);
return;
}
@ -144,8 +147,8 @@ $getPosts = $db->prepare('
ORDER BY p.`created_at` DESC
LIMIT :offset, :take
');
$getPosts->bindValue('offset', $posts_offset);
$getPosts->bindValue('take', $posts_take);
$getPosts->bindValue('offset', $postsOffset);
$getPosts->bindValue('take', $postsTake);
$posts = $getPosts->execute() ? $getPosts->fetchAll() : [];
if (!$posts) {

View file

@ -1,2 +1,2 @@
User-agent: *
Disallow:
Disallow: /manage/

View file

@ -7,138 +7,90 @@ require_once __DIR__ . '/../misuzu.php';
$db = Database::connection();
$templating = $app->getTemplating();
$query_offset = (int)($_GET['o'] ?? 0);
$query_take = 15;
$queryOffset = (int)($_GET['o'] ?? 0);
$queryTake = 15;
if (!$app->hasActiveSession()) {
echo render_error(403);
return;
}
$csrf_error_str = "Couldn't verify you, please refresh the page and retry.";
$csrfErrorString = "Couldn't verify you, please refresh the page and retry.";
$settings_profile_fields = [
'twitter' => [
'name' => 'Twitter',
'regex' => '#^(?:https?://(?:www\.)?twitter.com/(?:\#!\/)?)?@?([A-Za-z0-9_]{1,20})/?$#u',
'no-match' => 'Twitter field was invalid.',
],
'osu' => [
'name' => 'osu!',
'regex' => '#^(?:https?://osu.ppy.sh/u(?:sers)?/)?([a-zA-Z0-9-\[\]_ ]{1,20})/?$#u',
'no-match' => 'osu! field was invalid.',
],
'website' => [
'name' => 'Website',
'type' => 'url',
'regex' => '#^((?:https?)://.{1,240})$#u',
'no-match' => 'Website field was invalid.',
],
'youtube' => [
'name' => 'Youtube',
'regex' => '#^(?:https?://(?:www.)?youtube.com/(?:(?:user|c|channel)/)?)?(UC[a-zA-Z0-9-_]{1,22}|[a-zA-Z0-9-_%]{1,100})/?$#u',
'no-match' => 'Youtube field was invalid.',
],
'steam' => [
'name' => 'Steam',
'regex' => '#^(?:https?://(?:www.)?steamcommunity.com/(?:id|profiles)/)?([a-zA-Z0-9_-]{2,100})/?$#u',
'no-match' => 'Steam field was invalid.',
],
'twitchtv' => [
'name' => 'Twitch.tv',
'regex' => '#^(?:https?://(?:www.)?twitch.tv/)?([0-9A-Za-z_]{3,25})/?$#u',
'no-match' => 'Twitch.tv field was invalid.',
],
'lastfm' => [
'name' => 'Last.fm',
'regex' => '#^(?:https?://(?:www.)?last.fm/user/)?([a-zA-Z]{1}[a-zA-Z0-9_-]{1,14})/?$#u',
'no-match' => 'Last.fm field was invalid.',
],
'github' => [
'name' => 'Github',
'regex' => '#^(?:https?://(?:www.)?github.com/?)?([a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38})/?$#u',
'no-match' => 'Github field was invalid.',
],
'skype' => [
'name' => 'Skype',
'regex' => '#^((?:live:)?[a-zA-Z][\w\.,\-_@]{1,100})$#u',
'no-match' => 'Skype field was invalid.',
],
'discord' => [
'name' => 'Discord',
'regex' => '#^(.{1,32}\#[0-9]{4})$#u',
'no-match' => 'Discord field was invalid.',
],
];
$settings_modes = [
$settingsModes = [
'account' => 'Account',
'avatar' => 'Avatar',
'sessions' => 'Sessions',
'login-history' => 'Login History',
];
$settings_mode = $_GET['m'] ?? key($settings_modes);
$settingsMode = $_GET['m'] ?? key($settingsModes);
$templating->vars(compact('settings_mode', 'settings_modes'));
$templating->vars([
'settings_mode' => $settingsMode,
'settings_modes' => $settingsModes,
]);
if (!array_key_exists($settings_mode, $settings_modes)) {
if (!array_key_exists($settingsMode, $settingsModes)) {
http_response_code(404);
$templating->var('settings_title', 'Not Found');
echo $templating->render('settings.notfound');
return;
}
$settings_errors = [];
$settingsErrors = [];
$prevent_registration = $app->getConfig()->get('Auth', 'prevent_registration', 'bool', false);
$avatar_filename = "{$app->getUserId()}.msz";
$avatar_max_width = $app->getConfig()->get('Avatar', 'max_width', 'int', 4000);
$avatar_max_height = $app->getConfig()->get('Avatar', 'max_height', 'int', 4000);
$avatar_max_filesize = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000);
$avatar_max_filesize_human = byte_symbol($avatar_max_filesize, true);
$disableAccountOptions = $app->getConfig()->get('Auth', 'prevent_registration', 'bool', false);
$avatarFileName = "{$app->getUserId()}.msz";
$avatarWidthMax = $app->getConfig()->get('Avatar', 'max_width', 'int', 4000);
$avatarHeightMax = $app->getConfig()->get('Avatar', 'max_height', 'int', 4000);
$avatarFileSizeMax = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
switch ($settings_mode) {
switch ($settingsMode) {
case 'account':
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
$settings_errors[] = $csrf_error_str;
$settingsErrors[] = $csrfErrorString;
break;
}
$updatedUserFields = [];
if (isset($_POST['profile']) && is_array($_POST['profile'])) {
foreach ($settings_profile_fields as $name => $props) {
if (isset($_POST['profile'][$name])) {
$field_value = '';
$setUserFieldErrors = user_profile_fields_set($app->getUserId(), $_POST['profile']);
if (!empty($_POST['profile'][$name])) {
$field_regex = preg_match(
$props['regex'],
$_POST['profile'][$name],
$field_matches
if (count($setUserFieldErrors) > 0) {
foreach ($setUserFieldErrors as $name => $error) {
switch ($error) {
case MSZ_USER_PROFILE_INVALID_FIELD:
$settingsErrors[] = sprintf("Field '%s' does not exist!", $name);
break;
case MSZ_USER_PROFILE_FILTER_FAILED:
$settingsErrors[] = sprintf(
'%s field was invalid!',
user_profile_field_get_display_name($name)
);
break;
if ($field_regex !== 1) {
$settings_errors[] = $props['no-match'];
case MSZ_USER_PROFILE_UPDATE_FAILED:
$settingsErrors[] = 'Failed to update values, contact an administator.';
break;
default:
$settingsErrors[] = 'An unexpected error occurred, contact an administator.';
break;
}
$field_value = $field_matches[1];
}
$updatedUserFields["user_{$name}"] = $field_value;
}
}
}
if (!$prevent_registration) {
if (!$disableAccountOptions) {
if (!empty($_POST['current_password'])
|| (
(isset($_POST['password']) || isset($_OST['email']))
(isset($_POST['password']) || isset($_POST['email']))
&& (!empty($_POST['password']['new']) || !empty($_POST['email']['new']))
)
) {
$updateAccountFields = [];
$fetchPassword = $db->prepare('
SELECT `password`
FROM `msz_users`
@ -148,19 +100,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$currentPassword = $fetchPassword->execute() ? $fetchPassword->fetchColumn() : null;
if (empty($currentPassword)) {
$settings_errors[] = 'Something went horribly wrong.';
$settingsErrors[] = 'Something went horribly wrong.';
break;
}
if (!password_verify($_POST['current_password'], $currentPassword)) {
$settings_errors[] = 'Your current password was incorrect.';
$settingsErrors[] = 'Your current password was incorrect.';
break;
}
if (!empty($_POST['email']['new'])) {
if (empty($_POST['email']['confirm'])
|| $_POST['email']['new'] !== $_POST['email']['confirm']) {
$settings_errors[] = 'The given e-mail addresses did not match.';
$settingsErrors[] = 'The given e-mail addresses did not match.';
break;
}
@ -175,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
: false;
if ($isAlreadySet) {
$settings_errors[] = 'This is your e-mail address already!';
$settingsErrors[] = 'This is your e-mail address already!';
break;
}
@ -184,66 +136,66 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($email_validate !== '') {
switch ($email_validate) {
case 'dns':
$settings_errors[] = 'No valid MX record exists for this domain.';
$settingsErrors[] = 'No valid MX record exists for this domain.';
break;
case 'format':
$settings_errors[] = 'The given e-mail address was incorrectly formatted.';
$settingsErrors[] = 'The given e-mail address was incorrectly formatted.';
break;
case 'in-use':
$settings_errors[] = 'This e-mail address has already been used by another user.';
$settingsErrors[] = 'This e-mail address has already been used by another user.';
break;
default:
$settings_errors[] = 'Unknown e-mail validation error.';
$settingsErrors[] = 'Unknown e-mail validation error.';
}
break;
}
$updatedUserFields['email'] = strtolower($_POST['email']['new']);
$updateAccountFields['email'] = strtolower($_POST['email']['new']);
}
if (!empty($_POST['password']['new'])) {
if (empty($_POST['password']['confirm'])
|| $_POST['password']['new'] !== $_POST['password']['confirm']) {
$settings_errors[] = "The given passwords did not match.";
$settingsErrors[] = "The given passwords did not match.";
break;
}
$password_validate = user_validate_password($_POST['password']['new']);
if ($password_validate !== '') {
$settings_errors[] = "The given passwords was too weak.";
$settingsErrors[] = "The given passwords was too weak.";
break;
}
$updatedUserFields['password'] = password_hash($_POST['password']['new'], PASSWORD_ARGON2I);
}
}
$updateAccountFields['password'] = user_password_hash($_POST['password']['new']);
}
if (count($settings_errors) < 1 && count($updatedUserFields) > 0) {
if (count($updateAccountFields) > 0) {
$updateUser = $db->prepare('
UPDATE `msz_users`
SET ' . pdo_prepare_array_update($updatedUserFields, true) . '
SET ' . pdo_prepare_array_update($updateAccountFields, true) . '
WHERE `user_id` = :user_id
');
$updatedUserFields['user_id'] = $app->getUserId();
$updateUser->execute($updatedUserFields);
$updateAccountFields['user_id'] = $app->getUserId();
$updateUser->execute($updateAccountFields);
}
}
}
break;
case 'avatar':
if (isset($_POST['delete'])) {
if (!tmp_csrf_verify($_POST['delete'])) {
$settings_errors[] = $csrf_error_str;
$settingsErrors[] = $csrfErrorString;
break;
}
$delete_this = [
$app->getStore('avatars/original')->filename($avatar_filename),
$app->getStore('avatars/200x200')->filename($avatar_filename),
$app->getStore('avatars/original')->filename($avatarFileName),
$app->getStore('avatars/200x200')->filename($avatarFileName),
];
foreach ($delete_this as $delete_avatar) {
@ -256,7 +208,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['upload'])) {
if (!tmp_csrf_verify($_POST['upload'])) {
$settings_errors[] = $csrf_error_str;
$settingsErrors[] = $csrfErrorString;
break;
}
@ -265,30 +217,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break;
case UPLOAD_ERR_NO_FILE:
$settings_errors[] = 'Select a file before hitting upload!';
$settingsErrors[] = 'Select a file before hitting upload!';
break;
case UPLOAD_ERR_PARTIAL:
$settings_errors[] = 'The upload was interrupted, please try again!';
$settingsErrors[] = 'The upload was interrupted, please try again!';
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$settings_errors[] = "Your avatar is not allowed to be larger in filesize than {$avatar_max_filesize_human}!";
$settingsErrors[] = sprintf(
'Your avatar is not allowed to be larger in filesize than %s',
byte_symbol($avatarFileSizeMax, true)
);
break;
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
$settings_errors[] = 'Unable to save your avatar, contact an administator!';
$settingsErrors[] = 'Unable to save your avatar, contact an administator!';
break;
case UPLOAD_ERR_EXTENSION:
default:
$settings_errors[] = 'Something happened?';
$settingsErrors[] = 'Something happened?';
break;
}
if (count($settings_errors) > 0) {
if (count($settingsErrors) > 0) {
break;
}
@ -299,24 +254,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|| !in_array($upload_meta[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)
|| $upload_meta[0] < 1
|| $upload_meta[1] < 1) {
$settings_errors[] = 'Please provide a valid image.';
$settingsErrors[] = 'Please provide a valid image.';
break;
}
if ($upload_meta[0] > $avatar_max_width || $upload_meta[1] > $avatar_max_height) {
$settings_errors[] = "Your avatar can't be larger than {$avatar_max_width}x{$avatar_max_height}, yours was {$upload_meta[0]}x{$upload_meta[1]}";
if ($upload_meta[0] > $avatarWidthMax || $upload_meta[1] > $avatarHeightMax) {
$settingsErrors[] = sprintf(
"Your avatar can't be larger than %dx%d, yours was %dx%d",
$avatarWidthMax,
$avatarHeightMax,
$upload_meta[0],
$upload_meta[1]
);
break;
}
if (filesize($upload_path) > $avatar_max_filesize) {
$settings_errors[] = "Your avatar is not allowed to be larger in filesize than {$avatar_max_filesize_human}!";
if (filesize($upload_path) > $avatarFileSizeMax) {
$settingsErrors[] = sprintf(
'Your avatar is not allowed to be larger in filesize than %s!',
byte_symbol($avatarFileSizeMax, true)
);
break;
}
$avatar_path = $app->getStore('avatars/original')->filename($avatar_filename);
$avatar_path = $app->getStore('avatars/original')->filename($avatarFileName);
move_uploaded_file($upload_path, $avatar_path);
$crop_path = $app->getStore('avatars/200x200')->filename($avatar_filename);
$crop_path = $app->getStore('avatars/200x200')->filename($avatarFileName);
if (File::exists($crop_path)) {
File::delete($crop_path);
@ -324,19 +288,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break;
}
$settings_errors[] = "You shouldn't have done that.";
$settingsErrors[] = "You shouldn't have done that.";
break;
case 'sessions':
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
$settings_errors[] = $csrf_error_str;
$settingsErrors[] = $csrfErrorString;
break;
}
$session_id = (int)($_POST['session'] ?? 0);
if ($session_id < 1) {
$settings_errors[] = 'Invalid session.';
$settingsErrors[] = 'Invalid session.';
break;
}
@ -349,7 +313,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$session = $findSession->execute() ? $findSession->fetch() : null;
if (!$session || (int)$session['user_id'] !== $app->getUserId()) {
$settings_errors[] = 'You may only end your own sessions.';
$settingsErrors[] = 'You may only end your own sessions.';
break;
}
@ -358,42 +322,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
return;
}
$deleteSession = $db->prepare('
DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $session['session_id']);
$deleteSession->execute();
user_session_delete($session['session_id']);
break;
}
}
$templating->vars(compact('settings_errors'));
$templating->var('settings_title', $settings_modes[$settings_mode]);
$templating->var('settings_title', $settingsModes[$settingsMode]);
$templating->var('settings_errors', $settingsErrors);
switch ($settings_mode) {
switch ($settingsMode) {
case 'account':
$profileFields = user_profile_fields_get();
$getUserFields = $db->prepare('
SELECT ' . pdo_prepare_array($settings_profile_fields, true, '`user_%s`') . '
SELECT ' . pdo_prepare_array($profileFields, true, '`user_%s`') . '
FROM `msz_users`
WHERE `user_id` = :user_id
');
$getUserFields->bindValue('user_id', $app->getUserId());
$userFields = $getUserFields->execute() ? $getUserFields->fetch() : [];
$templating->var('settings_profile_values', $userFields);
$templating->vars(compact('settings_profile_fields', 'prevent_registration'));
$templating->vars([
'settings_profile_fields' => $profileFields,
'settings_profile_values' => $userFields,
'settings_disable_account_options' => $disableAccountOptions,
]);
break;
case 'avatar':
$user_has_avatar = File::exists($app->getStore('avatars/original')->filename($avatar_filename));
$templating->var('avatar_user_id', $app->getUserId());
$templating->vars(compact(
'avatar_max_width',
'avatar_max_height',
'avatar_max_filesize',
'user_has_avatar'
));
$userHasAvatar = File::exists($app->getStore('avatars/original')->filename($avatarFileName));
$templating->vars([
'avatar_user_id' => $app->getUserId(),
'avatar_max_width' => $avatarWidthMax,
'avatar_max_height' => $avatarHeightMax,
'avatar_max_filesize' => $avatarFileSizeMax,
'user_has_avatar' => $userHasAvatar,
]);
break;
case 'sessions':
@ -414,16 +377,16 @@ switch ($settings_mode) {
ORDER BY `session_id` DESC
LIMIT :offset, :take
');
$getSessions->bindValue('offset', $query_offset);
$getSessions->bindValue('take', $query_take);
$getSessions->bindValue('offset', $queryOffset);
$getSessions->bindValue('take', $queryTake);
$getSessions->bindValue('user_id', $app->getUserId());
$sessions = $getSessions->execute() ? $getSessions->fetchAll() : [];
$templating->vars([
'active_session_id' => $app->getSessionId(),
'user_sessions' => $sessions,
'sessions_offset' => $query_offset,
'sessions_take' => $query_take,
'sessions_offset' => $queryOffset,
'sessions_take' => $queryTake,
'sessions_count' => $sessionCount,
]);
break;
@ -446,18 +409,18 @@ switch ($settings_mode) {
ORDER BY `attempt_id` DESC
LIMIT :offset, :take
');
$getLoginAttempts->bindValue('offset', $query_offset);
$getLoginAttempts->bindValue('take', $query_take);
$getLoginAttempts->bindValue('offset', $queryOffset);
$getLoginAttempts->bindValue('take', $queryTake);
$getLoginAttempts->bindValue('user_id', $app->getUserId());
$loginAttempts = $getLoginAttempts->execute() ? $getLoginAttempts->fetchAll() : [];
$templating->vars([
'user_login_attempts' => $loginAttempts,
'login_attempts_offset' => $query_offset,
'login_attempts_take' => $query_take,
'login_attempts_offset' => $queryOffset,
'login_attempts_take' => $queryTake,
'login_attempts_count' => $loginAttemptsCount,
]);
break;
}
echo $templating->render("settings.{$settings_mode}");
echo $templating->render("settings.{$settingsMode}");

View file

@ -17,3 +17,19 @@ function user_login_attempt_record(bool $success, ?int $userId, string $ipAddres
$storeAttempt->bindValue('user_id', $userId, $userId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
$storeAttempt->execute();
}
function user_login_attempts_remaining(string $ipAddress): int
{
$getRemaining = Database::connection()->prepare('
SELECT 5 - COUNT(`attempt_id`)
FROM `msz_login_attempts`
WHERE `was_successful` = false
AND `created_at` > NOW() - INTERVAL 1 HOUR
AND `attempt_ip` = INET6_ATON(:remote_ip)
');
$getRemaining->bindValue('remote_ip', $ipAddress);
return $getRemaining->execute()
? (int)$getRemaining->fetchColumn()
: 0;
}

141
src/Users/profile.php Normal file
View file

@ -0,0 +1,141 @@
<?php
use Misuzu\Database;
define('MSZ_USER_PROFILE_SET_ERROR', '_err');
define('MSZ_USER_PROFILE_INVALID_FIELD', 1);
define('MSZ_USER_PROFILE_FILTER_FAILED', 2);
define('MSZ_USER_PROFILE_UPDATE_FAILED', 3);
define('MSZ_USER_PROFILE_FIELD_SET_ERRORS', [
MSZ_USER_PROFILE_INVALID_FIELD,
MSZ_USER_PROFILE_FILTER_FAILED,
MSZ_USER_PROFILE_UPDATE_FAILED,
]);
define('MSZ_USER_PROFILE_FIELD_FORMAT', 'user_%s');
define('MSZ_USER_PROFILE_FIELDS', [
'twitter' => [
'name' => 'Twitter',
'regex' => '#^(?:https?://(?:www\.)?twitter.com/(?:\#!\/)?)?@?([A-Za-z0-9_]{1,20})/?$#u',
],
'osu' => [
'name' => 'osu!',
'regex' => '#^(?:https?://osu.ppy.sh/u(?:sers)?/)?([a-zA-Z0-9-\[\]_ ]{1,20})/?$#u',
],
'website' => [
'name' => 'Website',
'type' => 'url',
'regex' => '#^((?:https?)://.{1,240})$#u',
],
'youtube' => [
'name' => 'Youtube',
'regex' => '#^(?:https?://(?:www.)?youtube.com/(?:(?:user|c|channel)/)?)?(UC[a-zA-Z0-9-_]{1,22}|[a-zA-Z0-9-_%]{1,100})/?$#u',
],
'steam' => [
'name' => 'Steam',
'regex' => '#^(?:https?://(?:www.)?steamcommunity.com/(?:id|profiles)/)?([a-zA-Z0-9_-]{2,100})/?$#u',
],
'twitchtv' => [
'name' => 'Twitch.tv',
'regex' => '#^(?:https?://(?:www.)?twitch.tv/)?([0-9A-Za-z_]{3,25})/?$#u',
],
'lastfm' => [
'name' => 'Last.fm',
'regex' => '#^(?:https?://(?:www.)?last.fm/user/)?([a-zA-Z]{1}[a-zA-Z0-9_-]{1,14})/?$#u',
],
'github' => [
'name' => 'Github',
'regex' => '#^(?:https?://(?:www.)?github.com/?)?([a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38})/?$#u',
],
'skype' => [
'name' => 'Skype',
'regex' => '#^((?:live:)?[a-zA-Z][\w\.,\-_@]{1,100})$#u',
],
'discord' => [
'name' => 'Discord',
'regex' => '#^(.{1,32}\#[0-9]{4})$#u',
],
]);
function user_profile_field_is_valid(string $name): bool
{
return array_key_exists($name, MSZ_USER_PROFILE_FIELDS);
}
function user_profile_field_get_display_name(string $name): string
{
return MSZ_USER_PROFILE_FIELDS[$name]['name'] ?? '';
}
function user_profile_fields_get(): array
{
return MSZ_USER_PROFILE_FIELDS;
}
function user_profile_field_get_regex(string $name): string
{
return MSZ_USER_PROFILE_FIELDS[$name]['regex'] ?? '';
}
// === NULL if field is invalid
function user_profile_field_filter(string $name, string $value): ?string
{
if (!user_profile_field_is_valid($name)) {
return null;
}
$regex = user_profile_field_get_regex($name);
if (empty($regex) || empty($value)) {
return $value;
}
$checkRegex = preg_match($regex, $value, $matches);
if (!$checkRegex || empty($matches[1])) {
return null;
}
return $matches[1];
}
function user_profile_fields_set(int $userId, array $fields): array
{
if (count($fields) < 1) {
return [];
}
$errors = [];
$values = [];
foreach ($fields as $name => $value) {
// should these just be ignored?
if (!user_profile_field_is_valid($name)) {
$errors[$name] = MSZ_USER_PROFILE_INVALID_FIELD;
continue;
}
$value = user_profile_field_filter($name, $value);
if ($value === null) {
$errors[$name] = MSZ_USER_PROFILE_FILTER_FAILED;
continue;
}
$values[sprintf(MSZ_USER_PROFILE_FIELD_FORMAT, $name)] = $value;
}
if (count($values) > 0) {
$updateFields = Database::connection()->prepare('
UPDATE `msz_users`
SET ' . pdo_prepare_array_update($values, true) . '
WHERE `user_id` = :user_id
');
$values['user_id'] = $userId;
if (!$updateFields->execute($values)) {
$errors[MSZ_USER_PROFILE_SET_ERROR] = MSZ_USER_PROFILE_UPDATE_FAILED;
}
}
return $errors;
}

17
src/Users/role.php Normal file
View file

@ -0,0 +1,17 @@
<?php
use Misuzu\Database;
define('MSZ_ROLE_MAIN', 1);
function user_role_add(int $userId, int $roleId): bool
{
$addRole = Database::connection()->prepare('
INSERT INTO `msz_user_roles`
(`user_id`, `role_id`)
VALUES
(:user_id, :role_id)
');
$addRole->bindValue('user_id', $userId);
$addRole->bindValue('role_id', $roleId);
return $addRole->execute();
}

47
src/Users/session.php Normal file
View file

@ -0,0 +1,47 @@
<?php
use Misuzu\Database;
define('MSZ_SESSION_KEY_SIZE', 64);
function user_session_create(
int $userId,
string $ipAddress,
string $userAgent
): string {
$sessionKey = user_session_generate_key();
$createSession = Database::connection()->prepare('
INSERT INTO `msz_sessions`
(
`user_id`, `session_ip`, `session_country`,
`user_agent`, `session_key`, `created_at`, `expires_on`
)
VALUES
(
:user_id, INET6_ATON(:session_ip), :session_country,
:user_agent, :session_key, NOW(), NOW() + INTERVAL 1 MONTH
)
');
$createSession->bindValue('user_id', $userId);
$createSession->bindValue('session_ip', $ipAddress);
$createSession->bindValue('session_country', get_country_code($ipAddress));
$createSession->bindValue('user_agent', $userAgent);
$createSession->bindValue('session_key', $sessionKey);
return $createSession->execute() ? $sessionKey : '';
}
function user_session_delete(int $sessionId): bool
{
$deleteSession = Database::connection()->prepare('
DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $sessionId);
return $deleteSession->execute();
}
function user_session_generate_key(): string
{
return bin2hex(random_bytes(MSZ_SESSION_KEY_SIZE / 2));
}

55
src/Users/user.php Normal file
View file

@ -0,0 +1,55 @@
<?php
use Misuzu\Database;
define('MSZ_USERS_PASSWORD_HASH_ALGO', PASSWORD_ARGON2I);
function user_create(
string $username,
string $password,
string $email,
string $ipAddress
): int {
$dbc = Database::connection();
$createUser = $dbc->prepare('
INSERT INTO `msz_users`
(
`username`, `password`, `email`, `register_ip`,
`last_ip`, `user_country`, `created_at`, `display_role`
)
VALUES
(
:username, :password, :email, INET6_ATON(:register_ip),
INET6_ATON(:last_ip), :user_country, NOW(), 1
)
');
$createUser->bindValue('username', $username);
$createUser->bindValue('password', user_password_hash($password));
$createUser->bindValue('email', $email);
$createUser->bindValue('register_ip', $ipAddress);
$createUser->bindValue('last_ip', $ipAddress);
$createUser->bindValue('user_country', get_country_code($ipAddress));
return $createUser->execute() ? (int)$dbc->lastInsertId() : 0;
}
function user_password_hash(string $password): string
{
return password_hash($password, MSZ_USERS_PASSWORD_HASH_ALGO);
}
// Temporary key generation for chat login.
// Should eventually be replaced with a callback login system.
function user_generate_chat_key(int $userId): string
{
$chatKey = bin2hex(random_bytes(16));
$setChatKey = Database::connection()->prepare('
UPDATE `msz_users`
SET `user_chat_key` = :user_chat_key
WHERE `user_id` = :user_id
');
$setChatKey->bindValue('user_chat_key', $chatKey);
$setChatKey->bindValue('user_id', $userId);
return $setChatKey->execute() ? $chatKey : '';
}

View file

@ -105,6 +105,7 @@
<title>Flashii Broom Closet</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<link href="/css/manage.css" rel="stylesheet">
<link href="/css/libraries.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body class="manage">
@ -164,5 +165,6 @@
{{ link('https://github.com/flashwave/misuzu/tree/' ~ git_branch(), git_branch(), 'footer__link') }} # {{ link('https://github.com/flashwave/misuzu/commit/' ~ git_hash(true), git_hash(), 'footer__link') }}
</footer>
</div>
<script src="/js/libraries.js" charset="utf-8"></script>
</body>
</html>

View file

@ -6,10 +6,12 @@
{% for user in manage_users %}
<a href="?v=view&amp;u={{ user.user_id }}" class="listing__entry user-listing__entry"{% if not user.colour|colour_get_inherit %} style="border-color: {{ user.colour|colour_get_css }}"{% endif %}>
<div class="listing__entry__content user-listing__entry__content">
<div class="listing__entry__column user-listing__entry__column user-listing__entry__column--username">
<div class="user-listing__info">
<div class="user-listing__username">
{{ user.username }}
</div>
<div class="user-listing__avatar" style="background-image:url('/profile.php?u={{ user.user_id }}&amp;m=avatar');"></div>
</div>
<div class="avatar user-listing__avatar" style="background-image:url('/profile.php?u={{ user.user_id }}&amp;m=avatar');"></div>
</div>
</a>
{% endfor %}

View file

@ -18,7 +18,7 @@
<a class="container__title container__title--link" href="/news.php">Featured News</a>
<div class="container__content news__container__content">
<div class="news__preview__listing">
{% for post in featuredNews %}
{% for post in featured_news %}
{{ news_preview(post) }}
{% endfor %}
</div>

View file

@ -18,7 +18,7 @@
{% endfor %}
</div>
{% if prevent_registration %}
{% if settings_disable_account_options %}
<div class="settings__account__column settings__account__column--no-margin settings__account__column--disabled">
<div class="settings__account__row">
<div class="settings__account__column">