2026-02-04 23:31:53 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
use MintyPHP\Analyzer;
|
|
|
|
|
use MintyPHP\Buffer;
|
2026-02-23 12:58:19 +01:00
|
|
|
use MintyPHP\DB;
|
|
|
|
|
use MintyPHP\Debugger;
|
|
|
|
|
use MintyPHP\Firewall;
|
2026-02-04 23:31:53 +01:00
|
|
|
use MintyPHP\Http\AccessControl;
|
|
|
|
|
use MintyPHP\Http\AssetDetector;
|
2026-03-13 14:28:49 +01:00
|
|
|
use MintyPHP\Http\IntendedUrlService;
|
2026-02-23 12:58:19 +01:00
|
|
|
use MintyPHP\Http\LocaleResolver;
|
2026-03-04 15:56:58 +01:00
|
|
|
use MintyPHP\Http\RequestContext;
|
2026-02-23 12:58:19 +01:00
|
|
|
use MintyPHP\Http\Request;
|
|
|
|
|
use MintyPHP\I18n;
|
|
|
|
|
use MintyPHP\Router;
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
use MintyPHP\App\Module\ModuleRegistry;
|
|
|
|
|
use MintyPHP\App\Module\ModuleRuntimePageBuilder;
|
2026-03-04 15:56:58 +01:00
|
|
|
use MintyPHP\Service\Access\UiAccessService;
|
2026-02-23 12:58:19 +01:00
|
|
|
use MintyPHP\Service\Auth\AuthServicesFactory;
|
|
|
|
|
use MintyPHP\Session;
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
use MintyPHP\Support\Flash;
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 1. Bootstrap ────────────────────────────────────────────────────
|
|
|
|
|
|
2026-02-04 23:31:53 +01:00
|
|
|
chdir(__DIR__ . '/..');
|
|
|
|
|
require 'vendor/autoload.php';
|
|
|
|
|
require 'config/config.php';
|
|
|
|
|
require 'lib/Support/helpers.php';
|
2026-03-04 15:56:58 +01:00
|
|
|
$container = require 'lib/App/registerContainer.php';
|
|
|
|
|
setAppContainer($container);
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 2. Module runtime validation ────────────────────────────────────
|
|
|
|
|
// Fail-fast if the runtime page root is stale (modules changed without
|
|
|
|
|
// running module-runtime-sync). No auto-build in the request path —
|
|
|
|
|
// that is the responsibility of bin/module-runtime-sync.php.
|
|
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
$moduleRegistry = app(ModuleRegistry::class);
|
2026-03-18 22:41:59 +01:00
|
|
|
$runtimePageRoot = __DIR__ . '/../storage/runtime/pages';
|
|
|
|
|
$activeModules = $moduleRegistry->getModules();
|
|
|
|
|
|
|
|
|
|
if (count($activeModules) > 0 || is_dir($runtimePageRoot) || is_link($runtimePageRoot)) {
|
|
|
|
|
$expectedFingerprint = ModuleRuntimePageBuilder::expectedFingerprint($activeModules);
|
|
|
|
|
$actualFingerprint = ModuleRuntimePageBuilder::readFingerprint($runtimePageRoot);
|
|
|
|
|
if ($expectedFingerprint !== $actualFingerprint) {
|
|
|
|
|
throw new RuntimeException(
|
|
|
|
|
"Module runtime is stale (expected: '{$expectedFingerprint}', actual: '{$actualFingerprint}'). "
|
|
|
|
|
. 'Run: php bin/module-runtime-sync.php'
|
|
|
|
|
);
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
}
|
|
|
|
|
if (is_dir($runtimePageRoot) || is_link($runtimePageRoot)) {
|
|
|
|
|
Router::$pageRoot = 'storage/runtime/pages/';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 3. Routes ───────────────────────────────────────────────────────
|
|
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
require 'config/router.php';
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 4. Request context ──────────────────────────────────────────────
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
RequestContext::start();
|
|
|
|
|
header('X-Request-Id: ' . RequestContext::requestHeaderValue());
|
2026-02-04 23:31:53 +01:00
|
|
|
|
|
|
|
|
Firewall::start();
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 5. Parse URI + detect channel ───────────────────────────────────
|
|
|
|
|
// Channel detection happens BEFORE session/auth so that API requests
|
|
|
|
|
// never touch cookies, session state or remember-me flows.
|
2026-03-09 19:16:26 +01:00
|
|
|
|
2026-02-04 23:31:53 +01:00
|
|
|
$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;
|
2026-03-04 15:56:58 +01:00
|
|
|
RequestContext::setPath($pathWithoutLocale);
|
2026-02-04 23:31:53 +01:00
|
|
|
|
|
|
|
|
if ($parsedUri['hadLocaleInUrl']) {
|
|
|
|
|
$_SERVER['REQUEST_URI'] = $localeResolver->buildUriWithoutLocale(
|
|
|
|
|
$pathWithoutLocale,
|
|
|
|
|
$parsedUri['query']
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$isAssetRequest = AssetDetector::isAssetRequest($pathWithoutLocale);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$isApiRequest = str_starts_with($pathWithoutLocale, 'api/');
|
2026-03-04 15:56:58 +01:00
|
|
|
RequestContext::setChannel($isApiRequest ? 'api' : 'web');
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 6. API channel ──────────────────────────────────────────────────
|
|
|
|
|
// API endpoints use Bearer-token auth (ApiBootstrap). They never need
|
|
|
|
|
// PHP sessions, remember-me cookies, locale redirects or tenant UI refresh.
|
|
|
|
|
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if ($isApiRequest) {
|
|
|
|
|
if (!defined('MINTY_API_REQUEST')) {
|
|
|
|
|
define('MINTY_API_REQUEST', true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// Set default locale so t() works in API error messages.
|
|
|
|
|
$defaultLocale = appDefaultLocale();
|
|
|
|
|
if ($defaultLocale !== null) {
|
|
|
|
|
I18n::$defaultLocale = $defaultLocale;
|
|
|
|
|
I18n::$locale = $defaultLocale;
|
|
|
|
|
}
|
|
|
|
|
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
// 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';
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 7. Web channel: session, auth, locale, tenant ───────────────────
|
|
|
|
|
|
|
|
|
|
if (!$isApiRequest) {
|
|
|
|
|
// Start the session
|
|
|
|
|
Session::start();
|
|
|
|
|
$authServicesFactory = app(AuthServicesFactory::class);
|
|
|
|
|
$authService = $authServicesFactory->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'])) {
|
|
|
|
|
$autoLoginResult = $rememberMeService->autoLoginFromCookie();
|
|
|
|
|
if ($autoLoginResult) {
|
|
|
|
|
// The session timeout flash is no longer relevant — the user was
|
|
|
|
|
// seamlessly re-authenticated and never saw the login page.
|
|
|
|
|
Flash::dismissByKey('session_timeout', 'login');
|
|
|
|
|
|
|
|
|
|
// After auto-login (e.g. session timeout with active remember-me cookie),
|
|
|
|
|
// honour ?return_to= so the user lands on the page they were on.
|
|
|
|
|
$returnToParam = trim((string) ($_GET['return_to'] ?? ''));
|
|
|
|
|
if ($returnToParam !== '') {
|
|
|
|
|
$intendedUrlService = app(IntendedUrlService::class);
|
|
|
|
|
$sanitized = $intendedUrlService->sanitize($returnToParam);
|
|
|
|
|
if ($sanitized !== '') {
|
|
|
|
|
$routePath = $intendedUrlService->toRoutePath($sanitized);
|
|
|
|
|
Router::redirect($routePath);
|
|
|
|
|
}
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-18 22:41:59 +01:00
|
|
|
}
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// 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;
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
2026-03-18 22:41:59 +01:00
|
|
|
|
|
|
|
|
$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);
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
2026-03-18 22:41:59 +01:00
|
|
|
|
|
|
|
|
$_SESSION['session_last_activity'] = $now;
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// Resolve effective locale from URL > Session > Cookie > Default
|
|
|
|
|
I18n::$locale = $localeResolver->resolveLocale(
|
|
|
|
|
$requestedLocale,
|
|
|
|
|
$_SESSION['user']['locale'] ?? null,
|
|
|
|
|
$_COOKIE['locale'] ?? null
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Refresh tenant context if stale or missing (skip for asset requests)
|
|
|
|
|
if (!empty($_SESSION['user']['id']) && !$isAssetRequest) {
|
|
|
|
|
$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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// Redirect to locale-prefixed URL if missing
|
|
|
|
|
if ($requestedLocale === '' && !$isAssetRequest) {
|
|
|
|
|
$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);
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: introduce module system and extract address book as first module (MODULAR-MONOLITH-V1-001)
Add a module kernel (ModuleManifest, ModuleRegistry) that allows modules to
contribute routes, UI slots, search providers, layout context, session
lifecycle, and permissions. Modules are activated via config/modules.php or
APP_ENABLED_MODULES env variable. Conflicts (duplicate routes, permissions,
slot keys) cause a fail-fast with a clear error message.
Extract the address book from hardcoded Core integration points into the
first module (modules/addressbook/). The module provides:
- Aside icon-bar tab + People panel via UI slot system
- Global search resource via AddressBookSearchProvider
- Layout context data via AddressBookLayoutProvider
- Session lifecycle via AddressBookSessionProvider
Core cleanup removes address-book hardcodings from SearchSqlResourceProvider,
SearchUiMetaProvider, SearchItemMapperProvider, appBuildLayoutNavContext(),
and the aside templates. Permissions (ADDRESS_BOOK_VIEW) and business logic
(AddressBookService) remain in Core as they gate general user visibility.
Includes 38 new tests (894 total), PHPStan level 5 clean, and architecture
tests verifying zero hardcoded address-book references in search/templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:43:25 +01:00
|
|
|
}
|
2026-03-18 22:41:59 +01:00
|
|
|
|
|
|
|
|
// ── 8. Access control guard ─────────────────────────────────────────
|
|
|
|
|
|
feat: introduce module system and extract address book as first module (MODULAR-MONOLITH-V1-001)
Add a module kernel (ModuleManifest, ModuleRegistry) that allows modules to
contribute routes, UI slots, search providers, layout context, session
lifecycle, and permissions. Modules are activated via config/modules.php or
APP_ENABLED_MODULES env variable. Conflicts (duplicate routes, permissions,
slot keys) cause a fail-fast with a clear error message.
Extract the address book from hardcoded Core integration points into the
first module (modules/addressbook/). The module provides:
- Aside icon-bar tab + People panel via UI slot system
- Global search resource via AddressBookSearchProvider
- Layout context data via AddressBookLayoutProvider
- Session lifecycle via AddressBookSessionProvider
Core cleanup removes address-book hardcodings from SearchSqlResourceProvider,
SearchUiMetaProvider, SearchItemMapperProvider, appBuildLayoutNavContext(),
and the aside templates. Permissions (ADDRESS_BOOK_VIEW) and business logic
(AddressBookService) remain in Core as they gate general user visibility.
Includes 38 new tests (894 total), PHPStan level 5 clean, and architecture
tests verifying zero hardcoded address-book references in search/templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:43:25 +01:00
|
|
|
$allPublicPaths = array_values(array_unique(array_merge(
|
|
|
|
|
defined('APP_PUBLIC_PATHS') ? APP_PUBLIC_PATHS : [],
|
2026-03-18 22:41:59 +01:00
|
|
|
$moduleRegistry->getPublicPaths()
|
feat: introduce module system and extract address book as first module (MODULAR-MONOLITH-V1-001)
Add a module kernel (ModuleManifest, ModuleRegistry) that allows modules to
contribute routes, UI slots, search providers, layout context, session
lifecycle, and permissions. Modules are activated via config/modules.php or
APP_ENABLED_MODULES env variable. Conflicts (duplicate routes, permissions,
slot keys) cause a fail-fast with a clear error message.
Extract the address book from hardcoded Core integration points into the
first module (modules/addressbook/). The module provides:
- Aside icon-bar tab + People panel via UI slot system
- Global search resource via AddressBookSearchProvider
- Layout context data via AddressBookLayoutProvider
- Session lifecycle via AddressBookSessionProvider
Core cleanup removes address-book hardcodings from SearchSqlResourceProvider,
SearchUiMetaProvider, SearchItemMapperProvider, appBuildLayoutNavContext(),
and the aside templates. Permissions (ADDRESS_BOOK_VIEW) and business logic
(AddressBookService) remain in Core as they gate general user visibility.
Includes 38 new tests (894 total), PHPStan level 5 clean, and architecture
tests verifying zero hardcoded address-book references in search/templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:43:25 +01:00
|
|
|
)));
|
2026-03-19 08:23:14 +01:00
|
|
|
$accessControl = new AccessControl($allPublicPaths, app(IntendedUrlService::class));
|
2026-02-04 23:31:53 +01:00
|
|
|
$accessControl->requireAuthOrRedirect($pathWithoutLocale, !empty($_SESSION['user']['id']));
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 9. Code analysis (debug only) ──────────────────────────────────
|
|
|
|
|
|
2026-02-04 23:31:53 +01:00
|
|
|
if (Debugger::$enabled) {
|
|
|
|
|
Analyzer::execute();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 10. Action execution ────────────────────────────────────────────
|
|
|
|
|
|
2026-02-04 23:31:53 +01:00
|
|
|
ob_start();
|
|
|
|
|
if (Router::getTemplateAction()) {
|
|
|
|
|
require Router::getTemplateAction();
|
|
|
|
|
}
|
|
|
|
|
if (ob_get_contents()) {
|
|
|
|
|
ob_end_flush();
|
2026-03-13 18:56:19 +01:00
|
|
|
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanNot.alwaysFalse
|
2026-02-11 19:28:12 +01:00
|
|
|
trigger_error('MintyPHP template action"' . Router::getTemplateAction() . '" should not send output. Error raised ', E_USER_WARNING);
|
|
|
|
|
}
|
2026-02-04 23:31:53 +01:00
|
|
|
} else {
|
|
|
|
|
ob_end_clean();
|
|
|
|
|
}
|
2026-03-13 18:56:19 +01:00
|
|
|
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanAnd.rightAlwaysTrue
|
2026-02-11 19:28:12 +01:00
|
|
|
Session::end();
|
|
|
|
|
DB::close();
|
|
|
|
|
exit;
|
|
|
|
|
}
|
2026-02-04 23:31:53 +01:00
|
|
|
|
|
|
|
|
ob_start();
|
|
|
|
|
if (Router::getAction()) {
|
2026-03-18 22:41:59 +01:00
|
|
|
extract(Router::getParameters(), EXTR_SKIP);
|
2026-02-04 23:31:53 +01:00
|
|
|
require Router::getAction();
|
|
|
|
|
}
|
|
|
|
|
if (ob_get_contents()) {
|
|
|
|
|
ob_end_flush();
|
2026-03-13 18:56:19 +01:00
|
|
|
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanNot.alwaysFalse
|
2026-02-11 19:28:12 +01:00
|
|
|
trigger_error('MintyPHP action "' . Router::getAction() . '" should not send output. Error raised ', E_USER_WARNING);
|
|
|
|
|
}
|
2026-02-04 23:31:53 +01:00
|
|
|
} else {
|
|
|
|
|
ob_end_clean();
|
|
|
|
|
}
|
2026-03-13 18:56:19 +01:00
|
|
|
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanAnd.rightAlwaysTrue
|
2026-02-11 19:28:12 +01:00
|
|
|
Session::end();
|
|
|
|
|
DB::close();
|
|
|
|
|
exit;
|
|
|
|
|
}
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 11. Layout preparation (web only, before session/DB close) ──────
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
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);
|
2026-03-05 08:26:51 +01:00
|
|
|
$layoutAuth = $viewAuth['layout'];
|
2026-03-04 15:56:58 +01:00
|
|
|
}
|
2026-03-05 08:26:51 +01:00
|
|
|
$layoutNav = appBuildLayoutNavContext(
|
|
|
|
|
is_array($layoutAuth) ? $layoutAuth : [],
|
|
|
|
|
is_array($_SESSION ?? null) ? $_SESSION : [],
|
|
|
|
|
requestInput()->queryAll()
|
|
|
|
|
);
|
2026-03-04 15:56:58 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 12. Session & DB close ──────────────────────────────────────────
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
Session::end();
|
2026-02-04 23:31:53 +01:00
|
|
|
DB::close();
|
|
|
|
|
|
2026-03-18 22:41:59 +01:00
|
|
|
// ── 13. View rendering ──────────────────────────────────────────────
|
|
|
|
|
|
2026-02-04 23:31:53 +01:00
|
|
|
if (Router::getTemplateView()) {
|
|
|
|
|
Buffer::start('html');
|
|
|
|
|
if (Router::getView()) {
|
|
|
|
|
require Router::getView();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Debugger::$enabled) {
|
|
|
|
|
Debugger::toolbar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Buffer::end('html');
|
|
|
|
|
require Router::getTemplateView();
|
2026-03-18 22:41:59 +01:00
|
|
|
} else {
|
2026-02-04 23:31:53 +01:00
|
|
|
if (Router::getView()) {
|
|
|
|
|
require Router::getView();
|
|
|
|
|
}
|
|
|
|
|
}
|