logFile = $dir . '/admin.log'; } else { $this->logFile = '/var/log/xamxam.log'; } $this->db = $db; } // ── Convenience factory ─────────────────────────────────────────────────── public static function make(): self { require_once APP_ROOT . '/src/Database.php'; return new self(new Database()); } // ── High-level log methods ──────────────────────────────────────────────── /** TFE list: CSV export */ public function logCsvExport(): void { $this->write('thesis', 'csv_export', 'success'); } /** TFE list: CSV import */ public function logCsvImport(int $imported, int $skipped): void { $this->write('thesis', 'csv_import', 'success', [ 'imported' => $imported, 'skipped' => $skipped, ]); } /** TFE: publish / unpublish (single or bulk) */ public function logPublish(bool $published, array $thesisIds): void { $this->write('thesis', $published ? 'publish' : 'unpublish', 'success', [ 'count' => count($thesisIds), 'ids' => $thesisIds, ]); } /** TFE: visibility change */ public function logVisibility(?int $accessTypeId, array $thesisIds): void { $this->write('thesis', 'set_visibility', 'success', [ 'access_type_id' => $accessTypeId, 'count' => count($thesisIds), 'ids' => $thesisIds, ]); } /** TFE: edit */ public function logEdit(int $thesisId, string $title = ''): void { $this->write('thesis', 'edit', 'success', [ 'thesis_id' => $thesisId, 'title' => $title, ]); } /** TFE: add (new form submission from admin) */ public function logAdd(int $thesisId, string $identifier, string $author): void { $this->write('thesis', 'add', 'success', [ 'thesis_id' => $thesisId, 'identifier' => $identifier, 'author' => $author, ]); } /** TFE: delete (single or bulk) */ public function logDelete(array $thesisIds, bool $deleteAll = false): void { $this->write('thesis', 'delete', 'success', [ 'delete_all' => $deleteAll, 'count' => count($thesisIds), 'ids' => $thesisIds, ]); } /** Tags: rename, merge, delete */ public function logTagAction(string $action, array $context = []): void { $this->write('tag', $action, 'success', $context); } /** Static pages / contenus */ public function logPageEdit(string $slug): void { $this->write('page', 'edit', 'success', ['slug' => $slug]); } /** À propos content */ public function logAproposEdit(string $key): void { $this->write('apropos', 'edit', 'success', ['key' => $key]); } /** Form structure (formulaire section in contenus) */ public function logFormStructureEdit(string $section): void { $this->write('form_structure', 'edit', 'success', ['section' => $section]); } /** Accès étudiant·e links: create */ public function logLinkCreate(string $slug, bool $hasPassword, ?string $expiresAt, ?string $objetRestriction): void { $this->write('share_link', 'create', 'success', [ 'slug' => $slug, 'has_password' => $hasPassword, 'expires_at' => $expiresAt, 'objet_restriction' => $objetRestriction, ]); } /** Accès étudiant·e links: toggle active/inactive */ public function logLinkToggle(int $id, bool $nowActive): void { $this->write('share_link', $nowActive ? 'activate' : 'deactivate', 'success', [ 'link_id' => $id, ]); } /** Accès étudiant·e links: password change */ public function logLinkPasswordChange(int $id, bool $removed): void { $this->write('share_link', 'set_password', 'success', [ 'link_id' => $id, 'removed' => $removed, ]); } /** Accès étudiant·e links: archive (replaces delete) */ public function logLinkArchive(int $id): void { $this->write('share_link', 'archive', 'success', ['link_id' => $id]); } /** File access requests: approve / reject */ public function logAccessRequest(int $requestId, string $action, string $email, string $thesisTitle): void { $this->write('file_access_request', $action, 'success', [ 'request_id' => $requestId, 'email' => $email, 'thesis_title' => $thesisTitle, ]); } /** Parametres: maintenance toggle */ public function logMaintenance(bool $enabled): void { $this->write('system', $enabled ? 'maintenance_on' : 'maintenance_off', 'success'); } /** Parametres: DB export */ public function logDbExport(): void { $this->write('system', 'db_export', 'success'); } /** Files export (ZIP with all thesis files + manifest) */ public function logFilesExport(int $fileCount, int $byteSize): void { $this->write('system', 'files_export', 'success', [ 'file_count' => $fileCount, 'byte_size' => $byteSize, ]); } /** Parametres: formulaire section toggles */ public function logFormSettingsUpdate(array $newValues): void { $this->write('settings', 'formulaire_update', 'success', ['values' => $newValues]); } /** Parametres: objet types toggles */ public function logObjetTypesUpdate(array $newValues): void { $this->write('settings', 'objet_types_update', 'success', ['values' => $newValues]); } /** Parametres: access restriction settings */ public function logAccessRestrictionUpdate(array $newValues): void { $this->write('settings', 'access_restriction_update', 'success', ['values' => $newValues]); } /** Parametres: SMTP credentials update */ public function logSmtpUpdate(bool $connectionOk): void { $this->write('settings', 'smtp_update', 'success', ['connection_ok' => $connectionOk]); } /** Parametres: PeerTube settings update */ public function logPeerTubeUpdate(bool $enabled): void { $this->write('settings', 'peertube_update', 'success', ['enabled' => $enabled]); } /** Parametres: SMTP test */ public function logSmtpTest(string $toEmail, bool $success, string $error = ''): void { $this->write('settings', 'smtp_test', $success ? 'success' : 'error', [ 'to' => $toEmail, 'error' => $error ?: null, ]); } /** Generic error entry (for catch blocks) */ public function logError(string $resource, string $action, string $message, array $context = []): void { $this->write($resource, $action, 'error', array_merge($context, ['error' => $message])); } // ── Core write ──────────────────────────────────────────────────────────── private function write(string $resource, string $action, string $status, array $context = []): void { $entry = [ 'timestamp' => date('c'), 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'resource' => $resource, 'action' => $action, 'status' => $status, ]; if (!empty($context)) { $entry['context'] = $context; } $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; if (is_writable($this->logFile) || (!file_exists($this->logFile) && is_writable(dirname($this->logFile)))) { error_log($line, 3, $this->logFile); } if ($this->db !== null) { $this->insertDb($resource, $action, $status, $context); } } private function insertDb(string $resource, string $action, string $status, array $context): void { try { $pdo = $this->db->getConnection(); $stmt = $pdo->prepare( 'INSERT INTO admin_audit_log (ip, user_agent, resource, action, status, context) VALUES (?, ?, ?, ?, ?, ?)' ); $stmt->execute([ $_SERVER['REMOTE_ADDR'] ?? 'cli', $_SERVER['HTTP_USER_AGENT'] ?? '', $resource, $action, $status, !empty($context) ? json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null, ]); } catch (\Throwable $e) { // DB logging is best-effort — never crash the app over it. error_log('[AdminLogger] DB insert failed: ' . $e->getMessage()); } } }