mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 07:11:18 +02:00
- Add monolog/monolog dependency (^3.10)
- Create app/Logger.php central factory with channels: app, admin, error, audit
- Each channel gets RotatingFileHandler (30-day retention) with pass-through LineFormatter
preserving existing JSON format contracts
- Rewrite AppLogger as thin facade delegating to Logger::get('app')
- Rewrite ErrorHandler::log() to delegate to Logger::get('error')
- Rewrite AdminLogger file output to delegate to Logger::get('admin'), keep DB writes
- Add Monolog file shadow to Audit via Logger::get('audit') (Option A per monolog-plan)
- Log level controlled by LOG_LEVEL env var (defaults: DEBUG in cli-server, WARNING otherwise)
- Graceful NullHandler fallback when log directory is not writable
- Update SystemController LOG_FILES: remove php_error, add app/admin/error/audit
- JSON app logs parsed to readable one-liners in the log viewer
- Remove nginx config tab (parametres + fragment + template + css)
- Friendly empty-state message when app log files don't exist yet (notYet)
- PHP tail fallback when exec() unavailable
- All 228 PHPUnit tests pass, no call sites changed
184 lines
7.7 KiB
PHP
184 lines
7.7 KiB
PHP
<?php
|
|
|
|
/**
|
|
* ErrorHandler — shared error-normalisation and logging utilities.
|
|
*
|
|
* Provides:
|
|
* - userMessage($e) — turns raw exceptions into user-friendly French messages
|
|
* - log($context, $e, array $extra) — structured error_log with trace
|
|
*
|
|
* Usage from any action/controller:
|
|
* ErrorHandler::log('thesis_edit', $e, ['thesis_id' => $id]);
|
|
* App::flash('error', ErrorHandler::userMessage($e));
|
|
*/
|
|
class ErrorHandler
|
|
{
|
|
/**
|
|
* Map SQLite FK constraint errors to human-readable field names.
|
|
*
|
|
* SQLite FK errors reference the *child* table (where the FK column lives),
|
|
* e.g. "INSERT INTO theses" when orientation_id FK fails.
|
|
*
|
|
* We map child table → the form field(s) whose values populate its FK columns.
|
|
*/
|
|
private const FK_TABLE_MAP = [
|
|
// Main thesis table — FK columns: orientation_id, ap_program_id,
|
|
// finality_id, access_type_id, license_id
|
|
'theses' => 'Orientation, AP, Finalité, Type d\'accès ou Licence',
|
|
// Junction tables — each maps to one specific field
|
|
'thesis_authors' => 'Auteur·ice',
|
|
'thesis_supervisors' => 'Composition du jury',
|
|
'thesis_languages' => 'Langue(s)',
|
|
'thesis_formats' => 'Format(s)',
|
|
'thesis_tags' => 'Mots-clés',
|
|
'thesis_files' => 'Fichiers',
|
|
// Admin / system tables
|
|
'share_links' => 'Lien étudiant·e',
|
|
'file_access_requests' => "Demande d'accès",
|
|
'form_help_blocks' => "Bloc d'aide",
|
|
'site_settings' => 'Paramètres',
|
|
'admin_log' => 'Journal admin',
|
|
// Parent tables (when referenced directly in "table" pattern)
|
|
'orientations' => 'Orientation',
|
|
'ap_programs' => 'AP',
|
|
'finality_types' => 'Finalité',
|
|
'access_types' => "Type d'accès",
|
|
'license_types' => 'Licence',
|
|
'authors' => 'Auteur·ice',
|
|
'supervisors' => 'Composition du jury',
|
|
'languages' => 'Langue(s)',
|
|
'format_types' => 'Format(s)',
|
|
'tags' => 'Mots-clés',
|
|
];
|
|
|
|
/**
|
|
* Map an exception to a user-facing message.
|
|
*
|
|
* Domain exceptions (validation, duplicates) carry their own message;
|
|
* system exceptions (PDO, generic) get sanitised explanations with
|
|
* specific field identification where possible.
|
|
*/
|
|
public static function userMessage(\Throwable $e): string
|
|
{
|
|
// ── Domain exceptions with meaningful messages ──────────────────────
|
|
if ($e instanceof DuplicateThesisException) {
|
|
return $e->getMessage(); // Already user-friendly
|
|
}
|
|
|
|
// ── Database errors ─────────────────────────────────────────────────
|
|
if ($e instanceof \PDOException) {
|
|
return self::pdoMessage($e);
|
|
}
|
|
|
|
// ── Validation errors (Exception, RuntimeException) ──────────────
|
|
// These are thrown with user-friendly French messages — pass through.
|
|
if ($e instanceof \Exception) {
|
|
return $e->getMessage();
|
|
}
|
|
|
|
// ── Everything else — generic fallback ──────────────────────────────
|
|
return 'Une erreur inattendue est survenue.'
|
|
. ' Veuillez réessayer ou contacter l\'équipe.';
|
|
}
|
|
|
|
/**
|
|
* Build a user-facing message from a PDOException.
|
|
*
|
|
* For FK constraint failures, the SQLite error message includes the
|
|
* table name (e.g. "FOREIGN KEY constraint failed INSERT INTO thesis_formats").
|
|
* We extract it and map to a field label.
|
|
*/
|
|
private static function pdoMessage(\PDOException $e): string
|
|
{
|
|
$msg = $e->getMessage();
|
|
|
|
if (str_contains($msg, 'FOREIGN KEY constraint failed')) {
|
|
$field = self::extractFkField($msg);
|
|
|
|
if ($field !== null) {
|
|
return "Erreur de base de données : la référence pour « {$field} » est invalide."
|
|
. ' Vérifiez que la valeur existe dans la base de données.';
|
|
}
|
|
|
|
return 'Erreur de base de données : une contrainte de référence est invalide.'
|
|
. ' Vérifiez les champs Orientation, AP, Finalité, Langues, Formats.';
|
|
}
|
|
|
|
// ── UNIQUE constraint ───────────────────────────────────────────────
|
|
if (str_contains($msg, 'UNIQUE constraint failed')) {
|
|
return 'Erreur de base de données : une valeur en double a été détectée.'
|
|
. ' Veuillez réessayer ou contacter l\'équipe.';
|
|
}
|
|
|
|
// ── NOT NULL ────────────────────────────────────────────────────────
|
|
if (str_contains($msg, 'NOT NULL constraint failed')) {
|
|
return 'Erreur de base de données : un champ obligatoire est manquant.'
|
|
. ' Veuillez réessayer ou contacter l\'équipe.';
|
|
}
|
|
|
|
// ── Generic SQL error — don't leak raw SQL to users ────────────────
|
|
return 'Une erreur de base de données est survenue.'
|
|
. ' Veuillez réessayer ou contacter l\'équipe.';
|
|
}
|
|
|
|
/**
|
|
* Try to extract the referenced table name from a SQLite FK error message.
|
|
*
|
|
* SQLite FK errors typically contain the table name in the message body:
|
|
* "FOREIGN KEY constraint failed INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)"
|
|
*
|
|
* Also handles quoted table names in newer SQLite messages:
|
|
* "FOREIGN KEY constraint failed (table \"orientations\")"
|
|
*/
|
|
private static function extractFkField(string $msg): ?string
|
|
{
|
|
// Pattern 1: "table \"tablename\"" (SQLite 3.37+)
|
|
if (preg_match('/table\s+"([^"]+)"/i', $msg, $m)) {
|
|
$table = strtolower($m[1]);
|
|
return self::FK_TABLE_MAP[$table] ?? null;
|
|
}
|
|
|
|
// Pattern 2: "INSERT INTO tablename" or "UPDATE tablename"
|
|
if (preg_match('/(?:INSERT\s+INTO|UPDATE)\s+"?(\w+)"?/i', $msg, $m)) {
|
|
$table = strtolower($m[1]);
|
|
return self::FK_TABLE_MAP[$table] ?? null;
|
|
}
|
|
|
|
// Pattern 3: "REFERENCES tablename" — the constraint itself
|
|
if (preg_match('/REFERENCES\s+"?(\w+)"?/i', $msg, $m)) {
|
|
$table = strtolower($m[1]);
|
|
return self::FK_TABLE_MAP[$table] ?? null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Write a structured error log entry — delegates to Monolog 'error' channel.
|
|
*
|
|
* @param string $context e.g. 'thesis_edit', 'partage_submit', 'csv_import'
|
|
* @param \Throwable $e
|
|
* @param array $extra arbitrary key-value context to include in the log
|
|
*/
|
|
public static function log(string $context, \Throwable $e, array $extra = []): void
|
|
{
|
|
$entry = [
|
|
'timestamp' => date('c'),
|
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
'context' => $context,
|
|
'exception' => get_class($e),
|
|
'message' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
];
|
|
|
|
if (!empty($extra)) {
|
|
$entry['extra'] = $extra;
|
|
}
|
|
|
|
$line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
|
|
Logger::get('error')->error($line);
|
|
}
|
|
}
|