bye bye ORM (successfully this time!)

This commit is contained in:
flash 2018-05-16 04:58:21 +02:00
parent b9f0e88f25
commit 966301c21e
44 changed files with 845 additions and 1316 deletions

View file

@ -14,7 +14,6 @@
"nesbot/carbon": "~1.22",
"illuminate/database": "~5.5",
"illuminate/filesystem": "~5.5",
"illuminate/pagination": "~5.5",
"doctrine/dbal": "~2.6",
"swiftmailer/swiftmailer": "~6.0",
"erusev/parsedown": "~1.6",

146
composer.lock generated
View file

@ -1,10 +1,10 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bfc5b8cbdbf22514c4b51ae1af8c333b",
"content-hash": "640791cee4f0e5a55e8fe6829d68c99b",
"packages": [
{
"name": "composer/ca-bundle",
@ -697,16 +697,16 @@
},
{
"name": "illuminate/container",
"version": "v5.6.17",
"version": "v5.6.22",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
"reference": "4a42d667a05ec6d31f05b532cdac7e8e68e5ea2a"
"reference": "1a29b314dd5c7a5a5bce3dd7b9da924cc1ec22ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/container/zipball/4a42d667a05ec6d31f05b532cdac7e8e68e5ea2a",
"reference": "4a42d667a05ec6d31f05b532cdac7e8e68e5ea2a",
"url": "https://api.github.com/repos/illuminate/container/zipball/1a29b314dd5c7a5a5bce3dd7b9da924cc1ec22ec",
"reference": "1a29b314dd5c7a5a5bce3dd7b9da924cc1ec22ec",
"shasum": ""
},
"require": {
@ -737,20 +737,20 @@
],
"description": "The Illuminate Container package.",
"homepage": "https://laravel.com",
"time": "2018-01-21T02:13:38+00:00"
"time": "2018-05-14T12:49:42+00:00"
},
{
"name": "illuminate/contracts",
"version": "v5.6.17",
"version": "v5.6.22",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "322ec80498b3bf85bc4025d028e130a9b50242b9"
"reference": "3dc639feabe0f302f574157a782ede323881a944"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/322ec80498b3bf85bc4025d028e130a9b50242b9",
"reference": "322ec80498b3bf85bc4025d028e130a9b50242b9",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/3dc639feabe0f302f574157a782ede323881a944",
"reference": "3dc639feabe0f302f574157a782ede323881a944",
"shasum": ""
},
"require": {
@ -781,20 +781,20 @@
],
"description": "The Illuminate Contracts package.",
"homepage": "https://laravel.com",
"time": "2018-04-07T17:05:26+00:00"
"time": "2018-05-11T23:38:58+00:00"
},
{
"name": "illuminate/database",
"version": "v5.6.17",
"version": "v5.6.22",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
"reference": "a949e082dbb520fdcb2798e0a5408669724aa197"
"reference": "713d376a2e2b9c7cded8916125c24b7b3adb4984"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/database/zipball/a949e082dbb520fdcb2798e0a5408669724aa197",
"reference": "a949e082dbb520fdcb2798e0a5408669724aa197",
"url": "https://api.github.com/repos/illuminate/database/zipball/713d376a2e2b9c7cded8916125c24b7b3adb4984",
"reference": "713d376a2e2b9c7cded8916125c24b7b3adb4984",
"shasum": ""
},
"require": {
@ -840,20 +840,20 @@
"orm",
"sql"
],
"time": "2018-04-17T12:36:27+00:00"
"time": "2018-05-14T22:33:47+00:00"
},
{
"name": "illuminate/filesystem",
"version": "v5.6.17",
"version": "v5.6.22",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
"reference": "c9ab9376076cedd88a374d7281d62b619634d578"
"reference": "a4ca4a9c2f969ec227748ab334693144995ba0ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/filesystem/zipball/c9ab9376076cedd88a374d7281d62b619634d578",
"reference": "c9ab9376076cedd88a374d7281d62b619634d578",
"url": "https://api.github.com/repos/illuminate/filesystem/zipball/a4ca4a9c2f969ec227748ab334693144995ba0ce",
"reference": "a4ca4a9c2f969ec227748ab334693144995ba0ce",
"shasum": ""
},
"require": {
@ -892,64 +892,20 @@
],
"description": "The Illuminate Filesystem package.",
"homepage": "https://laravel.com",
"time": "2018-04-06T13:15:37+00:00"
},
{
"name": "illuminate/pagination",
"version": "v5.6.17",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pagination.git",
"reference": "77e9cfd4daf526aab9bf9c75ee1676f3ba6dff51"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/pagination/zipball/77e9cfd4daf526aab9bf9c75ee1676f3ba6dff51",
"reference": "77e9cfd4daf526aab9bf9c75ee1676f3ba6dff51",
"shasum": ""
},
"require": {
"illuminate/contracts": "5.6.*",
"illuminate/support": "5.6.*",
"php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Pagination\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Pagination package.",
"homepage": "https://laravel.com",
"time": "2018-03-26T13:17:45+00:00"
"time": "2018-05-02T16:10:37+00:00"
},
{
"name": "illuminate/support",
"version": "v5.6.17",
"version": "v5.6.22",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "cc8d6f5cef3a901de6bb7d1b362102a6db001085"
"reference": "2ef559ad8840481d5247bd7ebfd04eb37d3f6889"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/cc8d6f5cef3a901de6bb7d1b362102a6db001085",
"reference": "cc8d6f5cef3a901de6bb7d1b362102a6db001085",
"url": "https://api.github.com/repos/illuminate/support/zipball/2ef559ad8840481d5247bd7ebfd04eb37d3f6889",
"reference": "2ef559ad8840481d5247bd7ebfd04eb37d3f6889",
"shasum": ""
},
"require": {
@ -993,7 +949,7 @@
],
"description": "The Illuminate Support package.",
"homepage": "https://laravel.com",
"time": "2018-04-17T12:26:47+00:00"
"time": "2018-05-12T17:43:47+00:00"
},
{
"name": "maxmind-db/reader",
@ -1099,16 +1055,16 @@
},
{
"name": "nesbot/carbon",
"version": "1.26.4",
"version": "1.27.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "e3d9014279133a3cccc01f6a691322a2d5a6a87b"
"reference": "ef81c39b67200dcd7401c24363dcac05ac3a4fe9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e3d9014279133a3cccc01f6a691322a2d5a6a87b",
"reference": "e3d9014279133a3cccc01f6a691322a2d5a6a87b",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/ef81c39b67200dcd7401c24363dcac05ac3a4fe9",
"reference": "ef81c39b67200dcd7401c24363dcac05ac3a4fe9",
"shasum": ""
},
"require": {
@ -1143,7 +1099,7 @@
"datetime",
"time"
],
"time": "2018-04-17T15:35:42+00:00"
"time": "2018-04-23T09:02:57+00:00"
},
{
"name": "psr/container",
@ -1299,7 +1255,7 @@
},
{
"name": "symfony/finder",
"version": "v4.0.8",
"version": "v4.0.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
@ -1348,16 +1304,16 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.7.0",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
"reference": "3296adf6a6454a050679cde90f95350ad604b171"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
"reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
"reference": "3296adf6a6454a050679cde90f95350ad604b171",
"shasum": ""
},
"require": {
@ -1369,7 +1325,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7-dev"
"dev-master": "1.8-dev"
}
},
"autoload": {
@ -1403,20 +1359,20 @@
"portable",
"shim"
],
"time": "2018-01-30T19:27:44+00:00"
"time": "2018-04-26T10:06:28+00:00"
},
{
"name": "symfony/translation",
"version": "v4.0.8",
"version": "v4.0.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "e20a9b7f9f62cb33a11638b345c248e7d510c938"
"reference": "ad3abf08eb3450491d8d76513100ef58194cd13e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/e20a9b7f9f62cb33a11638b345c248e7d510c938",
"reference": "e20a9b7f9f62cb33a11638b345c248e7d510c938",
"url": "https://api.github.com/repos/symfony/translation/zipball/ad3abf08eb3450491d8d76513100ef58194cd13e",
"reference": "ad3abf08eb3450491d8d76513100ef58194cd13e",
"shasum": ""
},
"require": {
@ -1437,7 +1393,7 @@
"symfony/yaml": "~3.4|~4.0"
},
"suggest": {
"psr/log": "To use logging capability in translator",
"psr/log-implementation": "To use logging capability in translator",
"symfony/config": "",
"symfony/yaml": ""
},
@ -1471,7 +1427,7 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2018-02-22T10:50:29+00:00"
"time": "2018-04-30T01:23:47+00:00"
},
{
"name": "twig/twig",
@ -1896,23 +1852,23 @@
},
{
"name": "phpspec/prophecy",
"version": "1.7.5",
"version": "1.7.6",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401"
"reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401",
"reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
"reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
"sebastian/comparator": "^1.1|^2.0",
"sebastian/comparator": "^1.1|^2.0|^3.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"require-dev": {
@ -1955,7 +1911,7 @@
"spy",
"stub"
],
"time": "2018-02-19T10:16:54+00:00"
"time": "2018-04-18T13:57:24+00:00"
},
{
"name": "phpunit/php-code-coverage",

View file

@ -4,6 +4,8 @@ namespace Misuzu;
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/colour.php';
require_once __DIR__ . '/src/zalgo.php';
require_once __DIR__ . '/src/Users/login_attempt.php';
require_once __DIR__ . '/src/Users/validation.php';
$app = new Application(
__DIR__ . '/config/config.ini',
@ -31,24 +33,44 @@ if (PHP_SAPI !== 'cli') {
exit;
}
$app->startTemplating();
$app->getTemplating()->addPath('mio', __DIR__ . '/views/mio');
if (isset($_COOKIE['msz_uid'], $_COOKIE['msz_sid'])) {
$app->startSession((int)$_COOKIE['msz_uid'], $_COOKIE['msz_sid']);
$session = $app->getSession();
if ($session !== null) {
$session->user->last_seen = \Carbon\Carbon::now();
$session->user->last_ip = Net\IPAddress::remote();
$session->user->save();
if ($app->hasActiveSession()) {
$db = Database::connection();
$bumpUserLast = $db->prepare('
UPDATE `msz_users` SET
`last_seen` = NOW(),
`last_ip` = INET6_ATON(:last_ip)
WHERE `user_id` = :user_id
');
$bumpUserLast->bindValue('last_ip', Net\IPAddress::remote()->getString());
$bumpUserLast->bindValue('user_id', $app->getUserId());
$bumpUserLast->execute();
$getUserDisplayInfo = $db->prepare('
SELECT
u.`user_id`, u.`username`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE `user_id` = :user_id
');
$getUserDisplayInfo->bindValue('user_id', $app->getUserId());
$userDisplayInfo = $getUserDisplayInfo->execute() ? $getUserDisplayInfo->fetch() : [];
$app->getTemplating()->var('current_user', $userDisplayInfo);
}
}
$manage_mode = starts_with($_SERVER['REQUEST_URI'], '/manage');
$app->startTemplating();
$app->getTemplating()->addPath('mio', __DIR__ . '/views/mio');
if ($manage_mode) {
if ($app->getSession() === null || $app->getSession()->user->user_id !== 1) {
if ($app->getUserId() !== 1) {
http_response_code(403);
echo $app->getTemplating()->render('errors.403');
exit;

View file

@ -6,6 +6,7 @@
namespace Misuzu;
exit;
use Illuminate\Database\Migrations\DatabaseMigrationRepository;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Filesystem\Filesystem;

View file

@ -6,19 +6,51 @@
namespace Misuzu;
use Misuzu\Users\Role;
use Misuzu\Users\User;
use Misuzu\Database;
require_once __DIR__ . '/misuzu.php';
$role = Role::find(1);
$db = Database::connection();
if ($role === null) {
$role = Role::createRole('Member');
$mainRoleId = (int)$db->query('
SELECT `role_id`
FROM `msz_roles`
WHERE `role_id` = 1
')->fetchColumn();
if ($mainRoleId !== 1) {
$db->query("
REPLACE INTO `msz_roles`
(`role_id`, `role_name`, `role_hierarchy`, `role_colour`, `role_description`, `created_at`)
VALUES
(1, 'Member', 1, 1073741824, NULL, NOW())
");
$mainRoleId = 1;
}
foreach (User::all() as $user) {
if (!$user->hasRole($role)) {
$user->addRole($role);
}
$notInMainRole = $db->query('
SELECT `user_id`
FROM `msz_users` as u
WHERE NOT EXISTS (
SELECT 1
FROM `msz_user_roles` as ur
WHERE `role_id` = 1
AND u.`user_id` = ur.`user_id`
)
')->fetchAll();
if (count($notInMainRole) < 1) {
exit;
}
$addToMainRole = $db->prepare('
INSERT INTO `msz_user_roles`
(`user_id`, `role_id`)
VALUES
(:user_id, 1)
');
foreach ($notInMainRole as $user) {
$addToMainRole->execute($user);
}

View file

@ -1,21 +1,19 @@
<?php
use Carbon\Carbon;
use Misuzu\Database;
use Misuzu\Net\IPAddress;
use Misuzu\Users\Role;
use Misuzu\Users\User;
use Misuzu\Users\Session;
use Misuzu\Users\LoginAttempt;
require_once __DIR__ . '/../misuzu.php';
$db = Database::connection();
$config = $app->getConfig();
$templating = $app->getTemplating();
$session = $app->getSession();
$username_validation_errors = [
'trim' => 'Your username may not start or end with spaces!',
'short' => "Your username is too short, it has to be at least " . User::USERNAME_MIN_LENGTH . " characters!",
'long' => "Your username is too long, it can't be longer than " . User::USERNAME_MAX_LENGTH . " characters!",
'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!",
'double-spaces' => "Your username can't contain double spaces.",
'invalid' => 'Your username contains invalid characters.',
'spacing' => 'Please use either underscores or spaces, not both!',
@ -38,17 +36,20 @@ if (!empty($_REQUEST['email'])) {
switch ($mode) {
case 'logout':
if ($session === null) {
if (!$app->hasActiveSession()) {
header('Location: /');
return;
}
// this is temporary, don't scream at me for using md5
if (isset($_GET['s']) && tmp_csrf_verify($_GET['s'])) {
set_cookie_m('uid', '', -3600);
set_cookie_m('sid', '', -3600);
$session->delete();
$app->setSession(null);
$deleteSession = $db->prepare('
DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $app->getSessionId());
$deleteSession->execute();
header('Location: /');
return;
}
@ -57,7 +58,7 @@ switch ($mode) {
break;
case 'login':
if ($session !== null) {
if ($app->hasActiveSession()) {
header('Location: /');
break;
}
@ -66,56 +67,103 @@ switch ($mode) {
$auth_login_error = '';
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ipAddress = IPAddress::remote();
$ipAddressObj = IPAddress::remote();
$ipAddress = $ipAddressObj->getString();
if (!isset($_POST['username'], $_POST['password'])) {
$auth_login_error = "You didn't fill all the forms!";
break;
}
$loginAttempts = LoginAttempt::fromIpAddress(IPAddress::remote())
->where('was_successful', false)
->where('created_at', '>', Carbon::now()->subHour()->toDateTimeString())
->get();
$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;
if ($loginAttempts->count() >= 5) {
if ($remainingAttempts < 1) {
$auth_login_error = 'Too many failed login attempts, try again later.';
break;
}
$remainingAttempts -= 1;
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$user = User::findLogin($username);
$getUser = $db->prepare('
SELECT `user_id`, `password`
FROM `msz_users`
WHERE LOWER(`email`) = LOWER(:email)
OR LOWER(`username`) = LOWER(:username)
');
$getUser->bindValue('email', $username);
$getUser->bindValue('username', $username);
$userData = $getUser->execute() ? $getUser->fetch() : [];
$userId = (int)($userData['user_id'] ?? 0);
if ($user === null) {
LoginAttempt::recordFail($ipAddress, null, $user_agent);
$auth_login_error = 'Invalid username or password!';
$auth_error_str = "Invalid username or password, {$remainingAttempts} attempt(s) remaining.";
if ($userId < 1) {
user_login_attempt_record(false, null, $ipAddress, $user_agent);
$auth_login_error = $auth_error_str;
break;
}
if (!$user->verifyPassword($password)) {
LoginAttempt::recordFail($ipAddress, $user, $user_agent);
$auth_login_error = 'Invalid username or password!';
if (!password_verify($password, $userData['password'])) {
user_login_attempt_record(false, $userId, $ipAddress, $user_agent);
$auth_login_error = $auth_error_str;
break;
}
LoginAttempt::recordSuccess($ipAddress, $user, $user_agent);
user_login_attempt_record(true, $userId, $ipAddress, $user_agent);
$session = Session::createSession($user, $user_agent, null, $ipAddress);
$app->setSession($session);
$cookie_life = Carbon::now()->addMonth()->timestamp;
set_cookie_m('uid', $session->user_id, $cookie_life);
set_cookie_m('sid', $session->session_key, $cookie_life);
$sessionKey = bin2hex(random_bytes(32));
$createSession = $db->prepare('
INSERT INTO `msz_sessions`
(`user_id`, `session_ip`, `user_agent`, `session_key`, `created_at`, `expires_on`)
VALUES
(:user_id, INET6_ATON(:session_ip), :user_agent, :session_key, NOW(), NOW() + INTERVAL 1 MONTH)
');
$createSession->bindValue('user_id', $userId);
$createSession->bindValue('session_ip', $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.';
break;
}
$app->startSession($userId, $sessionKey);
$cookieLife = Carbon::now()->addMonth()->timestamp;
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.
$user->user_chat_key = bin2hex(random_bytes(16));
$user->save();
$chatKey = bin2hex(random_bytes(16));
$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()) {
setcookie('msz_tmp_id', $userId, $cookieLife, '/', '.flashii.net');
setcookie('msz_tmp_key', $chatKey, $cookieLife, '/', '.flashii.net');
}
setcookie('msz_tmp_id', $user->user_id, $cookie_life, '/', '.flashii.net');
setcookie('msz_tmp_key', $user->user_chat_key, $cookie_life, '/', '.flashii.net');
header('Location: /');
return;
}
@ -128,7 +176,7 @@ switch ($mode) {
break;
case 'register':
if ($session !== null) {
if ($app->hasActiveSession()) {
header('Location: /');
}
@ -149,13 +197,13 @@ switch ($mode) {
$password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? '';
$username_validate = User::validateUsername($username, true);
$username_validate = user_validate_username($username, true);
if ($username_validate !== '') {
$auth_register_error = $username_validation_errors[$username_validate];
break;
}
$email_validate = User::validateEmail($email, true);
$email_validate = user_validate_email($email, true);
if ($email_validate !== '') {
$auth_register_error = $email_validate === 'in-use'
? 'This e-mail address has already been used!'
@ -163,13 +211,45 @@ switch ($mode) {
break;
}
if (User::validatePassword($password) !== '') {
if (user_validate_password($password) !== '') {
$auth_register_error = 'Your password is too weak!';
break;
}
$user = User::createUser($username, $password, $email);
$user->addRole(Role::find(1), true);
$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));
if (!$createUser->execute()) {
$auth_register_error = '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();
$templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
break;
}

View file

@ -1,33 +1,24 @@
<?php
use Misuzu\Database;
use Misuzu\News\NewsPost;
require_once __DIR__ . '/../misuzu.php';
//$featured_news = NewsPost::where('is_featured', true)->orderBy('created_at', 'desc')->take(3)->get();
$featuredNews = [];
$fetchNews = Database::connection()
$featuredNews = Database::connection()
->query('
SELECT
SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`,
u.`user_id`, u.`username`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `display_colour`
FROM `msz_news_posts` as p
LEFT JOIN `msz_users` as u
ON p.`user_id` = u.`user_id`
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE p.`is_featured` = true
ORDER BY p.`created_at` DESC
LIMIT 3
');
while (($newsPost = $fetchNews->fetchObject(NewsPost::class)) !== false) {
$featuredNews['post'] = $newsPost;
}
var_dump($featuredNews);
FROM `msz_news_posts` as p
LEFT JOIN `msz_users` as u
ON p.`user_id` = u.`user_id`
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE p.`is_featured` = true
ORDER BY p.`created_at` DESC
LIMIT 3
')->fetchAll();
//var_dump(Database::connection()->query('SHOW SESSION STATUS LIKE "Questions"')->fetch());
echo $app->getTemplating()->render('home.landing', compact('featuredNews'));

View file

@ -1,9 +1,9 @@
<?php
use Misuzu\Users\Role;
use Misuzu\Users\User;
use Misuzu\Database;
require_once __DIR__ . '/../../misuzu.php';
$db = Database::connection();
$templating = $app->getTemplating();
$is_post_request = $_SERVER['REQUEST_METHOD'] === 'POST';
@ -11,7 +11,17 @@ $page_id = (int)($_GET['p'] ?? 1);
switch ($_GET['v'] ?? null) {
case 'listing':
$manage_users = User::paginate(32, ['*'], 'p', $page_id);
$manage_users = $db->query('
SELECT
u.`user_id`, u.`username`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
LIMIT 0, 32
')->fetchAll();
//$manage_users = UserV1::paginate(32, ['*'], 'p', $page_id);
$templating->vars(compact('manage_users'));
echo $templating->render('@manage.users.listing');
break;
@ -24,19 +34,44 @@ switch ($_GET['v'] ?? null) {
break;
}
$view_user = User::find($user_id);
$getUser = $db->prepare('
SELECT
u.*,
INET6_NTOA(u.`register_ip`) as `register_ip_decoded`,
INET6_NTOA(u.`last_ip`) as `last_ip_decoded`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE `user_id` = :user_id
');
$getUser->bindValue('user_id', $user_id);
$getUser->execute();
$manageUser = $getUser->execute() ? $getUser->fetch() : [];
if ($view_user === null) {
if (!$manageUser) {
echo 'Could not find that user.';
break;
}
$templating->var('view_user', $view_user);
$templating->var('view_user', $manageUser);
echo $templating->render('@manage.users.view');
break;
case 'roles':
$manage_roles = Role::paginate(32, ['*'], 'p', $page_id);
$manage_roles = $db->query('
SELECT
`role_id`, `role_colour`, `role_name`,
(
SELECT COUNT(`user_id`)
FROM `msz_user_roles` as ur
WHERE ur.`role_id` = r.`role_id`
) as `users`
FROM `msz_roles` as r
LIMIT 0, 10
')->fetchAll();
//$manage_roles = Role::paginate(10, ['*'], 'p', $page_id);
$templating->vars(compact('manage_roles'));
echo $templating->render('@manage.users.roles');
break;
@ -97,15 +132,38 @@ switch ($_GET['v'] ?? null) {
break;
}
$edit_role = $role_id < 1 ? new Role : Role::find($role_id);
$edit_role->role_name = $role_name;
$edit_role->role_hierarchy = $role_hierarchy;
$edit_role->role_secret = $role_secret;
$edit_role->role_colour = $role_colour;
$edit_role->role_description = $role_description;
$edit_role->save();
if ($role_id < 1) {
$updateRole = $db->prepare('
INSERT INTO `msz_roles`
(`role_name`, `role_hierarchy`, `role_secret`, `role_colour`, `role_description`, `created_at`)
VALUES
(:role_name, :role_hierarchy, :role_secret, :role_colour, :role_description, NOW())
');
} else {
$updateRole = $db->prepare('
UPDATE `msz_roles` SET
`role_name` = :role_name,
`role_hierarchy` = :role_hierarchy,
`role_secret` = :role_secret,
`role_colour` = :role_colour,
`role_description` = :role_description
WHERE `role_id` = :role_id
');
$updateRole->bindValue('role_id', $role_id);
}
header("Location: ?v=role&r={$edit_role->role_id}");
$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->execute();
if ($role_id < 1) {
$role_id = (int)$db->lastInsertId();
}
header("Location: ?v=role&r={$role_id}");
break;
}
@ -115,9 +173,15 @@ switch ($_GET['v'] ?? null) {
break;
}
$edit_role = Role::find($role_id);
$getEditRole = $db->prepare('
SELECT *
FROM `msz_roles`
WHERE `role_id` = :role_id
');
$getEditRole->bindValue('role_id', $role_id);
$edit_role = $getEditRole->execute() ? $getEditRole->fetch() : [];
if ($edit_role === null) {
if (!$edit_role) {
echo 'invalid role';
break;
}

View file

@ -1,9 +1,9 @@
<?php
use Misuzu\News\NewsCategory;
use Misuzu\News\NewsPost;
use Misuzu\Database;
require_once __DIR__ . '/../misuzu.php';
$db = Database::connection();
$templating = $app->getTemplating();
$category_id = isset($_GET['c']) ? (int)$_GET['c'] : null;
@ -11,9 +11,25 @@ $post_id = isset($_GET['n']) ? (int)$_GET['n'] : null;
$page_id = (int)($_GET['p'] ?? 1);
if ($post_id !== null) {
$post = NewsPost::find($post_id);
$getPost = $db->prepare('
SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`,
c.`category_id`, c.`category_name`,
u.`user_id`, u.`username`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `display_colour`
FROM `msz_news_posts` as p
LEFT JOIN `msz_news_categories` as c
ON p.`category_id` = c.`category_id`
LEFT JOIN `msz_users` as u
ON p.`user_id` = u.`user_id`
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE `post_id` = :post_id
');
$getPost->bindValue(':post_id', $post_id, PDO::PARAM_INT);
$post = $getPost->execute() ? $getPost->fetch() : false;
if ($post === null) {
if ($post === false) {
http_response_code(404);
echo $templating->render('errors.404');
return;
@ -24,31 +40,102 @@ if ($post_id !== null) {
}
if ($category_id !== null) {
$category = NewsCategory::find($category_id);
$getCategory = $db->prepare('
SELECT
`category_id`, `category_name`, `category_description`
FROM `msz_news_categories`
WHERE `category_id` = :category_id
');
$getCategory->bindValue(':category_id', $category_id, PDO::PARAM_INT);
$category = $getCategory->execute() ? $getCategory->fetch() : false;
if ($category === null) {
if ($category === false) {
http_response_code(404);
echo $templating->render('errors.404');
return;
}
$posts = $category->posts()->orderBy('created_at', 'desc')->paginate(5, ['*'], 'p', $page_id);
$getPosts = $db->prepare('
SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`,
c.`category_id`, c.`category_name`,
u.`user_id`, u.`username`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `display_colour`
FROM `msz_news_posts` as p
LEFT JOIN `msz_news_categories` as c
ON p.`category_id` = c.`category_id`
LEFT JOIN `msz_users` as u
ON p.`user_id` = u.`user_id`
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE p.`category_id` = :category_id
ORDER BY `created_at` DESC
LIMIT 0, 5
');
$getPosts->bindValue('category_id', $category['category_id'], PDO::PARAM_INT);
$posts = $getPosts->execute() ? $getPosts->fetchAll() : false;
if (!is_valid_page($posts, $page_id)) {
//$posts = $category->posts()->orderBy('created_at', 'desc')->paginate(5, ['*'], 'p', $page_id);
//if (!is_valid_page($posts, $page_id)) {
if ($posts === false) {
http_response_code(404);
echo $templating->render('errors.404');
return;
}
$featured = $category->posts()->where('is_featured', 1)->orderBy('created_at', 'desc')->take(10)->get();
$getFeatured = $db->prepare('
SELECT `post_id`, `post_title`
FROM `msz_news_posts`
WHERE `category_id` = :category_id
AND `is_featured` = true
ORDER BY `created_at` DESC
LIMIT 10
');
$getFeatured->bindValue('category_id', $category['category_id'], PDO::PARAM_INT);
$featured = $getFeatured->execute() ? $getFeatured->fetchAll() : [];
echo $templating->render('news.category', compact('category', 'posts', 'featured', 'page_id'));
return;
}
$categories = NewsCategory::where('is_hidden', false)->get();
$posts = NewsPost::where('is_featured', true)->orderBy('created_at', 'desc')->paginate(5, ['*'], 'p', $page_id);
$getCategories = $db->prepare('
SELECT
c.`category_id`, c.`category_name`,
COUNT(p.`post_id`) AS count
FROM `msz_news_categories` as c
LEFT JOIN `msz_news_posts` as p
ON c.`category_id` = p.`category_id`
WHERE `is_hidden` = false
GROUP BY c.`category_id`
HAVING count > 0
');
$categories = $getCategories->execute() ? $getCategories->fetchAll() : [];
if (!is_valid_page($posts, $page_id)) {
$getPosts = $db->prepare('
SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`,
c.`category_id`, c.`category_name`,
u.`user_id`, u.`username`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `display_colour`
FROM `msz_news_posts` as p
LEFT JOIN `msz_news_categories` as c
ON p.`category_id` = c.`category_id`
LEFT JOIN `msz_users` as u
ON p.`user_id` = u.`user_id`
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE p.`is_featured` = true
AND c.`is_hidden` = false
ORDER BY p.`created_at` DESC
LIMIT 0, 5
');
$posts = $getPosts->execute() ? $getPosts->fetchAll() : [];
//$posts = NewsPost::where('is_featured', true)->orderBy('created_at', 'desc')->paginate(5, ['*'], 'p', $page_id);
//if (!is_valid_page($posts, $page_id)) {
if ($posts === false) {
http_response_code(404);
echo $templating->render('errors.404');
return;

View file

@ -1,12 +1,11 @@
<?php
use Misuzu\Database;
use Misuzu\IO\File;
use Misuzu\Users\User;
require_once __DIR__ . '/../misuzu.php';
$user_id = (int)($_GET['u'] ?? 0);
$mode = (string)($_GET['m'] ?? 'view');
$profile_user = User::find($user_id);
switch ($mode) {
case 'avatar':
@ -14,8 +13,7 @@ switch ($mode) {
$app->getConfig()->get('Avatar', 'default_path', 'string', 'public/images/no-avatar.png')
);
if ($profile_user !== null) {
$user_avatar = "{$profile_user->user_id}.msz";
$user_avatar = "{$user_id}.msz";
$cropped_avatar = $app->getStore('avatars/200x200')->filename($user_avatar);
if (File::exists($cropped_avatar)) {
@ -35,7 +33,6 @@ switch ($mode) {
}
}
}
}
header('Content-Type: ' . mime_content_type($avatar_filename));
echo File::readToEnd($avatar_filename);
@ -45,13 +42,26 @@ switch ($mode) {
default:
$templating = $app->getTemplating();
if ($profile_user === null) {
$getProfile = Database::connection()->prepare('
SELECT
u.*,
r.`role_title` as `user_title`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `display_colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON r.`role_id` = u.`display_role`
WHERE `user_id` = :user_id
');
$getProfile->bindValue('user_id', $user_id);
$profile = $getProfile->execute() ? $getProfile->fetch() : [];
if (!$profile) {
http_response_code(404);
echo $templating->render('user.notfound');
break;
}
$templating->var('profile', $profile_user);
$templating->vars(compact('profile'));
echo $templating->render('user.view');
break;
}

View file

@ -1,17 +1,15 @@
<?php
use Misuzu\Application;
use Misuzu\Database;
use Misuzu\IO\File;
use Misuzu\Users\User;
use Misuzu\Users\Session;
require_once __DIR__ . '/../misuzu.php';
$settings_session = $app->getSession();
$db = Database::connection();
$templating = $app->getTemplating();
$page_id = (int)($_GET['p'] ?? 1);
if (Application::getInstance()->getSession() === null) {
if (!$app->hasActiveSession()) {
http_response_code(403);
echo $templating->render('errors.403');
return;
@ -73,8 +71,6 @@ $settings_profile_fields = [
],
];
$settings_user = $settings_session->user;
$settings_modes = [
'account' => 'Account',
'avatar' => 'Avatar',
@ -83,7 +79,7 @@ $settings_modes = [
];
$settings_mode = $_GET['m'] ?? key($settings_modes);
$templating->vars(compact('settings_mode', 'settings_modes', 'settings_user', 'settings_session'));
$templating->vars(compact('settings_mode', 'settings_modes'));
if (!array_key_exists($settings_mode, $settings_modes)) {
http_response_code(404);
@ -95,7 +91,7 @@ if (!array_key_exists($settings_mode, $settings_modes)) {
$settings_errors = [];
$prevent_registration = $app->getConfig()->get('Auth', 'prevent_registration', 'bool', false);
$avatar_filename = "{$settings_user->user_id}.msz";
$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);
@ -109,6 +105,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break;
}
$updatedUserFields = [];
if (isset($_POST['profile']) && is_array($_POST['profile'])) {
foreach ($settings_profile_fields as $name => $props) {
if (isset($_POST['profile'][$name])) {
@ -129,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$field_value = $field_matches[1];
}
$settings_user->{"user_{$name}"} = $field_value;
$updatedUserFields["user_{$name}"] = $field_value;
}
}
}
@ -141,45 +139,69 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
&& (!empty($_POST['password']['new']) || !empty($_POST['email']['new']))
)
) {
if (!$settings_user->verifyPassword($_POST['current_password'])) {
$settings_errors[] = "Your current password was incorrect.";
$fetchPassword = $db->prepare('
SELECT `password`
FROM `msz_users`
WHERE `user_id` = :user_id
');
$fetchPassword->bindValue('user_id', $app->getUserId());
$currentPassword = $fetchPassword->execute() ? $fetchPassword->fetchColumn() : null;
if (empty($currentPassword)) {
$settings_errors[] = 'Something went horribly wrong.';
break;
}
if (!password_verify($_POST['current_password'], $currentPassword)) {
$settings_errors[] = '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.";
if (empty($_POST['email']['confirm'])
|| $_POST['email']['new'] !== $_POST['email']['confirm']) {
$settings_errors[] = 'The given e-mail addresses did not match.';
break;
}
if ($_POST['email']['new'] === $settings_user->email) {
$settings_errors[] = "This is your e-mail address already!";
$checkIfAlreadySet = $db->prepare('
SELECT COUNT(`user_id`)
FROM `msz_users`
WHERE LOWER(:email) = LOWER(:email)
');
$checkIfAlreadySet->bindValue('email', $_POST['email']['new']);
$isAlreadySet = $checkIfAlreadySet->execute()
? $checkIfAlreadySet->fetchColumn() > 0
: false;
if ($isAlreadySet) {
$settings_errors[] = 'This is your e-mail address already!';
break;
}
$email_validate = User::validateEmail($_POST['email']['new'], true);
$email_validate = user_validate_email($_POST['email']['new'], true);
if ($email_validate !== '') {
switch ($email_validate) {
case 'dns':
$settings_errors[] = "No valid MX record exists for this domain.";
$settings_errors[] = 'No valid MX record exists for this domain.';
break;
case 'format':
$settings_errors[] = "The given e-mail address was incorrectly formatted.";
$settings_errors[] = '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.";
$settings_errors[] = 'This e-mail address has already been used by another user.';
break;
default:
$settings_errors[] = "Unknown e-mail validation error.";
$settings_errors[] = 'Unknown e-mail validation error.';
}
break;
}
$settings_user->email = $_POST['email']['new'];
$updatedUserFields['email'] = strtolower($_POST['email']['new']);
}
if (!empty($_POST['password']['new'])) {
@ -189,20 +211,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break;
}
$password_validate = User::validatePassword($_POST['password']['new']);
$password_validate = user_validate_password($_POST['password']['new']);
if ($password_validate !== '') {
$settings_errors[] = "The given passwords was too weak.";
break;
}
$settings_user->password = $_POST['password']['new'];
$updatedUserFields['password'] = password_hash($_POST['password']['new'], PASSWORD_ARGON2I);
}
}
}
if (count($settings_errors) < 1 && $settings_user->isDirty()) {
$settings_user->save();
if (count($settings_errors) < 1 && count($updatedUserFields) > 0) {
$updateUser = $db->prepare('
UPDATE `msz_users`
SET ' . pdo_prepare_array_update($updatedUserFields, true) . '
WHERE `user_id` = :user_id
');
$updatedUserFields['user_id'] = $app->getUserId();
$updateUser->execute($updatedUserFields);
}
break;
@ -308,23 +336,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$session_id = (int)($_POST['session'] ?? 0);
if ($session_id < 1) {
$settings_errors[] = 'no';
$settings_errors[] = 'Invalid session.';
break;
}
$session = Session::find($session_id);
$findSession = $db->prepare('
SELECT `session_id`, `user_id`
FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$findSession->bindValue('session_id', $session_id);
$session = $findSession->execute() ? $findSession->fetch() : null;
if ($session === null || $session->user_id !== $settings_user->user_id) {
if (!$session || (int)$session['user_id'] !== $app->getUserId()) {
$settings_errors[] = 'You may only end your own sessions.';
break;
}
if ($session->session_id === $app->getSession()->session_id) {
if ((int)$session['session_id'] === $app->getSessionId()) {
header('Location: /auth.php?m=logout&s=' . tmp_csrf_token());
return;
}
$session->delete();
$deleteSession = $db->prepare('
DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $session['session_id']);
$deleteSession->execute();
break;
}
}
@ -334,11 +373,21 @@ $templating->var('settings_title', $settings_modes[$settings_mode]);
switch ($settings_mode) {
case 'account':
$getUserFields = $db->prepare('
SELECT ' . pdo_prepare_array($settings_profile_fields, 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'));
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',
@ -348,19 +397,44 @@ switch ($settings_mode) {
break;
case 'sessions':
$sessions = $settings_user->sessions()
/*$sessions = $settings_user->sessions()
->orderBy('session_id', 'desc')
->paginate(15, ['*'], 'p', $page_id);
->paginate(15, ['*'], 'p', $page_id);*/
$getSessions = $db->prepare('
SELECT
`session_id`, `session_country`, `user_agent`, `created_at`, `expires_on`,
INET6_NTOA(`session_ip`) as `session_ip_decoded`
FROM `msz_sessions`
WHERE `user_id` = :user_id
ORDER BY `session_id` DESC
LIMIT 0, 15
');
$getSessions->bindValue('user_id', $app->getUserId());
$sessions = $getSessions->execute() ? $getSessions->fetchAll() : [];
$templating->var('active_session_id', $app->getSessionId());
$templating->var('user_sessions', $sessions);
break;
case 'login-history':
$login_attempts = $settings_user->loginAttempts()
/*$login_attempts = $settings_user->loginAttempts()
->orderBy('attempt_id', 'desc')
->paginate(15, ['*'], 'p', $page_id);
->paginate(15, ['*'], 'p', $page_id);*/
$templating->var('user_login_attempts', $login_attempts);
$getLoginAttempts = $db->prepare('
SELECT
`attempt_id`, `attempt_country`, `was_successful`, `user_agent`, `created_at`,
INET6_NTOA(`attempt_ip`) as `attempt_ip_decoded`
FROM `msz_login_attempts`
WHERE `user_id` = :user_id
ORDER BY `attempt_id` DESC
LIMIT 0, 15
');
$getLoginAttempts->bindValue('user_id', $app->getUserId());
$loginAttempts = $getLoginAttempts->execute() ? $getLoginAttempts->fetchAll() : [];
$templating->var('user_login_attempts', $loginAttempts);
break;
}

View file

@ -1,23 +0,0 @@
<?php
use Misuzu\Application;
use Misuzu\Controllers\AuthController;
use Misuzu\Controllers\HomeController;
use Misuzu\Controllers\UserController;
$routes = Application::getInstance()->router;
$routes->get(['/', 'main.index'], [HomeController::class, 'index']);
$routes->group(['prefix' => '/auth'], function ($routes) {
$routes->get(['/login', 'auth.login'], [AuthController::class, 'login']);
$routes->post(['/login', 'auth.login'], [AuthController::class, 'login']);
$routes->get(['/logout', 'auth.logout'], [AuthController::class, 'logout']);
$routes->get(['/register', 'auth.register'], [AuthController::class, 'register']);
$routes->post(['/register', 'auth.register'], [AuthController::class, 'register']);
});
$routes->group(['prefix' => '/users'], function ($routes) {
$routes->get(['/{id:i}', 'users.view'], [UserController::class, 'view']);
});

View file

@ -1,6 +1,7 @@
<?php
namespace Misuzu;
use Carbon\Carbon;
use Misuzu\Config\ConfigManager;
use Misuzu\IO\Directory;
use Misuzu\IO\DirectoryDoesNotExistException;
@ -28,10 +29,16 @@ class Application extends ApplicationBase
];
/**
* Session instance.
* @var \Misuzu\Users\Session
* Active Session ID.
* @var int
*/
private $sessionInstance = null;
private $currentSessionId = 0;
/**
* Active User ID.
* @var int
*/
private $currentUserId = 0;
/**
* Database instance.
@ -39,12 +46,6 @@ class Application extends ApplicationBase
*/
private $databaseInstance = null;
/**
* Database instance.
* @var \Misuzu\Database
*/
private $database;
/**
* ConfigManager instance.
* @var \Misuzu\Config\ConfigManager
@ -108,7 +109,7 @@ class Application extends ApplicationBase
*/
public function getPath(string $path): string
{
if (!starts_with($path, '/')) {
if (!starts_with($path, '/') && substr($path, 1, 2) !== ':\\') {
$path = __DIR__ . '/../' . $path;
}
@ -161,40 +162,53 @@ class Application extends ApplicationBase
/**
* Starts a user session.
* @param int $user_id
* @param string $session_key
* @param int $userId
* @param string $sessionKey
*/
public function startSession(int $user_id, string $session_key): void
public function startSession(int $userId, string $sessionKey): void
{
$session = Session::where('session_key', $session_key)
->where('user_id', $user_id)
->first();
$dbc = Database::connection();
if ($session !== null) {
if ($session->hasExpired()) {
$session->delete();
$findSession = $dbc->prepare('
SELECT `session_id`, `expires_on`
FROM `msz_sessions`
WHERE `user_id` = :user_id
AND `session_key` = :session_key
');
$findSession->bindValue('user_id', $userId);
$findSession->bindValue('session_key', $sessionKey);
$sessionData = $findSession->execute() ? $findSession->fetch() : false;
if ($sessionData) {
$expiresOn = new Carbon($sessionData['expires_on']);
if ($expiresOn->isPast()) {
$deleteSession = $dbc->prepare('
DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $sessionData['session_id']);
$deleteSession->execute();
} else {
$this->setSession($session);
$this->currentSessionId = (int)$sessionData['session_id'];
$this->currentUserId = $userId;
}
}
}
/**
* Gets the current session instance.
* @return Session|null
*/
public function getSession(): ?Session
public function hasActiveSession(): bool
{
return $this->sessionInstance;
return $this->getSessionId() > 0;
}
/**
* Registers a session.
* @param Session|null $sessionInstance
*/
public function setSession(?Session $sessionInstance): void
public function getSessionId(): int
{
$this->sessionInstance = $sessionInstance;
return $this->currentSessionId;
}
public function getUserId(): int
{
return $this->currentUserId;
}
/**
@ -206,7 +220,7 @@ class Application extends ApplicationBase
throw new UnexpectedValueException('Database module has already been started.');
}
$this->database = new Database($this->configInstance, self::DATABASE_CONNECTIONS[0]);
new Database($this->configInstance, self::DATABASE_CONNECTIONS[0]);
$this->databaseInstance = new DatabaseV1($this->configInstance, self::DATABASE_CONNECTIONS[0]);
$this->loadDatabaseConnections();
}

View file

@ -136,7 +136,7 @@ final class Database
$dsn .= 'dbname=' . $this->configManager->get($section, 'database', 'string', 'misuzu') . ';';
$options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'";
$options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'";
break;
}

View file

@ -125,7 +125,7 @@ class DatabaseV1 extends LaravelDatabaseManager
? $this->configManager->get($section, 'collation', 'string')
: 'utf8mb4_bin';
$args['strict'] = true;
$args['strict'] = false; // breaks mysql 8
$args['engine'] = null;
break;

View file

@ -112,7 +112,7 @@ class ExceptionHandler
$is_http = false;//$exception instanceof HttpException;
if (PHP_SAPI === 'cli' || (!$is_http && static::$debugMode)) {
if (PHP_SAPI !== 'cli') {
if (PHP_SAPI !== 'cli' && !headers_sent()) {
http_response_code(500);
header('Content-Type: text/plain');
}

View file

@ -94,13 +94,18 @@ class Directory
throw new DirectoryExistsException;
}
$on_windows = running_on_windows();
$path = Directory::fixSlashes($path);
$split_path = explode(self::SEPARATOR, $path);
$existing_path = running_on_windows() ? '' : self::SEPARATOR;
$existing_path = $on_windows ? '' : self::SEPARATOR;
foreach ($split_path as $path_part) {
$existing_path .= $path_part . self::SEPARATOR;
if ($on_windows && substr($existing_path, 1, 2) === ':\\') {
continue;
}
if (!Directory::exists($existing_path)) {
mkdir($existing_path);
}

View file

@ -1,14 +0,0 @@
<?php
namespace Misuzu;
use Illuminate\Database\Eloquent\Model as BaseModel;
/**
* Class Model
* @package Misuzu
* @property-read \Carbon\Carbon|null $created_at
* @property-read \Carbon\Carbon|null $updated_at
*/
abstract class Model extends BaseModel
{
}

View file

@ -1,27 +0,0 @@
<?php
namespace Misuzu\News;
use Misuzu\Model;
/**
* Class NewsCategory
* @package Misuzu\News
* @property-read int $category_id
* @property string $category_name
* @property string $category_description
* @property bool $is_hidden
* @property-read array $posts
*/
final class NewsCategory extends Model
{
protected $table = 'news_categories';
protected $primaryKey = 'category_id';
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany(NewsPost::class, 'category_id');
}
}

View file

@ -1,41 +0,0 @@
<?php
namespace Misuzu\News;
use Carbon\Carbon;
use Misuzu\Database;
use Misuzu\Users\User;
use Parsedown;
/**
* Class NewsPost
* @package Misuzu\News
*/
final class NewsPost
{
/**
* Parses post_text and returns the final HTML.
* @return string
*/
public function getHtml(): string
{
return (new Parsedown)->text($this->post_text);
}
public function getUser(): ?User
{
if (empty($this->user_id) || $this->user_id < 1) {
return null;
}
return User::find($this->user_id);
}
public function getCategory(): ?NewsCategory
{
if (empty($this->category_id) || $this->category_id < 1) {
return null;
}
return NewsCategory::find($this->category_id);
}
}

View file

@ -1,117 +0,0 @@
<?php
namespace Misuzu\Users;
use Illuminate\Database\Eloquent\Builder;
use Misuzu\Model;
use Misuzu\Net\IPAddress;
/**
* Class LoginAttempt
* @package Misuzu\Users
* @property-read int $attempt_id
* @property bool $was_successful
* @property IPAddress $attempt_ip
* @property string $attempt_country
* @property int $user_id
* @property string $user_agent
* @property-read User $user
*/
class LoginAttempt extends Model
{
/**
* Primary table column.
* @var string
*/
protected $primaryKey = 'attempt_id';
/**
* Records a successful login attempt.
* @param IPAddress $ipAddress
* @param User $user
* @param null|string $userAgent
* @return LoginAttempt
*/
public static function recordSuccess(IPAddress $ipAddress, User $user, ?string $userAgent = null): LoginAttempt
{
return static::recordAttempt(true, $ipAddress, $user, $userAgent);
}
/**
* Records a failed login attempt.
* @param IPAddress $ipAddress
* @param User|null $user
* @param null|string $userAgent
* @return LoginAttempt
*/
public static function recordFail(IPAddress $ipAddress, ?User $user = null, ?string $userAgent = null): LoginAttempt
{
return static::recordAttempt(false, $ipAddress, $user, $userAgent);
}
/**
* Records a login attempt.
* @param bool $success
* @param IPAddress $ipAddress
* @param User|null $user
* @param null|string $userAgent
* @return LoginAttempt
*/
public static function recordAttempt(
bool $success,
IPAddress $ipAddress,
?User $user = null,
?string $userAgent = null
): LoginAttempt {
$attempt = new static;
$attempt->was_successful = $success;
$attempt->attempt_ip = $ipAddress;
$attempt->user_agent = $userAgent ?? '';
if ($user !== null) {
$attempt->user_id = $user->user_id;
}
$attempt->save();
return $attempt;
}
/**
* Gets all login attempts from a given IP address.
* @param IPAddress $ipAddress
* @return Builder
*/
public static function fromIpAddress(IPAddress $ipAddress): Builder
{
return static::where('attempt_ip', $ipAddress->getRaw());
}
/**
* Setter for the IP address property.
* @param IPAddress $ipAddress
*/
public function setAttemptIpAttribute(IPAddress $ipAddress): void
{
$this->attributes['attempt_ip'] = $ipAddress->getRaw();
$this->attributes['attempt_country'] = $ipAddress->getCountryCode();
}
/**
* Getter for the IP address property.
* @param string $ipAddress
* @return IPAddress
*/
public function getAttemptIpAttribute(string $ipAddress): IPAddress
{
return IPAddress::fromRaw($ipAddress);
}
/**
* Object relation definition for User.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}

View file

@ -1,114 +0,0 @@
<?php
namespace Misuzu\Users;
use Misuzu\Model;
/**
* Class Role
* @package Misuzu\Users
* @property-read int $role_id
* @property int $role_hierarchy
* @property string $role_name
* @property string $role_title
* @property string $role_description
* @property bool $role_secret
* @property int $role_colour
* @property-read array $users
*/
class Role extends Model
{
/**
* @var string
*/
protected $primaryKey = 'role_id';
/**
* Creates a new role.
* @param string $name
* @param int|null $hierarchy
* @param int|null $colour
* @param null|string $title
* @param null|string $description
* @param bool $secret
* @return Role
*/
public static function createRole(
string $name,
?int $hierarchy = null,
?int $colour = null,
?string $title = null,
?string $description = null,
bool $secret = false
): Role {
$hierarchy = $hierarchy ?? 1;
$colour = $colour ?? colour_none();
$role = new Role;
$role->role_hierarchy = $hierarchy;
$role->role_name = $name;
$role->role_title = $title;
$role->role_description = $description;
$role->role_secret = $secret;
$role->role_colour = $colour;
$role->save();
return $role;
}
/**
* Adds this role to a user.
* @param User $user
* @param bool $setDisplay
*/
public function addUser(User $user, bool $setDisplay = false): void
{
$user->addRole($this, $setDisplay);
}
/**
* Removes this role from a user.
* @param User $user
*/
public function removeUser(User $user): void
{
$user->removeRole($this);
}
/**
* Checks if this user has this role.
* @param User $user
* @return bool
*/
public function hasUser(User $user): bool
{
return $user->hasRole($this);
}
/**
* Getter for the role_description attribute.
* @param null|string $description
* @return string
*/
public function getRoleDescriptionAttribute(?string $description): string
{
return empty($description) ? '' : $description;
}
/**
* Setter for the role_description attribute.
* @param string $description
*/
public function setRoleDescriptionAttribute(string $description): void
{
$this->attributes['role_description'] = empty($description) ? null : $description;
}
/**
* Users relation.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function users()
{
return $this->hasMany(UserRole::class, 'role_id');
}
}

View file

@ -1,108 +0,0 @@
<?php
namespace Misuzu\Users;
use Carbon\Carbon;
use Misuzu\Model;
use Misuzu\Net\IPAddress;
/**
* Class Session
* @package Misuzu\Users
* @property-read int $session_id
* @property int $user_id
* @property string $session_key
* @property IPAddress $session_ip
* @property string $user_agent
* @property Carbon $expires_on
* @property string $session_country
* @property-read User $user
*/
class Session extends Model
{
/**
* @var string
*/
protected $primaryKey = 'session_id';
/**
* @var array
*/
protected $dates = ['expires_on'];
/**
* Creates a new session object.
* @param User $user
* @param null|string $userAgent
* @param Carbon|null $expires
* @param IPAddress|null $ipAddress
* @return Session
* @throws \Exception
*/
public static function createSession(
User $user,
?string $userAgent = null,
?Carbon $expires = null,
?IPAddress $ipAddress = null
): Session {
$ipAddress = $ipAddress ?? IPAddress::remote();
$userAgent = $userAgent ?? 'Misuzu';
$expires = $expires ?? Carbon::now()->addMonth();
$session = new Session;
$session->user_id = $user->user_id;
$session->session_ip = $ipAddress;
$session->user_agent = $userAgent;
$session->expires_on = $expires;
$session->session_key = self::generateKey();
$session->save();
return $session;
}
/**
* Generates a random key.
* @return string
* @throws \Exception
*/
public static function generateKey(): string
{
return bin2hex(random_bytes(32));
}
/**
* Returns if a session has expired.
* @return bool
*/
public function hasExpired(): bool
{
return $this->expires_on->isPast();
}
/**
* Getter for the session_ip attribute.
* @param string $ipAddress
* @return IPAddress
*/
public function getSessionIpAttribute(string $ipAddress): IPAddress
{
return IPAddress::fromRaw($ipAddress);
}
/**
* Setter for the session_ip attribute.
* @param IPAddress $ipAddress
*/
public function setSessionIpAttribute(IPAddress $ipAddress): void
{
$this->attributes['session_ip'] = $ipAddress->getRaw();
$this->attributes['session_country'] = $ipAddress->getCountryCode();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}

View file

@ -1,451 +0,0 @@
<?php
namespace Misuzu\Users;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\SoftDeletes;
use Misuzu\DatabaseV1;
use Misuzu\Model;
use Misuzu\Net\IPAddress;
/**
* Class User
* @package Misuzu\Users
* @property-read int $user_id
* @property string $username
* @property string $password
* @property string $email
* @property IPAddress $register_ip
* @property IPAddress $last_ip
* @property string $user_country
* @property Carbon $user_registered
* @property string $user_chat_key
* @property int $display_role
* @property string $user_website
* @property string $user_twitter
* @property string $user_github
* @property string $user_skype
* @property string $user_discord
* @property string $user_youtube
* @property string $user_steam
* @property string $user_twitchtv
* @property string $user_osu
* @property string $user_lastfm
* @property string $user_title
* @property Carbon $last_seen
* @property Carbon|null $deleted_at
* @property-read array $sessions
* @property-read array $roles
* @property-read array $loginAttempts
*/
class User extends Model
{
use SoftDeletes;
/**
* Define the preferred password hashing algoritm to be used to password_hash.
*/
private const PASSWORD_HASH_ALGO = PASSWORD_ARGON2I;
/**
* Minimum entropy value for passwords.
*/
public const PASSWORD_MIN_ENTROPY = 32;
/**
* Minimum username length.
*/
public const USERNAME_MIN_LENGTH = 3;
/**
* Maximum username length, unless your name is Flappyzor(WorldwideOnline2018).
*/
public const USERNAME_MAX_LENGTH = 16;
/**
* Username character constraint.
*/
public const USERNAME_REGEX = '#^[A-Za-z0-9-_ ]+$#u';
/**
* @var string
*/
protected $primaryKey = 'user_id';
/**
* Whether the display role has been validated to still be assigned to this user.
* @var bool
*/
private $displayRoleValidated = false;
/**
* Instance of the display role.
* @var Role
*/
private $displayRoleInstance;
/**
* Displayed user title.
* @var string
*/
private $userTitleValue;
/**
* Created a new user.
* @param string $username
* @param string $password
* @param string $email
* @param IPAddress|null $ipAddress
* @return User
*/
public static function createUser(
string $username,
string $password,
string $email,
?IPAddress $ipAddress = null
): User {
$ipAddress = $ipAddress ?? IPAddress::remote();
$user = new User;
$user->username = $username;
$user->password = $password;
$user->email = $email;
$user->register_ip = $ipAddress;
$user->last_ip = $ipAddress;
$user->user_country = $ipAddress->getCountryCode();
$user->save();
return $user;
}
/**
* Tries to find a user for the login page.
* @param string $usernameOrEmail
* @return User|null
*/
public static function findLogin(string $usernameOrEmail): ?User
{
$usernameOrEmail = strtolower($usernameOrEmail);
return User::whereRaw("LOWER(`username`) = '{$usernameOrEmail}'")
->orWhere('email', $usernameOrEmail)
->first();
}
/**
* Validates a username string.
* @param string $username
* @param bool $checkInUse
* @return string
*/
public static function validateUsername(string $username, bool $checkInUse = false): string
{
$username_length = strlen($username);
if ($username !== trim($username)) {
return 'trim';
}
if ($username_length < self::USERNAME_MIN_LENGTH) {
return 'short';
}
if ($username_length > self::USERNAME_MAX_LENGTH) {
return 'long';
}
if (strpos($username, ' ') !== false) {
return 'double-spaces';
}
if (!preg_match(self::USERNAME_REGEX, $username)) {
return 'invalid';
}
if (strpos($username, '_') !== false && strpos($username, ' ') !== false) {
return 'spacing';
}
if ($checkInUse && static::whereRaw("LOWER(`username`) = LOWER('{$username}')")->count() > 0) {
return 'in-use';
}
return '';
}
/**
* Validates an e-mail string.
* @param string $email
* @param bool $checkInUse
* @return string
*/
public static function validateEmail(string $email, bool $checkInUse = false): string
{
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
return 'format';
}
if (!check_mx_record($email)) {
return 'dns';
}
if ($checkInUse && static::whereRaw("LOWER(`email`) = LOWER('{$email}')")->count() > 0) {
return 'in-use';
}
return '';
}
/**
* Validates a password string.
* @param string $password
* @return string
*/
public static function validatePassword(string $password): string
{
if (password_entropy($password) < self::PASSWORD_MIN_ENTROPY) {
return 'weak';
}
return '';
}
/**
* Gets the user's display role, it's probably safe to assume that this will always return a valid role.
* @return Role|null
*/
public function getDisplayRole(): ?Role
{
if ($this->displayRoleInstance === null) {
$this->displayRoleInstance = Role::find($this->display_role);
}
return $this->displayRoleInstance;
}
/**
* Gets the display colour.
* @return int
*/
public function getColour(): int
{
$role = $this->getDisplayRole();
return $role === null ? colour_none() : $role->role_colour;
}
/**
* Gets the correct user title.
* @return string
*/
private function getUserTitlePrivate(): string
{
if (!empty($this->user_title)) {
return $this->user_title;
}
$role = $this->getDisplayRole();
if ($role !== null && !empty($role->role_title)) {
return $role->role_title;
}
return '';
}
/**
* Gets the user title (with memoization).
* @return string
*/
public function getUserTitle(): string
{
if (empty($this->userTitleValue)) {
$this->userTitleValue = $this->getUserTitlePrivate();
}
return $this->userTitleValue;
}
/**
* Assigns a role.
* @param Role $role
* @param bool $setDisplay
*/
public function addRole(Role $role, bool $setDisplay = false): void
{
$relation = new UserRole;
$relation->user_id = $this->user_id;
$relation->role_id = $role->role_id;
$relation->save();
if ($setDisplay) {
$this->display_role = $role->role_id;
}
}
/**
* Removes a role.
* @param Role $role
*/
public function removeRole(Role $role): void
{
UserRole::where('user_id', $this->user_id)
->where('role_id', $role->user_id)
->delete();
}
/**
* Checks if a role is assigned.
* @param Role $role
* @return bool
*/
public function hasRole(Role $role): bool
{
return UserRole::where('user_id', $this->user_id)
->where('role_id', $role->role_id)
->count() > 0;
}
/**
* Verifies a password.
* @param string $password
* @return bool
*/
public function verifyPassword(string $password): bool
{
if (password_verify($password, $this->password) !== true) {
return false;
}
if (password_needs_rehash($this->password, self::PASSWORD_HASH_ALGO)) {
$this->password = $password;
$this->save();
}
return true;
}
/**
* Getter for the display_role attribute.
* @param int|null $value
* @return int
*/
public function getDisplayRoleAttribute(?int $value): int
{
if (!$this->displayRoleValidated) {
if ($value === null
|| UserRole::where('user_id', $this->user_id)->where('role_id', $value)->count() < 1) {
$highestRole = DatabaseV1::table('roles')
->join('user_roles', 'roles.role_id', '=', 'user_roles.role_id')
->where('user_id', $this->user_id)
->orderBy('roles.role_hierarchy', 'desc')
->first(['roles.role_id']);
$value = $highestRole->role_id;
$this->display_role = $value;
$this->save();
}
$this->displayRoleValidated = true;
}
return $value;
}
/**
* Setter for the display_role attribute.
* @param int $value
*/
public function setDisplayRoleAttribute(int $value): void
{
if (UserRole::where('user_id', $this->user_id)->where('role_id', $value)->count() > 0) {
$this->attributes['display_role'] = $value;
}
}
/**
* @param null|string $dateTime
* @return Carbon
*/
public function getLastSeenAttribute(?string $dateTime): Carbon
{
return $dateTime === null ? Carbon::createFromTimestamp(-1) : new Carbon($dateTime);
}
/**
* Getter for the register_ip attribute.
* @param string $ipAddress
* @return IPAddress
*/
public function getRegisterIpAttribute(string $ipAddress): IPAddress
{
return IPAddress::fromRaw($ipAddress);
}
/**
* Setter for the register_ip attribute.
* @param IPAddress $ipAddress
*/
public function setRegisterIpAttribute(IPAddress $ipAddress): void
{
$this->attributes['register_ip'] = $ipAddress->getRaw();
}
/**
* Getter for the last_ip attribute.
* @param string $ipAddress
* @return IPAddress
*/
public function getLastIpAttribute(string $ipAddress): IPAddress
{
return IPAddress::fromRaw($ipAddress);
}
/**
* Setter for the last_ip attribute.
* @param IPAddress $ipAddress
*/
public function setLastIpAttribute(IPAddress $ipAddress): void
{
$this->attributes['last_ip'] = $ipAddress->getRaw();
}
/**
* Setter for the password attribute.
* @param string $password
*/
public function setPasswordAttribute(string $password): void
{
$this->attributes['password'] = password_hash($password, self::PASSWORD_HASH_ALGO);
}
/**
* Setter for the email attribute.
* @param string $email
*/
public function setEmailAttribute(string $email): void
{
$this->attributes['email'] = strtolower($email);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function sessions()
{
return $this->hasMany(Session::class, 'user_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function roles()
{
return $this->hasMany(UserRole::class, 'user_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function loginAttempts()
{
return $this->hasMany(LoginAttempt::class, 'user_id');
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace Misuzu\Users;
use Misuzu\Model;
/**
* Class UserRole
* @package Misuzu\Users
* @property int $user_id
* @property int $role_id
* @property-read User $user
* @property-read Role $role
*/
class UserRole extends Model
{
protected $primaryKey = null;
public $incrementing = false;
public $timestamps = false;
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function role()
{
return $this->belongsTo(Role::class, 'role_id');
}
}

View file

@ -0,0 +1,19 @@
<?php
use Misuzu\Database;
function user_login_attempt_record(bool $success, ?int $userId, string $ipAddress, string $userAgent): void
{
$storeAttempt = Database::connection()->prepare('
INSERT INTO `msz_login_attempts`
(`was_successful`, `attempt_ip`, `attempt_country`, `user_id`, `user_agent`, `created_at`)
VALUES
(:was_successful, INET6_ATON(:attempt_ip), :attempt_country, :user_id, :user_agent, NOW())
');
$storeAttempt->bindValue('was_successful', $success ? 1 : 0);
$storeAttempt->bindValue('attempt_ip', $ipAddress);
$storeAttempt->bindValue('attempt_country', get_country_code($ipAddress));
$storeAttempt->bindValue('user_agent', $userAgent);
$storeAttempt->bindValue('user_id', $userId, $userId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
$storeAttempt->execute();
}

95
src/Users/validation.php Normal file
View file

@ -0,0 +1,95 @@
<?php
use Misuzu\Database;
// Minimum username length.
define('MSZ_USERNAME_MIN_LENGTH', 3);
// Maximum username length, unless your name is Flappyzor(WorldwideOnline2018).
define('MSZ_USERNAME_MAX_LENGTH', 16);
// Username character constraint.
define('MSZ_USERNAME_REGEX', '#^[A-Za-z0-9-_ ]+$#u');
// Minimum entropy value for passwords.
define('MSZ_PASSWORD_MIN_ENTROPY', 32);
function user_validate_username(string $username, bool $checkInUse = false): string
{
$username_length = strlen($username);
if ($username !== trim($username)) {
return 'trim';
}
if ($username_length < MSZ_USERNAME_MIN_LENGTH) {
return 'short';
}
if ($username_length > MSZ_USERNAME_MAX_LENGTH) {
return 'long';
}
if (strpos($username, ' ') !== false) {
return 'double-spaces';
}
if (!preg_match(MSZ_USERNAME_REGEX, $username)) {
return 'invalid';
}
if (strpos($username, '_') !== false && strpos($username, ' ') !== false) {
return 'spacing';
}
if ($checkInUse) {
$getUser = Database::connection()->prepare('
SELECT COUNT(`user_id`)
FROM `msz_users`
WHERE LOWER(`username`) = LOWER(:username)
');
$getUser->bindValue('username', $username);
$userId = $getUser->execute() ? $getUser->fetchColumn() : 0;
if ($userId > 0) {
return 'in-use';
}
}
return '';
}
function user_validate_email(string $email, bool $checkInUse = false): string
{
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
return 'format';
}
if (!check_mx_record($email)) {
return 'dns';
}
if ($checkInUse) {
$getUser = Database::connection()->prepare('
SELECT COUNT(`user_id`)
FROM `msz_users`
WHERE LOWER(`email`) = LOWER(:email)
');
$getUser->bindValue('email', $email);
$userId = $getUser->execute() ? $getUser->fetchColumn() : 0;
if ($userId > 0) {
return 'in-use';
}
}
return '';
}
function user_validate_password(string $password): string
{
if (password_entropy($password) < MSZ_PASSWORD_MIN_ENTROPY) {
return 'weak';
}
return '';
}

View file

@ -2,18 +2,17 @@
namespace MisuzuTests;
use PHPUnit\Framework\TestCase;
use Misuzu\Users\User;
class UserTest extends TestCase
{
public function testUsernameValidation()
{
$this->assertEquals(User::validateUsername('flashwave'), '');
$this->assertEquals(User::validateUsername(' flash '), 'trim');
$this->assertEquals(User::validateUsername('f'), 'short');
$this->assertEquals(User::validateUsername('flaaaaaaaaaaaaaaaash'), 'long');
$this->assertEquals(User::validateUsername('F|@$h'), 'invalid');
$this->assertEquals(User::validateUsername('fl ash_wave'), 'spacing');
$this->assertEquals(User::validateUsername('fl ash'), 'double-spaces');
$this->assertEquals(user_validate_username('flashwave'), '');
$this->assertEquals(user_validate_username(' flash '), 'trim');
$this->assertEquals(user_validate_username('f'), 'short');
$this->assertEquals(user_validate_username('flaaaaaaaaaaaaaaaash'), 'long');
$this->assertEquals(user_validate_username('F|@$h'), 'invalid');
$this->assertEquals(user_validate_username('fl ash_wave'), 'spacing');
$this->assertEquals(user_validate_username('fl ash'), 'double-spaces');
}
}

View file

@ -133,22 +133,14 @@ function get_country_name(string $code): string
// this is temporary, don't scream at me for using md5
// BIG TODO: make these functions not dependent on sessions so they can be used outside of those.
function tmp_csrf_verify(string $token, ?\Misuzu\Users\Session $session = null): bool
function tmp_csrf_verify(string $token): bool
{
if ($session === null) {
$session = \Misuzu\Application::getInstance()->getSession();
}
return hash_equals(tmp_csrf_token($session), $token);
return hash_equals(tmp_csrf_token(), $token);
}
function tmp_csrf_token(?\Misuzu\Users\Session $session = null): string
function tmp_csrf_token(): string
{
if ($session === null) {
$session = \Misuzu\Application::getInstance()->getSession();
}
return md5($session->session_key);
return md5($_COOKIE['msz_sid'] ?? 'this is very insecure lmao');
}
function crop_image_centred_path(string $filename, int $target_width, int $target_height): \Imagick
@ -218,3 +210,23 @@ function is_valid_page(\Illuminate\Pagination\LengthAwarePaginator $paginator, i
{
return $attemptedPage >= 1 && $attemptedPage <= $paginator->lastPage();
}
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);
}

View file

@ -136,10 +136,10 @@
<div class="header__user">
<div class="header__menu">
<input type="checkbox" id="menu-user-state" class="header__menu__state">
<label for="menu-user-state" class="header__menu__toggle header__menu__toggle--profile" style="background-image:url('/profile.php?u={{ app.session.user.user_id }}&amp;m=avatar');color:{{ app.session.user.colour|colour_get_css }}">{{ app.session.user.username }}</label>
<label for="menu-user-state" class="header__menu__toggle header__menu__toggle--profile" style="background-image:url('/profile.php?u={{ current_user.user_id }}&amp;m=avatar');color:{{ current_user.colour|colour_get_css }}">{{ current_user.username }}</label>
<div class="header__menu__options header__menu__options--user">
<div class="header__menu__section">
<a class="header__menu__link" href="/profile.php?u={{ app.session.user.user_id }}">Profile</a>
<a class="header__menu__link" href="/profile.php?u={{ current_user.user_id }}">Profile</a>
<a class="header__menu__link" href="/settings.php">Settings</a>
</div>
<div class="header__menu__section">

View file

@ -16,6 +16,6 @@
</div>
<div class="container container--center">
{{ paginate(manage_users, '?v=listing') }}
{# paginate(manage_users, '?v=listing') #}
</div>
{% endblock %}

View file

@ -11,13 +11,13 @@
<a href="?v=role&amp;r={{ role.role_id }}" class="listing__entry role-listing__entry"{% if not role.role_colour|colour_get_inherit %} style="border-color: {{ role.role_colour|colour_get_css }}"{% endif %}>
<div class="listing__entry__content role-listing__entry__content">
{{ role.role_name }}
{{ role.users.count }} users
{{ role.users }} users
</div>
</a>
{% endfor %}
</div>
<div class="container container--center">
{{ paginate(manage_roles, '?v=roles') }}
{# paginate(manage_roles, '?v=roles') #}
</div>
{% endblock %}

View file

@ -23,14 +23,14 @@
<label class="form__label">
<div class="form__label__text">Register IP</div>
<div class="form__label__input">
<input class="input input--text" readonly type="text" value="{{ view_user.register_ip.string }}">
<input class="input input--text" readonly type="text" value="{{ view_user.register_ip_decoded }}">
</div>
</label>
<label class="form__label">
<div class="form__label__text">Last IP</div>
<div class="form__label__input">
<input class="input input--text" readonly type="text" value="{{ view_user.last_ip.string }}">
<input class="input input--text" readonly type="text" value="{{ view_user.last_ip_decoded }}">
</div>
</label>

View file

@ -5,7 +5,7 @@
{% set canonical_url = '/' %}
{% block content %}
{% if app.session != null %}
{% if app.hasActiveSession %}
<div class="container">
<div class="container__title">Welcome</div>
<div class="container__content">

View file

@ -25,17 +25,17 @@
</div>
<div class="header__menu">
{% if app.session is not null %}
{% if app.hasActiveSession %}
<div class="container header__user">
<div class="container__title">Hey, {{ app.session.user.username }}!</div>
<div class="container__title">Hey, {{ current_user.username }}!</div>
<div class="container__content header__user__content">
<a href="/settings.php?m=avatar" class="avatar header__user__avatar" style="background-image:url('/profile.php?u={{ app.session.user.user_id }}&amp;m=avatar');"></a>
<a href="/settings.php?m=avatar" class="avatar header__user__avatar" style="background-image:url('/profile.php?u={{ current_user.user_id }}&amp;m=avatar');"></a>
<div class="header__user__links__container">
<ul class="header__user__links">
<li class="header__user__option"><a class="header__user__link" href="/profile.php?u={{ app.session.user.user_id }}">Profile</a></li>
<li class="header__user__option"><a class="header__user__link" href="/profile.php?u={{ current_user.user_id }}">Profile</a></li>
<li class="header__user__option"><a class="header__user__link" href="/settings.php">Settings</a></li>
{% if app.session.user.user_id == 1 %}
{% if current_user.user_id == 1 %}
<li class="header__user__option"><a class="header__user__link" href="{{ manage_link|default('/manage/index.php') }}">Manage</a></li>
{% endif %}
<li class="header__user__option"><a class="header__user__link" href="/auth.php?m=logout&amp;s={{ csrf_token() }}">Logout</a></li>

View file

@ -39,7 +39,7 @@
</div>
</div>
{% endif %}
{{ paginate(posts, '?c=' ~ category.category_id, 'news__') }}
{# paginate(posts, '?c=' ~ category.category_id, 'news__') #}
</div>
</div>
</div>

View file

@ -23,22 +23,18 @@
</div>
<div class="container__content">
{% for category in categories %}
{% set post_count = category.posts.count %}
{% if post_count > 0 %}
<a class="news__list__item news__list__item--kvp" href="/news.php?c={{ category.category_id }}">
<div class="news__list__name">
{{ category.category_name }}
</div>
<div class="news__list__value">
{{ post_count }} post{{ post_count == 1 ? '' : 's' }}
{{ category.count }} post{{ category.count == 1 ? '' : 's' }}
</div>
</a>
{% endif %}
{% endfor %}
</div>
</div>
{{ paginate(posts, '', 'news__') }}
{# paginate(posts, '', 'news__') #}
</div>
</div>
</div>

View file

@ -5,18 +5,18 @@
</a>
<div class="container__content news__preview__content">
<div class="news__preview__text">
{{ post.html|first_paragraph|raw }}
{{ post.post_text|first_paragraph|raw }}
<p><b><i><a href="/news.php?n={{ post.post_id }}">View full post</a></i></b></p>
</div>
<div class="news__preview__info">
<div class="news__preview__date">
{{ post.created_at }}
</div>
<a class="news__preview__user" href="/profile.php?u={{ post.user.user_id }}">
<div class="news__preview__user__name" style="color:{{ post.user.colour|colour_get_css }}">
{{ post.user.username }}
<a class="news__preview__user" href="/profile.php?u={{ post.user_id }}">
<div class="news__preview__user__name" style="color:{{ post.display_colour|colour_get_css }}">
{{ post.username }}
</div>
<div class="avatar news__preview__user__avatar" style="background-image:url('/profile.php?u={{ post.user.user_id }}&amp;m=avatar')"></div>
<div class="avatar news__preview__user__avatar" style="background-image:url('/profile.php?u={{ post.user_id }}&amp;m=avatar')"></div>
</a>
</div>
</div>

View file

@ -6,27 +6,27 @@
{% block news_content %}
<div class="container news__post">
<div class="container__title">
<a href="/news.php?c={{ post.category.category_id }}" class="container__title__link">{{ post.category.category_name }}</a> »
<a href="/news.php?c={{ post.category_id }}" class="container__title__link">{{ post.category_name }}</a> »
{{ post.post_title }}
</div>
<div class="container__content news__post__content">
<div class="news__post__text">
{{ post.html|raw }}
{{ post.post_text|raw }}
</div>
<div class="news__sidebar news__post__details">
<a class="news__post__detail news__post__user" href="/profile.php?u={{ post.user.user_id }}">
<div class="news__post__username" style="color:post.user.colour|colour_get_css">{{ post.user.username }}</div>
<div class="avatar news__post__avatar" style="background-image:url('/profile.php?u={{ post.user.user_id }}&amp;m=avatar');"></div>
<a class="news__post__detail news__post__user" href="/profile.php?u={{ post.user_id }}">
<div class="news__post__username" style="color:{{ post.display_colour|colour_get_css }}">{{ post.username }}</div>
<div class="avatar news__post__avatar" style="background-image:url('/profile.php?u={{ post.user_id }}&amp;m=avatar');"></div>
</a>
<a class="news__post__detail news__post__info news__post__info--link" href="/news.php?c={{ post.category.category_id }}">
<a class="news__post__detail news__post__info news__post__info--link" href="/news.php?c={{ post.category_id }}">
<div class="news__post__info__name">
Category
</div>
<div class="news__post__info__value">
{{ post.category.category_name }}
{{ post.category_name }}
</div>
</a>

View file

@ -12,7 +12,7 @@
{{ props.name }}
</div>
<div class="settings__account__input__value">
<input type="{{ props.type|default('text') }}" name="profile[{{ name }}]" value="{{ settings_user['user_' ~ name] }}" class="input__text settings__account__input__value__text">
<input type="{{ props.type|default('text') }}" name="profile[{{ name }}]" value="{{ settings_profile_values['user_' ~ name] }}" class="input__text settings__account__input__value__text">
</div>
</label>
{% endfor %}

View file

@ -28,12 +28,12 @@
</div>
</div>
<div class="settings__avatar__preview__container">
<div class="avatar settings__avatar__preview" id="avatar-preview" style="background-image:url('/profile.php?u={{ settings_user.user_id }}&amp;m=avatar')"></div>
<div class="avatar settings__avatar__preview" id="avatar-preview" style="background-image:url('/profile.php?u={{ avatar_user_id }}&amp;m=avatar')"></div>
</div>
</div>
<script>
function updateAvatarPreview(url, element) {
url = url || "/profile.php?u={{ settings_user.user_id }}&m=avatar";
url = url || "/profile.php?u={{ avatar_user_id }}&m=avatar";
element = element || document.getElementById('avatar-preview');
element.style.backgroundImage = 'url(\'' + url + '\')';
}

View file

@ -14,7 +14,7 @@
IP
</div>
<div class="settings__login-history__column__value">
{{ attempt.attempt_ip.string }}
{{ attempt.attempt_ip_decoded }}
{% if attempt.attempt_country != 'XX' %}
<img class="settings__login-history__country" src="https://static.flash.moe/flags/fff/{{ attempt.attempt_country|lower }}.png" alt="{{ attempt.attempt_country }}" title="{{ attempt.attempt_country|country_name }}">
{% endif %}
@ -28,12 +28,12 @@
{{ attempt.was_successful ? 'Yes' : 'No' }}
</div>
</div>
<div class="settings__login-history__column settings__login-history__column--created" onmouseenter="this.children[1].textContent = '{{ attempt.created_at }}';" onmouseleave="this.children[1].textContent = '{{ attempt.created_at.diffForHumans }}';">
<div class="settings__login-history__column settings__login-history__column--created" onmouseenter="this.children[1].textContent = '{{ attempt.created_at }}';" onmouseleave="this.children[1].textContent = '{{ attempt.created_at }}';"> {#.diffForHumans #}
<div class="settings__login-history__column__name">
Attempted
</div>
<div class="settings__login-history__column__value">
{{ attempt.created_at.diffForHumans }}
{{ attempt.created_at }} {#.diffForHumans #}
</div>
</div>
{% if attempt.user_agent|length > 0 %}
@ -49,6 +49,6 @@
</div>
{% endfor %}
{{ paginate(user_login_attempts, '?m=login-history', 'settings__') }}
{# paginate(user_login_attempts, '?m=login-history', 'settings__') #}
</div>
{% endblock %}

View file

@ -8,34 +8,34 @@
<div class="settings__sessions">
{% for session in user_sessions %}
<div class="settings__sessions__entry{% if session.session_id == settings_session.session_id %} settings__sessions__entry--current{% endif %}" id="session-{{ session.session_id }}">
<div class="settings__sessions__entry{% if session.session_id == active_session_id %} settings__sessions__entry--current{% endif %}" id="session-{{ session.session_id }}">
<div class="settings__sessions__column settings__sessions__column--ip">
<div class="settings__sessions__column__name">
IP
</div>
<div class="settings__sessions__column__value">
{{ session.session_ip.string }}
{{ session.session_ip_decoded }}
{% if session.session_country != 'XX' %}
<img class="settings__sessions__country" src="https://static.flash.moe/flags/fff/{{ session.session_country|lower }}.png" alt="{{ session.session_country }}" title="{{ session.session_country|country_name }}">
{% endif %}
</div>
</div>
<div class="settings__sessions__column settings__sessions__column--created" onmouseenter="this.children[1].textContent = '{{ session.created_at }}';" onmouseleave="this.children[1].textContent = '{{ session.created_at.diffForHumans }}';">
<div class="settings__sessions__column settings__sessions__column--created" onmouseenter="this.children[1].textContent = '{{ session.created_at }}';" onmouseleave="this.children[1].textContent = '{{ session.created_at }}';"> {# .diffForHumans #}
<div class="settings__sessions__column__name">
Created
</div>
<div class="settings__sessions__column__value">
{{ session.created_at.diffForHumans }}
{{ session.created_at }} {# .diffForHumans #}
</div>
</div>
<div class="settings__sessions__column settings__sessions__column--expires" onmouseenter="this.children[1].textContent = '{{ session.expires_on }}';" onmouseleave="this.children[1].textContent = '{{ session.expires_on.diffForHumans }}';">
<div class="settings__sessions__column settings__sessions__column--expires" onmouseenter="this.children[1].textContent = '{{ session.expires_on }}';" onmouseleave="this.children[1].textContent = '{{ session.expires_on }}';"> {# .diffForHumans #}
<div class="settings__sessions__column__name">
Expires
</div>
<div class="settings__sessions__column__value">
{{ session.expires_on.diffForHumans }}
{{ session.expires_on }} {# .diffForHumans #}
</div>
</div>
@ -53,11 +53,13 @@
<form class="settings__sessions__column settings__sessions__column--options" method="post" action="?m=sessions">
<input type="hidden" name="csrf" value="{{ csrf_token() }}">
<input type="hidden" name="session" value="{{ session.session_id }}">
<button class="input__button settings__sessions__button">Kill</button>
<button class="input__button settings__sessions__button">
{{ session.session_id == active_session_id ? 'Logout' : 'Kill' }}
</button>
</form>
</div>
{% endfor %}
{{ paginate(user_sessions, '?m=sessions', 'settings__') }}
{# paginate(user_sessions, '?m=sessions', 'settings__') #}
</div>
{% endblock %}

View file

@ -72,10 +72,10 @@
<div class="profile__info">
<div class="profile__info__section">
<div class="profile__info__block">
{% if profile.userTitle is not empty %}
{% if profile.user_title is not empty %}
<div class="profile__info__row">
<div class="profile__info__column profile__info__column--user-title" style="color:{{ profile.colour|colour_get_css }}">
{{ profile.userTitle }}
<div class="profile__info__column profile__info__column--user-title" style="color:{{ profile.display_colour|colour_get_css }}">
{{ profile.user_title }}
</div>
</div>
{% endif %}
@ -91,33 +91,34 @@
</div>
<div class="profile__info__block">
<div class="profile__info__row" title="{{ profile.created_at.format('r') }}">
<div class="profile__info__row" title="{{ profile.created_at }}">{# .format('r') #}
<div class="profile__info__column profile__info__column--heading">
Joined
</div>
<div class="profile__info__column">
{{ profile.created_at.diffForHumans }}
{{ profile.created_at }}{# .diffForHumans #}
</div>
</div>
{% if profile.last_seen.timestamp > 0 %}
<div class="profile__info__row" title="{{ profile.last_seen.format('r') }}">
{# if profile.last_seen.timestamp > 0 #}
<div class="profile__info__row" title="{{ profile.last_seen }}">
<div class="profile__info__column profile__info__column--heading">
Last Seen
</div>
<div class="profile__info__column">
{% if profile.last_seen.addMinute.timestamp >= ''|date('U') %}
{#{% if profile.last_seen.addMinute.timestamp >= ''|date('U') %}
Online now
{% else %}
{{ profile.last_seen.diffForHumans }}
{% endif %}
{{ profile.last_seen }}{# .diffForHumans #\}
{% endif %}#}
{{ profile.last_seen }}
</div>
</div>
{% endif %}
{# endif #}
</div>
</div>
{% if app.session != null %}
{% if app.hasActiveSession %}
{% spaceless %}
<div class="profile__info__section">
<div class="profile__info__block profile__info__block--links">