Files
xamxam/app/public/admin/actions/restore-trash.php
T
Pontoporeia 3cecee10c9 fix: prevent file deletion on relink + restore button now visible + OOB-style in-place update
Two critical fixes:

1. Relink flow no longer destroys/recreates FilePond instances:
   The relink (XamxamRelinkFile) and PeerTube relink (XamxamRelinkPeerTube)
   previously refreshed the entire fichiers fragment via HTMX after
   pond.addFile(). This triggered destroyFilePondsIn on ALL pools, which
   could fire server.remove callbacks and move existing files to corbeille.
   Now just closes the modal — the file is already added to the pool in-place,
   and syncOrderInput creates the hidden form input.

2. Cleanup page « Corbeille (restaurable) » now actually shows files:
   _cleanup-stats-data.php previously classified trash files by checking if
   the thesis_files DB row still existed. But both deleteThesisFileToTrash
   and FilepondHandler::handleRemove DELETE the DB row. So ALL trash files
   appeared as `stale` (not restorable). Now uses the JSON sidecar file
   presence as the classification criterion — if the sidecar exists and is
   recent, the file is restorable regardless of DB row state.

Also removed unused DB query from _cleanup-stats-data.php.
2026-07-10 16:29:04 +02:00

158 lines
4.9 KiB
PHP

<?php
/**
* Restore a trashed file back to its original location (admin).
*
* POST /admin/actions/restore-trash.php
* Body: trash_file=123_filename.pdf
*
* Reads the JSON sidecar file (trash_file.json) to get the original
* thesis_files row metadata, re-inserts the DB row, and moves the file
* back from tmp/_trash to its original storage path.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Méthode non autorisée.']);
exit;
}
if (!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(['ok' => false, 'error' => 'CSRF invalide.']);
exit;
}
$trashFile = trim($_POST['trash_file'] ?? '');
if ($trashFile === '') {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Nom de fichier invalide.']);
exit;
}
$storageRoot = STORAGE_ROOT;
$trashDir = $storageRoot . '/tmp/_trash';
$trashPath = $trashDir . '/' . basename($trashFile);
$sidecarPath = $trashPath . '.json';
// Validate the trash file is inside the trash directory
if (!is_file($trashPath) || !str_starts_with(realpath($trashPath), realpath($trashDir))) {
http_response_code(404);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Fichier introuvable dans la corbeille.']);
exit;
}
// Read sidecar metadata
if (!file_exists($sidecarPath)) {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => false,
'error' => 'Métadonnées de restauration absentes (fichier trop ancien ?). Utilisez "Relier un fichier existant".',
]);
exit;
}
$sidecar = json_decode(file_get_contents($sidecarPath), true);
if (!is_array($sidecar) || empty($sidecar['file_path']) || empty($sidecar['thesis_id'])) {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Métadonnées corrompues.']);
exit;
}
$originalPath = $sidecar['file_path'];
$thesisId = (int)$sidecar['thesis_id'];
$fileType = $sidecar['file_type'] ?? 'other';
$fileName = $sidecar['file_name'] ?? basename($originalPath);
$mimeType = $sidecar['mime_type'] ?? 'application/octet-stream';
$fileSize = (int)($sidecar['file_size'] ?? 0);
$displayLabel = $sidecar['display_label'] ?? null;
// Verify the thesis still exists
require_once __DIR__ . '/../../../src/Database.php';
$db = new Database();
$thesis = $db->getThesis($thesisId);
if (!$thesis) {
http_response_code(404);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Le TFE associé n\'existe plus.']);
exit;
}
// Check no file already exists at the original path
$absOriginal = $storageRoot . '/' . $originalPath;
if (file_exists($absOriginal)) {
// File already restored or a new file replaced it — just clean up the trash
@unlink($trashPath);
@unlink($sidecarPath);
http_response_code(409);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => false,
'error' => 'Un fichier existe déjà à l\'emplacement d\'origine. Fichier corbeille nettoyé.',
]);
exit;
}
// Ensure parent directory exists
$parentDir = dirname($absOriginal);
if (!is_dir($parentDir)) {
mkdir($parentDir, 0755, true);
}
// Move file back from trash to original location
if (!rename($trashPath, $absOriginal)) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Échec du déplacement du fichier.']);
exit;
}
chmod($absOriginal, 0644);
// Delete the sidecar file
@unlink($sidecarPath);
// Re-insert the thesis_files DB row
$db->insertThesisFile(
$thesisId,
$fileType,
$originalPath,
$fileName,
$fileSize,
$mimeType,
$displayLabel,
null
);
$newId = $db->getConnection()->lastInsertId();
error_log("[restore-trash] thesis_id=$thesisId file_id=$newId restored from trash: $trashFile$originalPath");
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// HTMX request: re-render the fragment
if (isset($_SERVER['HTTP_HX_REQUEST'])) {
header('HX-Trigger: refreshStats');
require __DIR__ . '/cleanup-stats-fragment.php';
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => true,
'id' => $newId,
'thesis_id' => $thesisId,
'message' => 'Fichier restauré avec succès.',
]);
exit;