Fix dialog margins, add admin-dialog__body/styles, give trash page horizontal margins

This commit is contained in:
Pontoporeia
2026-05-19 18:08:00 +02:00
parent d619d2f116
commit 2cb8d71fe9
29 changed files with 608 additions and 11 deletions

View File

@@ -0,0 +1,194 @@
<?php
/**
* On-demand temp file cleanup (admin).
*
* POST /admin/actions/cleanup-tmp.php
*
* Smart cleanup: detects truly orphaned files by checking:
* - filepond dirs: whose PHP session no longer exists (abandoned upload)
* - _trash files: whose DB thesis_files row is gone (thesis already hard-deleted)
*
* Falls back to time-based (>2h filepond, >30d trash) as safety net.
* Returns JSON with counts of removed items.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|| !isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'CSRF invalide.']);
exit;
}
require_once __DIR__ . '/../../../src/Database.php';
$storageRoot = STORAGE_ROOT;
$filepondDir = $storageRoot . '/tmp/filepond';
$trashDir = $storageRoot . '/tmp/_trash';
$maxAgeFilepond = 7200; // 2 hours (fallback)
$maxAgeTrash = 2592000; // 30 days (fallback)
$now = time();
$filepondRemoved = 0;
$trashRemoved = 0;
$errors = [];
$details = [];
$db = new Database();
$pdo = $db->getPDO();
// ── Determine PHP session save path ──────────────────────────────────────
$sessionSavePath = session_save_path();
if (!$sessionSavePath || $sessionSavePath === '') {
$sessionSavePath = sys_get_temp_dir();
}
// ── Cleanup stale filepond dirs ──────────────────────────────────────────
if (is_dir($filepondDir)) {
$items = @scandir($filepondDir);
if ($items === false) {
$errors[] = 'Impossible de lire tmp/filepond/.';
} else {
foreach ($items as $item) {
if ($item === '.' || $item === '..' || $item === '.gitkeep') {
continue;
}
$dirPath = $filepondDir . '/' . $item;
if (!is_dir($dirPath)) {
continue;
}
$shouldDelete = false;
$reason = '';
$manifestPath = $dirPath . '/manifest.json';
$mtime = filemtime($dirPath);
$ageMinutes = (int)(($now - $mtime) / 60);
// Strategy 1: session-based (preferred)
if (file_exists($manifestPath)) {
$manifest = json_decode(file_get_contents($manifestPath), true);
if (is_array($manifest) && !empty($manifest['session_id'])) {
$sessionFile = $sessionSavePath . '/sess_' . $manifest['session_id'];
if (!file_exists($sessionFile)) {
$shouldDelete = true;
$reason = "session expirée (" . $manifest['session_id'] . ")";
}
}
}
// Strategy 2: time-based fallback (no manifest or session still exists but very old)
if (!$shouldDelete && $ageMinutes > ($maxAgeFilepond / 60)) {
$shouldDelete = true;
$reason = "plus de " . ($maxAgeFilepond / 3600) . "h";
}
if ($shouldDelete) {
rmdirRecursive($dirPath);
$filepondRemoved++;
$details[] = "filepond/$item: $reason";
}
}
}
}
// ── Cleanup orphaned _trash files ────────────────────────────────────────
if (is_dir($trashDir)) {
// Build a set of existing thesis_files IDs for fast lookup
$existingFileIds = [];
$stmt = $pdo->query('SELECT id FROM thesis_files');
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$existingFileIds[(int)$row['id']] = true;
}
// Also collect thesis_ids from theses table (a file might refer to a hard-deleted thesis)
// — but thesis_files rows are CASCADE-deleted when a thesis is hard-deleted, so if the
// thesis_files row is gone, the trash file is truly orphaned.
$items = @scandir($trashDir);
if ($items === false) {
$errors[] = 'Impossible de lire tmp/_trash/.';
} else {
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$filePath = $trashDir . '/' . $item;
if (!is_file($filePath)) {
continue;
}
$shouldDelete = false;
$reason = '';
$mtime = filemtime($filePath);
$ageDays = (int)(($now - $mtime) / 86400);
// Strategy 1: DB-based — file was trashed by deleteThesisFile(id=N),
// file is named "N_basename". If thesis_files row N no longer exists, it's orphaned.
if (preg_match('/^(\d+)_/', $item, $m)) {
$dbId = (int)$m[1];
if (!isset($existingFileIds[$dbId])) {
$shouldDelete = true;
$reason = "ligne DB thesis_files#$dbId supprimée";
}
}
// Strategy 2: time-based fallback
if (!$shouldDelete && $ageDays > ($maxAgeTrash / 86400)) {
$shouldDelete = true;
$reason = "plus de " . ($maxAgeTrash / 86400) . "j";
}
if ($shouldDelete) {
if (!@unlink($filePath)) {
$errors[] = "Impossible de supprimer: $item";
} else {
$trashRemoved++;
$details[] = "_trash/$item: $reason";
}
}
}
}
}
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'filepond_removed' => $filepondRemoved,
'trash_removed' => $trashRemoved,
'errors' => $errors,
'details' => $details,
]);
// ── Helper ────────────────────────────────────────────────────────────────
function rmdirRecursive(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$items = @scandir($dir);
if ($items === false) {
return;
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
if (is_dir($path)) {
rmdirRecursive($path);
} else {
@unlink($path);
}
}
@rmdir($dir);
}