mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
Add src/App.php foundation class and flash-messages partial
Create the central App helper that eliminates ~170 lines of duplicated bootstrap/auth/CSRF preamble across 24 page and action handler files. src/App.php provides: - boot(): loads Database + ensures CSRF token (public pages) - adminGuard(): requires AdminAuth login + boot (admin pages) - verifyCsrf() / rotateCsrf(): centralised CSRF lifecycle - flash() / consumeFlash(): unified flash messages with legacy key drain (error, success, admin_error, admin_success, edit_error, edit_success, form_error all consumed transparently for incremental migration) - redirect(): flash + Location header + exit in one call - render(): head → header → content → footer pipeline with auto admin footer selection App.php is auto-loaded from config/bootstrap.php so all existing pages get the class for free without any changes. templates/partials/flash-messages.php uses App::consumeFlash() to replace the 5+ copy-pasted flash blocks across admin templates. All existing tests pass. No existing page files modified — this is a non-breaking addition that enables incremental controller extraction.
This commit is contained in:
15
TODO.md
15
TODO.md
@@ -1,6 +1,21 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
## In Progress
|
||||||
|
- [ ] Extract `SearchController` — most complex public page (§2 step 4)
|
||||||
|
- [ ] Extract `SystemController` — biggest single-file win, 500→8 lines (§2 step 3, §5)
|
||||||
|
- [ ] Extract `ThesisEditController` — merges edit.php + actions/edit.php, deduplicate jury fieldset (§2 step 5)
|
||||||
|
- [ ] Extract remaining controllers one by one (§2 step 6)
|
||||||
|
- [ ] Consolidate action handlers into controller methods (§4)
|
||||||
|
- [ ] Introduce pagination partial `templates/partials/pagination.php` (§6)
|
||||||
|
- [ ] Introduce admin form partials: select-field, checkbox-list, jury-fieldset (§6)
|
||||||
|
- [ ] Unify flash message keys project-wide to `_flash_error` / `_flash_success` (§7)
|
||||||
|
- [ ] Move OG tag construction into controller logic (§8)
|
||||||
|
- [ ] Extract inline CSS/JS from `system.php` into separate assets (§5)
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
- [x] Create `src/App.php` — boot, adminGuard, verifyCsrf, rotateCsrf, redirect, flash, consumeFlash, render
|
||||||
|
- [x] Auto-load `App.php` from `config/bootstrap.php`
|
||||||
|
- [x] Create `templates/partials/flash-messages.php` — unified flash partial with legacy key drain
|
||||||
- [x] Merge public and admin head/nav templates into unified `templates/head.php` and `templates/header.php`
|
- [x] Merge public and admin head/nav templates into unified `templates/head.php` and `templates/header.php`
|
||||||
- `templates/head.php` — outputs `<!DOCTYPE html>…</head><body class="…">`, reads `$bodyClass`, `$isAdmin`; handles admin title suffix, admin.css prepend, and OG tag suppression internally
|
- `templates/head.php` — outputs `<!DOCTYPE html>…</head><body class="…">`, reads `$bodyClass`, `$isAdmin`; handles admin title suffix, admin.css prepend, and OG tag suppression internally
|
||||||
- `templates/header.php` — outputs `<header>…</header>` with public nav + search bar or admin nav depending on `$isAdmin`
|
- `templates/header.php` — outputs `<header>…</header>` with public nav + search bar or admin nav depending on `$isAdmin`
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ if (file_exists(APP_ROOT . '/config/admin_credentials.php')) {
|
|||||||
require_once APP_ROOT . '/config/admin_credentials.php';
|
require_once APP_ROOT . '/config/admin_credentials.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Central application helper (boot, auth guard, CSRF, flash, render)
|
||||||
|
require_once APP_ROOT . '/src/App.php';
|
||||||
|
|
||||||
// Maintenance mode gate — block public pages; allow /admin/ through.
|
// Maintenance mode gate — block public pages; allow /admin/ through.
|
||||||
// The flag file lives in storage/ (outside webroot) to avoid web exposure.
|
// The flag file lives in storage/ (outside webroot) to avoid web exposure.
|
||||||
define('MAINTENANCE_FLAG', APP_ROOT . '/storage/maintenance.flag');
|
define('MAINTENANCE_FLAG', APP_ROOT . '/storage/maintenance.flag');
|
||||||
|
|||||||
168
src/App.php
Normal file
168
src/App.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Thin application helper — centralises bootstrap, auth gating, CSRF lifecycle,
|
||||||
|
* flash messages, redirect, and template rendering.
|
||||||
|
*
|
||||||
|
* Eliminates the ~6-8 lines of identical preamble repeated across every page
|
||||||
|
* and action handler. See REFACTORING_RECOMMENDATIONS.md §1 + §3.
|
||||||
|
*/
|
||||||
|
class App
|
||||||
|
{
|
||||||
|
private static bool $booted = false;
|
||||||
|
|
||||||
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot once per request: load Database, ensure CSRF token exists.
|
||||||
|
* Suitable for public pages (no auth required).
|
||||||
|
*/
|
||||||
|
public static function boot(): Database
|
||||||
|
{
|
||||||
|
if (!self::$booted) {
|
||||||
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
|
self::$booted = true;
|
||||||
|
}
|
||||||
|
self::ensureCsrf();
|
||||||
|
return Database::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gate for admin pages: require auth + CSRF token + load Database.
|
||||||
|
*/
|
||||||
|
public static function adminGuard(): Database
|
||||||
|
{
|
||||||
|
require_once APP_ROOT . '/src/AdminAuth.php';
|
||||||
|
AdminAuth::requireLogin();
|
||||||
|
return self::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CSRF lifecycle ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a CSRF token exists in the session.
|
||||||
|
* Called automatically by boot() / adminGuard().
|
||||||
|
*/
|
||||||
|
private static function ensureCsrf(): void
|
||||||
|
{
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the CSRF token on a POST request.
|
||||||
|
* Halts with 403 if the token is missing or invalid.
|
||||||
|
*/
|
||||||
|
public static function verifyCsrf(): void
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||||
|
|| !isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||||
|
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('CSRF token invalide.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate the CSRF token after a successful mutation.
|
||||||
|
*/
|
||||||
|
public static function rotateCsrf(): void
|
||||||
|
{
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Flash messages ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a flash message in the session.
|
||||||
|
*
|
||||||
|
* @param 'success'|'error' $type
|
||||||
|
*/
|
||||||
|
public static function flash(string $type, string $message): void
|
||||||
|
{
|
||||||
|
$_SESSION["_flash_{$type}"] = $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume and return flash messages, then clear them from the session.
|
||||||
|
*
|
||||||
|
* Also drains the legacy per-page keys (error, success, admin_error,
|
||||||
|
* admin_success, edit_error, edit_success, form_error) so the transition
|
||||||
|
* to unified keys can happen incrementally.
|
||||||
|
*
|
||||||
|
* @return array{error: ?string, success: ?string}
|
||||||
|
*/
|
||||||
|
public static function consumeFlash(): array
|
||||||
|
{
|
||||||
|
// Unified keys first, fall back to any legacy key that is set.
|
||||||
|
$error = $_SESSION['_flash_error']
|
||||||
|
?? $_SESSION['error']
|
||||||
|
?? $_SESSION['admin_error']
|
||||||
|
?? $_SESSION['edit_error']
|
||||||
|
?? $_SESSION['form_error']
|
||||||
|
?? null;
|
||||||
|
|
||||||
|
$success = $_SESSION['_flash_success']
|
||||||
|
?? $_SESSION['success']
|
||||||
|
?? $_SESSION['admin_success']
|
||||||
|
?? $_SESSION['edit_success']
|
||||||
|
?? null;
|
||||||
|
|
||||||
|
// Clear all variants.
|
||||||
|
unset(
|
||||||
|
$_SESSION['_flash_error'], $_SESSION['_flash_success'],
|
||||||
|
$_SESSION['error'], $_SESSION['success'],
|
||||||
|
$_SESSION['admin_error'], $_SESSION['admin_success'],
|
||||||
|
$_SESSION['edit_error'], $_SESSION['edit_success'],
|
||||||
|
$_SESSION['form_error']
|
||||||
|
);
|
||||||
|
|
||||||
|
return ['error' => $error, 'success' => $success];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Redirect ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flash a message and redirect. Terminates the script.
|
||||||
|
*/
|
||||||
|
public static function redirect(string $url, ?string $success = null, ?string $error = null): never
|
||||||
|
{
|
||||||
|
if ($success !== null) {
|
||||||
|
self::flash('success', $success);
|
||||||
|
}
|
||||||
|
if ($error !== null) {
|
||||||
|
self::flash('error', $error);
|
||||||
|
}
|
||||||
|
header('Location: ' . $url);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Template rendering ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a full page: head → header → content template → footer.
|
||||||
|
*
|
||||||
|
* Expects $vars to contain the same keys the templates already rely on
|
||||||
|
* ($pageTitle, $bodyClass, $isAdmin, $extraCss, $ogTags, etc.).
|
||||||
|
* The footer variant (public vs admin) is chosen automatically based
|
||||||
|
* on $isAdmin.
|
||||||
|
*
|
||||||
|
* @param string $template Path relative to APP_ROOT/templates/
|
||||||
|
* @param array $vars Variables to expose inside the templates
|
||||||
|
*/
|
||||||
|
public static function render(string $template, array $vars = []): void
|
||||||
|
{
|
||||||
|
// Make all vars available in the template scope.
|
||||||
|
extract($vars);
|
||||||
|
|
||||||
|
include APP_ROOT . '/templates/head.php';
|
||||||
|
include APP_ROOT . '/templates/header.php';
|
||||||
|
include APP_ROOT . '/templates/' . $template;
|
||||||
|
|
||||||
|
if (!empty($isAdmin)) {
|
||||||
|
include APP_ROOT . '/templates/admin/footer.php';
|
||||||
|
} else {
|
||||||
|
include APP_ROOT . '/templates/footer.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
[1774721459]
|
[1775039085]
|
||||||
18
templates/partials/flash-messages.php
Normal file
18
templates/partials/flash-messages.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Shared flash-message partial.
|
||||||
|
*
|
||||||
|
* Consumes all flash variants (unified _flash_* keys and legacy per-page keys)
|
||||||
|
* via App::consumeFlash(), then renders the standard alert markup.
|
||||||
|
*
|
||||||
|
* Usage: include this partial wherever flash messages should appear.
|
||||||
|
* No variables need to be set beforehand — it reads from the session directly.
|
||||||
|
*/
|
||||||
|
$_flash = App::consumeFlash();
|
||||||
|
?>
|
||||||
|
<?php if ($_flash['error']): ?>
|
||||||
|
<div class="admin-alert admin-alert--error">⚠ <?= htmlspecialchars($_flash['error']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($_flash['success']): ?>
|
||||||
|
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($_flash['success']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
Reference in New Issue
Block a user