diff --git a/database/.gitkeep b/database/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/database/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/misuzu.php b/misuzu.php index 7c994f28..2945027b 100644 --- a/misuzu.php +++ b/misuzu.php @@ -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'); } } diff --git a/misuzu_migrate.php b/misuzu_migrate.php index 7f6df615..3b052f9b 100644 --- a/misuzu_migrate.php +++ b/misuzu_migrate.php @@ -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()) { diff --git a/public/auth.php b/public/auth.php index 55c48abc..5786facb 100644 --- a/public/auth.php +++ b/public/auth.php @@ -1,8 +1,5 @@ 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; } diff --git a/public/index.php b/public/index.php index 684fc107..6b99f5b1 100644 --- a/public/index.php +++ b/public/index.php @@ -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')); diff --git a/public/manage/index.php b/public/manage/index.php index 121ba232..a22bdc71 100644 --- a/public/manage/index.php +++ b/public/manage/index.php @@ -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': diff --git a/public/manage/users.php b/public/manage/users.php index 786c2acc..1cced089 100644 --- a/public/manage/users.php +++ b/public/manage/users.php @@ -1,5 +1,4 @@ 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; } diff --git a/public/news.php b/public/news.php index 4acf0df4..2b4bddda 100644 --- a/public/news.php +++ b/public/news.php @@ -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')); diff --git a/public/not-found.php b/public/not-found.php index ed17ef68..59033598 100644 --- a/public/not-found.php +++ b/public/not-found.php @@ -1,4 +1,4 @@ templating->render('errors.404'); +echo $app->getTemplating()->render('errors.404'); diff --git a/public/profile.php b/public/profile.php index decc0126..bb08cf08 100644 --- a/public/profile.php +++ b/public/profile.php @@ -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; } diff --git a/public/settings.php b/public/settings.php index 9cda11c8..403fe6ef 100644 --- a/public/settings.php +++ b/public/settings.php @@ -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}"); diff --git a/src/Application.php b/src/Application.php index 035b853a..e295c6ce 100644 --- a/src/Application.php +++ b/src/Application.php @@ -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 bool $debug + * @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; } } diff --git a/src/ApplicationBase.php b/src/ApplicationBase.php index 8e624fdb..a51aa89d 100644 --- a/src/ApplicationBase.php +++ b/src/ApplicationBase.php @@ -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]); - } } diff --git a/src/Colour.php b/src/Colour.php index 88f7a83f..e39aa01e 100644 --- a/src/Colour.php +++ b/src/Colour.php @@ -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()}"; diff --git a/src/Config/ConfigManager.php b/src/Config/ConfigManager.php index b6944f09..792fc070 100644 --- a/src/Config/ConfigManager.php +++ b/src/Config/ConfigManager.php @@ -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 { diff --git a/src/Database.php b/src/Database.php index b81a4faf..f110f5a0 100644 --- a/src/Database.php +++ b/src/Database.php @@ -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')) { diff --git a/src/ExceptionHandler.php b/src/ExceptionHandler.php index 8fdcfd22..33ab16b8 100644 --- a/src/ExceptionHandler.php +++ b/src/ExceptionHandler.php @@ -78,10 +78,10 @@ class ExceptionHandler /** * Converts regular errors to ErrorException instances. - * @param int $severity + * @param int $severity * @param string $message * @param string $file - * @param int $line + * @param int $line * @throws ErrorException */ public static function error(int $severity, string $message, string $file, int $line): void @@ -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 diff --git a/src/IO/Directory.php b/src/IO/Directory.php index 160e9b9d..be25aa62 100644 --- a/src/IO/Directory.php +++ b/src/IO/Directory.php @@ -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)) { @@ -107,8 +127,9 @@ class Directory /** * Deletes a directory, recursively if requested. Use $purge with care! * @param string $path - * @param bool $purge + * @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 diff --git a/src/IO/File.php b/src/IO/File.php index e2dbc057..aca19c40 100644 --- a/src/IO/File.php +++ b/src/IO/File.php @@ -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 { diff --git a/src/IO/FileStream.php b/src/IO/FileStream.php index 5777d67e..75a935b8 100644 --- a/src/IO/FileStream.php +++ b/src/IO/FileStream.php @@ -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(); diff --git a/src/IO/NetworkStream.php b/src/IO/NetworkStream.php index f1d3a8b6..3a311d32 100644 --- a/src/IO/NetworkStream.php +++ b/src/IO/NetworkStream.php @@ -1,6 +1,11 @@ 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 diff --git a/src/IO/Stream.php b/src/IO/Stream.php index 96aed6eb..1b3f00cd 100644 --- a/src/IO/Stream.php +++ b/src/IO/Stream.php @@ -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; diff --git a/src/Model.php b/src/Model.php index e20332e6..534c20c9 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1,9 +1,16 @@ 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) { diff --git a/src/Net/IPAddressRange.php b/src/Net/IPAddressRange.php index 8871e426..189e5797 100644 --- a/src/Net/IPAddressRange.php +++ b/src/Net/IPAddressRange.php @@ -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 / 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) { diff --git a/src/News/NewsCategory.php b/src/News/NewsCategory.php index dae1a563..6f207bc1 100644 --- a/src/News/NewsCategory.php +++ b/src/News/NewsCategory.php @@ -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'); diff --git a/src/News/NewsPost.php b/src/News/NewsPost.php index cec78208..3726220f 100644 --- a/src/News/NewsPost.php +++ b/src/News/NewsPost.php @@ -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'); diff --git a/src/TemplateEngine.php b/src/TemplateEngine.php index f194132a..51de1da9 100644 --- a/src/TemplateEngine.php +++ b/src/TemplateEngine.php @@ -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 { - if (count($this->loader->getPaths()) < 1) { - $this->loader->addPath($path); - } + try { + if (count($this->loader->getPaths()) < 1) { + $this->loader->addPath($path); + } - $this->loader->addPath($path, $name); + $this->loader->addPath($path, $name); + } catch (\Twig_Error_Loader $e) { + } } /** @@ -149,9 +156,12 @@ class TemplateEngine /** * Renders a template file. - * @param string $path - * @param array $vars + * @param string $path + * @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 { diff --git a/src/Users/LoginAttempt.php b/src/Users/LoginAttempt.php index 746fe55a..b05b6771 100644 --- a/src/Users/LoginAttempt.php +++ b/src/Users/LoginAttempt.php @@ -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'); diff --git a/src/Users/Role.php b/src/Users/Role.php index 1beb32fb..03eb7f0e 100644 --- a/src/Users/Role.php +++ b/src/Users/Role.php @@ -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'); diff --git a/src/Users/Session.php b/src/Users/Session.php index 67f8ce9d..45e86251 100644 --- a/src/Users/Session.php +++ b/src/Users/Session.php @@ -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'); diff --git a/src/Users/User.php b/src/Users/User.php index 97b49564..33d00428 100644 --- a/src/Users/User.php +++ b/src/Users/User.php @@ -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'); diff --git a/src/Users/UserRole.php b/src/Users/UserRole.php index 7a3312dc..a1a7ece9 100644 --- a/src/Users/UserRole.php +++ b/src/Users/UserRole.php @@ -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'); diff --git a/src/Zalgo.php b/src/Zalgo.php index db686288..235122f7 100644 --- a/src/Zalgo.php +++ b/src/Zalgo.php @@ -1,8 +1,15 @@