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", "nesbot/carbon": "~1.22",
"illuminate/database": "~5.5", "illuminate/database": "~5.5",
"illuminate/filesystem": "~5.5", "illuminate/filesystem": "~5.5",
"illuminate/pagination": "~5.5",
"doctrine/dbal": "~2.6", "doctrine/dbal": "~2.6",
"swiftmailer/swiftmailer": "~6.0", "swiftmailer/swiftmailer": "~6.0",
"erusev/parsedown": "~1.6", "erusev/parsedown": "~1.6",

146
composer.lock generated
View file

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

View file

@ -4,6 +4,8 @@ namespace Misuzu;
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/colour.php'; require_once __DIR__ . '/src/colour.php';
require_once __DIR__ . '/src/zalgo.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( $app = new Application(
__DIR__ . '/config/config.ini', __DIR__ . '/config/config.ini',
@ -31,24 +33,44 @@ if (PHP_SAPI !== 'cli') {
exit; exit;
} }
$app->startTemplating();
$app->getTemplating()->addPath('mio', __DIR__ . '/views/mio');
if (isset($_COOKIE['msz_uid'], $_COOKIE['msz_sid'])) { if (isset($_COOKIE['msz_uid'], $_COOKIE['msz_sid'])) {
$app->startSession((int)$_COOKIE['msz_uid'], $_COOKIE['msz_sid']); $app->startSession((int)$_COOKIE['msz_uid'], $_COOKIE['msz_sid']);
$session = $app->getSession();
if ($session !== null) { if ($app->hasActiveSession()) {
$session->user->last_seen = \Carbon\Carbon::now(); $db = Database::connection();
$session->user->last_ip = Net\IPAddress::remote();
$session->user->save(); $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'); $manage_mode = starts_with($_SERVER['REQUEST_URI'], '/manage');
$app->startTemplating();
$app->getTemplating()->addPath('mio', __DIR__ . '/views/mio');
if ($manage_mode) { if ($manage_mode) {
if ($app->getSession() === null || $app->getSession()->user->user_id !== 1) { if ($app->getUserId() !== 1) {
http_response_code(403); http_response_code(403);
echo $app->getTemplating()->render('errors.403'); echo $app->getTemplating()->render('errors.403');
exit; exit;

View file

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

View file

@ -6,19 +6,51 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\Role; use Misuzu\Database;
use Misuzu\Users\User;
require_once __DIR__ . '/misuzu.php'; require_once __DIR__ . '/misuzu.php';
$role = Role::find(1); $db = Database::connection();
if ($role === null) { $mainRoleId = (int)$db->query('
$role = Role::createRole('Member'); 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) { $notInMainRole = $db->query('
if (!$user->hasRole($role)) { SELECT `user_id`
$user->addRole($role); 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 <?php
use Carbon\Carbon; use Carbon\Carbon;
use Misuzu\Database;
use Misuzu\Net\IPAddress; use Misuzu\Net\IPAddress;
use Misuzu\Users\Role;
use Misuzu\Users\User;
use Misuzu\Users\Session; use Misuzu\Users\Session;
use Misuzu\Users\LoginAttempt;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
$db = Database::connection();
$config = $app->getConfig(); $config = $app->getConfig();
$templating = $app->getTemplating(); $templating = $app->getTemplating();
$session = $app->getSession();
$username_validation_errors = [ $username_validation_errors = [
'trim' => 'Your username may not start or end with spaces!', '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!", '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 " . User::USERNAME_MAX_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.", 'double-spaces' => "Your username can't contain double spaces.",
'invalid' => 'Your username contains invalid characters.', 'invalid' => 'Your username contains invalid characters.',
'spacing' => 'Please use either underscores or spaces, not both!', 'spacing' => 'Please use either underscores or spaces, not both!',
@ -38,17 +36,20 @@ if (!empty($_REQUEST['email'])) {
switch ($mode) { switch ($mode) {
case 'logout': case 'logout':
if ($session === null) { if (!$app->hasActiveSession()) {
header('Location: /'); header('Location: /');
return; return;
} }
// this is temporary, don't scream at me for using md5
if (isset($_GET['s']) && tmp_csrf_verify($_GET['s'])) { if (isset($_GET['s']) && tmp_csrf_verify($_GET['s'])) {
set_cookie_m('uid', '', -3600); set_cookie_m('uid', '', -3600);
set_cookie_m('sid', '', -3600); set_cookie_m('sid', '', -3600);
$session->delete(); $deleteSession = $db->prepare('
$app->setSession(null); DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $app->getSessionId());
$deleteSession->execute();
header('Location: /'); header('Location: /');
return; return;
} }
@ -57,7 +58,7 @@ switch ($mode) {
break; break;
case 'login': case 'login':
if ($session !== null) { if ($app->hasActiveSession()) {
header('Location: /'); header('Location: /');
break; break;
} }
@ -66,56 +67,103 @@ switch ($mode) {
$auth_login_error = ''; $auth_login_error = '';
while ($_SERVER['REQUEST_METHOD'] === 'POST') { while ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ipAddress = IPAddress::remote(); $ipAddressObj = IPAddress::remote();
$ipAddress = $ipAddressObj->getString();
if (!isset($_POST['username'], $_POST['password'])) { if (!isset($_POST['username'], $_POST['password'])) {
$auth_login_error = "You didn't fill all the forms!"; $auth_login_error = "You didn't fill all the forms!";
break; break;
} }
$loginAttempts = LoginAttempt::fromIpAddress(IPAddress::remote()) $fetchRemainingAttempts = $db->prepare('
->where('was_successful', false) SELECT 5 - COUNT(`attempt_id`)
->where('created_at', '>', Carbon::now()->subHour()->toDateTimeString()) FROM `msz_login_attempts`
->get(); 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.'; $auth_login_error = 'Too many failed login attempts, try again later.';
break; break;
} }
$remainingAttempts -= 1;
$username = $_POST['username'] ?? ''; $username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? ''; $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) { $auth_error_str = "Invalid username or password, {$remainingAttempts} attempt(s) remaining.";
LoginAttempt::recordFail($ipAddress, null, $user_agent);
$auth_login_error = 'Invalid username or password!'; if ($userId < 1) {
user_login_attempt_record(false, null, $ipAddress, $user_agent);
$auth_login_error = $auth_error_str;
break; break;
} }
if (!$user->verifyPassword($password)) { if (!password_verify($password, $userData['password'])) {
LoginAttempt::recordFail($ipAddress, $user, $user_agent); user_login_attempt_record(false, $userId, $ipAddress, $user_agent);
$auth_login_error = 'Invalid username or password!'; $auth_login_error = $auth_error_str;
break; break;
} }
LoginAttempt::recordSuccess($ipAddress, $user, $user_agent); user_login_attempt_record(true, $userId, $ipAddress, $user_agent);
$session = Session::createSession($user, $user_agent, null, $ipAddress); $sessionKey = bin2hex(random_bytes(32));
$app->setSession($session);
$cookie_life = Carbon::now()->addMonth()->timestamp; $createSession = $db->prepare('
set_cookie_m('uid', $session->user_id, $cookie_life); INSERT INTO `msz_sessions`
set_cookie_m('sid', $session->session_key, $cookie_life); (`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. // Temporary key generation for chat login.
// Should eventually be replaced with a callback login system. // Should eventually be replaced with a callback login system.
// Also uses different cookies since $httponly is required to be false for these. // Also uses different cookies since $httponly is required to be false for these.
$user->user_chat_key = bin2hex(random_bytes(16)); $chatKey = bin2hex(random_bytes(16));
$user->save();
$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: /'); header('Location: /');
return; return;
} }
@ -128,7 +176,7 @@ switch ($mode) {
break; break;
case 'register': case 'register':
if ($session !== null) { if ($app->hasActiveSession()) {
header('Location: /'); header('Location: /');
} }
@ -149,13 +197,13 @@ switch ($mode) {
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? ''; $email = $_POST['email'] ?? '';
$username_validate = User::validateUsername($username, true); $username_validate = user_validate_username($username, true);
if ($username_validate !== '') { if ($username_validate !== '') {
$auth_register_error = $username_validation_errors[$username_validate]; $auth_register_error = $username_validation_errors[$username_validate];
break; break;
} }
$email_validate = User::validateEmail($email, true); $email_validate = user_validate_email($email, true);
if ($email_validate !== '') { if ($email_validate !== '') {
$auth_register_error = $email_validate === 'in-use' $auth_register_error = $email_validate === 'in-use'
? 'This e-mail address has already been used!' ? 'This e-mail address has already been used!'
@ -163,13 +211,45 @@ switch ($mode) {
break; break;
} }
if (User::validatePassword($password) !== '') { if (user_validate_password($password) !== '') {
$auth_register_error = 'Your password is too weak!'; $auth_register_error = 'Your password is too weak!';
break; break;
} }
$user = User::createUser($username, $password, $email); $ipAddress = IPAddress::remote()->getString();
$user->addRole(Role::find(1), true); $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.'); $templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
break; break;
} }

View file

@ -1,13 +1,9 @@
<?php <?php
use Misuzu\Database; use Misuzu\Database;
use Misuzu\News\NewsPost;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
//$featured_news = NewsPost::where('is_featured', true)->orderBy('created_at', 'desc')->take(3)->get(); $featuredNews = Database::connection()
$featuredNews = [];
$fetchNews = Database::connection()
->query(' ->query('
SELECT SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`, p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`,
@ -21,13 +17,8 @@ ON u.`display_role` = r.`role_id`
WHERE p.`is_featured` = true WHERE p.`is_featured` = true
ORDER BY p.`created_at` DESC ORDER BY p.`created_at` DESC
LIMIT 3 LIMIT 3
'); ')->fetchAll();
while (($newsPost = $fetchNews->fetchObject(NewsPost::class)) !== false) {
$featuredNews['post'] = $newsPost;
}
var_dump($featuredNews);
//var_dump(Database::connection()->query('SHOW SESSION STATUS LIKE "Questions"')->fetch());
echo $app->getTemplating()->render('home.landing', compact('featuredNews')); echo $app->getTemplating()->render('home.landing', compact('featuredNews'));

View file

@ -1,9 +1,9 @@
<?php <?php
use Misuzu\Users\Role; use Misuzu\Database;
use Misuzu\Users\User;
require_once __DIR__ . '/../../misuzu.php'; require_once __DIR__ . '/../../misuzu.php';
$db = Database::connection();
$templating = $app->getTemplating(); $templating = $app->getTemplating();
$is_post_request = $_SERVER['REQUEST_METHOD'] === 'POST'; $is_post_request = $_SERVER['REQUEST_METHOD'] === 'POST';
@ -11,7 +11,17 @@ $page_id = (int)($_GET['p'] ?? 1);
switch ($_GET['v'] ?? null) { switch ($_GET['v'] ?? null) {
case 'listing': 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')); $templating->vars(compact('manage_users'));
echo $templating->render('@manage.users.listing'); echo $templating->render('@manage.users.listing');
break; break;
@ -24,19 +34,44 @@ switch ($_GET['v'] ?? null) {
break; 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.'; echo 'Could not find that user.';
break; break;
} }
$templating->var('view_user', $view_user); $templating->var('view_user', $manageUser);
echo $templating->render('@manage.users.view'); echo $templating->render('@manage.users.view');
break; break;
case 'roles': 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')); $templating->vars(compact('manage_roles'));
echo $templating->render('@manage.users.roles'); echo $templating->render('@manage.users.roles');
break; break;
@ -97,15 +132,38 @@ switch ($_GET['v'] ?? null) {
break; break;
} }
$edit_role = $role_id < 1 ? new Role : Role::find($role_id); if ($role_id < 1) {
$edit_role->role_name = $role_name; $updateRole = $db->prepare('
$edit_role->role_hierarchy = $role_hierarchy; INSERT INTO `msz_roles`
$edit_role->role_secret = $role_secret; (`role_name`, `role_hierarchy`, `role_secret`, `role_colour`, `role_description`, `created_at`)
$edit_role->role_colour = $role_colour; VALUES
$edit_role->role_description = $role_description; (:role_name, :role_hierarchy, :role_secret, :role_colour, :role_description, NOW())
$edit_role->save(); ');
} 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; break;
} }
@ -115,9 +173,15 @@ switch ($_GET['v'] ?? null) {
break; 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'; echo 'invalid role';
break; break;
} }

View file

@ -1,9 +1,9 @@
<?php <?php
use Misuzu\News\NewsCategory; use Misuzu\Database;
use Misuzu\News\NewsPost;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
$db = Database::connection();
$templating = $app->getTemplating(); $templating = $app->getTemplating();
$category_id = isset($_GET['c']) ? (int)$_GET['c'] : null; $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); $page_id = (int)($_GET['p'] ?? 1);
if ($post_id !== null) { 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); http_response_code(404);
echo $templating->render('errors.404'); echo $templating->render('errors.404');
return; return;
@ -24,31 +40,102 @@ if ($post_id !== null) {
} }
if ($category_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); http_response_code(404);
echo $templating->render('errors.404'); echo $templating->render('errors.404');
return; 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); http_response_code(404);
echo $templating->render('errors.404'); echo $templating->render('errors.404');
return; 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')); echo $templating->render('news.category', compact('category', 'posts', 'featured', 'page_id'));
return; return;
} }
$categories = NewsCategory::where('is_hidden', false)->get(); $getCategories = $db->prepare('
$posts = NewsPost::where('is_featured', true)->orderBy('created_at', 'desc')->paginate(5, ['*'], 'p', $page_id); 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); http_response_code(404);
echo $templating->render('errors.404'); echo $templating->render('errors.404');
return; return;

View file

@ -1,12 +1,11 @@
<?php <?php
use Misuzu\Database;
use Misuzu\IO\File; use Misuzu\IO\File;
use Misuzu\Users\User;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
$user_id = (int)($_GET['u'] ?? 0); $user_id = (int)($_GET['u'] ?? 0);
$mode = (string)($_GET['m'] ?? 'view'); $mode = (string)($_GET['m'] ?? 'view');
$profile_user = User::find($user_id);
switch ($mode) { switch ($mode) {
case 'avatar': case 'avatar':
@ -14,8 +13,7 @@ switch ($mode) {
$app->getConfig()->get('Avatar', 'default_path', 'string', 'public/images/no-avatar.png') $app->getConfig()->get('Avatar', 'default_path', 'string', 'public/images/no-avatar.png')
); );
if ($profile_user !== null) { $user_avatar = "{$user_id}.msz";
$user_avatar = "{$profile_user->user_id}.msz";
$cropped_avatar = $app->getStore('avatars/200x200')->filename($user_avatar); $cropped_avatar = $app->getStore('avatars/200x200')->filename($user_avatar);
if (File::exists($cropped_avatar)) { if (File::exists($cropped_avatar)) {
@ -35,7 +33,6 @@ switch ($mode) {
} }
} }
} }
}
header('Content-Type: ' . mime_content_type($avatar_filename)); header('Content-Type: ' . mime_content_type($avatar_filename));
echo File::readToEnd($avatar_filename); echo File::readToEnd($avatar_filename);
@ -45,13 +42,26 @@ switch ($mode) {
default: default:
$templating = $app->getTemplating(); $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); http_response_code(404);
echo $templating->render('user.notfound'); echo $templating->render('user.notfound');
break; break;
} }
$templating->var('profile', $profile_user); $templating->vars(compact('profile'));
echo $templating->render('user.view'); echo $templating->render('user.view');
break; break;
} }

View file

@ -1,17 +1,15 @@
<?php <?php
use Misuzu\Application; use Misuzu\Database;
use Misuzu\IO\File; use Misuzu\IO\File;
use Misuzu\Users\User;
use Misuzu\Users\Session;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
$settings_session = $app->getSession(); $db = Database::connection();
$templating = $app->getTemplating(); $templating = $app->getTemplating();
$page_id = (int)($_GET['p'] ?? 1); $page_id = (int)($_GET['p'] ?? 1);
if (Application::getInstance()->getSession() === null) { if (!$app->hasActiveSession()) {
http_response_code(403); http_response_code(403);
echo $templating->render('errors.403'); echo $templating->render('errors.403');
return; return;
@ -73,8 +71,6 @@ $settings_profile_fields = [
], ],
]; ];
$settings_user = $settings_session->user;
$settings_modes = [ $settings_modes = [
'account' => 'Account', 'account' => 'Account',
'avatar' => 'Avatar', 'avatar' => 'Avatar',
@ -83,7 +79,7 @@ $settings_modes = [
]; ];
$settings_mode = $_GET['m'] ?? key($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)) { if (!array_key_exists($settings_mode, $settings_modes)) {
http_response_code(404); http_response_code(404);
@ -95,7 +91,7 @@ if (!array_key_exists($settings_mode, $settings_modes)) {
$settings_errors = []; $settings_errors = [];
$prevent_registration = $app->getConfig()->get('Auth', 'prevent_registration', 'bool', false); $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_width = $app->getConfig()->get('Avatar', 'max_width', 'int', 4000);
$avatar_max_height = $app->getConfig()->get('Avatar', 'max_height', '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 = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000);
@ -109,6 +105,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break; break;
} }
$updatedUserFields = [];
if (isset($_POST['profile']) && is_array($_POST['profile'])) { if (isset($_POST['profile']) && is_array($_POST['profile'])) {
foreach ($settings_profile_fields as $name => $props) { foreach ($settings_profile_fields as $name => $props) {
if (isset($_POST['profile'][$name])) { if (isset($_POST['profile'][$name])) {
@ -129,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$field_value = $field_matches[1]; $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'])) && (!empty($_POST['password']['new']) || !empty($_POST['email']['new']))
) )
) { ) {
if (!$settings_user->verifyPassword($_POST['current_password'])) { $fetchPassword = $db->prepare('
$settings_errors[] = "Your current password was incorrect."; 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; break;
} }
if (!empty($_POST['email']['new'])) { if (!empty($_POST['email']['new'])) {
if (empty($_POST['email']['confirm']) || $_POST['email']['new'] !== $_POST['email']['confirm']) { if (empty($_POST['email']['confirm'])
$settings_errors[] = "The given e-mail addresses did not match."; || $_POST['email']['new'] !== $_POST['email']['confirm']) {
$settings_errors[] = 'The given e-mail addresses did not match.';
break; break;
} }
if ($_POST['email']['new'] === $settings_user->email) { $checkIfAlreadySet = $db->prepare('
$settings_errors[] = "This is your e-mail address already!"; 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; break;
} }
$email_validate = User::validateEmail($_POST['email']['new'], true); $email_validate = user_validate_email($_POST['email']['new'], true);
if ($email_validate !== '') { if ($email_validate !== '') {
switch ($email_validate) { switch ($email_validate) {
case 'dns': case 'dns':
$settings_errors[] = "No valid MX record exists for this domain."; $settings_errors[] = 'No valid MX record exists for this domain.';
break; break;
case 'format': case 'format':
$settings_errors[] = "The given e-mail address was incorrectly formatted."; $settings_errors[] = 'The given e-mail address was incorrectly formatted.';
break; break;
case 'in-use': 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; break;
default: default:
$settings_errors[] = "Unknown e-mail validation error."; $settings_errors[] = 'Unknown e-mail validation error.';
} }
break; break;
} }
$settings_user->email = $_POST['email']['new']; $updatedUserFields['email'] = strtolower($_POST['email']['new']);
} }
if (!empty($_POST['password']['new'])) { if (!empty($_POST['password']['new'])) {
@ -189,20 +211,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break; break;
} }
$password_validate = User::validatePassword($_POST['password']['new']); $password_validate = user_validate_password($_POST['password']['new']);
if ($password_validate !== '') { if ($password_validate !== '') {
$settings_errors[] = "The given passwords was too weak."; $settings_errors[] = "The given passwords was too weak.";
break; break;
} }
$settings_user->password = $_POST['password']['new']; $updatedUserFields['password'] = password_hash($_POST['password']['new'], PASSWORD_ARGON2I);
} }
} }
} }
if (count($settings_errors) < 1 && $settings_user->isDirty()) { if (count($settings_errors) < 1 && count($updatedUserFields) > 0) {
$settings_user->save(); $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; break;
@ -308,23 +336,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$session_id = (int)($_POST['session'] ?? 0); $session_id = (int)($_POST['session'] ?? 0);
if ($session_id < 1) { if ($session_id < 1) {
$settings_errors[] = 'no'; $settings_errors[] = 'Invalid session.';
break; 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.'; $settings_errors[] = 'You may only end your own sessions.';
break; 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()); header('Location: /auth.php?m=logout&s=' . tmp_csrf_token());
return; 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; break;
} }
} }
@ -334,11 +373,21 @@ $templating->var('settings_title', $settings_modes[$settings_mode]);
switch ($settings_mode) { switch ($settings_mode) {
case 'account': 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')); $templating->vars(compact('settings_profile_fields', 'prevent_registration'));
break; break;
case 'avatar': case 'avatar':
$user_has_avatar = File::exists($app->getStore('avatars/original')->filename($avatar_filename)); $user_has_avatar = File::exists($app->getStore('avatars/original')->filename($avatar_filename));
$templating->var('avatar_user_id', $app->getUserId());
$templating->vars(compact( $templating->vars(compact(
'avatar_max_width', 'avatar_max_width',
'avatar_max_height', 'avatar_max_height',
@ -348,19 +397,44 @@ switch ($settings_mode) {
break; break;
case 'sessions': case 'sessions':
$sessions = $settings_user->sessions() /*$sessions = $settings_user->sessions()
->orderBy('session_id', 'desc') ->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); $templating->var('user_sessions', $sessions);
break; break;
case 'login-history': case 'login-history':
$login_attempts = $settings_user->loginAttempts() /*$login_attempts = $settings_user->loginAttempts()
->orderBy('attempt_id', 'desc') ->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; 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 <?php
namespace Misuzu; namespace Misuzu;
use Carbon\Carbon;
use Misuzu\Config\ConfigManager; use Misuzu\Config\ConfigManager;
use Misuzu\IO\Directory; use Misuzu\IO\Directory;
use Misuzu\IO\DirectoryDoesNotExistException; use Misuzu\IO\DirectoryDoesNotExistException;
@ -28,10 +29,16 @@ class Application extends ApplicationBase
]; ];
/** /**
* Session instance. * Active Session ID.
* @var \Misuzu\Users\Session * @var int
*/ */
private $sessionInstance = null; private $currentSessionId = 0;
/**
* Active User ID.
* @var int
*/
private $currentUserId = 0;
/** /**
* Database instance. * Database instance.
@ -39,12 +46,6 @@ class Application extends ApplicationBase
*/ */
private $databaseInstance = null; private $databaseInstance = null;
/**
* Database instance.
* @var \Misuzu\Database
*/
private $database;
/** /**
* ConfigManager instance. * ConfigManager instance.
* @var \Misuzu\Config\ConfigManager * @var \Misuzu\Config\ConfigManager
@ -108,7 +109,7 @@ class Application extends ApplicationBase
*/ */
public function getPath(string $path): string public function getPath(string $path): string
{ {
if (!starts_with($path, '/')) { if (!starts_with($path, '/') && substr($path, 1, 2) !== ':\\') {
$path = __DIR__ . '/../' . $path; $path = __DIR__ . '/../' . $path;
} }
@ -161,40 +162,53 @@ class Application extends ApplicationBase
/** /**
* Starts a user session. * Starts a user session.
* @param int $user_id * @param int $userId
* @param string $session_key * @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) $dbc = Database::connection();
->where('user_id', $user_id)
->first();
if ($session !== null) { $findSession = $dbc->prepare('
if ($session->hasExpired()) { SELECT `session_id`, `expires_on`
$session->delete(); 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 { } else {
$this->setSession($session); $this->currentSessionId = (int)$sessionData['session_id'];
$this->currentUserId = $userId;
} }
} }
} }
/** public function hasActiveSession(): bool
* Gets the current session instance.
* @return Session|null
*/
public function getSession(): ?Session
{ {
return $this->sessionInstance; return $this->getSessionId() > 0;
} }
/** public function getSessionId(): int
* Registers a session.
* @param Session|null $sessionInstance
*/
public function setSession(?Session $sessionInstance): void
{ {
$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.'); 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->databaseInstance = new DatabaseV1($this->configInstance, self::DATABASE_CONNECTIONS[0]);
$this->loadDatabaseConnections(); $this->loadDatabaseConnections();
} }

View file

@ -136,7 +136,7 @@ final class Database
$dsn .= 'dbname=' . $this->configManager->get($section, 'database', 'string', 'misuzu') . ';'; $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; break;
} }

View file

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

View file

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

View file

@ -94,13 +94,18 @@ class Directory
throw new DirectoryExistsException; throw new DirectoryExistsException;
} }
$on_windows = running_on_windows();
$path = Directory::fixSlashes($path); $path = Directory::fixSlashes($path);
$split_path = explode(self::SEPARATOR, $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) { foreach ($split_path as $path_part) {
$existing_path .= $path_part . self::SEPARATOR; $existing_path .= $path_part . self::SEPARATOR;
if ($on_windows && substr($existing_path, 1, 2) === ':\\') {
continue;
}
if (!Directory::exists($existing_path)) { if (!Directory::exists($existing_path)) {
mkdir($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; namespace MisuzuTests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Misuzu\Users\User;
class UserTest extends TestCase class UserTest extends TestCase
{ {
public function testUsernameValidation() public function testUsernameValidation()
{ {
$this->assertEquals(User::validateUsername('flashwave'), ''); $this->assertEquals(user_validate_username('flashwave'), '');
$this->assertEquals(User::validateUsername(' flash '), 'trim'); $this->assertEquals(user_validate_username(' flash '), 'trim');
$this->assertEquals(User::validateUsername('f'), 'short'); $this->assertEquals(user_validate_username('f'), 'short');
$this->assertEquals(User::validateUsername('flaaaaaaaaaaaaaaaash'), 'long'); $this->assertEquals(user_validate_username('flaaaaaaaaaaaaaaaash'), 'long');
$this->assertEquals(User::validateUsername('F|@$h'), 'invalid'); $this->assertEquals(user_validate_username('F|@$h'), 'invalid');
$this->assertEquals(User::validateUsername('fl ash_wave'), 'spacing'); $this->assertEquals(user_validate_username('fl ash_wave'), 'spacing');
$this->assertEquals(User::validateUsername('fl ash'), 'double-spaces'); $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 // 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. // 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) { return hash_equals(tmp_csrf_token(), $token);
$session = \Misuzu\Application::getInstance()->getSession();
} }
return hash_equals(tmp_csrf_token($session), $token); function tmp_csrf_token(): string
}
function tmp_csrf_token(?\Misuzu\Users\Session $session = null): string
{ {
if ($session === null) { return md5($_COOKIE['msz_sid'] ?? 'this is very insecure lmao');
$session = \Misuzu\Application::getInstance()->getSession();
}
return md5($session->session_key);
} }
function crop_image_centred_path(string $filename, int $target_width, int $target_height): \Imagick 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(); 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__user">
<div class="header__menu"> <div class="header__menu">
<input type="checkbox" id="menu-user-state" class="header__menu__state"> <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__options header__menu__options--user">
<div class="header__menu__section"> <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> <a class="header__menu__link" href="/settings.php">Settings</a>
</div> </div>
<div class="header__menu__section"> <div class="header__menu__section">

View file

@ -16,6 +16,6 @@
</div> </div>
<div class="container container--center"> <div class="container container--center">
{{ paginate(manage_users, '?v=listing') }} {# paginate(manage_users, '?v=listing') #}
</div> </div>
{% endblock %} {% 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 %}> <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"> <div class="listing__entry__content role-listing__entry__content">
{{ role.role_name }} {{ role.role_name }}
{{ role.users.count }} users {{ role.users }} users
</div> </div>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div class="container container--center"> <div class="container container--center">
{{ paginate(manage_roles, '?v=roles') }} {# paginate(manage_roles, '?v=roles') #}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -23,14 +23,14 @@
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Register IP</div> <div class="form__label__text">Register IP</div>
<div class="form__label__input"> <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> </div>
</label> </label>
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Last IP</div> <div class="form__label__text">Last IP</div>
<div class="form__label__input"> <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> </div>
</label> </label>

View file

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

View file

@ -25,17 +25,17 @@
</div> </div>
<div class="header__menu"> <div class="header__menu">
{% if app.session is not null %} {% if app.hasActiveSession %}
<div class="container header__user"> <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"> <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"> <div class="header__user__links__container">
<ul class="header__user__links"> <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> <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> <li class="header__user__option"><a class="header__user__link" href="{{ manage_link|default('/manage/index.php') }}">Manage</a></li>
{% endif %} {% endif %}
<li class="header__user__option"><a class="header__user__link" href="/auth.php?m=logout&amp;s={{ csrf_token() }}">Logout</a></li> <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>
</div> </div>
{% endif %} {% endif %}
{{ paginate(posts, '?c=' ~ category.category_id, 'news__') }} {# paginate(posts, '?c=' ~ category.category_id, 'news__') #}
</div> </div>
</div> </div>
</div> </div>

View file

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

View file

@ -5,18 +5,18 @@
</a> </a>
<div class="container__content news__preview__content"> <div class="container__content news__preview__content">
<div class="news__preview__text"> <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> <p><b><i><a href="/news.php?n={{ post.post_id }}">View full post</a></i></b></p>
</div> </div>
<div class="news__preview__info"> <div class="news__preview__info">
<div class="news__preview__date"> <div class="news__preview__date">
{{ post.created_at }} {{ post.created_at }}
</div> </div>
<a class="news__preview__user" href="/profile.php?u={{ post.user.user_id }}"> <a class="news__preview__user" href="/profile.php?u={{ post.user_id }}">
<div class="news__preview__user__name" style="color:{{ post.user.colour|colour_get_css }}"> <div class="news__preview__user__name" style="color:{{ post.display_colour|colour_get_css }}">
{{ post.user.username }} {{ post.username }}
</div> </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> </a>
</div> </div>
</div> </div>

View file

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

View file

@ -12,7 +12,7 @@
{{ props.name }} {{ props.name }}
</div> </div>
<div class="settings__account__input__value"> <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> </div>
</label> </label>
{% endfor %} {% endfor %}

View file

@ -28,12 +28,12 @@
</div> </div>
</div> </div>
<div class="settings__avatar__preview__container"> <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>
</div> </div>
<script> <script>
function updateAvatarPreview(url, element) { 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 = element || document.getElementById('avatar-preview');
element.style.backgroundImage = 'url(\'' + url + '\')'; element.style.backgroundImage = 'url(\'' + url + '\')';
} }

View file

@ -14,7 +14,7 @@
IP IP
</div> </div>
<div class="settings__login-history__column__value"> <div class="settings__login-history__column__value">
{{ attempt.attempt_ip.string }} {{ attempt.attempt_ip_decoded }}
{% if attempt.attempt_country != 'XX' %} {% 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 }}"> <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 %} {% endif %}
@ -28,12 +28,12 @@
{{ attempt.was_successful ? 'Yes' : 'No' }} {{ attempt.was_successful ? 'Yes' : 'No' }}
</div> </div>
</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"> <div class="settings__login-history__column__name">
Attempted Attempted
</div> </div>
<div class="settings__login-history__column__value"> <div class="settings__login-history__column__value">
{{ attempt.created_at.diffForHumans }} {{ attempt.created_at }} {#.diffForHumans #}
</div> </div>
</div> </div>
{% if attempt.user_agent|length > 0 %} {% if attempt.user_agent|length > 0 %}
@ -49,6 +49,6 @@
</div> </div>
{% endfor %} {% endfor %}
{{ paginate(user_login_attempts, '?m=login-history', 'settings__') }} {# paginate(user_login_attempts, '?m=login-history', 'settings__') #}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -8,34 +8,34 @@
<div class="settings__sessions"> <div class="settings__sessions">
{% for session in user_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 settings__sessions__column--ip">
<div class="settings__sessions__column__name"> <div class="settings__sessions__column__name">
IP IP
</div> </div>
<div class="settings__sessions__column__value"> <div class="settings__sessions__column__value">
{{ session.session_ip.string }} {{ session.session_ip_decoded }}
{% if session.session_country != 'XX' %} {% 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 }}"> <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 %} {% endif %}
</div> </div>
</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"> <div class="settings__sessions__column__name">
Created Created
</div> </div>
<div class="settings__sessions__column__value"> <div class="settings__sessions__column__value">
{{ session.created_at.diffForHumans }} {{ session.created_at }} {# .diffForHumans #}
</div> </div>
</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"> <div class="settings__sessions__column__name">
Expires Expires
</div> </div>
<div class="settings__sessions__column__value"> <div class="settings__sessions__column__value">
{{ session.expires_on.diffForHumans }} {{ session.expires_on }} {# .diffForHumans #}
</div> </div>
</div> </div>
@ -53,11 +53,13 @@
<form class="settings__sessions__column settings__sessions__column--options" method="post" action="?m=sessions"> <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="csrf" value="{{ csrf_token() }}">
<input type="hidden" name="session" value="{{ session.session_id }}"> <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> </form>
</div> </div>
{% endfor %} {% endfor %}
{{ paginate(user_sessions, '?m=sessions', 'settings__') }} {# paginate(user_sessions, '?m=sessions', 'settings__') #}
</div> </div>
{% endblock %} {% endblock %}

View file

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