V7.10 — Tableau de bord, guide, semaine type et enveloppe de formation
Reprise dans le répertoire de production du travail mené dans le worktree ux-service-enfance : - tableau de bord Service Enfance (dashboard.php) et navigation regroupée ; - guide d'utilisation (faq.php) accessible depuis toute la navigation ; - éditeur de semaine type mis en avant sur l'écran de saisie ; - droit à l'enveloppe de formation par agent, en remplacement de la case « formation déjà prévue dans la répartition annuelle » ; - correctif de « [object Object] » sur les cartes de quota du PTA ; - motif de travail par défaut à la saisie, heures travaillées complètes dans les PDF. Le docker-compose.yml de production est conservé : celui du worktree décrit une pile de test (conteneurs et volume suffixés « test », ports 9080/9081), qui aurait fait redémarrer l'application sur un volume MariaDB vide. Les migrations database/migration_v7_9*.sql sont obligatoires ; le service db-migrate les applique au déploiement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
89
app/Core/Security.php
Normal file
89
app/Core/Security.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* En-têtes de sécurité des réponses HTML.
|
||||
*
|
||||
* La CSP est nominative : aucun script inline n'est autorisé sans le nonce
|
||||
* généré à chaque requête. PTA ne contient aucun gestionnaire d'événement
|
||||
* inline (onclick=, onchange=…) ni style inline, la politique peut donc rester
|
||||
* stricte sans dégrader l'interface.
|
||||
*/
|
||||
final class Security
|
||||
{
|
||||
private static ?string $nonce = null;
|
||||
|
||||
public static function nonce(): string
|
||||
{
|
||||
if (self::$nonce === null) {
|
||||
self::$nonce = base64_encode(random_bytes(16));
|
||||
}
|
||||
|
||||
return self::$nonce;
|
||||
}
|
||||
|
||||
public static function sendHtmlHeaders(): void
|
||||
{
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nonce = self::nonce();
|
||||
$policy = [
|
||||
"default-src 'self'",
|
||||
"base-uri 'self'",
|
||||
"object-src 'none'",
|
||||
"frame-ancestors 'none'",
|
||||
"form-action 'self'",
|
||||
"img-src 'self' data:",
|
||||
"style-src 'self'",
|
||||
"font-src 'self'",
|
||||
// Aucune requête sortante depuis le navigateur : les appels à
|
||||
// data.education.gouv.fr sont faits côté serveur.
|
||||
"connect-src 'self'",
|
||||
sprintf("script-src 'self' 'nonce-%s'", $nonce),
|
||||
];
|
||||
|
||||
header('Content-Security-Policy: ' . implode('; ', $policy));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()');
|
||||
header('Cross-Origin-Opener-Policy: same-origin');
|
||||
|
||||
if (Session::isHttps()) {
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrai uniquement si le mode debug a été activé explicitement.
|
||||
* Par défaut, aucun détail technique ne sort du serveur.
|
||||
*/
|
||||
public static function debugEnabled(): bool
|
||||
{
|
||||
return in_array(strtolower((string) (getenv('PTA_DEBUG') ?: '0')), ['1', 'true', 'on', 'yes'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Journalise l'exception côté serveur et renvoie le message à afficher.
|
||||
* Le détail technique n'est rendu que si PTA_DEBUG est explicitement actif.
|
||||
*/
|
||||
public static function reportThrowable(\Throwable $exception, string $publicMessage): string
|
||||
{
|
||||
error_log(sprintf(
|
||||
'[PTA] %s: %s in %s:%d',
|
||||
get_class($exception),
|
||||
$exception->getMessage(),
|
||||
$exception->getFile(),
|
||||
$exception->getLine()
|
||||
));
|
||||
|
||||
return self::debugEnabled()
|
||||
? $publicMessage . ' Détail : ' . $exception->getMessage()
|
||||
: $publicMessage;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user