createAuthService(); $rememberMeService = $authServicesFactory->createRememberMeService(); // Initialize default locale from tenant/app settings $defaultLocale = appDefaultLocale(); if ($defaultLocale !== null) { I18n::$defaultLocale = $defaultLocale; if (I18n::$locale === '') { I18n::$locale = $defaultLocale; } } // Auto-login from remember-me cookie if (empty($_SESSION['user']['id'])) { $rememberMeService->autoLoginFromCookie(); } // Session timeout enforcement (idle + absolute) if (!empty($_SESSION['user']['id'])) { $now = time(); $sessionStartedAt = (int) ($_SESSION['session_started_at'] ?? 0); $sessionLastActivity = (int) ($_SESSION['session_last_activity'] ?? 0); // Gracefully handle pre-existing sessions that lack timestamps. if ($sessionStartedAt === 0) { $_SESSION['session_started_at'] = $now; $sessionStartedAt = $now; } if ($sessionLastActivity === 0) { $_SESSION['session_last_activity'] = $now; $sessionLastActivity = $now; } $sessionGateway = app(\MintyPHP\Service\Settings\SettingsSessionGateway::class); $idleTimeoutSeconds = $sessionGateway->getSessionIdleTimeoutSeconds(); $absoluteTimeoutSeconds = $sessionGateway->getSessionAbsoluteTimeoutSeconds(); $isIdle = ($now - $sessionLastActivity) > $idleTimeoutSeconds; $isAbsolute = ($now - $sessionStartedAt) > $absoluteTimeoutSeconds; if ($isIdle || $isAbsolute) { // Capture the current URL before session destruction so the user // can be redirected back after re-authentication. $intendedUrl = $_SERVER['REQUEST_URI'] ?? ''; $authService->logoutDueToSessionTimeout(); Flash::error(t('Your session has expired. Please log in again.'), 'login', 'session_timeout'); $intendedUrlService = app(IntendedUrlService::class); $locale = I18n::$locale ?? I18n::$defaultLocale; $loginTarget = $intendedUrlService->buildLoginRedirect( Request::withLocale('login', $locale), $intendedUrl ); Router::redirect($loginTarget); } $_SESSION['session_last_activity'] = $now; } // Parse request URI and resolve locale $originalUri = $_SERVER['REQUEST_URI'] ?? ''; $_SERVER['MINTY_ORIGINAL_URI'] = $originalUri; $localeResolver = new LocaleResolver(); $parsedUri = $localeResolver->parseUri($originalUri); $pathWithoutLocale = $parsedUri['pathWithoutLocale']; $requestedLocale = $parsedUri['locale']; $_SERVER['MINTY_LOCALE'] = $requestedLocale; RequestContext::setPath($pathWithoutLocale); // Rewrite REQUEST_URI without locale segment for router if ($parsedUri['hadLocaleInUrl']) { $_SERVER['REQUEST_URI'] = $localeResolver->buildUriWithoutLocale( $pathWithoutLocale, $parsedUri['query'] ); } // Resolve effective locale from URL > Session > Cookie > Default I18n::$locale = $localeResolver->resolveLocale( $requestedLocale, $_SESSION['user']['locale'] ?? null, $_COOKIE['locale'] ?? null ); $isAssetRequest = AssetDetector::isAssetRequest($pathWithoutLocale); $isApiRequest = str_starts_with($pathWithoutLocale, 'api/'); RequestContext::setChannel($isApiRequest ? 'api' : 'web'); if ($isApiRequest) { if (!defined('MINTY_API_REQUEST')) { define('MINTY_API_REQUEST', true); } // Minty router enforces CSRF for non-GET requests before action execution. // Our API is Bearer-token based and does not use form-CSRF, so mark only // API write requests as AJAX to bypass the router-level form CSRF gate. $requestMethod = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); $isApiWriteMethod = !in_array($requestMethod, ['GET', 'HEAD', 'OPTIONS'], true); if ($isApiWriteMethod && empty($_SERVER['HTTP_X_REQUESTED_WITH'])) { $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'; } } // Refresh tenant context if stale or missing (skip for assets and API requests) if (!empty($_SESSION['user']['id']) && !$isAssetRequest && !$isApiRequest) { $userId = (int) ($_SESSION['user']['id'] ?? 0); if ($userId > 0) { $authState = $authService->refreshSessionAuthState($userId); if (!empty($authState['logout_required'])) { $authService->logout(); $reason = (string) ($authState['reason'] ?? ''); if ($reason === 'inactive') { Flash::error(t('Account is inactive'), 'login', 'login_inactive'); } else { Flash::error(t('No active tenant assigned'), 'login', 'no_active_tenant'); } Router::redirect('login'); } $ttl = 120; $lastRefresh = (int) ($_SESSION['tenant_snapshot_at'] ?? 0); $needsRefresh = empty($_SESSION['current_tenant']) || empty($_SESSION['available_tenants']); if ((time() - $lastRefresh) >= $ttl) { $needsRefresh = true; } if ($needsRefresh) { $authService->loadTenantDataIntoSession($userId); $_SESSION['tenant_snapshot_at'] = time(); } } } if ($requestedLocale === '' && !$isAssetRequest && !$isApiRequest) { $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); if (in_array($method, ['GET', 'HEAD'], true)) { $target = Request::withLocale($pathWithoutLocale, I18n::$locale ?? I18n::$defaultLocale); $target .= $parsedUri['query'] !== '' ? '?' . $parsedUri['query'] : ''; Router::redirect($target); } } // Guard: restrict non-public pages to logged-in users $accessControl = new AccessControl(); $accessControl->requireAuthOrRedirect($pathWithoutLocale, !empty($_SESSION['user']['id'])); // Analyze the PHP code if (Debugger::$enabled) { Analyzer::execute(); } // Load the action into body ob_start(); if (Router::getTemplateAction()) { require Router::getTemplateAction(); } if (ob_get_contents()) { ob_end_flush(); if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) { trigger_error('MintyPHP template action"' . Router::getTemplateAction() . '" should not send output. Error raised ', E_USER_WARNING); } } else { ob_end_clean(); } if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) { Session::end(); DB::close(); exit; } ob_start(); if (Router::getAction()) { extract(Router::getParameters()); require Router::getAction(); } if (ob_get_contents()) { ob_end_flush(); if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) { trigger_error('MintyPHP action "' . Router::getAction() . '" should not send output. Error raised ', E_USER_WARNING); } } else { ob_end_clean(); } if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) { Session::end(); DB::close(); exit; } // Resolve layout capabilities while DB/session are still open. // Templates render after DB::close(), so authz lookup must be prepared here. if (Router::getTemplateView()) { $viewAuth = is_array($viewAuth ?? null) ? $viewAuth : []; $layoutAuth = $viewAuth['layout'] ?? null; if (!is_array($layoutAuth)) { $actorUserId = (int) (($_SESSION['user']['id'] ?? 0)); $viewAuth['layout'] = app(UiAccessService::class)->layoutCapabilities($actorUserId); $layoutAuth = $viewAuth['layout']; } $layoutNav = appBuildLayoutNavContext( is_array($layoutAuth) ? $layoutAuth : [], is_array($_SESSION ?? null) ? $_SESSION : [], requestInput()->queryAll() ); } // End the session Session::end(); // Close the database connection DB::close(); if (Router::getTemplateView()) { Buffer::start('html'); if (Router::getView()) { require Router::getView(); } // Show developer toolbar if (Debugger::$enabled) { Debugger::toolbar(); } Buffer::end('html'); // Load body into template require Router::getTemplateView(); } else { // Handle the 'none' template case if (Router::getView()) { require Router::getView(); } }