mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Mirrors the mots-clé tag-search system: dropdown suggestions from existing languages via HTMX, pill display with bin-icon remove buttons, 'Créer' option for new languages. Replaces the plain text input. - New partial: templates/partials/form/language-search.php - New fragment: public/partage/language-search-fragment.php - Admin wrapper: public/admin/language-search-fragment.php - Updated language-autre-fragment to return just the required asterisk indicator - Updated both controllers to handle language_autre as array (pill-based) with backward-compatible string path - Updated edit form to compute selectedOtherLanguages from DB - Registered new route in partage/index.php - Fix CSV importer: split comma-separated language column into individual entries - Add htmx active search to admin index, title line-clamp, predefined languages only in checkboxes - Admin index: filter form now uses htmx triggers (input delay:300ms on search, change on selects) to actively search without page reload - Sort links include hx-push-url for back-button support - Added loading indicator bar (.admin-search-indicator) - Title column: line-clamp at 2 lines with overflow hidden, native title attr tooltip for full text - Language checkboxes now show only 3 predefined languages (Français, Anglais, Néerlandais); all others go via the Autre langue search component - Added Database::getPredefinedLanguages() and excluded predefined from language-search-fragment suggestions - Included hidden sort/dir inputs in table-wrap so sort state preserved across filter changes - Fix language-search: block 'Créer' for predefined languages in dropdown The 'Créer' option in the language-search dropdown now also checks against the predefined set (français, anglais, néerlandais) to avoid offering creation of languages that already exist as checkboxes.
291 lines
9.8 KiB
PHP
291 lines
9.8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Admin audit logger.
|
|
*
|
|
* Writes JSON-lines to /var/log/xamxam.log (production) or
|
|
* storage/logs/admin.log (dev / cli-server).
|
|
*
|
|
* Each entry: timestamp, actor (admin IP/UA), action, resource, status, context.
|
|
*
|
|
* DB mirroring: if the admin_audit_log table exists, every entry is also
|
|
* inserted there — giving the admin panel instant queryable history without
|
|
* depending on filesystem log parsing.
|
|
*/
|
|
class AdminLogger
|
|
{
|
|
private string $logFile;
|
|
private ?Database $db;
|
|
|
|
public function __construct(?Database $db = null)
|
|
{
|
|
if (php_sapi_name() === 'cli-server') {
|
|
$dir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : __DIR__ . '/../storage/logs';
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
$this->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());
|
|
}
|
|
}
|
|
}
|