diff --git a/misuzu.php b/misuzu.php index 41140f9f..5094bb24 100644 --- a/misuzu.php +++ b/misuzu.php @@ -32,15 +32,19 @@ date_default_timezone_set('utc'); set_include_path(get_include_path() . PATH_SEPARATOR . MSZ_ROOT); set_exception_handler(function(\Throwable $ex) { - http_response_code(500); - ob_clean(); - - if(MSZ_CLI || MSZ_DEBUG) { - header('Content-Type: text/plain; charset=utf-8'); + if(MSZ_CLI) { echo (string)$ex; } else { - header('Content-Type: text/html; charset-utf-8'); - echo file_get_contents(MSZ_ROOT . '/templates/500.html'); + http_response_code(500); + ob_clean(); + + if(MSZ_DEBUG) { + header('Content-Type: text/plain; charset=utf-8'); + echo (string)$ex; + } else { + header('Content-Type: text/html; charset-utf-8'); + echo file_get_contents(MSZ_ROOT . '/templates/500.html'); + } } exit; }); @@ -113,418 +117,160 @@ Mailer::init(Config::get('mail.method', Config::TYPE_STR), [ define('MSZ_STORAGE', Config::get('storage.path', Config::TYPE_STR, MSZ_ROOT . '/store')); mkdirs(MSZ_STORAGE, true); -if(MSZ_CLI) { +if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later if(realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) { - switch($argv[1] ?? null) { - case 'cron': - $runLowFreq = (bool)(!empty($argv[2]) && $argv[2] == 'low'); - - $cronTasks = [ - [ - 'name' => 'Ensures main role exists.', - 'type' => 'sql', - 'run' => $runLowFreq, - 'command' => " - INSERT IGNORE INTO `msz_roles` - (`role_id`, `role_name`, `role_hierarchy`, `role_colour`, `role_description`, `role_created`) - VALUES - (1, 'Member', 1, 1073741824, NULL, NOW()) - ", - ], - [ - 'name' => 'Ensures all users are in the main role.', - 'type' => 'sql', - 'run' => $runLowFreq, - 'command' => " - INSERT INTO `msz_user_roles` - (`user_id`, `role_id`) - SELECT `user_id`, 1 FROM `msz_users` as u - WHERE NOT EXISTS ( - SELECT 1 - FROM `msz_user_roles` as ur - WHERE `role_id` = 1 - AND u.`user_id` = ur.`user_id` - ) - ", - ], - [ - 'name' => 'Ensures all display_role values are correct with `msz_user_roles`.', - 'type' => 'sql', - 'run' => $runLowFreq, - 'command' => " - UPDATE `msz_users` as u - SET `display_role` = ( - SELECT ur.`role_id` - FROM `msz_user_roles` as ur - LEFT JOIN `msz_roles` as r - ON r.`role_id` = ur.`role_id` - WHERE ur.`user_id` = u.`user_id` - ORDER BY `role_hierarchy` DESC - LIMIT 1 - ) - WHERE NOT EXISTS ( - SELECT 1 - FROM `msz_user_roles` as ur - WHERE ur.`role_id` = u.`display_role` - AND `ur`.`user_id` = u.`user_id` - ) - ", - ], - [ - 'name' => 'Remove expired sessions.', - 'type' => 'sql', - 'run' => true, - 'command' => " - DELETE FROM `msz_sessions` - WHERE `session_expires` < NOW() - ", - ], - [ - 'name' => 'Remove old password reset records.', - 'type' => 'sql', - 'run' => true, - 'command' => " - DELETE FROM `msz_users_password_resets` - WHERE `reset_requested` < NOW() - INTERVAL 1 WEEK - ", - ], - [ - 'name' => 'Remove old chat login tokens.', - 'type' => 'sql', - 'run' => true, - 'command' => " - DELETE FROM `msz_user_chat_tokens` - WHERE `token_created` < NOW() - INTERVAL 1 WEEK - ", - ], - [ - 'name' => 'Clean up login history.', - 'type' => 'sql', - 'run' => true, - 'command' => " - DELETE FROM `msz_login_attempts` - WHERE `attempt_created` < NOW() - INTERVAL 1 MONTH - ", - ], - [ - 'name' => 'Clean up audit log.', - 'type' => 'sql', - 'run' => true, - 'command' => " - DELETE FROM `msz_audit_log` - WHERE `log_created` < NOW() - INTERVAL 3 MONTH - ", - ], - [ - 'name' => 'Remove stale forum tracking entries.', - 'type' => 'sql', - 'run' => true, - 'command' => " - DELETE tt FROM `msz_forum_topics_track` as tt - LEFT JOIN `msz_forum_topics` as t - ON t.`topic_id` = tt.`topic_id` - WHERE t.`topic_bumped` < NOW() - INTERVAL 1 MONTH - ", - ], - [ - 'name' => 'Synchronise forum_id.', - 'type' => 'sql', - 'run' => $runLowFreq, - 'command' => " - UPDATE `msz_forum_posts` AS p - INNER JOIN `msz_forum_topics` AS t - ON t.`topic_id` = p.`topic_id` - SET p.`forum_id` = t.`forum_id` - ", - ], - [ - 'name' => 'Recount forum topics and posts.', - 'type' => 'func', - 'run' => $runLowFreq, - 'command' => 'forum_count_synchronise', - ], - [ - 'name' => 'Clean up expired tfa tokens.', - 'type' => 'sql', - 'run' => true, - 'command' => " - DELETE FROM `msz_auth_tfa` - WHERE `tfa_created` < NOW() - INTERVAL 15 MINUTE - ", - ], - ]; - - foreach($cronTasks as $cronTask) { - if($cronTask['run']) { - echo $cronTask['name'] . PHP_EOL; - - switch($cronTask['type']) { - case 'sql': - DB::exec($cronTask['command']); - break; - - case 'func': - call_user_func($cronTask['command']); - break; - } - } - } - break; - - case 'migrate': - $doRollback = !empty($argv[2]) && $argv[2] === 'rollback'; - - touch(MSZ_ROOT . '/.migrating'); - - echo "Creating migration manager.." . PHP_EOL; - $migrationManager = new DatabaseMigrationManager(DB::getPDO(), MSZ_ROOT . '/database'); - $migrationManager->setLogger(function ($log) { - echo $log . PHP_EOL; - }); - - if($doRollback) { - $migrationManager->rollback(); - } else { - $migrationManager->migrate(); - } - - $errors = $migrationManager->getErrors(); - $errorCount = count($errors); - - if($errorCount < 1) { - echo 'Completed with no errors!' . PHP_EOL; - } else { - echo PHP_EOL . "There were {$errorCount} errors during the migrations..." . PHP_EOL; - - foreach($errors as $error) { - echo $error . PHP_EOL; - } - } - - unlink(MSZ_ROOT . '/.migrating'); - break; - - case 'new-mig': - if(empty($argv[2])) { - echo 'Specify a migration name.' . PHP_EOL; - return; - } - - if(!preg_match('#^([a-z_]+)$#', $argv[2])) { - echo 'Migration name may only contain alpha and _ characters.' . PHP_EOL; - return; - } - - $filename = date('Y_m_d_His_') . trim($argv[2], '_') . '.php'; - $filepath = MSZ_ROOT . '/database/' . $filename; - $namespace = snake_to_camel($argv[2]); - $template = <<exec(" - CREATE TABLE ... - "); + if(($argv[1] ?? '') === 'cron' && ($argv[2] ?? '') === 'low') + $argv[2] = '--slow'; + array_shift($argv); + echo shell_exec(__DIR__ . '/msz ' . implode(' ', $argv)); + } + return; } -function migrate_down(PDO \$conn): void { - \$conn->exec(" - DROP TABLE ... - "); +// Everything below here should eventually be moved to index.php, probably only initialised when required. +// Serving things like the css/js doesn't need to initialise sessions. + +if(!mb_check_encoding()) { + http_response_code(415); + echo 'Invalid request encoding.'; + exit; } -MIG; +ob_start(); - file_put_contents($filepath, $template); +if(!is_readable(MSZ_STORAGE) || !is_writable(MSZ_STORAGE)) { + echo 'Cannot access storage directory.'; + exit; +} - echo "Template for '{$namespace}' has been created." . PHP_EOL; - break; +GeoIP::init(Config::get('geoip.database', Config::TYPE_STR, '/var/lib/GeoIP/GeoLite2-Country.mmdb')); - case 'twitter-auth': - $apiKey = Config::get('twitter.api.key', Config::TYPE_STR); - $apiSecret = Config::get('twitter.api.secret', Config::TYPE_STR); +if(!MSZ_DEBUG) { + $twigCache = sys_get_temp_dir() . '/msz-tpl-cache-' . md5(MSZ_ROOT); + mkdirs($twigCache, true); +} - if(empty($apiKey) || empty($apiSecret)) { - echo 'No Twitter api keys set in config.' . PHP_EOL; - break; - } +Template::init($twigCache ?? null, MSZ_DEBUG); - Twitter::init($apiKey, $apiSecret); - echo 'Twitter Authentication' . PHP_EOL; +Template::set('globals', [ + 'site_name' => Config::get('site.name', Config::TYPE_STR, 'Misuzu'), + 'site_description' => Config::get('site.desc', Config::TYPE_STR), + 'site_url' => Config::get('site.url', Config::TYPE_STR), + 'site_twitter' => Config::get('social.twitter', Config::TYPE_STR), +]); - $authPage = Twitter::createAuth(); +Template::addPath(MSZ_ROOT . '/templates'); - if(empty($authPage)) { - echo 'Request to begin authentication failed.' . PHP_EOL; - break; - } +if(file_exists(MSZ_ROOT . '/.migrating')) { + http_response_code(503); + Template::render('home.migration'); + exit; +} - echo 'Go to the page below and paste the pin code displayed.' . PHP_EOL . $authPage . PHP_EOL; +if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) { + $authToken = (new AuthToken) + ->setUserId(filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT) ?? 0) + ->setSessionToken(filter_input(INPUT_COOKIE, 'msz_sid', FILTER_SANITIZE_STRING) ?? ''); - $pin = readline('Pin: '); - $authComplete = Twitter::completeAuth($pin); + if($authToken->isValid()) + setcookie('msz_auth', $authToken->pack(), strtotime('1 year'), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true); - if(empty($authComplete)) { - echo 'Invalid pin code.' . PHP_EOL; - break; - } + setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true); + setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true); +} - echo 'Authentication successful!' . PHP_EOL - . "Token: {$authComplete['token']}" . PHP_EOL - . "Token Secret: {$authComplete['token_secret']}" . PHP_EOL; - break; +if(!isset($authToken)) + $authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? ''); +if($authToken->isValid()) { + try { + $sessionInfo = $authToken->getSession(); + if($sessionInfo->hasExpired()) { + $sessionInfo->delete(); + } elseif($sessionInfo->getUserId() === $authToken->getUserId()) { + $userInfo = $sessionInfo->getUser(); + if(!$userInfo->isDeleted()) { + $sessionInfo->setCurrent(); + $userInfo->setCurrent(); - default: - echo 'Unknown command.' . PHP_EOL; - break; - } - } -} else { - if(!mb_check_encoding()) { - http_response_code(415); - echo 'Invalid request encoding.'; - exit; - } + $sessionInfo->bump(); - ob_start(); - - if(!is_readable(MSZ_STORAGE) || !is_writable(MSZ_STORAGE)) { - echo 'Cannot access storage directory.'; - exit; - } - - GeoIP::init(Config::get('geoip.database', Config::TYPE_STR, '/var/lib/GeoIP/GeoLite2-Country.mmdb')); - - if(!MSZ_DEBUG) { - $twigCache = sys_get_temp_dir() . '/msz-tpl-cache-' . md5(MSZ_ROOT); - mkdirs($twigCache, true); - } - - Template::init($twigCache ?? null, MSZ_DEBUG); - - Template::set('globals', [ - 'site_name' => Config::get('site.name', Config::TYPE_STR, 'Misuzu'), - 'site_description' => Config::get('site.desc', Config::TYPE_STR), - 'site_url' => Config::get('site.url', Config::TYPE_STR), - 'site_twitter' => Config::get('social.twitter', Config::TYPE_STR), - ]); - - Template::addPath(MSZ_ROOT . '/templates'); - - if(file_exists(MSZ_ROOT . '/.migrating')) { - http_response_code(503); - Template::render('home.migration'); - exit; - } - - if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) { - $authToken = (new AuthToken) - ->setUserId(filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT) ?? 0) - ->setSessionToken(filter_input(INPUT_COOKIE, 'msz_sid', FILTER_SANITIZE_STRING) ?? ''); - - if($authToken->isValid()) - setcookie('msz_auth', $authToken->pack(), strtotime('1 year'), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true); - - setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true); - setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true); - } - - if(!isset($authToken)) - $authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? ''); - if($authToken->isValid()) { - try { - $sessionInfo = $authToken->getSession(); - if($sessionInfo->hasExpired()) { - $sessionInfo->delete(); - } elseif($sessionInfo->getUserId() === $authToken->getUserId()) { - $userInfo = $sessionInfo->getUser(); - if(!$userInfo->isDeleted()) { - $sessionInfo->setCurrent(); - $userInfo->setCurrent(); - - $sessionInfo->bump(); - - if($sessionInfo->shouldBumpExpire()) - setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true); - } - } - } catch(UserNotFoundException $ex) { - UserSession::unsetCurrent(); - User::unsetCurrent(); - } catch(UserSessionNotFoundException $ex) { - UserSession::unsetCurrent(); - User::unsetCurrent(); - } - - if(!UserSession::hasCurrent()) { - setcookie('msz_auth', '', -9001, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true); - setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true); - } else { - $userDisplayInfo = DB::prepare(' - SELECT - u.`user_id`, u.`username`, u.`user_background_settings`, u.`user_deleted`, - COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour` - FROM `msz_users` AS u - LEFT JOIN `msz_roles` AS r - ON u.`display_role` = r.`role_id` - WHERE `user_id` = :user_id - ') ->bind('user_id', $userInfo->getId()) - ->fetch(); - - user_bump_last_active($userInfo->getId()); - - $userDisplayInfo['perms'] = perms_get_user($userInfo->getId()); - $userDisplayInfo['ban_expiration'] = user_warning_check_expiration($userInfo->getId(), MSZ_WARN_BAN); - $userDisplayInfo['silence_expiration'] = $userDisplayInfo['ban_expiration'] > 0 ? 0 : user_warning_check_expiration($userInfo->getId(), MSZ_WARN_SILENCE); - } - } - - CSRF::setGlobalSecretKey(Config::get('csrf.secret', Config::TYPE_STR, 'soup')); - CSRF::setGlobalIdentity(UserSession::hasCurrent() ? UserSession::getCurrent()->getToken() : IPAddress::remote()); - - if(Config::get('private.enabled', Config::TYPE_BOOL)) { - $onLoginPage = $_SERVER['PHP_SELF'] === url('auth-login'); - $onPasswordPage = parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH) === url('auth-forgot'); - $misuzuBypassLockdown = !empty($misuzuBypassLockdown) || $onLoginPage; - - if(!$misuzuBypassLockdown) { - if(UserSession::hasCurrent()) { - $privatePermCat = Config::get('private.perm.cat', Config::TYPE_STR); - $privatePermVal = Config::get('private.perm.val', Config::TYPE_INT); - - if(!empty($privatePermCat) && $privatePermVal > 0) { - if(!perms_check_user($privatePermCat, User::getCurrent()->getId(), $privatePermVal)) { - // au revoir - unset($userDisplayInfo); - UserSession::unsetCurrent(); - User::unsetCurrent(); - } - } - } elseif(!$onLoginPage && !($onPasswordPage && Config::get('private.allow_password_reset', Config::TYPE_BOOL, true))) { - url_redirect('auth-login'); - exit; + if($sessionInfo->shouldBumpExpire()) + setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true); } } + } catch(UserNotFoundException $ex) { + UserSession::unsetCurrent(); + User::unsetCurrent(); + } catch(UserSessionNotFoundException $ex) { + UserSession::unsetCurrent(); + User::unsetCurrent(); } - if(!empty($userDisplayInfo)) // delete this - Template::set('current_user', $userDisplayInfo); + if(!UserSession::hasCurrent()) { + setcookie('msz_auth', '', -9001, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true); + setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true); + } else { + $userDisplayInfo = DB::prepare(' + SELECT + u.`user_id`, u.`username`, u.`user_background_settings`, u.`user_deleted`, + COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour` + FROM `msz_users` AS u + LEFT JOIN `msz_roles` AS r + ON u.`display_role` = r.`role_id` + WHERE `user_id` = :user_id + ') ->bind('user_id', $userInfo->getId()) + ->fetch(); - $inManageMode = starts_with($_SERVER['REQUEST_URI'], '/manage'); - $hasManageAccess = User::hasCurrent() - && !user_warning_check_restriction(User::getCurrent()->getId()) - && perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_CAN_MANAGE); - Template::set('has_manage_access', $hasManageAccess); + user_bump_last_active($userInfo->getId()); - if($inManageMode) { - if(!$hasManageAccess) { - echo render_error(403); + $userDisplayInfo['perms'] = perms_get_user($userInfo->getId()); + $userDisplayInfo['ban_expiration'] = user_warning_check_expiration($userInfo->getId(), MSZ_WARN_BAN); + $userDisplayInfo['silence_expiration'] = $userDisplayInfo['ban_expiration'] > 0 ? 0 : user_warning_check_expiration($userInfo->getId(), MSZ_WARN_SILENCE); + } +} + +CSRF::setGlobalSecretKey(Config::get('csrf.secret', Config::TYPE_STR, 'soup')); +CSRF::setGlobalIdentity(UserSession::hasCurrent() ? UserSession::getCurrent()->getToken() : IPAddress::remote()); + +if(Config::get('private.enabled', Config::TYPE_BOOL)) { + $onLoginPage = $_SERVER['PHP_SELF'] === url('auth-login'); + $onPasswordPage = parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH) === url('auth-forgot'); + $misuzuBypassLockdown = !empty($misuzuBypassLockdown) || $onLoginPage; + + if(!$misuzuBypassLockdown) { + if(UserSession::hasCurrent()) { + $privatePermCat = Config::get('private.perm.cat', Config::TYPE_STR); + $privatePermVal = Config::get('private.perm.val', Config::TYPE_INT); + + if(!empty($privatePermCat) && $privatePermVal > 0) { + if(!perms_check_user($privatePermCat, User::getCurrent()->getId(), $privatePermVal)) { + // au revoir + unset($userDisplayInfo); + UserSession::unsetCurrent(); + User::unsetCurrent(); + } + } + } elseif(!$onLoginPage && !($onPasswordPage && Config::get('private.allow_password_reset', Config::TYPE_BOOL, true))) { + url_redirect('auth-login'); exit; } - - Template::set('manage_menu', manage_get_menu(User::getCurrent()->getId())); } } + +if(!empty($userDisplayInfo)) // delete this + Template::set('current_user', $userDisplayInfo); + +$inManageMode = starts_with($_SERVER['REQUEST_URI'], '/manage'); +$hasManageAccess = User::hasCurrent() + && !user_warning_check_restriction(User::getCurrent()->getId()) + && perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_CAN_MANAGE); +Template::set('has_manage_access', $hasManageAccess); + +if($inManageMode) { + if(!$hasManageAccess) { + echo render_error(403); + exit; + } + + Template::set('manage_menu', manage_get_menu(User::getCurrent()->getId())); +} diff --git a/msz b/msz new file mode 100755 index 00000000..7969017e --- /dev/null +++ b/msz @@ -0,0 +1,20 @@ +#!/usr/bin/env php +addCommands( + new \Misuzu\Console\Commands\CronCommand, + new \Misuzu\Console\Commands\MigrateCommand, + new \Misuzu\Console\Commands\NewMigrationCommand, + new \Misuzu\Console\Commands\TwitterAuthCommand, +); +$commands->dispatch(new CommandArgs($argv)); diff --git a/msz.php b/msz.php deleted file mode 100644 index 0af4197c..00000000 --- a/msz.php +++ /dev/null @@ -1,9 +0,0 @@ -args = $args; + } + + public function getArgs(): array { + return $this->args; + } + + public function getCommand(): string { + return $this->args[1] ?? ''; + } + + public function getArg(int $index): string { + return $this->args[2 + $index] ?? ''; + } + + public function flagIndex(string $long, string $short = ''): int { + $long = '--' . $long; + $short = '-' . $short; + for($i = 2; $i < count($this->args); ++$i) + if(($long !== '--' && $this->args[$i] === $long) || ($short !== '-' && $short === $this->args[$i])) + return $i; + return -1; + } + + public function hasFlag(string $long, string $short = ''): bool { + return $this->flagIndex($long, $short) >= 0; + } + + public function getFlag(string $long, string $short = ''): string { + $index = $this->flagIndex($long, $short); + if($index < 0) + return ''; + $arg = $this->args[$index + 1] ?? ''; + if($arg[0] == '-') + return ''; + return $arg; + } +} diff --git a/src/Console/CommandCollection.php b/src/Console/CommandCollection.php new file mode 100644 index 00000000..9d0820b5 --- /dev/null +++ b/src/Console/CommandCollection.php @@ -0,0 +1,33 @@ +matchCommand($command->getName()); + } catch(CommandNotFoundException $ex) { + $this->commands[] = $command; + } + } + + public function matchCommand(string $name): CommandInterface { + foreach($this->commands as $command) + if($command->getName() === $name) + return $command; + throw new CommandNotFoundException; + } + + public function dispatch(CommandArgs $args): void { + try { + $this->matchCommand($args->getCommand())->dispatch($args); + } catch(CommandNotFoundException $ex) { + echo 'Command not found.' . PHP_EOL; + } + } +} diff --git a/src/Console/CommandDispatchInterface.php b/src/Console/CommandDispatchInterface.php new file mode 100644 index 00000000..7b0074e0 --- /dev/null +++ b/src/Console/CommandDispatchInterface.php @@ -0,0 +1,6 @@ +hasFlag('slow'); + + foreach(self::TASKS as $task) { + if($runSlow || empty($task['slow'])) { + echo $task['name'] . PHP_EOL; + + switch($task['type']) { + case 'sql': + DB::exec($task['command']); + break; + + case 'func': + call_user_func($task['command']); + break; + } + } + } + } + + private const TASKS = [ + [ + 'name' => 'Ensures main role exists.', + 'type' => 'sql', + 'slow' => true, + 'command' => " + INSERT IGNORE INTO `msz_roles` + (`role_id`, `role_name`, `role_hierarchy`, `role_colour`, `role_description`, `role_created`) + VALUES + (1, 'Member', 1, 1073741824, NULL, NOW()) + ", + ], + [ + 'name' => 'Ensures all users are in the main role.', + 'type' => 'sql', + 'slow' => true, + 'command' => " + INSERT INTO `msz_user_roles` + (`user_id`, `role_id`) + SELECT `user_id`, 1 FROM `msz_users` as u + WHERE NOT EXISTS ( + SELECT 1 + FROM `msz_user_roles` as ur + WHERE `role_id` = 1 + AND u.`user_id` = ur.`user_id` + ) + ", + ], + [ + 'name' => 'Ensures all display_role values are correct with `msz_user_roles`.', + 'type' => 'sql', + 'slow' => true, + 'command' => " + UPDATE `msz_users` as u + SET `display_role` = ( + SELECT ur.`role_id` + FROM `msz_user_roles` as ur + LEFT JOIN `msz_roles` as r + ON r.`role_id` = ur.`role_id` + WHERE ur.`user_id` = u.`user_id` + ORDER BY `role_hierarchy` DESC + LIMIT 1 + ) + WHERE NOT EXISTS ( + SELECT 1 + FROM `msz_user_roles` as ur + WHERE ur.`role_id` = u.`display_role` + AND `ur`.`user_id` = u.`user_id` + ) + ", + ], + [ + 'name' => 'Remove expired sessions.', + 'type' => 'sql', + 'command' => " + DELETE FROM `msz_sessions` + WHERE `session_expires` < NOW() + ", + ], + [ + 'name' => 'Remove old password reset records.', + 'type' => 'sql', + 'command' => " + DELETE FROM `msz_users_password_resets` + WHERE `reset_requested` < NOW() - INTERVAL 1 WEEK + ", + ], + [ + 'name' => 'Remove old chat login tokens.', + 'type' => 'sql', + 'command' => " + DELETE FROM `msz_user_chat_tokens` + WHERE `token_created` < NOW() - INTERVAL 1 WEEK + ", + ], + [ + 'name' => 'Clean up login history.', + 'type' => 'sql', + 'command' => " + DELETE FROM `msz_login_attempts` + WHERE `attempt_created` < NOW() - INTERVAL 1 MONTH + ", + ], + [ + 'name' => 'Clean up audit log.', + 'type' => 'sql', + 'command' => " + DELETE FROM `msz_audit_log` + WHERE `log_created` < NOW() - INTERVAL 3 MONTH + ", + ], + [ + 'name' => 'Remove stale forum tracking entries.', + 'type' => 'sql', + 'command' => " + DELETE tt FROM `msz_forum_topics_track` as tt + LEFT JOIN `msz_forum_topics` as t + ON t.`topic_id` = tt.`topic_id` + WHERE t.`topic_bumped` < NOW() - INTERVAL 1 MONTH + ", + ], + [ + 'name' => 'Synchronise forum_id.', + 'type' => 'sql', + 'slow' => true, + 'command' => " + UPDATE `msz_forum_posts` AS p + INNER JOIN `msz_forum_topics` AS t + ON t.`topic_id` = p.`topic_id` + SET p.`forum_id` = t.`forum_id` + ", + ], + [ + 'name' => 'Recount forum topics and posts.', + 'type' => 'func', + 'slow' => true, + 'command' => 'forum_count_synchronise', + ], + [ + 'name' => 'Clean up expired tfa tokens.', + 'type' => 'sql', + 'command' => " + DELETE FROM `msz_auth_tfa` + WHERE `tfa_created` < NOW() - INTERVAL 15 MINUTE + ", + ], + ]; +} diff --git a/src/Console/Commands/MigrateCommand.php b/src/Console/Commands/MigrateCommand.php new file mode 100644 index 00000000..6a4dd45d --- /dev/null +++ b/src/Console/Commands/MigrateCommand.php @@ -0,0 +1,45 @@ +setLogger(function ($log) { + echo $log . PHP_EOL; + }); + + if($args->getArg(0) === 'rollback') + $migrationManager->rollback(); + else + $migrationManager->migrate(); + + $errors = $migrationManager->getErrors(); + $errorCount = count($errors); + + if($errorCount < 1) { + echo 'Completed with no errors!' . PHP_EOL; + } else { + echo PHP_EOL . "There were {$errorCount} errors during the migrations..." . PHP_EOL; + foreach($errors as $error) + echo $error . PHP_EOL; + } + + unlink(MSZ_ROOT . '/.migrating'); + } +} diff --git a/src/Console/Commands/NewMigrationCommand.php b/src/Console/Commands/NewMigrationCommand.php new file mode 100644 index 00000000..385709f6 --- /dev/null +++ b/src/Console/Commands/NewMigrationCommand.php @@ -0,0 +1,56 @@ +exec(" + CREATE TABLE ... + "); +} + +function migrate_down(PDO \$conn): void { + \$conn->exec(" + DROP TABLE ... + "); +} + +MIG; + + public function getName(): string { + return 'new-mig'; + } + public function getSummary(): string { + return 'Creates a new database migration.'; + } + + public function dispatch(CommandArgs $args): void { + $name = trim($args->getArg(0)); + + if(empty($name)) { + echo 'Specify a migration name.' . PHP_EOL; + return; + } + + if(!preg_match('#^([a-z_]+)$#', $name)) { + echo 'Migration name may only contain alpha and _ characters.' . PHP_EOL; + return; + } + + $fileName = date('Y_m_d_His_') . trim($name, '_') . '.php'; + $filePath = MSZ_ROOT . '/database/' . $fileName; + $namespace = snake_to_camel($name); + + file_put_contents($filePath, sprintf(self::TEMPLATE, $namespace)); + + echo "Template for '{$namespace}' has been created." . PHP_EOL; + } +} diff --git a/src/Console/Commands/TwitterAuthCommand.php b/src/Console/Commands/TwitterAuthCommand.php new file mode 100644 index 00000000..421c2c1c --- /dev/null +++ b/src/Console/Commands/TwitterAuthCommand.php @@ -0,0 +1,50 @@ +bind('user_id', $userId); $createToken->bind('token', $token); - if(!$createToken->execute()) { + if(!$createToken->execute()) return ''; - } return $token; } diff --git a/src/Users/avatar.php b/src/Users/avatar.php index 86e0672d..0b8ef08d 100644 --- a/src/Users/avatar.php +++ b/src/Users/avatar.php @@ -12,20 +12,17 @@ function user_avatar_valid_resolution(int $resolution): bool { } function user_avatar_resolution_closest(int $resolution): int { - if($resolution === 0) { + if($resolution === 0) return MSZ_USER_AVATAR_RESOLUTION_ORIGINAL; - } $closest = null; foreach(MSZ_USER_AVATAR_RESOLUTIONS as $res) { - if($res === MSZ_USER_AVATAR_RESOLUTION_ORIGINAL) { + if($res === MSZ_USER_AVATAR_RESOLUTION_ORIGINAL) continue; - } - if($closest === null || abs($resolution - $closest) >= abs($res - $resolution)) { + if($closest === null || abs($resolution - $closest) >= abs($res - $resolution)) $closest = $res; - } } return $closest; @@ -77,9 +74,8 @@ define('MSZ_USER_AVATAR_ERROR_STORE_FAILED', 6); define('MSZ_USER_AVATAR_ERROR_FILE_NOT_FOUND', 7); function user_avatar_set_from_path(int $userId, string $path, array $options = []): int { - if(!file_exists($path)) { + if(!file_exists($path)) return MSZ_USER_AVATAR_ERROR_FILE_NOT_FOUND; - } $options = array_merge(user_avatar_default_options(), $options); @@ -89,22 +85,18 @@ function user_avatar_set_from_path(int $userId, string $path, array $options = [ if($imageInfo === false || count($imageInfo) < 3 || $imageInfo[0] < 1 - || $imageInfo[1] < 1) { + || $imageInfo[1] < 1) return MSZ_USER_AVATAR_ERROR_INVALID_IMAGE; - } - if(!user_avatar_is_allowed_type($imageInfo[2])) { + if(!user_avatar_is_allowed_type($imageInfo[2])) return MSZ_USER_AVATAR_ERROR_PROHIBITED_TYPE; - } if($imageInfo[0] > $options['max_width'] - || $imageInfo[1] > $options['max_height']) { + || $imageInfo[1] > $options['max_height']) return MSZ_USER_AVATAR_ERROR_DIMENSIONS_TOO_LARGE; - } - if(filesize($path) > $options['max_size']) { + if(filesize($path) > $options['max_size']) return MSZ_USER_AVATAR_ERROR_DATA_TOO_LARGE; - } user_avatar_delete($userId); @@ -113,9 +105,8 @@ function user_avatar_set_from_path(int $userId, string $path, array $options = [ mkdirs($storageDir, true); $avatarPath = "{$storageDir}/{$fileName}"; - if(!copy($path, $avatarPath)) { + if(!copy($path, $avatarPath)) return MSZ_USER_AVATAR_ERROR_STORE_FAILED; - } return MSZ_USER_AVATAR_NO_ERRORS; } @@ -123,9 +114,8 @@ function user_avatar_set_from_path(int $userId, string $path, array $options = [ function user_avatar_set_from_data(int $userId, string $data, array $options = []): int { $tmp = tempnam(sys_get_temp_dir(), 'msz'); - if($tmp === false || !file_exists($tmp)) { + if($tmp === false || !file_exists($tmp)) return MSZ_USER_AVATAR_ERROR_TMP_FAILED; - } chmod($tmp, 644); file_put_contents($tmp, $data); diff --git a/src/Users/background.php b/src/Users/background.php index 7bd1b2e0..a2eede09 100644 --- a/src/Users/background.php +++ b/src/Users/background.php @@ -45,23 +45,19 @@ function user_background_settings_strings(int $settings, string $format = '%s'): $attachment = $settings & 0x0F; - if(array_key_exists($attachment, MSZ_USER_BACKGROUND_ATTACHMENTS_NAMES)) { + if(array_key_exists($attachment, MSZ_USER_BACKGROUND_ATTACHMENTS_NAMES)) $arr[] = sprintf($format, MSZ_USER_BACKGROUND_ATTACHMENTS_NAMES[$attachment]); - } - foreach(MSZ_USER_BACKGROUND_ATTRIBUTES_NAMES as $flag => $name) { - if(($settings & $flag) > 0) { + foreach(MSZ_USER_BACKGROUND_ATTRIBUTES_NAMES as $flag => $name) + if(($settings & $flag) > 0) $arr[] = sprintf($format, $name); - } - } return $arr; } function user_background_set_settings(int $userId, int $settings): void { - if($userId < 1) { + if($userId < 1) return; - } $setAttrs = \Misuzu\DB::prepare(' UPDATE `msz_users` @@ -109,9 +105,8 @@ define('MSZ_USER_BACKGROUND_ERROR_STORE_FAILED', 6); define('MSZ_USER_BACKGROUND_ERROR_FILE_NOT_FOUND', 7); function user_background_set_from_path(int $userId, string $path, array $options = []): int { - if(!file_exists($path)) { + if(!file_exists($path)) return MSZ_USER_BACKGROUND_ERROR_FILE_NOT_FOUND; - } $options = array_merge(user_background_default_options(), $options); @@ -121,22 +116,18 @@ function user_background_set_from_path(int $userId, string $path, array $options if($imageInfo === false || count($imageInfo) < 3 || $imageInfo[0] < 1 - || $imageInfo[1] < 1) { + || $imageInfo[1] < 1) return MSZ_USER_BACKGROUND_ERROR_INVALID_IMAGE; - } - if(!user_background_is_allowed_type($imageInfo[2])) { + if(!user_background_is_allowed_type($imageInfo[2])) return MSZ_USER_BACKGROUND_ERROR_PROHIBITED_TYPE; - } if($imageInfo[0] > $options['max_width'] - || $imageInfo[1] > $options['max_height']) { + || $imageInfo[1] > $options['max_height']) return MSZ_USER_BACKGROUND_ERROR_DIMENSIONS_TOO_LARGE; - } - if(filesize($path) > $options['max_size']) { + if(filesize($path) > $options['max_size']) return MSZ_USER_BACKGROUND_ERROR_DATA_TOO_LARGE; - } user_background_delete($userId); @@ -145,9 +136,8 @@ function user_background_set_from_path(int $userId, string $path, array $options mkdirs($storageDir, true); $backgroundPath = "{$storageDir}/{$fileName}"; - if(!copy($path, $backgroundPath)) { + if(!copy($path, $backgroundPath)) return MSZ_USER_BACKGROUND_ERROR_STORE_FAILED; - } return MSZ_USER_BACKGROUND_NO_ERRORS; } @@ -155,9 +145,8 @@ function user_background_set_from_path(int $userId, string $path, array $options function user_background_set_from_data(int $userId, string $data, array $options = []): int { $tmp = tempnam(sys_get_temp_dir(), 'msz'); - if($tmp === false || !file_exists($tmp)) { + if($tmp === false || !file_exists($tmp)) return MSZ_USER_BACKGROUND_ERROR_TMP_FAILED; - } chmod($tmp, 644); file_put_contents($tmp, $data); diff --git a/src/Users/relations.php b/src/Users/relations.php index 9a223447..44be45f7 100644 --- a/src/Users/relations.php +++ b/src/Users/relations.php @@ -14,13 +14,11 @@ function user_relation_is_valid_type(int $type): bool { } function user_relation_set(int $userId, int $subjectId, int $type = MSZ_USER_RELATION_FOLLOW): bool { - if($type === MSZ_USER_RELATION_NONE) { + if($type === MSZ_USER_RELATION_NONE) return user_relation_remove($userId, $subjectId); - } - if($userId < 1 || $subjectId < 1 || !user_relation_is_valid_type($type)) { + if($userId < 1 || $subjectId < 1 || !user_relation_is_valid_type($type)) return false; - } $addRelation = \Misuzu\DB::prepare(' REPLACE INTO `msz_user_relations` @@ -37,9 +35,8 @@ function user_relation_set(int $userId, int $subjectId, int $type = MSZ_USER_REL } function user_relation_remove(int $userId, int $subjectId): bool { - if($userId < 1 || $subjectId < 1) { + if($userId < 1 || $subjectId < 1) return false; - } $removeRelation = \Misuzu\DB::prepare(' DELETE FROM `msz_user_relations` @@ -81,9 +78,8 @@ function user_relation_info(int $userId, int $subjectId): array { } function user_relation_count(int $userId, int $type, bool $from): int { - if($userId < 1 || $type <= MSZ_USER_RELATION_NONE || !user_relation_is_valid_type($type)) { + if($userId < 1 || $type <= MSZ_USER_RELATION_NONE || !user_relation_is_valid_type($type)) return 0; - } static $getCount = []; $fetchCount = $getCount[$from] ?? null; @@ -123,9 +119,8 @@ function user_relation_users( int $offset = 0, int $requestingUserId = 0 ): array { - if($userId < 1 || $type <= MSZ_USER_RELATION_NONE || !user_relation_is_valid_type($type)) { + if($userId < 1 || $type <= MSZ_USER_RELATION_NONE || !user_relation_is_valid_type($type)) return []; - } $fetchAll = $take < 1; $key = sprintf('%s,%s', $from ? 'from' : 'to', $fetchAll ? 'all' : 'page'); diff --git a/src/Users/role.php b/src/Users/role.php index 5d0e263e..544494b4 100644 --- a/src/Users/role.php +++ b/src/Users/role.php @@ -47,9 +47,8 @@ function user_role_has(int $userId, int $roleId): bool { } function user_role_set_display(int $userId, int $roleId): bool { - if(!user_role_has($userId, $roleId)) { + if(!user_role_has($userId, $roleId)) return false; - } $setDisplay = \Misuzu\DB::prepare(' UPDATE `msz_users` @@ -63,9 +62,8 @@ function user_role_set_display(int $userId, int $roleId): bool { } function user_role_get_display(int $userId): int { - if($userId < 1) { + if($userId < 1) return MSZ_ROLE_MAIN; - } $fetchRole = \Misuzu\DB::prepare(' SELECT `display_role` diff --git a/src/Users/user_legacy.php b/src/Users/user_legacy.php index a5be0f00..b8bb6af2 100644 --- a/src/Users/user_legacy.php +++ b/src/Users/user_legacy.php @@ -41,9 +41,8 @@ function user_password_set(int $userId, string $password): bool { } function user_totp_info(int $userId): array { - if($userId < 1) { + if($userId < 1) return []; - } $getTwoFactorInfo = \Misuzu\DB::prepare(' SELECT @@ -58,9 +57,8 @@ function user_totp_info(int $userId): array { } function user_totp_update(int $userId, ?string $key): void { - if($userId < 1) { + if($userId < 1) return; - } $key = empty($key) ? null : $key; @@ -75,9 +73,8 @@ function user_totp_update(int $userId, ?string $key): void { } function user_email_get(int $userId): string { - if($userId < 1) { + if($userId < 1) return ''; - } $fetchMail = \Misuzu\DB::prepare(' SELECT `email` @@ -150,9 +147,8 @@ function user_check_super(int $userId): bool { } function user_check_authority(int $userId, int $subjectId, bool $canManageSelf = true): bool { - if($canManageSelf && $userId === $subjectId) { + if($canManageSelf && $userId === $subjectId) return true; - } $checkHierarchy = \Misuzu\DB::prepare(' SELECT ( @@ -193,25 +189,22 @@ define('MSZ_E_USER_BIRTHDATE_FAIL', 3); define('MSZ_E_USER_BIRTHDATE_YEAR', 4); function user_set_birthdate(int $userId, int $day, int $month, int $year, int $yearRange = 100): int { - if($userId < 1) { + if($userId < 1) return MSZ_E_USER_BIRTHDATE_USER; - } $unset = $day === 0 && $month === 0; if($year === 0) { $checkYear = date('Y'); } else { - if($year < date('Y') - $yearRange || $year > date('Y')) { + if($year < date('Y') - $yearRange || $year > date('Y')) return MSZ_E_USER_BIRTHDATE_YEAR; - } $checkYear = $year; } - if(!$unset && !checkdate($month, $day, $checkYear)) { + if(!$unset && !checkdate($month, $day, $checkYear)) return MSZ_E_USER_BIRTHDATE_DATE; - } $birthdate = $unset ? null : implode('-', [$year, $month, $day]); $setBirthdate = \Misuzu\DB::prepare(' @@ -228,11 +221,7 @@ function user_set_birthdate(int $userId, int $day, int $month, int $year, int $y } function user_get_birthdays(int $day = 0, int $month = 0) { - if($day < 1 || $month < 1) { - $date = date('%-m-d'); - } else { - $date = "%-{$month}-{$day}"; - } + $date = ($day < 1 || $month < 1) ? date('%-m-d') : "%-{$month}-{$day}"; $getBirthdays = \Misuzu\DB::prepare(' SELECT `user_id`, `username`, `user_birthdate`, @@ -254,19 +243,16 @@ define('MSZ_E_USER_ABOUT_TOO_LONG', 3); define('MSZ_E_USER_ABOUT_UPDATE_FAILED', 4); function user_set_about_page(int $userId, string $content, int $parser = \Misuzu\Parsers\Parser::PLAIN): int { - if($userId < 1) { + if($userId < 1) return MSZ_E_USER_ABOUT_INVALID_USER; - } - if(!\Misuzu\Parsers\Parser::isValid($parser)) { + if(!\Misuzu\Parsers\Parser::isValid($parser)) return MSZ_E_USER_ABOUT_INVALID_PARSER; - } $length = strlen($content); - if($length > MSZ_USER_ABOUT_MAX_LENGTH) { + if($length > MSZ_USER_ABOUT_MAX_LENGTH) return MSZ_E_USER_ABOUT_TOO_LONG; - } $setAbout = \Misuzu\DB::prepare(' UPDATE `msz_users` @@ -292,19 +278,16 @@ define('MSZ_E_USER_SIGNATURE_TOO_LONG', 3); define('MSZ_E_USER_SIGNATURE_UPDATE_FAILED', 4); function user_set_signature(int $userId, string $content, int $parser = \Misuzu\Parsers\Parser::PLAIN): int { - if($userId < 1) { + if($userId < 1) return MSZ_E_USER_SIGNATURE_INVALID_USER; - } - if(!\Misuzu\Parsers\Parser::isValid($parser)) { + if(!\Misuzu\Parsers\Parser::isValid($parser)) return MSZ_E_USER_SIGNATURE_INVALID_PARSER; - } $length = strlen($content); - if($length > MSZ_USER_SIGNATURE_MAX_LENGTH) { + if($length > MSZ_USER_SIGNATURE_MAX_LENGTH) return MSZ_E_USER_SIGNATURE_TOO_LONG; - } $setSignature = \Misuzu\DB::prepare(' UPDATE `msz_users` diff --git a/src/Users/warning.php b/src/Users/warning.php index b4c17a3f..09870fe4 100644 --- a/src/Users/warning.php +++ b/src/Users/warning.php @@ -58,21 +58,17 @@ function user_warning_add( string $privateNote, ?int $duration = null ): int { - if(!user_warning_type_is_valid($type)) { + if(!user_warning_type_is_valid($type)) return MSZ_E_WARNING_ADD_TYPE; - } - if($userId < 1) { + if($userId < 1) return MSZ_E_WARNING_ADD_USER; - } if(user_warning_has_duration($type)) { - if($duration <= time()) { + if($duration <= time()) return MSZ_E_WARNING_ADD_DURATION; - } - } else { + } else $duration = 0; - } $addWarning = \Misuzu\DB::prepare(' INSERT INTO `msz_user_warnings` @@ -89,17 +85,15 @@ function user_warning_add( $addWarning->bind('note_private', $privateNote); $addWarning->bind('duration', $duration < 1 ? null : date('Y-m-d H:i:s', $duration)); - if(!$addWarning->execute()) { + if(!$addWarning->execute()) return MSZ_E_WARNING_ADD_DB; - } return \Misuzu\DB::lastId(); } function user_warning_count(int $userId): int { - if($userId < 1) { + if($userId < 1) return 0; - } $countWarnings = \Misuzu\DB::prepare(' SELECT COUNT(`warning_id`) @@ -111,9 +105,8 @@ function user_warning_count(int $userId): int { } function user_warning_remove(int $warningId): bool { - if($warningId < 1) { + if($warningId < 1) return false; - } $removeWarning = \Misuzu\DB::prepare(' DELETE FROM `msz_user_warnings` @@ -148,9 +141,8 @@ function user_warning_fetch( )); $fetchWarnings->bind('user_id', $userId); - if($days !== null) { + if($days !== null) $fetchWarnings->bind('days', $days); - } return $fetchWarnings->fetchAll(); } @@ -162,9 +154,8 @@ function user_warning_global_count(?int $userId = null): int { %s ', $userId > 0 ? 'WHERE `user_id` = :user_id' : '')); - if($userId > 0) { + if($userId > 0) $countWarnings->bind('user_id', $userId); - } return (int)$countWarnings->fetchColumn(0, 0); } @@ -191,9 +182,8 @@ function user_warning_global_fetch(int $offset = 0, int $take = 50, ?int $userId $fetchWarnings->bind('offset', $offset); $fetchWarnings->bind('take', $take); - if($userId > 0) { + if($userId > 0) $fetchWarnings->bind('user_id', $userId); - } return $fetchWarnings->fetchAll(); } @@ -215,16 +205,14 @@ function user_warning_check_ip(string $address): bool { } function user_warning_check_expiration(int $userId, int $type): int { - if($userId < 1 || !user_warning_has_duration($type)) { + if($userId < 1 || !user_warning_has_duration($type)) return 0; - } static $memo = []; $memoId = "{$userId}-{$type}"; - if(array_key_exists($memoId, $memo)) { + if(array_key_exists($memoId, $memo)) return $memo[$memoId]; - } $getExpiration = \Misuzu\DB::prepare(' SELECT `warning_duration` @@ -244,15 +232,13 @@ function user_warning_check_expiration(int $userId, int $type): int { } function user_warning_check_restriction(int $userId): bool { - if($userId < 1) { + if($userId < 1) return false; - } static $memo = []; - if(array_key_exists($userId, $memo)) { + if(array_key_exists($userId, $memo)) return $memo[$userId]; - } $checkAddress = \Misuzu\DB::prepare(sprintf( '