Commit cbd6fc0a authored by Administrator's avatar Administrator

Update 5 files via Son of Anton

parent 035fbfe8
...@@ -10,30 +10,40 @@ final class CSRF ...@@ -10,30 +10,40 @@ final class CSRF
public static function generate(): string public static function generate(): string
{ {
$token = bin2hex(random_bytes(32)); $token = bin2hex(random_bytes(32));
$session = App::getInstance()->session(); $_SESSION[self::$tokenKey] = $token;
$session->set(self::$tokenKey, $token);
return $token; return $token;
} }
public static function token(): string public static function token(): string
{ {
$session = App::getInstance()->session(); if (empty($_SESSION[self::$tokenKey])) {
$token = $session->get(self::$tokenKey); return self::generate();
if ($token === null) {
$token = self::generate();
} }
return $token; return $_SESSION[self::$tokenKey];
} }
public static function validate(string $token): bool public static function validate(?string $token): bool
{ {
$session = App::getInstance()->session(); if ($token === null || $token === '') {
$stored = $session->get(self::$tokenKey, ''); return false;
return hash_equals($stored, $token); }
$sessionToken = $_SESSION[self::$tokenKey] ?? '';
if ($sessionToken === '') {
return false;
}
return hash_equals($sessionToken, $token);
} }
public static function field(): string public static function field(): string
{ {
return '<input type="hidden" name="_csrf_token" value="' . self::token() . '">'; return '<input type="hidden" name="_csrf_token" value="' . htmlspecialchars(self::token(), ENT_QUOTES, 'UTF-8') . '">';
}
// Regenerate token after successful validation (prevents reuse)
public static function regenerate(): void
{
self::generate();
} }
} }
\ No newline at end of file
...@@ -5,45 +5,55 @@ namespace App\Core; ...@@ -5,45 +5,55 @@ namespace App\Core;
final class Session final class Session
{ {
private int $lifetime; private bool $started = false;
public function __construct(int $lifetimeMinutes = 30) public function __construct()
{ {
$this->lifetime = $lifetimeMinutes * 60; $this->start();
} }
public function start(): void private function start(): void
{ {
if (session_status() === PHP_SESSION_ACTIVE) { if ($this->started || session_status() === PHP_SESSION_ACTIVE) {
$this->started = true;
return; return;
} }
if (php_sapi_name() === 'cli') { // Detect if behind HTTPS proxy (CapRover/nginx)
return; $secure = false;
if (
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
(!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') ||
(isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443)
) {
$secure = true;
} }
ini_set('session.gc_maxlifetime', (string) $this->lifetime); $savePath = dirname(__DIR__, 2) . '/storage/sessions';
ini_set('session.cookie_httponly', '1'); if (!is_dir($savePath)) {
ini_set('session.use_strict_mode', '1'); mkdir($savePath, 0775, true);
$storagePath = Autoloader::basePath() . '/storage/sessions';
if (is_dir($storagePath) && is_writable($storagePath)) {
ini_set('session.save_path', $storagePath);
} }
ini_set('session.save_handler', 'files');
ini_set('session.save_path', $savePath);
ini_set('session.gc_maxlifetime', '7200');
ini_set('session.cookie_lifetime', '0');
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_name('club_erp_session');
session_start(); session_start();
$this->started = true;
$this->processFlash();
}
private function processFlash(): void
{
$previousFlash = $_SESSION['_flash_keys'] ?? [];
foreach ($previousFlash as $key) {
unset($_SESSION[$key]);
}
$_SESSION['_flash_keys'] = $_SESSION['_next_flash_keys'] ?? [];
$_SESSION['_next_flash_keys'] = [];
} }
public function get(string $key, $default = null) public function get(string $key, $default = null)
...@@ -68,29 +78,26 @@ final class Session ...@@ -68,29 +78,26 @@ final class Session
public function flash(string $key, $value): void public function flash(string $key, $value): void
{ {
$_SESSION[$key] = $value; $_SESSION['_flash'][$key] = $value;
$flashKeys = $_SESSION['_next_flash_keys'] ?? [];
if (!in_array($key, $flashKeys)) {
$flashKeys[] = $key;
}
$_SESSION['_next_flash_keys'] = $flashKeys;
} }
public function getFlash(string $key, $default = null) public function getFlash(string $key, $default = null)
{ {
$value = $_SESSION[$key] ?? $default; $value = $_SESSION['_flash'][$key] ?? $default;
unset($_SESSION['_flash'][$key]);
return $value; return $value;
} }
public function getAlerts(): array public function getAlerts(): array
{ {
$alerts = $this->get('_alerts', []); $alerts = $_SESSION['_flash']['_alerts'] ?? [];
unset($_SESSION['_flash']['_alerts']);
return $alerts; return $alerts;
} }
public function regenerate(): void public function regenerate(): void
{ {
if (php_sapi_name() !== 'cli') { if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true); session_regenerate_id(true);
} }
} }
...@@ -98,16 +105,25 @@ final class Session ...@@ -98,16 +105,25 @@ final class Session
public function destroy(): void public function destroy(): void
{ {
$_SESSION = []; $_SESSION = [];
if (ini_get('session.use_cookies')) { if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params(); $params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, setcookie(
$params['path'], $params['domain'], session_name(),
$params['secure'], $params['httponly'] '',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
); );
} }
if (php_sapi_name() !== 'cli') {
if (session_status() === PHP_SESSION_ACTIVE) {
session_destroy(); session_destroy();
} }
$this->started = false;
} }
public function id(): string public function id(): string
......
...@@ -8,27 +8,55 @@ use App\Core\Request; ...@@ -8,27 +8,55 @@ use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Core\CSRF; use App\Core\CSRF;
final class CSRFMiddleware implements MiddlewareInterface class CSRFMiddleware implements MiddlewareInterface
{ {
public function handle(Request $request, callable $next): Response public function handle(Request $request, callable $next): Response
{ {
if (in_array($request->method(), ['POST', 'PUT', 'PATCH', 'DELETE'])) { // Only validate on state-changing methods
$token = $request->post('_csrf_token') ?? $request->header('X-CSRF-TOKEN'); $method = strtoupper($request->method());
if ($token === null || !CSRF::validate($token)) { if (in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
if ($request->isAjax()) { return $next($request);
return (new Response())->json(['error' => 'CSRF token mismatch'], 419); }
}
return (new Response())->html( // Skip CSRF for API routes that use token auth
'<html dir="rtl" lang="ar"><head><meta charset="utf-8"><title>خطأ أمني</title>' $path = $request->path();
. '<style>body{font-family:Cairo,sans-serif;text-align:center;padding:60px;}' if (str_starts_with($path, '/api/') && $request->bearerToken()) {
. 'h1{color:#DC2626;}</style></head>' return $next($request);
. '<body><h1>419</h1><p>انتهت صلاحية الجلسة. يرجى إعادة تحميل الصفحة.</p>' }
. '<a href="javascript:location.reload()">إعادة تحميل</a></body></html>',
419 // Get token from POST data or header
); $token = $request->post('_csrf_token', '')
?: $request->header('X-CSRF-TOKEN')
?: '';
if (!CSRF::validate($token)) {
// If AJAX request, return JSON error
if ($request->isAjax() || $request->isJson()) {
$response = new Response();
return $response->json([
'error' => true,
'message' => 'انتهت صلاحية الجلسة. يرجى إعادة تحميل الصفحة.',
], 419);
} }
// For regular form submissions, show error page
http_response_code(419);
echo '<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><title>419</title>'
. '<style>body{font-family:Cairo,Arial,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#F3F4F6;}'
. '.box{text-align:center;padding:40px;}'
. '.box h1{color:#DC2626;font-size:60px;margin:0;}'
. '.box p{color:#4B5563;font-size:18px;margin:15px 0;}'
. '.box a{color:#0D7377;font-size:16px;}</style></head>'
. '<body><div class="box"><h1>419</h1>'
. '<p>انتهت صلاحية الجلسة. يرجى إعادة تحميل الصفحة.</p>'
. '<a href="' . htmlspecialchars($path) . '">إعادة تحميل</a>'
. '</div></body></html>';
exit;
} }
// Token valid — regenerate for next request
CSRF::regenerate();
return $next($request); return $next($request);
} }
} }
\ No newline at end of file
<?php
use App\Core\CSRF;
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ar" dir="rtl"> <html lang="ar" dir="rtl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="<?= \App\Core\CSRF::token() ?>"> <meta name="csrf-token" content="<?= e(CSRF::token()) ?>">
<title><?= $__template->yield('title', 'تسجيل الدخول — نادي النادي شيراتون') ?></title> <title><?= $__template->yield('title', 'تسجيل الدخول') ?> — نادي النادي شيراتون</title>
<link rel="stylesheet" href="<?= url('assets/css/main.css') ?>"> <link rel="stylesheet" href="<?= url('assets/css/main.css') ?>">
<style> <style>
body.auth-body { background: linear-gradient(135deg, #0D7377 0%, #095355 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; padding: 20px; } body {
.auth-card { background: #fff; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); padding: 40px; width: 100%; max-width: 420px; } display: flex;
.auth-logo { text-align: center; margin-bottom: 30px; } justify-content: center;
.auth-logo h1 { color: #0D7377; font-size: 24px; margin: 0; } align-items: center;
.auth-logo p { color: #6B7280; font-size: 14px; margin-top: 5px; } min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1A1A2E 0%, #0D7377 100%);
font-family: 'Cairo', 'Segoe UI', Tahoma, Arial, sans-serif;
direction: rtl;
}
.auth-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 40px;
width: 100%;
max-width: 420px;
}
.auth-header {
text-align: center;
margin-bottom: 30px;
}
.auth-header h1 {
color: #0D7377;
font-size: 24px;
margin: 0 0 5px;
}
.auth-header p {
color: #6B7280;
font-size: 14px;
margin: 0;
}
</style> </style>
</head> </head>
<body class="auth-body"> <body>
<div class="auth-card"> <div class="auth-card">
<div class="auth-logo"> <div class="auth-header">
<h1>THE CLUB</h1> <h1>نادي النادي شيراتون</h1>
<p>نادي النادي شيراتون</p> <p>THE CLUB Sheraton</p>
</div> </div>
<?php <?php
$alerts = $app->session()->getAlerts(); $session = \App\Core\App::getInstance()->session();
$alerts = $session->getAlerts();
if (!empty($alerts)): if (!empty($alerts)):
foreach ($alerts as $alert): ?> foreach ($alerts as $alert):
<div class="alert alert-<?= e($alert['type'] ?? 'info') ?>" style="margin-bottom:15px;padding:10px 15px;border-radius:6px;font-size:14px;<?= ($alert['type'] ?? '') === 'error' ? 'background:#FEF2F2;color:#DC2626;border:1px solid #FECACA;' : 'background:#F0FDF4;color:#059669;border:1px solid #BBF7D0;' ?>"> ?>
<?= e($alert['message'] ?? '') ?> <div style="padding:10px 15px;border-radius:6px;margin-bottom:15px;font-size:13px;
</div> background:<?= ($alert['type'] ?? '') === 'error' ? '#FEF2F2' : '#F0FDF4' ?>;
<?php endforeach; color:<?= ($alert['type'] ?? '') === 'error' ? '#DC2626' : '#059669' ?>;
endif; ?> border:1px solid <?= ($alert['type'] ?? '') === 'error' ? '#FECACA' : '#BBF7D0' ?>;">
<?= e($alert['message'] ?? '') ?>
</div>
<?php
endforeach;
endif;
?>
<?= $__template->yield('content', '') ?> <?= $__template->yield('content', '') ?>
</div> </div>
<script src="<?= url('assets/js/app.js') ?>"></script>
</body> </body>
</html> </html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment