amélioration de la liste des fichiers

This commit is contained in:
Esenjin 2025-01-15 18:02:24 +01:00
parent 5d901cb8b2
commit 9f54cc1519
3 changed files with 436 additions and 87 deletions

257
admin.php
View File

@ -240,103 +240,196 @@ ob_start(); ?>
} }
</script> </script>
<div class="card"> <?php
<h2>Fichiers hébergés</h2> // Traitement de la suppression de fichier
<?php if (empty($files)): ?> if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_file') {
<p class="text-muted">Aucun fichier hébergé</p> if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
<?php else: ?> header('Location: admin.php?error=' . urlencode('Token de sécurité invalide'));
<div class="file-list"> exit;
<?php foreach ($files as $file): ?> } else if (!isset($_POST['filename'])) {
<div class="file-item"> header('Location: admin.php?error=' . urlencode('Nom de fichier manquant'));
<div class="file-preview"> exit;
<?php if ($file['preview_type'] === 'image'): ?> } else {
<img src="fichiers/<?php echo Cyla::escape($file['name']); ?>" alt="<?php echo Cyla::escape($file['name']); ?>"> $result = Cyla::deleteFile($_POST['filename']);
<?php else: ?> if ($result['success']) {
<div class="preview-placeholder"> header('Location: admin.php?success=' . urlencode('Fichier supprimé avec succès'));
<?php echo strtoupper($file['extension']); ?> exit;
</div> } else {
<?php endif; ?> header('Location: admin.php?error=' . urlencode($result['error']));
</div> exit;
}
<div class="file-info"> }
<p class="file-name"><?php echo Cyla::escape($file['name']); ?></p> }
<p class="file-meta">
<?php echo Cyla::escape(round($file['size'] / 1024, 2)); ?> Ko // Récupération de la page courante depuis l'URL
· <?php echo date('d/m/Y H:i', $file['uploaded']); ?> $currentPage = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
</p> $filesPerPage = 20;
<div class="file-actions">
<a href="share.php?file=<?php echo urlencode($file['name']); ?>" class="btn btn-secondary" target="_blank">Voir</a> // Récupération des fichiers avec pagination
<button class="btn" onclick="copyShareLink('<?php echo SITE_URL; ?>share.php?file=<?php echo urlencode($file['name']); ?>')">Copier le lien</button> $fileData = Cyla::listFiles($currentPage, $filesPerPage);
$files = $fileData['files'];
?>
<div class="card">
<h2>Fichiers hébergés</h2>
<?php if (empty($files)): ?>
<p class="text-muted">Aucun fichier hébergé</p>
<?php else: ?>
<div class="files-header">
<p class="files-count">
<?php
$start = (($fileData['currentPage'] - 1) * $fileData['perPage']) + 1;
$end = min($start + count($files) - 1, $fileData['total']);
echo "Affichage de $start-$end sur {$fileData['total']} fichiers";
?>
</p>
</div>
<div class="file-list">
<?php foreach ($files as $file): ?>
<div class="file-item">
<div class="file-preview">
<?php if ($file['preview_type'] === 'image'): ?>
<img src="fichiers/<?php echo Cyla::escape($file['name']); ?>"
alt="<?php echo Cyla::escape($file['name']); ?>">
<?php else: ?>
<div class="preview-placeholder">
<?php echo strtoupper($file['extension']); ?>
</div> </div>
<?php endif; ?>
</div>
<div class="file-info">
<p class="file-name"><?php echo Cyla::escape($file['name']); ?></p>
<p class="file-meta">
<?php echo Cyla::escape(round($file['size'] / 1024, 2)); ?> Ko
· <?php echo date('d/m/Y H:i', $file['uploaded']); ?>
</p>
<div class="file-actions">
<a href="share.php?file=<?php echo urlencode($file['name']); ?>"
class="btn btn-secondary"
target="_blank">Voir</a>
<button class="btn"
onclick="copyShareLink('<?php echo SITE_URL; ?>share.php?file=<?php echo urlencode($file['name']); ?>')">
Copier le lien
</button>
<button class="btn btn-danger"
onclick="confirmDelete('<?php echo Cyla::escape($file['name']); ?>')">
×
</button>
</div> </div>
</div> </div>
<?php endforeach; ?> </div>
<!-- Formulaire caché pour la suppression -->
<form id="deleteForm-<?php echo Cyla::escape($file['name']); ?>"
action="admin.php"
method="POST"
style="display: none;">
<input type="hidden" name="action" value="delete_file">
<input type="hidden" name="filename" value="<?php echo Cyla::escape($file['name']); ?>">
<input type="hidden" name="csrf_token" value="<?php echo Cyla::generateCSRFToken(); ?>">
</form>
<?php endforeach; ?>
</div>
<?php if ($fileData['totalPages'] > 1): ?>
<div class="pagination">
<?php if ($fileData['currentPage'] > 1): ?>
<a href="?page=<?php echo $fileData['currentPage'] - 1; ?>"
class="pagination-link">&laquo; Précédent</a>
<?php endif; ?>
<?php
// Afficher max 5 pages autour de la page courante
$start = max(1, $fileData['currentPage'] - 2);
$end = min($fileData['totalPages'], $fileData['currentPage'] + 2);
if ($start > 1) {
echo '<a href="?page=1" class="pagination-link">1</a>';
if ($start > 2) echo '<span class="pagination-ellipsis">...</span>';
}
for ($i = $start; $i <= $end; $i++) {
$isActive = $i === $fileData['currentPage'];
echo '<a href="?page=' . $i . '" class="pagination-link ' .
($isActive ? 'pagination-active' : '') . '">' . $i . '</a>';
}
if ($end < $fileData['totalPages']) {
if ($end < $fileData['totalPages'] - 1) echo '<span class="pagination-ellipsis">...</span>';
echo '<a href="?page=' . $fileData['totalPages'] .
'" class="pagination-link">' . $fileData['totalPages'] . '</a>';
}
if ($fileData['currentPage'] < $fileData['totalPages']): ?>
<a href="?page=<?php echo $fileData['currentPage'] + 1; ?>"
class="pagination-link">Suivant &raquo;</a>
<?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> <?php endif; ?>
</div> </div>
<style>
/* Styles spécifiques à la page d'administration */
.admin-container {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.admin-container h2 {
color: var(--color-primary);
margin-bottom: var(--spacing-md);
}
.help-text {
font-size: 0.9rem;
color: var(--color-text-muted);
margin-top: var(--spacing-xs);
}
.preview-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg);
color: var(--color-text-muted);
font-size: 2rem;
font-weight: bold;
}
.file-name {
font-weight: bold;
word-break: break-all;
}
.file-meta {
font-size: 0.9rem;
color: var(--color-text-muted);
margin: var(--spacing-xs) 0;
}
.file-actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
</style>
<script> <script>
// Fonction pour copier le lien de partage
function copyShareLink(url) { function copyShareLink(url) {
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
alert('Lien copié dans le presse-papier'); // Créer et afficher la notification
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = 'Lien copié dans le presse-papier !';
document.body.appendChild(notification);
// Supprimer la notification après l'animation
setTimeout(() => {
notification.remove();
}, 3000);
}).catch(err => { }).catch(err => {
console.error('Erreur lors de la copie :', err); console.error('Erreur lors de la copie :', err);
alert('Erreur lors de la copie du lien'); // En cas d'erreur, afficher une notification d'erreur
const notification = document.createElement('div');
notification.className = 'notification error';
notification.textContent = 'Erreur lors de la copie';
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}); });
} }
// Fonction de confirmation de suppression
function confirmDelete(filename) {
const overlay = document.createElement('div');
overlay.className = 'confirmation-overlay';
const dialog = document.createElement('div');
dialog.className = 'confirmation-dialog';
dialog.innerHTML = `
<h3>Confirmer la suppression</h3>
<p>Voulez-vous vraiment supprimer le fichier "${filename}" ?</p>
<div class="confirmation-actions">
<button class="btn btn-secondary" onclick="closeConfirmDialog()">Annuler</button>
<button class="btn" onclick="submitDelete('${filename}')">Supprimer</button>
</div>
`;
document.body.appendChild(overlay);
document.body.appendChild(dialog);
}
// Fonction pour fermer la modale
function closeConfirmDialog() {
document.querySelector('.confirmation-overlay')?.remove();
document.querySelector('.confirmation-dialog')?.remove();
}
// Fonction pour soumettre la suppression
function submitDelete(filename) {
document.getElementById('deleteForm-' + filename).submit();
closeConfirmDialog();
}
</script> </script>
<div id="passwordModalRoot"></div>
<script> <script>
window.CSRF_TOKEN = "<?php echo Cyla::generateCSRFToken(); ?>"; window.CSRF_TOKEN = "<?php echo Cyla::generateCSRFToken(); ?>";
</script> </script>

View File

@ -131,12 +131,30 @@ class Cyla {
} }
/** /**
* Liste les fichiers uploadés * Liste les fichiers uploadés avec pagination
* @return array * @param int $page Numéro de la page (commence à 1)
* @param int $perPage Nombre d'éléments par page
* @return array ['files' => array, 'total' => int, 'totalPages' => int]
*/ */
public static function listFiles() { public static function listFiles($page = 1, $perPage = 20) {
$files = []; $files = [];
foreach (glob(UPLOAD_DIR . '*') as $file) { $allFiles = glob(UPLOAD_DIR . '*');
// Trier les fichiers par date de modification (plus récent en premier)
usort($allFiles, function($a, $b) {
return filemtime($b) - filemtime($a);
});
// Calculer la pagination
$total = count($allFiles);
$totalPages = ceil($total / $perPage);
$page = max(1, min($page, $totalPages)); // Garantir que la page est dans les limites
$offset = ($page - 1) * $perPage;
// Récupérer uniquement les fichiers de la page courante
$pageFiles = array_slice($allFiles, $offset, $perPage);
foreach ($pageFiles as $file) {
$info = pathinfo($file); $info = pathinfo($file);
$files[] = [ $files[] = [
'name' => basename($file), 'name' => basename($file),
@ -146,7 +164,41 @@ class Cyla {
'preview_type' => getPreviewType($info['extension']) 'preview_type' => getPreviewType($info['extension'])
]; ];
} }
return $files;
return [
'files' => $files,
'total' => $total,
'totalPages' => $totalPages,
'currentPage' => $page,
'perPage' => $perPage
];
}
/**
* Supprime un fichier
* @param string $filename Nom du fichier à supprimer
* @return array ['success' => bool, 'error' => string|null]
*/
public static function deleteFile($filename) {
$filepath = UPLOAD_DIR . $filename;
// Vérifier que le fichier existe et est dans le dossier d'upload
if (!file_exists($filepath) || !is_file($filepath)) {
return ['success' => false, 'error' => 'Fichier introuvable'];
}
// Vérifier que le fichier est bien dans le dossier d'upload
$realpath = realpath($filepath);
$uploadDir = realpath(UPLOAD_DIR);
if (strpos($realpath, $uploadDir) !== 0) {
return ['success' => false, 'error' => 'Chemin de fichier non autorisé'];
}
// Supprimer le fichier
if (unlink($filepath)) {
return ['success' => true];
} else {
return ['success' => false, 'error' => 'Erreur lors de la suppression du fichier'];
}
} }
} }

View File

@ -386,4 +386,208 @@ footer {
opacity: 1; opacity: 1;
transform: scale(1) translateY(0); transform: scale(1) translateY(0);
} }
}
/* Styles spécifiques à la page d'administration */
.admin-container {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.admin-container h2 {
color: var(--color-primary);
margin-bottom: var(--spacing-md);
}
.help-text {
font-size: 0.9rem;
color: var(--color-text-muted);
margin-top: var(--spacing-xs);
}
.preview-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg);
color: var(--color-text-muted);
font-size: 2rem;
font-weight: bold;
}
.file-name {
font-weight: bold;
word-break: break-all;
}
.file-meta {
font-size: 0.9rem;
color: var(--color-text-muted);
margin: var(--spacing-xs) 0;
}
.file-actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
/* Styles pour la pagination et l'affichage des fichiers */
.files-header {
margin-bottom: var(--spacing-md);
color: var(--color-text-muted);
}
.files-count {
font-size: 0.9rem;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-lg);
padding-top: var(--spacing-md);
border-top: 1px solid var(--color-border);
}
.pagination-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0 var(--spacing-sm);
color: var(--color-text);
text-decoration: none;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
transition: all 0.2s ease;
}
.pagination-link:hover {
background-color: var(--color-border);
}
.pagination-active {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-text);
}
.pagination-ellipsis {
color: var(--color-text-muted);
padding: 0 var(--spacing-xs);
}
/* Bouton de suppression */
.btn-danger {
background-color: var(--color-accent);
width: 2.5rem;
height: 2.5rem;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
line-height: 1;
}
.btn-danger:hover {
background-color: #a85651;
}
/* Boîte de dialogue de confirmation */
.confirmation-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--spacing-lg);
max-width: 400px;
width: 90%;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.confirmation-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.confirmation-dialog h3 {
color: var(--color-primary);
margin-bottom: var(--spacing-md);
}
.confirmation-dialog p {
margin-bottom: var(--spacing-lg);
}
.confirmation-actions {
display: flex;
gap: var(--spacing-sm);
justify-content: flex-end;
}
.confirmation-actions .btn {
min-width: 100px;
padding: var(--spacing-sm) var(--spacing-md);
}
/* Responsive */
@media (max-width: 768px) {
.pagination {
flex-wrap: wrap;
}
.file-actions {
flex-wrap: wrap;
}
.btn-danger {
width: 100%;
}
}
/* Styles de notification */
.notification {
position: fixed;
bottom: var(--spacing-lg);
right: var(--spacing-lg);
background-color: var(--color-success);
color: white;
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--border-radius);
animation: slideIn 0.3s ease-out, fadeOut 0.3s ease-in 2.7s forwards;
z-index: 1000;
pointer-events: none;
}
.notification.error {
background-color: var(--color-error);
}
@keyframes slideIn {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
} }