diff --git a/core/classes/Middleware/AbstractMiddleware.php b/core/classes/Middleware/AbstractMiddleware.php new file mode 100644 index 0000000000..e1009a289b --- /dev/null +++ b/core/classes/Middleware/AbstractMiddleware.php @@ -0,0 +1,8 @@ +[] + */ + private array $middleware = []; + + /** + * Register a middleware class. + * + * @param class-string $class + */ + public function register(string $class): void + { + $this->middleware[] = $class; + } + + /** + * Get all registered middleware classes. + * + * @return class-string[] + */ + public function getMiddleware(): array + { + return $this->middleware; + } + + public function call(MiddlewareType $type, Container $container) + { + $middlewareClasses = $this->getMiddleware(); + + foreach ($middlewareClasses as $class) { + $middleware = $container->get($class); + $request = $container->get(Request::class); + + foreach ($middleware->exemptRoutes as $exemptedRoute) { + if (str_starts_with($request->get('route'), $exemptedRoute)) { + continue 2; // Skip this middleware if the route is exempted + } + } + + if ($middleware->type === $type) { + $container->call([$middleware, 'handle']); + } + } + } +} diff --git a/core/classes/Middleware/MiddlewareType.php b/core/classes/Middleware/MiddlewareType.php new file mode 100644 index 0000000000..bc075f01f6 --- /dev/null +++ b/core/classes/Middleware/MiddlewareType.php @@ -0,0 +1,7 @@ +set(\Symfony\Component\HttpFoundation\Request::class, function () { + return \Symfony\Component\HttpFoundation\Request::createFromGlobals(); + }); + $container->set(Cache::class, function () { return new Cache([ 'name' => 'nameless', @@ -159,6 +164,8 @@ } } + $container->set(User::class, $user); + // Check if we're in a subdirectory if (isset($directories)) { if (empty($directories[0])) { @@ -357,30 +364,8 @@ } } - // Maintenance mode? - if (Settings::get('maintenance') === '1') { - // Enabled - // Admins only beyond this point - if (!$user->isLoggedIn() || !$user->canViewStaffCP()) { - // Maintenance mode - if (isset($_GET['route']) && ( - rtrim($_GET['route'], '/') === '/login' - || rtrim($_GET['route'], '/') === '/forgot_password' - || str_contains($_GET['route'], '/api/') - || str_contains($_GET['route'], 'queries') - || str_contains($_GET['route'], 'oauth/') - || str_contains($_GET['route'], 'store/listener') - )) { - // Can continue as normal - } else { - require(ROOT_PATH . '/core/includes/maintenance.php'); - die; - } - } else { - // Display notice to admin stating maintenance mode is enabled - define('BYPASS_MAINTENANCE', true); - } - } + // Execute middleware events + MiddlewareHandler::getInstance()->call(MiddlewareType::Global, $container); // Webhooks $hook_array = []; @@ -424,21 +409,6 @@ Debugging::setCanViewDetailedError($user->hasPermission('admincp.errors')); Debugging::setCanGenerateDebugLink($user->hasPermission('admincp.core.debugging')); - // Ensure a user is not banned - if ($user->data()->isbanned == 1) { - $user->logout(); - Session::flash('home_error', $language->get('user', 'you_have_been_banned')); - Redirect::to(URL::build('/')); - } - - // Is the IP address banned? - $ip_bans = DB::getInstance()->get('ip_bans', ['ip', $ip])->results(); - if (count($ip_bans)) { - $user->logout(); - Session::flash('home_error', $language->get('user', 'you_have_been_banned')); - Redirect::to(URL::build('/')); - } - // Update user last IP and last online if (filter_var($ip, FILTER_VALIDATE_IP)) { $user->update([ @@ -488,24 +458,6 @@ } } - // Does their group have TFA forced? - foreach ($user->getGroups() as $group) { - if ($group->force_tfa) { - $forced = true; - break; - } - } - - if (isset($forced) && $forced) { - // Do they have TFA configured? - if (!$user->data()->tfa_enabled && rtrim($_GET['route'], '/') != '/logout') { - if (!str_contains($_SERVER['REQUEST_URI'], 'do=enable_tfa') && !isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { - Session::put('force_tfa_alert', $language->get('admin', 'force_tfa_alert')); - Redirect::to(URL::build('/user/settings', 'do=enable_tfa')); - } - } - } - $user_integrations = []; foreach ($user->getIntegrations() as $integrationUser) { $user_integrations[$integrationUser->getIntegration()->getName()] = [ diff --git a/core/templates/frontend_init.php b/core/templates/frontend_init.php index b1bc78bd12..599f414f89 100644 --- a/core/templates/frontend_init.php +++ b/core/templates/frontend_init.php @@ -40,19 +40,6 @@ } } -// Check if any integrations is required before user can continue -if ($user->isLoggedIn() && defined('PAGE') && PAGE != 'cc_connections' && PAGE != 'oauth' && !(PAGE == 'cc_settings' && $_GET['do'] == 'enable_tfa')) { - foreach (Integrations::getInstance()->getEnabledIntegrations() as $integration) { - if ($integration->data()->required && $integration->allowLinking()) { - $integrationUser = $user->getIntegration($integration->getName()); - if ($integrationUser === null || !$integrationUser->isVerified()) { - Session::flash('connections_error', $language->get('user', 'integration_required_to_continue')); - Redirect::to(URL::build('/user/connections')); - } - } - } -} - if (defined('PAGE') && PAGE != 404) { // Auto unset signin tfa variables if set if ( @@ -72,6 +59,10 @@ require(ROOT_PATH . '/custom/templates/DefaultRevamp/template.php'); } +$container->set(TemplateBase::class, $template); + +MiddlewareHandler::getInstance()->call(MiddlewareType::Frontend, $container); + // Basic template variables $template->getEngine()->addVariables([ 'CONFIG_PATH' => defined('CONFIG_PATH') ? CONFIG_PATH . '/' : '/', @@ -88,32 +79,6 @@ $template->getEngine()->addVariable('OG_IMAGE', rtrim(URL::getSelfURL(), '/') . Output::getClean($og_image)); } -// User related actions -if ($user->isLoggedIn()) { - // Warnings - $warnings = DB::getInstance()->get('infractions', ['punished', $user->data()->id])->results(); - if (count($warnings)) { - foreach ($warnings as $warning) { - if ($warning->revoked == 0 && $warning->acknowledged == 0) { - $template->getEngine()->addVariables([ - 'GLOBAL_WARNING_TITLE' => $language->get('user', 'you_have_received_a_warning'), - 'GLOBAL_WARNING_REASON' => Output::getClean($warning->reason), - 'GLOBAL_WARNING_ACKNOWLEDGE' => $language->get('user', 'acknowledge'), - 'GLOBAL_WARNING_ACKNOWLEDGE_LINK' => URL::build('/user/acknowledge/' . urlencode($warning->id)), - ]); - break; - } - } - } - - // Does the account need verifying? - // Get default group ID - $cache->setCache('default_group'); - $default_group = $cache->fetch('default_group', function () { - return Group::find(1, 'default_group')->id; - }); -} - // Page metadata if (isset($_GET['route']) && $_GET['route'] != '/') { $route = rtrim($_GET['route'], '/'); diff --git a/modules/Core/classes/Middleware/Frontend/EnsureUserIntegrationsLinkedMiddleware.php b/modules/Core/classes/Middleware/Frontend/EnsureUserIntegrationsLinkedMiddleware.php new file mode 100644 index 0000000000..9d827f63f3 --- /dev/null +++ b/modules/Core/classes/Middleware/Frontend/EnsureUserIntegrationsLinkedMiddleware.php @@ -0,0 +1,30 @@ +isLoggedIn()) { + return; + } + + // Check if any integrations is required before user can continue + foreach (Integrations::getInstance()->getEnabledIntegrations() as $integration) { + if ($integration->data()->required && $integration->allowLinking()) { + $integrationUser = $user->getIntegration($integration->getName()); + if ($integrationUser === null || !$integrationUser->isVerified()) { + Session::flash('connections_error', $language->get('user', 'integration_required_to_continue')); + Redirect::to(URL::build('/user/connections')); + } + } + } + } +} diff --git a/modules/Core/classes/Middleware/Frontend/GlobalWarningsMiddleware.php b/modules/Core/classes/Middleware/Frontend/GlobalWarningsMiddleware.php new file mode 100644 index 0000000000..c895782965 --- /dev/null +++ b/modules/Core/classes/Middleware/Frontend/GlobalWarningsMiddleware.php @@ -0,0 +1,20 @@ +query('SELECT * FROM nl2_infractions WHERE punished = ? AND revoked = 0 AND acknowledged = 0', [$user->data()->id])->results(); + foreach ($warnings as $warning) { + $template->getEngine()->addVariables([ + 'GLOBAL_WARNING_TITLE' => $language->get('user', 'you_have_received_a_warning'), + 'GLOBAL_WARNING_REASON' => Output::getClean($warning->reason), + 'GLOBAL_WARNING_ACKNOWLEDGE' => $language->get('user', 'acknowledge'), + 'GLOBAL_WARNING_ACKNOWLEDGE_LINK' => URL::build('/user/acknowledge/' . urlencode($warning->id)), + ]); + break; + } + } +} diff --git a/modules/Core/classes/Middleware/Global/BannedUserMiddleware.php b/modules/Core/classes/Middleware/Global/BannedUserMiddleware.php new file mode 100644 index 0000000000..714673fe44 --- /dev/null +++ b/modules/Core/classes/Middleware/Global/BannedUserMiddleware.php @@ -0,0 +1,23 @@ +isLoggedIn() && $user->data()->isbanned) || DB::getInstance()->get('ip_bans', ['ip', HttpUtils::getRemoteAddress()])->exists()) { + $user->logout(); + + Session::flash('home_error', $language->get('user', 'you_have_been_banned')); + Redirect::to(URL::build('/')); + } + } +} diff --git a/modules/Core/classes/Middleware/Global/MaintenanceModeMiddleware.php b/modules/Core/classes/Middleware/Global/MaintenanceModeMiddleware.php new file mode 100644 index 0000000000..e093744808 --- /dev/null +++ b/modules/Core/classes/Middleware/Global/MaintenanceModeMiddleware.php @@ -0,0 +1,40 @@ +isLoggedIn() && $user->canViewStaffCP()) { + // Display notice to admin stating maintenance mode is enabled + define('BYPASS_MAINTENANCE', true); + return; + } + + Redirect::to(URL::build('/maintenance')); + } +} diff --git a/modules/Core/classes/Middleware/Global/TFAMiddleware.php b/modules/Core/classes/Middleware/Global/TFAMiddleware.php new file mode 100644 index 0000000000..e2f88117d6 --- /dev/null +++ b/modules/Core/classes/Middleware/Global/TFAMiddleware.php @@ -0,0 +1,48 @@ +isLoggedIn()) { + return; + } + + // Skip if AJAX request, such as Alert or PM checks + if ($request->isXmlHttpRequest()) { + return; + } + + // Check if any of the user's groups have TFA forced + $forced_tfa = false; + foreach ($user->getGroups() as $group) { + if ($group->force_tfa) { + $forced_tfa = true; + break; + } + } + + // If TFA is forced and user doesn't have it enabled, redirect + if ($forced_tfa && !$user->data()->tfa_enabled) { + Session::put('force_tfa_alert', $language->get('user', 'force_tfa_alert')); + Redirect::to(URL::build('/user/settings', 'do=enable_tfa')); + } + } +} diff --git a/modules/Core/language/en_UK.json b/modules/Core/language/en_UK.json index 81e1bba545..143f7ab009 100644 --- a/modules/Core/language/en_UK.json +++ b/modules/Core/language/en_UK.json @@ -248,7 +248,7 @@ "admin/force_https_help": "If enabled, all requests to your website will be redirected to https. You must have a valid SSL certificate active for this to work.", "admin/force_premium_accounts": "Force premium Minecraft accounts?", "admin/force_tfa": "Force Two Factor Authentication for group members?", - "admin/force_tfa_alert": "Your group requires you to have Two Factor Authentication enabled.", + "user/force_tfa_alert": "Your group requires you to have Two Factor Authentication enabled.", "admin/force_tfa_warning": "Please ensure you know what this does, or else you risk locking out yourself and all the group members.", "admin/edit_user_tfa_disabled": "Two factor authentication has successfully been disabled for this user.", "admin/disable_tfa": "Disable 2FA", diff --git a/modules/Core/module.php b/modules/Core/module.php index d9be3ffc9e..826354ad13 100644 --- a/modules/Core/module.php +++ b/modules/Core/module.php @@ -51,6 +51,7 @@ public function __construct(Language $language, Pages $pages, User $user, Naviga $pages->add('Core', '/queries/tinymce_image_upload', 'queries/tinymce_image_upload.php'); $pages->add('Core', '/queries/reactions', 'queries/reactions.php'); $pages->add('Core', '/banner', 'pages/minecraft/banner.php'); + $pages->add('Core', '/maintenance', 'pages/maintenance.php'); $pages->add('Core', '/terms', 'pages/terms.php'); $pages->add('Core', '/privacy', 'pages/privacy.php'); $pages->add('Core', '/forgot_password', 'pages/forgot_password.php'); @@ -452,6 +453,13 @@ public function __construct(Language $language, Pages $pages, User $user, Naviga EventHandler::registerListener(UserRegisteredEvent::class, DefaultUserNotificationPreferencesHook::class); + // -- Register middleware + MiddlewareHandler::getInstance()->register(MaintenanceModeMiddleware::class); + MiddlewareHandler::getInstance()->register(BannedUserMiddleware::class); + MiddlewareHandler::getInstance()->register(TFAMiddleware::class); + MiddlewareHandler::getInstance()->register(EnsureUserIntegrationsLinkedMiddleware::class); + MiddlewareHandler::getInstance()->register(GlobalWarningsMiddleware::class); + Email::addPlaceholder('[Sitename]', Output::getClean(SITE_NAME)); Email::addPlaceholder('[Greeting]', static fn(Language $viewing_language) => $viewing_language->get('emails', 'greeting')); Email::addPlaceholder('[Message]', static fn(Language $viewing_language, string $email) => $viewing_language->get('emails', $email . '_message')); diff --git a/core/includes/maintenance.php b/modules/Core/pages/maintenance.php similarity index 50% rename from core/includes/maintenance.php rename to modules/Core/pages/maintenance.php index 5205a3736a..5aee82749f 100644 --- a/core/includes/maintenance.php +++ b/modules/Core/pages/maintenance.php @@ -1,13 +1,9 @@ isLoggedIn() && $user->canViewStaffCP()) { + define('BYPASS_MAINTENANCE', true); + Redirect::back(); +} + $pages = new Pages(); const PAGE = 'maintenance'; @@ -27,22 +35,18 @@ require_once ROOT_PATH . '/core/templates/frontend_init.php'; if (!$user->isLoggedIn()) { - $template->getEngine()->addVariables( - [ - 'LOGIN' => $language->get('general', 'sign_in'), - 'LOGIN_LINK' => URL::build('/login'), - ] - ); + $template->getEngine()->addVariables([ + 'LOGIN' => $language->get('general', 'sign_in'), + 'LOGIN_LINK' => URL::build('/login'), + ]); } // Assign template variables -$template->getEngine()->addVariables( - [ - 'MAINTENANCE_TITLE' => $language->get('errors', 'maintenance_title'), - 'MAINTENANCE_MESSAGE' => Output::getPurified(Settings::get('maintenance_message', 'Maintenance mode is enabled.')), - 'RETRY' => $language->get('errors', 'maintenance_retry'), - ] -); +$template->getEngine()->addVariables([ + 'MAINTENANCE_TITLE' => $language->get('errors', 'maintenance_title'), + 'MAINTENANCE_MESSAGE' => Output::getPurified(Settings::get('maintenance_message', 'Maintenance mode is enabled.')), + 'RETRY' => $language->get('errors', 'maintenance_retry'), +]); // Load modules + template Module::loadPage($user, $pages, $cache, $smarty, [$navigation, $cc_nav, $staffcp_nav], $widgets, $template);