Removed wonky modules system and added documentation all over.

This commit is contained in:
flash 2018-04-25 00:55:46 +02:00
parent aa83955c7b
commit e716af9b5a
34 changed files with 1176 additions and 183 deletions

View file

@ -1 +0,0 @@

View file

@ -3,7 +3,7 @@ namespace Misuzu;
require_once 'vendor/autoload.php';
$app = Application::start(
$app = new Application(
__DIR__ . '/config/config.ini',
IO\Directory::exists(__DIR__ . '/vendor/phpunit/phpunit')
);
@ -21,11 +21,11 @@ if (PHP_SAPI !== 'cli') {
ob_start('ob_gzhandler');
}
if ($app->config->get('Auth', 'lockdown', 'bool', false)) {
if ($app->getConfig()->get('Auth', 'lockdown', 'bool', false)) {
http_response_code(503);
$app->startTemplating();
$app->templating->addPath('auth', __DIR__ . '/views/auth');
echo $app->templating->render('lockdown');
$app->getTemplating()->addPath('auth', __DIR__ . '/views/auth');
echo $app->getTemplating()->render('lockdown');
exit;
}
@ -35,7 +35,7 @@ if (PHP_SAPI !== 'cli') {
if ($session !== null) {
$session->user->last_seen = \Carbon\Carbon::now();
$session->user->last_ip = \Misuzu\Net\IPAddress::remote();
$session->user->last_ip = Net\IPAddress::remote();
$session->user->save();
}
}
@ -43,15 +43,15 @@ if (PHP_SAPI !== 'cli') {
$manage_mode = starts_with($_SERVER['REQUEST_URI'], '/manage');
$app->startTemplating();
$app->templating->addPath('mio', __DIR__ . '/views/mio');
$app->getTemplating()->addPath('mio', __DIR__ . '/views/mio');
if ($manage_mode) {
if ($app->getSession() === null || $app->getSession()->user->user_id !== 1) {
http_response_code(403);
echo $app->templating->render('errors.403');
echo $app->getTemplating()->render('errors.403');
exit;
}
$app->templating->addPath('manage', __DIR__ . '/views/manage');
$app->getTemplating()->addPath('manage', __DIR__ . '/views/manage');
}
}

View file

@ -6,16 +6,13 @@
namespace Misuzu;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\ConnectionResolver;
use Illuminate\Database\Migrations\DatabaseMigrationRepository;
use Illuminate\Database\Schema\Builder;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Filesystem\Filesystem;
require_once __DIR__ . '/misuzu.php';
$repository = new DatabaseMigrationRepository(Application::getInstance()->database->getDatabaseManager(), 'migrations');
$repository = new DatabaseMigrationRepository(Application::getInstance()->getDatabase()->getDatabaseManager(), 'migrations');
$migrator = new Migrator($repository, $repository->getConnectionResolver(), new Filesystem);
if (!$migrator->repositoryExists()) {

View file

@ -1,8 +1,5 @@
<?php
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Misuzu\Application;
use Misuzu\Database;
use Misuzu\Net\IPAddress;
use Misuzu\Users\Role;
use Misuzu\Users\User;
@ -23,16 +20,16 @@ $username_validation_errors = [
$mode = $_GET['m'] ?? 'login';
$prevent_registration = $app->config->get('Auth', 'prevent_registration', 'bool', false);
$app->templating->var('auth_mode', $mode);
$app->templating->addPath('auth', __DIR__ . '/../views/auth');
$app->templating->var('prevent_registration', $prevent_registration);
$app->getTemplating()->var('auth_mode', $mode);
$app->getTemplating()->addPath('auth', __DIR__ . '/../views/auth');
$app->getTemplating()->var('prevent_registration', $prevent_registration);
if (!empty($_REQUEST['username'])) {
$app->templating->var('auth_username', $_REQUEST['username']);
$app->getTemplating()->var('auth_username', $_REQUEST['username']);
}
if (!empty($_REQUEST['email'])) {
$app->templating->var('auth_email', $_REQUEST['email']);
$app->getTemplating()->var('auth_email', $_REQUEST['email']);
}
switch ($mode) {
@ -52,7 +49,7 @@ switch ($mode) {
return;
}
echo $app->templating->render('@auth.logout');
echo $app->getTemplating()->render('@auth.logout');
break;
case 'login':
@ -120,10 +117,10 @@ switch ($mode) {
}
if (!empty($auth_login_error)) {
$app->templating->var('auth_login_error', $auth_login_error);
$app->getTemplating()->var('auth_login_error', $auth_login_error);
}
echo $app->templating->render('auth');
echo $app->getTemplating()->render('auth');
break;
case 'register':
@ -169,14 +166,14 @@ switch ($mode) {
$user = User::createUser($username, $password, $email);
$user->addRole(Role::find(1), true);
$app->templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
$app->getTemplating()->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
break;
}
if (!empty($auth_register_error)) {
$app->templating->var('auth_register_error', $auth_register_error);
$app->getTemplating()->var('auth_register_error', $auth_register_error);
}
echo $app->templating->render('@auth.auth');
echo $app->getTemplating()->render('@auth.auth');
break;
}

View file

@ -5,4 +5,4 @@ require_once __DIR__ . '/../misuzu.php';
$featured_news = NewsPost::where('is_featured', true)->orderBy('created_at', 'desc')->take(3)->get();
echo $app->templating->render('home.landing', compact('featured_news'));
echo $app->getTemplating()->render('home.landing', compact('featured_news'));

View file

@ -3,7 +3,7 @@ require_once __DIR__ . '/../../misuzu.php';
switch ($_GET['v'] ?? null) {
case 'overview':
echo $app->templating->render('@manage.general.overview');
echo $app->getTemplating()->render('@manage.general.overview');
break;
case 'logs':

View file

@ -1,5 +1,4 @@
<?php
use Misuzu\Application;
use Misuzu\Colour;
use Misuzu\Users\Role;
use Misuzu\Users\User;
@ -12,8 +11,8 @@ $page_id = (int)($_GET['p'] ?? 1);
switch ($_GET['v'] ?? null) {
case 'listing':
$manage_users = User::paginate(32, ['*'], 'p', $page_id);
$app->templating->vars(compact('manage_users'));
echo $app->templating->render('@manage.users.listing');
$app->getTemplating()->vars(compact('manage_users'));
echo $app->getTemplating()->render('@manage.users.listing');
break;
case 'view':
@ -31,14 +30,14 @@ switch ($_GET['v'] ?? null) {
break;
}
$app->templating->var('view_user', $view_user);
echo $app->templating->render('@manage.users.view');
$app->getTemplating()->var('view_user', $view_user);
echo $app->getTemplating()->render('@manage.users.view');
break;
case 'roles':
$manage_roles = Role::paginate(32, ['*'], 'p', $page_id);
$app->templating->vars(compact('manage_roles'));
echo $app->templating->render('@manage.users.roles');
$app->getTemplating()->vars(compact('manage_roles'));
echo $app->getTemplating()->render('@manage.users.roles');
break;
case 'role':
@ -121,9 +120,9 @@ switch ($_GET['v'] ?? null) {
break;
}
$app->templating->vars(compact('edit_role'));
$app->getTemplating()->vars(compact('edit_role'));
}
echo $app->templating->render('@manage.users.roles_create');
echo $app->getTemplating()->render('@manage.users.roles_create');
break;
}

View file

@ -13,11 +13,11 @@ if ($post_id !== null) {
if ($post === null) {
http_response_code(404);
echo $app->templating->render('errors.404');
echo $app->getTemplating()->render('errors.404');
return;
}
echo $app->templating->render('news.post', compact('post'));
echo $app->getTemplating()->render('news.post', compact('post'));
return;
}
@ -26,7 +26,7 @@ if ($category_id !== null) {
if ($category === null) {
http_response_code(404);
echo $app->templating->render('errors.404');
echo $app->getTemplating()->render('errors.404');
return;
}
@ -34,12 +34,12 @@ if ($category_id !== null) {
if (!is_valid_page($posts, $page_id)) {
http_response_code(404);
echo $app->templating->render('errors.404');
echo $app->getTemplating()->render('errors.404');
return;
}
$featured = $category->posts()->where('is_featured', 1)->orderBy('created_at', 'desc')->take(10)->get();
echo $app->templating->render('news.category', compact('category', 'posts', 'featured', 'page_id'));
echo $app->getTemplating()->render('news.category', compact('category', 'posts', 'featured', 'page_id'));
return;
}
@ -48,8 +48,8 @@ $posts = NewsPost::where('is_featured', true)->orderBy('created_at', 'desc')->pa
if (!is_valid_page($posts, $page_id)) {
http_response_code(404);
echo $app->templating->render('errors.404');
echo $app->getTemplating()->render('errors.404');
return;
}
echo $app->templating->render('news.index', compact('categories', 'posts', 'page_id'));
echo $app->getTemplating()->render('news.index', compact('categories', 'posts', 'page_id'));

View file

@ -1,4 +1,4 @@
<?php
require_once __DIR__ . '/../misuzu.php';
echo $app->templating->render('errors.404');
echo $app->getTemplating()->render('errors.404');

View file

@ -12,7 +12,7 @@ $profile_user = User::find($user_id);
switch ($mode) {
case 'avatar':
$avatar_filename = $app->getPath(
$app->config->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) {
@ -46,11 +46,11 @@ switch ($mode) {
default:
if ($profile_user === null) {
http_response_code(404);
echo $app->templating->render('user.notfound');
echo $app->getTemplating()->render('user.notfound');
break;
}
$app->templating->var('profile', $profile_user);
echo $app->templating->render('user.view');
$app->getTemplating()->var('profile', $profile_user);
echo $app->getTemplating()->render('user.view');
break;
}

View file

@ -12,7 +12,7 @@ $page_id = (int)($_GET['p'] ?? 1);
if (Application::getInstance()->getSession() === null) {
http_response_code(403);
echo $app->templating->render('errors.403');
echo $app->getTemplating()->render('errors.403');
return;
}
@ -82,22 +82,22 @@ $settings_modes = [
];
$settings_mode = $_GET['m'] ?? key($settings_modes);
$app->templating->vars(compact('settings_mode', 'settings_modes', 'settings_user', 'settings_session'));
$app->getTemplating()->vars(compact('settings_mode', 'settings_modes', 'settings_user', 'settings_session'));
if (!array_key_exists($settings_mode, $settings_modes)) {
http_response_code(404);
$app->templating->var('settings_title', 'Not Found');
echo $app->templating->render('settings.notfound');
$app->getTemplating()->var('settings_title', 'Not Found');
echo $app->getTemplating()->render('settings.notfound');
return;
}
$settings_errors = [];
$prevent_registration = $app->config->get('Auth', 'prevent_registration', 'bool', false);
$prevent_registration = $app->getConfig()->get('Auth', 'prevent_registration', 'bool', false);
$avatar_filename = "{$settings_user->user_id}.msz";
$avatar_max_width = $app->config->get('Avatar', 'max_width', 'int', 4000);
$avatar_max_height = $app->config->get('Avatar', 'max_height', 'int', 4000);
$avatar_max_filesize = $app->config->get('Avatar', 'max_filesize', 'int', 1000000);
$avatar_max_width = $app->getConfig()->get('Avatar', 'max_width', 'int', 4000);
$avatar_max_height = $app->getConfig()->get('Avatar', 'max_height', 'int', 4000);
$avatar_max_filesize = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000);
$avatar_max_filesize_human = byte_symbol($avatar_max_filesize, true);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@ -328,17 +328,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
$app->templating->vars(compact('settings_errors'));
$app->templating->var('settings_title', $settings_modes[$settings_mode]);
$app->getTemplating()->vars(compact('settings_errors'));
$app->getTemplating()->var('settings_title', $settings_modes[$settings_mode]);
switch ($settings_mode) {
case 'account':
$app->templating->vars(compact('settings_profile_fields', 'prevent_registration'));
$app->getTemplating()->vars(compact('settings_profile_fields', 'prevent_registration'));
break;
case 'avatar':
$user_has_avatar = File::exists($app->getStore('avatars/original')->filename($avatar_filename));
$app->templating->vars(compact(
$app->getTemplating()->vars(compact(
'avatar_max_width',
'avatar_max_height',
'avatar_max_filesize',
@ -351,7 +351,7 @@ switch ($settings_mode) {
->orderBy('session_id', 'desc')
->paginate(15, ['*'], 'p', $page_id);
$app->templating->var('user_sessions', $sessions);
$app->getTemplating()->var('user_sessions', $sessions);
break;
case 'login-history':
@ -359,8 +359,8 @@ switch ($settings_mode) {
->orderBy('attempt_id', 'desc')
->paginate(15, ['*'], 'p', $page_id);
$app->templating->var('user_login_attempts', $login_attempts);
$app->getTemplating()->var('user_login_attempts', $login_attempts);
break;
}
echo $app->templating->render("settings.{$settings_mode}");
echo $app->getTemplating()->render("settings.{$settings_mode}");

View file

@ -3,12 +3,14 @@ namespace Misuzu;
use Misuzu\Config\ConfigManager;
use Misuzu\IO\Directory;
use Misuzu\IO\DirectoryDoesNotExistException;
use Misuzu\Users\Session;
use UnexpectedValueException;
use InvalidArgumentException;
/**
* Handles the set up procedures.
* @package Misuzu
*/
class Application extends ApplicationBase
{
@ -29,31 +31,75 @@ class Application extends ApplicationBase
* Session instance.
* @var \Misuzu\Users\Session
*/
private $session = null;
private $sessionInstance = null;
/**
* Database instance.
* @var \Misuzu\Database
*/
private $databaseInstance = null;
/**
* ConfigManager instance.
* @var \Misuzu\Config\ConfigManager
*/
private $configInstance = null;
/**
* TemplatingEngine instance.
* @var \Misuzu\TemplateEngine
*/
private $templatingInstance = null;
/**
* Constructor, called by ApplicationBase::start() which also passes the arguments through.
* @param ?string $configFile
* @param null|string $configFile
* @param bool $debug
*/
protected function __construct(?string $configFile = null, bool $debug = false)
public function __construct(?string $configFile = null, bool $debug = false)
{
parent::__construct();
$this->debugMode = $debug;
ExceptionHandler::register();
ExceptionHandler::debug($this->debugMode);
$this->addModule('config', new ConfigManager($configFile));
$this->configInstance = new ConfigManager($configFile);
}
/**
* Gets instance of the config manager.
* @return ConfigManager
*/
public function getConfig(): ConfigManager
{
if (is_null($this->configInstance)){
throw new UnexpectedValueException('Internal ConfigManager instance is null, how did you even manage to do this?');
}
return $this->configInstance;
}
/**
* Shuts the application down.
*/
public function __destruct()
{
ExceptionHandler::unregister();
}
/**
* Gets whether we're in debug mode or not.
* @return bool
*/
public function inDebugMode(): bool
{
return $this->debugMode;
}
/**
* Gets a storage path.
* @param string $path
* @return string
*/
public function getPath(string $path): string
{
if (!starts_with($path, '/')) {
@ -63,14 +109,19 @@ class Application extends ApplicationBase
return Directory::fixSlashes(rtrim($path, '/'));
}
/**
* Gets a data storage path, with config storage path prefix.
* @param string $append
* @return Directory
* @throws DirectoryDoesNotExistException
* @throws IO\DirectoryExistsException
*/
public function getStoragePath(string $append = ''): Directory
{
$path = '';
if (starts_with($append, '/')) {
$path = $append;
} else {
$path = $this->config->get('Storage', 'path', 'string', __DIR__ . '/../store');
$path = $this->getConfig()->get('Storage', 'path', 'string', __DIR__ . '/../store');
if (!empty($append)) {
$path .= '/' . $append;
@ -80,17 +131,33 @@ class Application extends ApplicationBase
return Directory::createOrOpen($this->getPath($path));
}
/**
* Gets a data store, with config overrides!
* @param string $purpose
* @return Directory
* @throws DirectoryDoesNotExistException
* @throws IO\DirectoryExistsException
*/
public function getStore(string $purpose): Directory
{
$override_key = 'override_' . str_replace('/', '_', $purpose);
if ($this->config->contains('Storage', $override_key)) {
return new Directory($this->config->get('Storage', $override_key));
if ($this->configInstance->contains('Storage', $override_key)) {
try {
return new Directory($this->configInstance->get('Storage', $override_key));
} catch (DirectoryDoesNotExistException $ex) {
// fall through and just get the default path.
}
}
return $this->getStoragePath($purpose);
}
/**
* Starts a user session.
* @param int $user_id
* @param string $session_key
*/
public function startSession(int $user_id, string $session_key): void
{
$session = Session::where('session_key', $session_key)
@ -106,14 +173,22 @@ class Application extends ApplicationBase
}
}
/**
* Gets the current session instance.
* @return Session|null
*/
public function getSession(): ?Session
{
return $this->session;
return $this->sessionInstance;
}
public function setSession(?Session $session): void
/**
* Registers a session.
* @param Session|null $sessionInstance
*/
public function setSession(?Session $sessionInstance): void
{
$this->session = $session;
$this->sessionInstance = $sessionInstance;
}
/**
@ -121,21 +196,34 @@ class Application extends ApplicationBase
*/
public function startDatabase(): void
{
if ($this->hasDatabase) {
if (!is_null($this->databaseInstance)) {
throw new UnexpectedValueException('Database module has already been started.');
}
$this->addModule('database', new Database($this->config, self::DATABASE_CONNECTIONS[0]));
$this->databaseInstance = new Database($this->configInstance, self::DATABASE_CONNECTIONS[0]);
$this->loadDatabaseConnections();
}
/**
* Gets the active database instance.
* @return Database
*/
public function getDatabase(): Database
{
if (is_null($this->databaseInstance)) {
throw new UnexpectedValueException('Internal database instance is null, did you run startDatabase yet?');
}
return $this->databaseInstance;
}
/**
* Sets up the required database connections defined in the DATABASE_CONNECTIONS constant.
*/
private function loadDatabaseConnections(): void
{
$config = $this->config;
$database = $this->database;
$config = $this->getConfig();
$database = $this->getDatabase();
foreach (self::DATABASE_CONNECTIONS as $name) {
$section = "Database.{$name}";
@ -153,31 +241,44 @@ class Application extends ApplicationBase
*/
public function startTemplating(): void
{
if ($this->hasTemplating) {
if (!is_null($this->templatingInstance)) {
throw new UnexpectedValueException('Templating module has already been started.');
}
$this->addModule('templating', $twig = new TemplateEngine);
$twig->debug($this->debugMode);
$this->templatingInstance = new TemplateEngine;
$this->templatingInstance->debug($this->debugMode);
$twig->var('globals', [
'site_name' => $this->config->get('Site', 'name', 'string', 'Flashii'),
'site_description' => $this->config->get('Site', 'description'),
'site_twitter' => $this->config->get('Site', 'twitter'),
'site_url' => $this->config->get('Site', 'url'),
$this->templatingInstance->var('globals', [
'site_name' => $this->configInstance->get('Site', 'name', 'string', 'Flashii'),
'site_description' => $this->configInstance->get('Site', 'description'),
'site_twitter' => $this->configInstance->get('Site', 'twitter'),
'site_url' => $this->configInstance->get('Site', 'url'),
]);
$twig->addFilter('json_decode');
$twig->addFilter('byte_symbol');
$twig->addFilter('country_name', 'get_country_name');
$twig->addFilter('flip', 'array_flip');
$twig->addFilter('create_pagination');
$twig->addFilter('first_paragraph');
$this->templatingInstance->addFilter('json_decode');
$this->templatingInstance->addFilter('byte_symbol');
$this->templatingInstance->addFilter('country_name', 'get_country_name');
$this->templatingInstance->addFilter('flip', 'array_flip');
$this->templatingInstance->addFilter('create_pagination');
$this->templatingInstance->addFilter('first_paragraph');
$twig->addFunction('git_hash', [Application::class, 'gitCommitHash']);
$twig->addFunction('git_branch', [Application::class, 'gitBranch']);
$twig->addFunction('csrf_token', 'tmp_csrf_token');
$this->templatingInstance->addFunction('git_hash', [Application::class, 'gitCommitHash']);
$this->templatingInstance->addFunction('git_branch', [Application::class, 'gitBranch']);
$this->templatingInstance->addFunction('csrf_token', 'tmp_csrf_token');
$twig->var('app', $this);
$this->templatingInstance->var('app', $this);
}
/**
* Gets an instance of the templating engine.
* @return TemplateEngine
*/
public function getTemplating(): TemplateEngine
{
if (is_null($this->templatingInstance)){
throw new UnexpectedValueException('Internal templating engine instance is null, did you run startDatabase yet?');
}
return $this->templatingInstance;
}
}

View file

@ -35,18 +35,15 @@ abstract class ApplicationBase
}
/**
* Creates an instance of whichever class extends ApplicationBase.
* I have no idea how to make a param for the ... thingy so ech.
* @return ApplicationBase
* ApplicationBase constructor.
*/
public static function start(...$params): ApplicationBase
public function __construct()
{
if (!is_null(self::$instance) || self::$instance instanceof ApplicationBase) {
throw new UnexpectedValueException('An Application has already been set up.');
}
self::$instance = new static(...$params);
return self::getInstance();
self::$instance = $this;
}
/**
@ -77,42 +74,4 @@ abstract class ApplicationBase
{
return trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
}
public function __get($name)
{
if (starts_with($name, 'has') && strlen($name) > 3 && ctype_upper($name[3])) {
$name = lcfirst(substr($name, 3));
return $this->hasModule($name);
}
if ($this->hasModule($name)) {
return $this->modules[$name];
}
throw new InvalidArgumentException('Invalid property.');
}
/**
* Adds a module to this application.
* @param string $name
* @param mixed $module
*/
public function addModule(string $name, $module): void
{
if ($this->hasModule($name)) {
throw new InvalidArgumentException('This module has already been registered.');
}
$this->modules[$name] = $module;
}
/**
* Checks if a module is registered.
* @param string $name
* @return bool
*/
public function hasModule(string $name): bool
{
return array_key_exists($name, $this->modules) && !is_null($this->modules[$name]);
}
}

View file

@ -3,27 +3,54 @@ namespace Misuzu;
use InvalidArgumentException;
/**
* Class Colour
* @package Misuzu
*/
class Colour
{
/**
* Flag to set when this this should inherit a parent's colour.
*/
private const INHERIT = 0x40000000;
/**
* Raw colour value, 32-bit integer (although only 25 bits are used).
* @var int
*/
private $rawValue = 0;
/**
* Gets the raw colour value.
* @return int
*/
public function getRaw(): int
{
return $this->rawValue;
}
/**
* Sets a raw colour value.
* @param int $raw
*/
public function setRaw(int $raw): void
{
$this->rawValue = $raw;
$this->rawValue = $raw & 0xFFFFFFFF;
}
/**
* Gets whether the inheritance flag is set.
* @return bool
*/
public function getInherit(): bool
{
return ($this->rawValue & self::INHERIT) > 0;
}
/**
* Toggles the inheritance flag.
* @param bool $state
*/
public function setInherit(bool $state): void
{
if ($state) {
@ -33,11 +60,19 @@ class Colour
}
}
/**
* Gets the red colour byte.
* @return int
*/
public function getRed(): int
{
return $this->rawValue >> 16 & 0xFF;
}
/**
* Sets the red colour byte.
* @param int $red
*/
public function setRed(int $red): void
{
$red = $red & 0xFF;
@ -45,11 +80,19 @@ class Colour
$this->rawValue |= $red << 16;
}
/**
* Gets the green colour byte.
* @return int
*/
public function getGreen(): int
{
return $this->rawValue >> 8 & 0xFF;
}
/**
* Sets the green colour byte.
* @param int $green
*/
public function setGreen(int $green): void
{
$green = $green & 0xFF;
@ -57,11 +100,19 @@ class Colour
$this->rawValue |= $green << 8;
}
/**
* Gets the blue colour byte.
* @return int
*/
public function getBlue(): int
{
return $this->rawValue & 0xFF;
}
/**
* Sets the blue colour byte.
* @param int $blue
*/
public function setBlue(int $blue): void
{
$blue = $blue & 0xFF;
@ -69,16 +120,30 @@ class Colour
$this->rawValue |= $blue;
}
/**
* Gets the hexidecimal value for this colour, without # prefix.
* @return string
*/
public function getHex(): string
{
return dechex_pad($this->getRed()) . dechex_pad($this->getGreen()) . dechex_pad($this->getBlue());
return dechex_pad($this->getRaw() & 0xFFFFFF, 6);
}
/**
* Colour constructor.
* @param int|null $raw
*/
public function __construct(?int $raw)
{
$this->rawValue = $raw ?? self::INHERIT;
}
/**
* @param int $red
* @param int $green
* @param int $blue
* @return Colour
*/
public static function fromRGB(int $red, int $green, int $blue): Colour
{
$raw = 0;
@ -88,6 +153,10 @@ class Colour
return new static($raw);
}
/**
* @param string $hex
* @return Colour
*/
public static function fromHex(string $hex): Colour
{
$hex = ltrim(strtolower($hex), '#');
@ -106,11 +175,18 @@ class Colour
);
}
/**
* @return Colour
*/
public static function none(): Colour
{
return new static(static::INHERIT);
}
/**
* Gets the hexidecimal value for this colour, with # prefix.
* @return string
*/
public function __toString()
{
return "#{$this->getHex()}";

View file

@ -21,7 +21,7 @@ class ConfigManager
/**
* Creates a file object with the given path and reloads the context.
* @param string $filename
* @param string|null $filename
*/
public function __construct(?string $filename = null)
{
@ -162,8 +162,11 @@ class ConfigManager
}
/**
* Serialises the $this->collection array to the human readable config format.
* @return string
* Serialises the $this->collection array to the human managable config format.
* @param string $filename
* @param array $collection
* @throws \Misuzu\IO\FileDoesNotExistException
* @throws \Misuzu\IO\IOException
*/
public static function write(string $filename, array $collection): void
{
@ -188,7 +191,10 @@ class ConfigManager
/**
* Parses the config file.
* @param string $config
* @param string $filename
* @return array
* @throws \Misuzu\IO\FileDoesNotExistException
* @throws \Misuzu\IO\IOException
*/
private static function read(string $filename): array
{

View file

@ -5,10 +5,20 @@ use Illuminate\Database\Capsule\Manager as LaravelDatabaseManager;
use Misuzu\Config\ConfigManager;
use InvalidArgumentException;
/**
* Class Database
* @package Misuzu
*/
class Database extends LaravelDatabaseManager
{
/**
* @var ConfigManager
*/
private $configManager;
/**
* Array of supported abstraction layers, primarily depends on what Illuminate\Database supports in reality.
*/
private const SUPPORTED_DB_ALS = [
'mysql',
'sqlite',
@ -16,12 +26,33 @@ class Database extends LaravelDatabaseManager
'sqlsrv',
];
/**
* The default port for MySQL.
*/
private const DEFAULT_PORT_MYSQL = 3306;
/**
* The default port for PostgreSQL.
*/
private const DEFAULT_PORT_PGSQL = 5432;
/**
* Default port for Microsoft SQL Server.
*/
private const DEFAULT_PORT_MSSQL = 1433;
/**
* Default hostname.
*/
private const DEFAULT_HOST = '127.0.0.1';
/**
* Database constructor.
* @param ConfigManager $config
* @param string $default
* @param bool $startEloquent
* @param bool $setAsGlobal
*/
public function __construct(
ConfigManager $config,
string $default = 'default',
@ -42,6 +73,11 @@ class Database extends LaravelDatabaseManager
}
}
/**
* Creates a new connection using a ConfigManager instance.
* @param string $section
* @param string $name
*/
public function addConnectionFromConfig(string $section, string $name = 'default'): void
{
if (!$this->configManager->contains($section, 'driver')) {

View file

@ -91,7 +91,7 @@ class ExceptionHandler
/**
* Shoots a POST request to the report URL.
* @todo Implement this (depends on Aitemu\Net\WebClient).
* @todo Implement this.
* @param Throwable $exception
*/
private static function report(Throwable $exception): void

View file

@ -14,18 +14,31 @@ class Directory
*/
private $path;
/**
* Directory separator used on this system, usually either \ for Windows or / for basically everything else.
*/
public const SEPARATOR = DIRECTORY_SEPARATOR;
/**
* Get the path of this directory.
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return bool
*/
public function isReadable(): bool
{
return is_readable($this->getPath());
}
/**
* @return bool
*/
public function isWritable(): bool
{
return is_writable($this->getPath());
@ -71,8 +84,9 @@ class Directory
/**
* Creates a directory if it doesn't already exist.
* @param string $path
* @throws DirectoryExistsException
* @return Directory
* @throws DirectoryDoesNotExistException
* @throws DirectoryExistsException
*/
public static function create(string $path): Directory
{
@ -95,6 +109,12 @@ class Directory
return new static($path);
}
/**
* @param string $path
* @return Directory
* @throws DirectoryDoesNotExistException
* @throws DirectoryExistsException
*/
public static function createOrOpen(string $path): Directory
{
if (static::exists($path)) {
@ -109,6 +129,7 @@ class Directory
* @param string $path
* @param bool $purge
* @throws DirectoryDoesNotExistException
* @throws FileDoesNotExistException
*/
public static function delete(string $path, bool $purge = false): void
{
@ -147,6 +168,7 @@ class Directory
/**
* Fixes operating system specific slashing.
* @param string $path
* @param string $separator
* @return string
*/
public static function fixSlashes(string $path, string $separator = self::SEPARATOR): string

View file

@ -10,11 +10,22 @@ use Exception;
*/
class File
{
/**
* @param string $filename
* @return FileStream
* @throws FileDoesNotExistException
* @throws IOException
*/
public static function open(string $filename): FileStream
{
return new FileStream($filename, FileStream::MODE_READ_WRITE, true);
}
/**
* @param string $filename
* @param bool $lock
* @return string
*/
public static function readToEnd(string $filename, bool $lock = false): string
{
$output = '';
@ -29,6 +40,12 @@ class File
return $output;
}
/**
* @param string $filename
* @param string $data
* @throws FileDoesNotExistException
* @throws IOException
*/
public static function writeAll(string $filename, string $data): void
{
$file = new FileStream($filename, FileStream::MODE_TRUNCATE, true);
@ -40,6 +57,8 @@ class File
* Creates an instance of a temporary file.
* @param string $prefix
* @return FileStream
* @throws FileDoesNotExistException
* @throws IOException
*/
public static function temp(string $prefix = 'Misuzu'): FileStream
{

View file

@ -3,14 +3,45 @@ namespace Misuzu\IO;
use ErrorException;
/**
* Class FileStream
* @package Misuzu\IO
*/
class FileStream extends Stream
{
/**
* Open a file for reading only.
*/
public const MODE_READ = 0x1;
/**
* Open a file for writing only.
*/
public const MODE_WRITE = 0x2;
/**
* Truncate a file.
*/
private const MODE_TRUNCATE_RAW = 0x4;
/**
* Append to a file.
*/
private const MODE_APPEND_RAW = 0x8;
/**
* Open a file for reading and writing.
*/
public const MODE_READ_WRITE = self::MODE_READ | self::MODE_WRITE;
/**
* Truncate and open a file for writing.
*/
public const MODE_TRUNCATE = self::MODE_TRUNCATE_RAW | self::MODE_WRITE;
/**
* Open a file for writing and append to the end.
*/
public const MODE_APPEND = self::MODE_APPEND_RAW | self::MODE_WRITE;
protected $fileHandle;
@ -18,6 +49,14 @@ class FileStream extends Stream
protected $fileMode;
protected $isLocked;
/**
* FileStream constructor.
* @param string $path
* @param int $mode
* @param bool $lock
* @throws FileDoesNotExistException
* @throws IOException
*/
public function __construct(string $path, int $mode, bool $lock = true)
{
$this->isLocked = $lock;
@ -37,6 +76,9 @@ class FileStream extends Stream
}
}
/**
* Clears up the resources used by this stream.
*/
public function __destruct()
{
if (!is_resource($this->fileHandle)) {
@ -46,6 +88,12 @@ class FileStream extends Stream
$this->close();
}
/**
* Creates a file mode string from our own flags.
* @param int $mode
* @return string
* @throws IOException
*/
protected static function constructFileMode(int $mode): string
{
$mode_read = ($mode & static::MODE_READ) > 0;
@ -84,18 +132,25 @@ class FileStream extends Stream
// should be at least two characters because of the b flag
if (strlen($mode_string) < 2) {
throw IOException('Failed to construct mode???');
throw new IOException('Failed to construct mode???');
}
return $mode_string;
}
public function getResource(): resource
/**
* @return int
* @throws IOException
*/
public function getResource(): int
{
$this->ensureHandleActive();
return $this->fileHandle;
}
/**
* @throws IOException
*/
protected function ensureHandleActive(): void
{
if (!is_resource($this->fileHandle)) {
@ -103,6 +158,9 @@ class FileStream extends Stream
}
}
/**
* @throws IOException
*/
protected function ensureCanRead(): void
{
if (!$this->getCanRead()) {
@ -110,6 +168,9 @@ class FileStream extends Stream
}
}
/**
* @throws IOException
*/
protected function ensureCanWrite(): void
{
if (!$this->getCanWrite()) {
@ -117,6 +178,9 @@ class FileStream extends Stream
}
}
/**
* @throws IOException
*/
protected function ensureCanSeek(): void
{
if (!$this->getCanSeek()) {
@ -124,54 +188,86 @@ class FileStream extends Stream
}
}
/**
* @return bool
*/
public function getCanRead(): bool
{
return ($this->fileMode & static::MODE_READ) > 0 && is_readable($this->filePath);
}
/**
* @return bool
*/
public function getCanSeek(): bool
{
return ($this->fileMode & static::MODE_APPEND_RAW) == 0 && $this->getCanRead();
}
/**
* @return bool
*/
public function getCanTimeout(): bool
{
return false;
}
/**
* @return bool
*/
public function getCanWrite(): bool
{
return ($this->fileMode & static::MODE_WRITE) > 0 && is_writable($this->filePath);
}
/**
* @return int
* @throws IOException
*/
public function getLength(): int
{
$this->ensureHandleActive();
return fstat($this->fileHandle)['size'];
}
/**
* @return int
* @throws IOException
*/
public function getPosition(): int
{
$this->ensureHandleActive();
return ftell($this->fileHandle);
}
/**
* @return int
*/
public function getReadTimeout(): int
{
return -1;
}
/**
* @return int
*/
public function getWriteTimeout(): int
{
return -1;
}
/**
* @throws IOException
*/
public function flush(): void
{
$this->ensureHandleActive();
fflush($this->fileHandle);
}
/**
* @throws IOException
*/
public function close(): void
{
$this->ensureHandleActive();
@ -183,6 +279,11 @@ class FileStream extends Stream
fclose($this->fileHandle);
}
/**
* @param int $length
* @return string
* @throws IOException
*/
public function read(int $length): string
{
$this->ensureHandleActive();
@ -197,6 +298,10 @@ class FileStream extends Stream
return $read;
}
/**
* @return int
* @throws IOException
*/
public function readChar(): int
{
$this->ensureHandleActive();
@ -205,6 +310,11 @@ class FileStream extends Stream
return ord(fgetc($this->fileHandle));
}
/**
* @param string $data
* @return int
* @throws IOException
*/
public function write(string $data): int
{
$this->ensureHandleActive();
@ -219,11 +329,20 @@ class FileStream extends Stream
return $write;
}
/**
* @param int $char
* @throws IOException
*/
public function writeChar(int $char): void
{
$this->write(chr($char), 0, 1);
$this->write(chr($char));
}
/**
* @param int $offset
* @param int $origin
* @throws IOException
*/
public function seek(int $offset, int $origin): void
{
$this->ensureHandleActive();

View file

@ -1,6 +1,11 @@
<?php
namespace Misuzu\IO;
/**
* Provides a wrapper for fsockopen.
* Class NetworkStream
* @package Misuzu\IO
*/
class NetworkStream extends Stream
{
protected $resourceHandle;
@ -8,6 +13,13 @@ class NetworkStream extends Stream
protected $port;
protected $timeout;
/**
* NetworkStream constructor.
* @param string $host
* @param int $port
* @param int|null $timeout
* @throws IOException
*/
public function __construct(string $host, int $port = -1, ?int $timeout = null)
{
$this->host = $host;
@ -27,6 +39,9 @@ class NetworkStream extends Stream
$this->ensureHandleActive();
}
/**
* Cleans the resources used by this object up.
*/
public function __destruct()
{
if (!is_resource($this->resourceHandle)) {
@ -36,12 +51,19 @@ class NetworkStream extends Stream
$this->close();
}
/**
* @return resource
* @throws IOException
*/
public function getResource(): resource
{
$this->ensureHandleActive();
return $this->resourceHandle;
}
/**
* @throws IOException
*/
protected function ensureHandleActive(): void
{
if (!is_resource($this->resourceHandle)) {
@ -49,58 +71,93 @@ class NetworkStream extends Stream
}
}
/**
* @return bool
*/
public function getCanRead(): bool
{
return true;
}
/**
* @return bool
*/
public function getCanSeek(): bool
{
return false;
}
/**
* @return bool
*/
public function getCanTimeout(): bool
{
return true;
}
/**
* @return bool
*/
public function getCanWrite(): bool
{
return true;
}
/**
* @return int
*/
public function getLength(): int
{
return -1;
}
/**
* @return int
*/
public function getPosition(): int
{
return -1;
}
/**
* @return int
*/
public function getReadTimeout(): int
{
return -1;
}
/**
* @return int
*/
public function getWriteTimeout(): int
{
return -1;
}
/**
* @throws IOException
*/
public function flush(): void
{
$this->ensureHandleActive();
fflush($this->resourceHandle);
}
/**
* @throws IOException
*/
public function close(): void
{
$this->ensureHandleActive();
fclose($this->resourceHandle);
}
/**
* @param int $length
* @return string
* @throws IOException
*/
public function read(int $length): string
{
$this->ensureHandleActive();
@ -114,13 +171,21 @@ class NetworkStream extends Stream
return $read;
}
/**
* @return int
* @throws IOException
*/
public function readChar(): int
{
$this->ensureHandleActive();
return ord(fgetc($this->resourceHandle));
}
/**
* @param string $data
* @return int
* @throws IOException
*/
public function write(string $data): int
{
$this->ensureHandleActive();
@ -134,12 +199,19 @@ class NetworkStream extends Stream
return $write;
}
/**
* @param int $char
* @throws IOException
*/
public function writeChar(int $char): void
{
$this->write(chr($char), 0, 1);
}
/**
* @param int $offset
* @param int $origin
* @throws IOException
* @SuppressWarnings("unused")
*/
public function seek(int $offset, int $origin): void

View file

@ -3,6 +3,18 @@ namespace Misuzu\IO;
use InvalidArgumentException;
/**
* Class Stream
* @package Misuzu\IO
* @property-read bool $canRead
* @property-read bool $canSeek
* @property-read bool $canTimeout
* @property-read bool $canWrite
* @property-read int $length
* @property-read int $position
* @property-read int $readTimeout
* @property-read int $writeTimeout
*/
abstract class Stream
{
public const ORIGIN_CURRENT = 0;

View file

@ -1,9 +1,16 @@
<?php
namespace Misuzu;
use Illuminate\Database\Eloquent\Collection;
use Closure;
use Illuminate\Database\Eloquent\Model as BaseModel;
use Illuminate\Support\Collection;
/**
* 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

@ -10,40 +10,84 @@ use InvalidArgumentException;
*/
final class IPAddress
{
/**
* Default IP Address if $_SERVER['REMOTE_ADDR'] is not set.
*/
private const FALLBACK_ADDRESS = '::1';
/**
* Fallback version number.
*/
public const UNKNOWN_VERSION = 0;
/**
* IPv4.
*/
public const V4 = 4;
/**
* IPv6.
*/
public const V6 = 6;
/**
* String lengths of expanded IP addresses.
*/
public const BYTE_COUNT = [
self::V4 => 4,
self::V6 => 16,
];
/**
* IP address version.
* @var int
*/
private $ipVersion = self::UNKNOWN_VERSION;
/**
* Raw IP address.
* @var null|string
*/
private $ipRaw = null;
/**
* @return int
*/
public function getVersion(): int
{
return $this->ipVersion;
}
/**
* @return string
*/
public function getRaw(): string
{
return $this->ipRaw;
}
/**
* @return string
*/
public function getString(): string
{
return inet_ntop($this->ipRaw);
}
/**
* Gets GeoIP country for this IP address.
* @return string
*/
public function getCountryCode(): string
{
return get_country_code($this->getString());
}
/**
* IPAddress constructor.
* @param int $version
* @param string $rawIp
*/
public function __construct(int $version, string $rawIp)
{
if (!array_key_exists($version, self::BYTE_COUNT)) {
@ -58,6 +102,12 @@ final class IPAddress
$this->ipRaw = $rawIp;
}
/**
* Compares one IP to another.
* @param IPAddress $other
* @return int
* @throws InvalidArgumentException If the versions of the IP mismatch.
*/
public function compareTo(IPAddress $other): int
{
if ($other->getVersion() !== $this->getVersion()) {
@ -83,6 +133,11 @@ final class IPAddress
return 0;
}
/**
* Gets the remote address.
* @param string $fallbackAddress
* @return IPAddress
*/
public static function remote(string $fallbackAddress = self::FALLBACK_ADDRESS): IPAddress
{
try {
@ -92,6 +147,11 @@ final class IPAddress
}
}
/**
* Creates an IPAddress instance from just a raw IP string.
* @param string $rawIp
* @return IPAddress
*/
public static function fromRaw(string $rawIp): IPAddress
{
$version = self::detectVersionFromRaw($rawIp);
@ -103,6 +163,11 @@ final class IPAddress
return new static($version, $rawIp);
}
/**
* Creates an IPAddress instance from a human readable address string.
* @param string $ipAddress
* @return IPAddress
*/
public static function fromString(string $ipAddress): IPAddress
{
$version = self::detectVersionFromString($ipAddress);
@ -114,6 +179,11 @@ final class IPAddress
return new static($version, inet_pton($ipAddress));
}
/**
* Detects the version of a raw address string.
* @param string $rawIp
* @return int
*/
public static function detectVersionFromRaw(string $rawIp): int
{
$rawLength = strlen($rawIp);
@ -127,6 +197,11 @@ final class IPAddress
return self::UNKNOWN_VERSION;
}
/**
* Detects the version of a human readable address string.
* @param string $ipAddress
* @return int
*/
public static function detectVersionFromString(string $ipAddress): int
{
if (filter_var($ipAddress, FILTER_VALIDATE_IP) === false) {

View file

@ -5,19 +5,37 @@ use InvalidArgumentException;
final class IPAddressRange
{
/**
* @var IPAddress
*/
private $maskAddress;
/**
* @var int
*/
private $cidrLength;
/**
* @return IPAddress
*/
public function getMaskAddress(): IPAddress
{
return $this->maskAddress;
}
/**
* @return int
*/
public function getCidrLength(): int
{
return $this->cidrLength;
}
/**
* IPAddressRange constructor.
* @param IPAddress $maskAddress
* @param int $cidrLength
*/
public function __construct(IPAddress $maskAddress, int $cidrLength)
{
if ($cidrLength > IPAddress::BYTE_COUNT[$maskAddress->getVersion()] * 8) {
@ -28,11 +46,21 @@ final class IPAddressRange
$this->cidrLength = $cidrLength;
}
/**
* Gets a conventional <MASKED ADDRESS>/<CIDR LENGTH> format string.
* @return string
*/
public function getMaskedString(): string
{
return $this->getMaskAddress()->getString() . '/' . $this->getCidrLength();
}
/**
* Matches an IPAddress to this range.
* @param IPAddress $ipAddress
* @param bool $explicitExceptions
* @return bool
*/
public function match(IPAddress $ipAddress, bool $explicitExceptions = false): bool
{
if ($ipAddress->getVersion() !== $this->getMaskAddress()->getVersion()) {
@ -62,6 +90,11 @@ final class IPAddressRange
return $this->getMaskAddress()->getRaw() === pack('N*', ...$ipParts);
}
/**
* Creates an IPAddressRange instance using the conventional notation format.
* @param string $maskedString
* @return IPAddressRange
*/
public static function fromMaskedString(string $maskedString): IPAddressRange
{
if (strpos($maskedString, '/') === false) {
@ -75,8 +108,14 @@ final class IPAddressRange
return new static($maskedAddress, $cidrLength);
}
// very uncertain about this logic in regards to any ip larger than 32 bits
// if you _do_ know what you're doing, review this and call me an idiot please
/**
* Creates an IPAddresRange instance from a dash separated range.
* I'm very uncertain about the logic here when it comes to addresses larger than 32 bits.
* If you do know what you're doing, please review this and call me an idiot.
*
* @param string $rangeString
* @return IPAddressRange
*/
public static function fromRangeString(string $rangeString): IPAddressRange
{
if (strpos($rangeString, '-') === false) {

View file

@ -3,11 +3,23 @@ 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

@ -6,23 +6,48 @@ use Misuzu\Users\User;
use Misuzu\Model;
use Parsedown;
/**
* Class NewsPost
* @package Misuzu\News
* @property-read int $post_id
* @property int $category_id
* @property bool $is_featured
* @property int $user_id
* @property string $post_title
* @property string $post_text
* @property Carbon $scheduled_for
* @property Carbon|null $deleted_at
* @property-read User $user
* @property-read NewsCategory $category
*/
final class NewsPost extends Model
{
use SoftDeletes;
protected $table = 'news_posts';
protected $primaryKey = 'post_id';
protected $dates = ['scheduled_for'];
/**
* Parses post_text and returns the final HTML.
* @return string
*/
public function getHtml(): string
{
return (new Parsedown)->text($this->post_text);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function category()
{
return $this->belongsTo(NewsCategory::class, 'category_id');

View file

@ -37,7 +37,11 @@ class TemplateEngine
private $vars = [];
/**
* Creates the twig environment.
* TemplateEngine constructor.
* @param null|string $cache
* @param bool $strict
* @param bool $autoReload
* @param bool $debug
*/
public function __construct(
?string $cache = null,
@ -106,11 +110,14 @@ class TemplateEngine
*/
public function addPath(string $name, string $path): void
{
try {
if (count($this->loader->getPaths()) < 1) {
$this->loader->addPath($path);
}
$this->loader->addPath($path, $name);
} catch (\Twig_Error_Loader $e) {
}
}
/**
@ -150,8 +157,11 @@ class TemplateEngine
/**
* Renders a template file.
* @param string $path
* @param array $vars
* @param array|null $vars
* @return string
* @throws \Twig_Error_Loader
* @throws \Twig_Error_Runtime
* @throws \Twig_Error_Syntax
*/
public function render(string $path, ?array $vars = null): string
{

View file

@ -5,20 +5,57 @@ 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,
@ -31,7 +68,7 @@ class LoginAttempt extends Model
$attempt->user_agent = $userAgent ?? '';
if ($user !== null) {
$attempt->user_id = $user;
$attempt->user_id = $user->user_id;
}
$attempt->save();
@ -39,32 +76,40 @@ class LoginAttempt extends Model
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);
}
public function setUserIdAttribute(User $user): void
{
$this->attributes['user_id'] = $user->user_id;
}
public function getUserIdAttribute(int $userId): User
{
return User::findOrFail($userId);
}
/**
* Object relation definition for User.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');

View file

@ -4,10 +4,35 @@ namespace Misuzu\Users;
use Misuzu\Colour;
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 Colour $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 Colour|null $colour
* @param null|string $title
* @param null|string $description
* @param bool $secret
* @return Role
*/
public static function createRole(
string $name,
?int $hierarchy = null,
@ -25,47 +50,83 @@ class Role extends Model
$role->role_title = $title;
$role->role_description = $description;
$role->role_secret = $secret;
$role->role_colour = $colour->raw;
$role->role_colour = $colour->getRaw();
$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_colour attribute.
* @param int $colour
* @return Colour
*/
public function getRoleColourAttribute(int $colour): Colour
{
return new Colour($colour);
}
/**
* Setter for the role_colour attribute.
* @param Colour $colour
*/
public function setRoleColourAttribute(Colour $colour): void
{
$this->attributes['role_colour'] = $colour->getRaw();
}
/**
* 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

@ -5,11 +5,39 @@ 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,
@ -31,27 +59,48 @@ class Session extends Model
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

@ -8,22 +8,96 @@ use Misuzu\Database;
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,
@ -44,6 +118,11 @@ class User extends Model
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);
@ -52,6 +131,12 @@ class User extends Model
->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);
@ -87,6 +172,12 @@ class User extends Model
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) {
@ -104,16 +195,24 @@ class User extends Model
return '';
}
/**
* Validates a password string.
* @param string $password
* @return string
*/
public static function validatePassword(string $password): string
{
if (password_entropy($password) < 32) {
if (password_entropy($password) < self::PASSWORD_MIN_ENTROPY) {
return 'weak';
}
return '';
}
// it's probably safe to assume that this will always return a valid role
/**
* 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) {
@ -123,12 +222,20 @@ class User extends Model
return $this->displayRoleInstance;
}
/**
* Gets the display colour.
* @return Colour
*/
public function getDisplayColour(): Colour
{
$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)) {
@ -144,6 +251,10 @@ class User extends Model
return '';
}
/**
* Gets the user title (with memoization).
* @return string
*/
public function getUserTitle(): string
{
if (empty($this->userTitleValue)) {
@ -153,6 +264,11 @@ class User extends Model
return $this->userTitleValue;
}
/**
* Assigns a role.
* @param Role $role
* @param bool $setDisplay
*/
public function addRole(Role $role, bool $setDisplay = false): void
{
$relation = new UserRole;
@ -165,6 +281,10 @@ class User extends Model
}
}
/**
* Removes a role.
* @param Role $role
*/
public function removeRole(Role $role): void
{
UserRole::where('user_id', $this->user_id)
@ -172,6 +292,11 @@ class User extends Model
->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)
@ -179,6 +304,11 @@ class User extends Model
->count() > 0;
}
/**
* Verifies a password.
* @param string $password
* @return bool
*/
public function verifyPassword(string $password): bool
{
if (password_verify($password, $this->password) !== true) {
@ -193,6 +323,11 @@ class User extends Model
return true;
}
/**
* Getter for the display_role attribute.
* @param int|null $value
* @return int
*/
public function getDisplayRoleAttribute(?int $value): int
{
if (!$this->displayRoleValidated) {
@ -215,6 +350,10 @@ class User extends Model
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) {
@ -222,51 +361,90 @@ class User extends Model
}
}
/**
* @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

@ -3,17 +3,31 @@ 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

@ -1,8 +1,15 @@
<?php
namespace Misuzu;
/**
* Class Zalgo
* @package Misuzu
*/
class Zalgo
{
/**
* Characters that crawl upwards.
*/
private const ZALGO_CHARS_UP = [
"\u{030d}", "\u{030e}", "\u{0304}", "\u{0305}", "\u{033f}",
"\u{0311}", "\u{0306}", "\u{0310}", "\u{0352}", "\u{0357}",
@ -16,6 +23,9 @@ class Zalgo
"\u{033e}", "\u{035b}", "\u{0346}", "\u{031a}",
];
/**
* Characters that crawl downwards.
*/
private const ZALGO_CHARS_DOWN = [
"\u{0316}", "\u{0317}", "\u{0318}", "\u{0319}", "\u{031c}",
"\u{031d}", "\u{031e}", "\u{031f}", "\u{0320}", "\u{0324}",
@ -27,6 +37,9 @@ class Zalgo
"\u{0355}", "\u{0356}", "\u{0359}", "\u{035a}", "\u{0323}",
];
/**
* Characters that dwell in the middle.
*/
private const ZALGO_CHARS_MIDDLE = [
"\u{0315}", "\u{031b}", "\u{0340}", "\u{0341}", "\u{0358}",
"\u{0321}", "\u{0322}", "\u{0327}", "\u{0328}", "\u{0334}",
@ -35,20 +48,50 @@ class Zalgo
"\u{0337}", "\u{0361}", "\u{0489}",
];
/**
* Minimal Zalgo.
*/
public const ZALGO_MODE_MINI = 1;
/**
* Medium Zalgo.
*/
public const ZALGO_MODE_NORMAL = 2;
/**
* MAXIMUM ZALGO.
*/
public const ZALGO_MODE_MAX = 3;
/**
* Index of valid modes.
*/
private const ZALGO_MODES = [
self::ZALGO_MODE_MINI,
self::ZALGO_MODE_NORMAL,
self::ZALGO_MODE_MAX,
];
/**
* Flag for having characters crawl upwards.
*/
public const ZALGO_DIR_UP = 0x1;
/**
* Flag for having characters dwell in the middle.
*/
public const ZALGO_DIR_MID = 0x2;
/**
* Flag for having characters crawl downwards.
*/
public const ZALGO_DIR_DOWN = 0x4;
/**
* (Try to) restore a Zalgofied string back to normal.
* @param string $text
* @return string
*/
public static function strip(string $text): string
{
$text = str_replace(self::ZALGO_CHARS_UP, '', $text);
@ -58,6 +101,11 @@ class Zalgo
return $text;
}
/**
* Checks a string for Zalgo characters.
* @param string $char
* @return bool
*/
private static function isZalgo(string $char): bool
{
return in_array($char, self::ZALGO_CHARS_UP)
@ -65,6 +113,12 @@ class Zalgo
|| in_array($char, self::ZALGO_CHARS_DOWN);
}
/**
* Gets a random Zalgo character.
* @param array $array
* @param int $length
* @return string
*/
private static function getZalgo(array $array, int $length): string
{
$string = '';
@ -76,6 +130,13 @@ class Zalgo
return $string;
}
/**
* Runs Zalgo over a string.
* @param string $text
* @param int $mode
* @param int $direction
* @return string
*/
public static function run(
string $text,
int $mode = self::ZALGO_MODE_MINI,
@ -105,6 +166,9 @@ class Zalgo
}
$str .= $char;
$num_up = 0;
$num_mid = 0;
$num_down = 0;
switch ($mode) {
case self::ZALGO_MODE_MINI: