Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
ae8476d404 | |||
acb80e4668 | |||
fd431a332a | |||
fd1ac715db | |||
3cacb4645f | |||
0018ac8554 | |||
b31f6fff8d | |||
9f54cc1519 | |||
5d901cb8b2 | |||
f8675a312b | |||
eebe3372bc | |||
b100499a15 |
@ -1,8 +1,4 @@
|
|||||||
# Cyla
|
# Cyla
|
||||||
Site d'hébergement de fichiers simple, réduit au strict nécessaire.
|
Site d'hébergement de fichiers simple, réduit au strict nécessaire.
|
||||||
|
|
||||||
|
![image](https://concepts.esenjin.xyz/cyla/fichiers/6787d3fe886e4_1736954878.png)
|
||||||
Le projet se base sur la première version de [QuadFile](https://github.com/QuadPiece/QuadFile).
|
|
||||||
|
|
||||||
|
|
||||||
![image](https://concepts.esenjin.xyz/cyla/v2/file/CE513E.png)
|
|
291
admin.php
291
admin.php
@ -240,128 +240,201 @@ ob_start(); ?>
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card">
|
<?php
|
||||||
<h2>Changer le mot de passe</h2>
|
// Traitement de la suppression de fichier
|
||||||
<form method="POST" class="password-form">
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_file') {
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo Cyla::generateCSRFToken(); ?>">
|
if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
|
||||||
<input type="hidden" name="action" value="change_password">
|
header('Location: admin.php?error=' . urlencode('Token de sécurité invalide'));
|
||||||
|
exit;
|
||||||
<div class="form-group">
|
} else if (!isset($_POST['filename'])) {
|
||||||
<label for="current_password">Mot de passe actuel</label>
|
header('Location: admin.php?error=' . urlencode('Nom de fichier manquant'));
|
||||||
<input type="password" id="current_password" name="current_password" required>
|
exit;
|
||||||
</div>
|
} else {
|
||||||
|
$result = Cyla::deleteFile($_POST['filename']);
|
||||||
<div class="form-group">
|
if ($result['success']) {
|
||||||
<label for="new_password">Nouveau mot de passe</label>
|
header('Location: admin.php?success=' . urlencode('Fichier supprimé avec succès'));
|
||||||
<input type="password" id="new_password" name="new_password" required>
|
exit;
|
||||||
</div>
|
} else {
|
||||||
|
header('Location: admin.php?error=' . urlencode($result['error']));
|
||||||
<div class="form-group">
|
exit;
|
||||||
<label for="confirm_password">Confirmer le nouveau mot de passe</label>
|
}
|
||||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
}
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<button type="submit" class="btn">Changer le mot de passe</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
// Récupération de la page courante depuis l'URL
|
||||||
<h2>Fichiers hébergés</h2>
|
$currentPage = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||||
<?php if (empty($files)): ?>
|
$filesPerPage = 20;
|
||||||
<p class="text-muted">Aucun fichier hébergé</p>
|
|
||||||
<?php else: ?>
|
// Récupération des fichiers avec pagination
|
||||||
<div class="file-list">
|
$fileData = Cyla::listFiles($currentPage, $filesPerPage);
|
||||||
<?php foreach ($files as $file): ?>
|
$files = $fileData['files'];
|
||||||
<div class="file-item">
|
?>
|
||||||
<div class="file-preview">
|
|
||||||
<?php if ($file['preview_type'] === 'image'): ?>
|
<div class="card">
|
||||||
<img src="fichiers/<?php echo Cyla::escape($file['name']); ?>" alt="<?php echo Cyla::escape($file['name']); ?>">
|
<h2>Fichiers hébergés</h2>
|
||||||
<?php else: ?>
|
|
||||||
<div class="preview-placeholder">
|
<?php if (empty($files)): ?>
|
||||||
<?php echo strtoupper($file['extension']); ?>
|
<p class="text-muted">Aucun fichier hébergé</p>
|
||||||
</div>
|
<?php else: ?>
|
||||||
<?php endif; ?>
|
<div class="files-header">
|
||||||
</div>
|
<p class="files-count">
|
||||||
|
<?php
|
||||||
<div class="file-info">
|
$start = (($fileData['currentPage'] - 1) * $fileData['perPage']) + 1;
|
||||||
<p class="file-name"><?php echo Cyla::escape($file['name']); ?></p>
|
$end = min($start + count($files) - 1, $fileData['total']);
|
||||||
<p class="file-meta">
|
echo "Affichage de $start-$end sur {$fileData['total']} fichiers";
|
||||||
<?php echo Cyla::escape(round($file['size'] / 1024, 2)); ?> Ko
|
?>
|
||||||
· <?php echo date('d/m/Y H:i', $file['uploaded']); ?>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
<div class="file-actions">
|
|
||||||
<a href="share.php?file=<?php echo urlencode($file['name']); ?>" class="btn btn-secondary" target="_blank">Voir</a>
|
<div class="file-list">
|
||||||
<button class="btn" onclick="copyShareLink('<?php echo SITE_URL; ?>share.php?file=<?php echo urlencode($file['name']); ?>')">Copier le lien</button>
|
<?php foreach ($files as $file): ?>
|
||||||
</div>
|
<div class="file-item">
|
||||||
</div>
|
<div class="file-preview">
|
||||||
</div>
|
<?php if ($file['preview_type'] === 'image'): ?>
|
||||||
<?php endforeach; ?>
|
<img src="<?php echo $file['path'] . Cyla::escape($file['name']); ?>"
|
||||||
|
alt="<?php echo Cyla::escape($file['name']); ?>">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="preview-placeholder">
|
||||||
|
<?php echo strtoupper($file['extension']); ?>
|
||||||
|
</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']); ?>
|
||||||
|
· <?php echo $file['path']; ?>
|
||||||
|
</p>
|
||||||
|
<div class="file-actions">
|
||||||
|
<a href="share.php?file=<?php echo urlencode($file['name']); ?>&path=<?php echo urlencode($file['path']); ?>"
|
||||||
|
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']); ?>&path=<?php echo urlencode($file['path']); ?>')">
|
||||||
|
Copier le lien
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger"
|
||||||
|
onclick="confirmDelete('<?php echo Cyla::escape($file['name']); ?>', '<?php echo Cyla::escape($file['path']); ?>')">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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="path" value="<?php echo Cyla::escape($file['path']); ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo Cyla::generateCSRFToken(); ?>">
|
||||||
|
</form>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php if ($fileData['totalPages'] > 1): ?>
|
||||||
|
<div class="pagination">
|
||||||
|
<?php if ($fileData['currentPage'] > 1): ?>
|
||||||
|
<a href="?page=<?php echo $fileData['currentPage'] - 1; ?>"
|
||||||
|
class="pagination-link">«</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">»</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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
// 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>
|
||||||
|
window.CSRF_TOKEN = "<?php echo Cyla::generateCSRFToken(); ?>";
|
||||||
|
</script>
|
||||||
|
<script src="js/password-modal.js"></script>
|
||||||
<?php
|
<?php
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
require 'layout.php';
|
require 'layout.php';
|
||||||
|
51
api.php
51
api.php
@ -2,8 +2,12 @@
|
|||||||
define('CYLA_CORE', true);
|
define('CYLA_CORE', true);
|
||||||
require_once 'core.php';
|
require_once 'core.php';
|
||||||
|
|
||||||
|
// Augmenter la limite de temps d'exécution pour les uploads multiples
|
||||||
|
set_time_limit(300); // 5 minutes
|
||||||
|
|
||||||
// Vérifier si l'utilisateur est connecté
|
// Vérifier si l'utilisateur est connecté
|
||||||
if (!Cyla::isLoggedIn()) {
|
if (!Cyla::isLoggedIn()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(['error' => 'Non autorisé']);
|
echo json_encode(['error' => 'Non autorisé']);
|
||||||
exit;
|
exit;
|
||||||
@ -11,6 +15,7 @@ if (!Cyla::isLoggedIn()) {
|
|||||||
|
|
||||||
// Vérifier le token CSRF
|
// Vérifier le token CSRF
|
||||||
if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
|
if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(['error' => 'Token CSRF invalide']);
|
echo json_encode(['error' => 'Token CSRF invalide']);
|
||||||
exit;
|
exit;
|
||||||
@ -18,18 +23,35 @@ if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])
|
|||||||
|
|
||||||
// Gérer l'upload de fichier
|
// Gérer l'upload de fichier
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
|
||||||
$validation = Cyla::validateUpload($_FILES['file']);
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
if (!$validation['valid']) {
|
try {
|
||||||
http_response_code(400);
|
// Vérification de l'espace disque disponible
|
||||||
echo json_encode(['error' => $validation['error']]);
|
$uploadDir = UPLOAD_DIR;
|
||||||
exit;
|
$freeSpace = disk_free_space($uploadDir);
|
||||||
}
|
if ($freeSpace < $_FILES['file']['size']) {
|
||||||
|
throw new Exception('Espace disque insuffisant');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validation = Cyla::validateUpload($_FILES['file']);
|
||||||
|
if (!$validation['valid']) {
|
||||||
|
throw new Exception($validation['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = Cyla::generateUniqueFilename($_FILES['file']['name']);
|
||||||
|
$destination = $uploadDir . $filename;
|
||||||
|
|
||||||
|
// Upload avec gestion de la mémoire
|
||||||
|
if (!move_uploaded_file($_FILES['file']['tmp_name'], $destination)) {
|
||||||
|
throw new Exception('Erreur lors du déplacement du fichier');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'intégrité du fichier
|
||||||
|
if (!file_exists($destination) || filesize($destination) !== $_FILES['file']['size']) {
|
||||||
|
unlink($destination); // Nettoyer en cas d'erreur
|
||||||
|
throw new Exception('Erreur d\'intégrité du fichier');
|
||||||
|
}
|
||||||
|
|
||||||
$filename = Cyla::generateUniqueFilename($_FILES['file']['name']);
|
|
||||||
$destination = UPLOAD_DIR . $filename;
|
|
||||||
|
|
||||||
if (move_uploaded_file($_FILES['file']['tmp_name'], $destination)) {
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'file' => [
|
'file' => [
|
||||||
@ -38,9 +60,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
|
|||||||
'url' => 'share.php?file=' . urlencode($filename)
|
'url' => 'share.php?file=' . urlencode($filename)
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
} else {
|
} catch (Exception $e) {
|
||||||
http_response_code(500);
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'Erreur lors de l\'upload du fichier']);
|
echo json_encode([
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'details' => error_get_last()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,14 @@ if (!defined('CYLA_CORE')) {
|
|||||||
|
|
||||||
// Site configuration
|
// Site configuration
|
||||||
define('SITE_NAME', 'Cyla');
|
define('SITE_NAME', 'Cyla');
|
||||||
define('SITE_VERSION', '3.0.0');
|
define('SITE_VERSION', '3.0.2');
|
||||||
define('SITE_URL', 'https://concepts.esenjin.xyz/cyla/');
|
define('SITE_URL', 'https://concepts.esenjin.xyz/cyla/');
|
||||||
|
|
||||||
// Files configuration
|
// Files configuration
|
||||||
|
define('LEGACY_UPLOAD_DIRS', [
|
||||||
|
__DIR__ . '/v1/img/fichiers/',
|
||||||
|
__DIR__ . '/v2/file/'
|
||||||
|
]);
|
||||||
define('UPLOAD_DIR', __DIR__ . '/fichiers/');
|
define('UPLOAD_DIR', __DIR__ . '/fichiers/');
|
||||||
define('MAX_FILE_SIZE', 100 * 1024 * 1024); // 100 Mo en octets
|
define('MAX_FILE_SIZE', 100 * 1024 * 1024); // 100 Mo en octets
|
||||||
define('ALLOWED_EXTENSIONS', [
|
define('ALLOWED_EXTENSIONS', [
|
||||||
|
115
core.php
115
core.php
@ -131,22 +131,123 @@ 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 = [];
|
||||||
|
|
||||||
|
// Ajouter les fichiers du dossier principal
|
||||||
|
$allFiles = array_merge($allFiles, glob(UPLOAD_DIR . '*'));
|
||||||
|
|
||||||
|
// Ajouter les fichiers des dossiers hérités
|
||||||
|
foreach (LEGACY_UPLOAD_DIRS as $dir) {
|
||||||
|
if (is_dir($dir)) {
|
||||||
|
$allFiles = array_merge($allFiles, glob($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));
|
||||||
|
$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);
|
||||||
|
$relativePath = '';
|
||||||
|
|
||||||
|
// Déterminer le chemin relatif selon le dossier
|
||||||
|
if (strpos($file, UPLOAD_DIR) === 0) {
|
||||||
|
$relativePath = 'fichiers/';
|
||||||
|
} elseif (strpos($file, __DIR__ . '/v1/img/fichiers/') === 0) {
|
||||||
|
$relativePath = 'v1/img/fichiers/';
|
||||||
|
} elseif (strpos($file, __DIR__ . '/v2/file/') === 0) {
|
||||||
|
$relativePath = 'v2/file/';
|
||||||
|
}
|
||||||
|
|
||||||
$files[] = [
|
$files[] = [
|
||||||
'name' => basename($file),
|
'name' => basename($file),
|
||||||
'size' => filesize($file),
|
'size' => filesize($file),
|
||||||
'extension' => strtolower($info['extension']),
|
'extension' => strtolower($info['extension'] ?? ''),
|
||||||
'uploaded' => filemtime($file),
|
'uploaded' => filemtime($file),
|
||||||
'preview_type' => getPreviewType($info['extension'])
|
'preview_type' => getPreviewType($info['extension'] ?? ''),
|
||||||
|
'path' => $relativePath // Ajout du chemin relatif
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return $files;
|
|
||||||
|
return [
|
||||||
|
'files' => $files,
|
||||||
|
'total' => $total,
|
||||||
|
'totalPages' => $totalPages,
|
||||||
|
'currentPage' => $page,
|
||||||
|
'perPage' => $perPage
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un fichier
|
||||||
|
* @param string $filename Nom du fichier à supprimer
|
||||||
|
* @param string $path Chemin relatif du fichier
|
||||||
|
* @return array ['success' => bool, 'error' => string|null]
|
||||||
|
*/
|
||||||
|
public static function deleteFile($filename, $path = 'fichiers/') {
|
||||||
|
// Déterminer le chemin complet selon le dossier
|
||||||
|
$basePath = '';
|
||||||
|
switch ($path) {
|
||||||
|
case 'v1/img/fichiers/':
|
||||||
|
$basePath = __DIR__ . '/v1/img/fichiers/';
|
||||||
|
break;
|
||||||
|
case 'v2/file/':
|
||||||
|
$basePath = __DIR__ . '/v2/file/';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$basePath = UPLOAD_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filepath = $basePath . $filename;
|
||||||
|
|
||||||
|
// Vérifier que le fichier existe et est dans le bon dossier
|
||||||
|
if (!file_exists($filepath) || !is_file($filepath)) {
|
||||||
|
return ['success' => false, 'error' => 'Fichier introuvable'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le fichier est bien dans un des dossiers autorisés
|
||||||
|
$realpath = realpath($filepath);
|
||||||
|
$allowed = false;
|
||||||
|
|
||||||
|
if (strpos($realpath, realpath(UPLOAD_DIR)) === 0) {
|
||||||
|
$allowed = true;
|
||||||
|
} else {
|
||||||
|
foreach (LEGACY_UPLOAD_DIRS as $dir) {
|
||||||
|
if (strpos($realpath, realpath($dir)) === 0) {
|
||||||
|
$allowed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$allowed) {
|
||||||
|
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'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
379
css/style.css
379
css/style.css
@ -69,15 +69,23 @@ nav {
|
|||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
/* Navigation */
|
||||||
|
.nav-link,
|
||||||
|
button.nav-link {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
|
font: inherit; /* Pour que le bouton utilise la même police */
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover {
|
.nav-link:hover,
|
||||||
|
button.nav-link:hover {
|
||||||
background-color: var(--color-border);
|
background-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,4 +350,371 @@ footer {
|
|||||||
.file-list {
|
.file-list {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles pour la modale */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
background-color: var(--color-bg-alt);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 1rem;
|
||||||
|
animation: modalAppear 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
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-xs);
|
||||||
|
margin: var(--spacing-lg) auto;
|
||||||
|
padding-top: var(--spacing-md);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
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;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 0.25rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-link {
|
||||||
|
min-width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
padding: 0 var(--spacing-sm);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-container h1 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-preview {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-preview {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.extension-badge {
|
||||||
|
background-color: var(--color-bg-alt);
|
||||||
|
color: var(--color-primary);
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles responsifs */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateY(100%); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
}
|
}
|
121
index.php
Normal file
121
index.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
define('CYLA_CORE', true);
|
||||||
|
require_once 'core.php';
|
||||||
|
|
||||||
|
// Rediriger vers l'admin si déjà connecté
|
||||||
|
if (Cyla::isLoggedIn()) {
|
||||||
|
header('Location: admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo SITE_NAME; ?> - Accueil</title>
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="home-container">
|
||||||
|
<main class="home-hero">
|
||||||
|
<div class="home-content">
|
||||||
|
<h1>Cyla</h1>
|
||||||
|
<p class="home-description">Le disque nuagique personnel d'Esenjin.</p>
|
||||||
|
<a href="admin.php" class="home-button">
|
||||||
|
Accéder à l'administration
|
||||||
|
<svg class="home-button-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="home-footer">
|
||||||
|
<p>© <?php echo date('Y'); ?> <?php echo SITE_NAME; ?> - Version <?php echo SITE_VERSION; ?></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Styles spécifiques à la page d'accueil */
|
||||||
|
.home-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-content h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-description {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-button:hover {
|
||||||
|
background-color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-button-icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-left: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
background-color: var(--color-bg-alt);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation d'entrée */
|
||||||
|
.home-content {
|
||||||
|
animation: fadeInUp 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,25 +1,16 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
import { X, Upload, CheckCircle, AlertCircle } from 'lucide-react';
|
import { X, Upload, CheckCircle, AlertCircle, Loader } from 'lucide-react';
|
||||||
|
|
||||||
const UploadZone = () => {
|
const UploadZone = () => {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 Mo
|
const MAX_FILES = 10; // Limite stricte à 10 fichiers
|
||||||
const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt'];
|
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
||||||
|
const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt'];
|
||||||
|
|
||||||
const handleDragOver = (e) => {
|
const validateFile = useCallback((file) => {
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragLeave = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateFile = (file) => {
|
|
||||||
const extension = file.name.split('.').pop().toLowerCase();
|
const extension = file.name.split('.').pop().toLowerCase();
|
||||||
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
||||||
return { valid: false, error: `Extension .${extension} non autorisée` };
|
return { valid: false, error: `Extension .${extension} non autorisée` };
|
||||||
@ -28,95 +19,86 @@ const UploadZone = () => {
|
|||||||
return { valid: false, error: 'Fichier trop volumineux (max 100 Mo)' };
|
return { valid: false, error: 'Fichier trop volumineux (max 100 Mo)' };
|
||||||
}
|
}
|
||||||
return { valid: true, error: null };
|
return { valid: true, error: null };
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleDrop = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(false);
|
|
||||||
|
|
||||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
|
||||||
addFiles(droppedFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (e) => {
|
|
||||||
const selectedFiles = Array.from(e.target.files);
|
|
||||||
addFiles(selectedFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addFiles = (newFiles) => {
|
|
||||||
const processedFiles = newFiles.map(file => {
|
|
||||||
const validation = validateFile(file);
|
|
||||||
return {
|
|
||||||
file,
|
|
||||||
id: Math.random().toString(36).substring(7),
|
|
||||||
status: validation.valid ? 'pending' : 'error',
|
|
||||||
error: validation.error,
|
|
||||||
progress: 0
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
setFiles(currentFiles => [...currentFiles, ...processedFiles]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFile = (fileId) => {
|
|
||||||
setFiles(files => files.filter(f => f.id !== fileId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadFile = async (fileInfo) => {
|
const uploadFile = async (fileInfo) => {
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', fileInfo.file);
|
|
||||||
formData.append('action', 'upload');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Simuler un upload progressif pour la démo
|
const formData = new FormData();
|
||||||
await new Promise(resolve => {
|
formData.append('file', fileInfo.file);
|
||||||
let progress = 0;
|
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]')?.value || '');
|
||||||
const interval = setInterval(() => {
|
|
||||||
progress += 10;
|
// Mettre le statut en uploading
|
||||||
setFiles(files =>
|
setFiles(files =>
|
||||||
files.map(f =>
|
files.map(f =>
|
||||||
f.id === fileInfo.id
|
f.id === fileInfo.id ? { ...f, status: 'uploading' } : f
|
||||||
? { ...f, progress, status: progress === 100 ? 'complete' : 'uploading' }
|
)
|
||||||
: f
|
);
|
||||||
)
|
|
||||||
);
|
const response = await fetch('api.php', {
|
||||||
if (progress >= 100) {
|
method: 'POST',
|
||||||
clearInterval(interval);
|
body: formData
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
throw new Error(result.error || 'Erreur lors du téléversement');
|
||||||
|
}
|
||||||
|
|
||||||
|
setFiles(files =>
|
||||||
|
files.map(f =>
|
||||||
|
f.id === fileInfo.id ? { ...f, status: 'complete' } : f
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFiles(files =>
|
setFiles(files =>
|
||||||
files.map(f =>
|
files.map(f =>
|
||||||
f.id === fileInfo.id
|
f.id === fileInfo.id
|
||||||
? { ...f, status: 'error', error: 'Erreur lors du téléversement' }
|
? { ...f, status: 'error', error: error.message }
|
||||||
: f
|
: f
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadAllFiles = () => {
|
const handleFiles = (newFiles) => {
|
||||||
const pendingFiles = files.filter(f => f.status === 'pending');
|
// Vérifier si on n'a pas déjà atteint la limite
|
||||||
pendingFiles.forEach(uploadFile);
|
if (files.length >= MAX_FILES) {
|
||||||
|
alert(`Vous pouvez téléverser uniquement ${MAX_FILES} fichiers à la fois`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer combien de fichiers on peut encore ajouter
|
||||||
|
const remainingSlots = MAX_FILES - files.length;
|
||||||
|
const filesToAdd = Array.from(newFiles).slice(0, remainingSlots);
|
||||||
|
|
||||||
|
if (filesToAdd.length < newFiles.length) {
|
||||||
|
alert(`Seuls les ${remainingSlots} premiers fichiers seront traités. Maximum ${MAX_FILES} fichiers à la fois.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedFiles = filesToAdd.map(file => ({
|
||||||
|
file,
|
||||||
|
id: Math.random().toString(36).substring(7),
|
||||||
|
status: validateFile(file).valid ? 'pending' : 'error',
|
||||||
|
error: validateFile(file).error
|
||||||
|
}));
|
||||||
|
|
||||||
|
setFiles(current => [...current, ...processedFiles]);
|
||||||
|
|
||||||
|
// Lancer l'upload pour chaque fichier valide
|
||||||
|
processedFiles
|
||||||
|
.filter(f => f.status === 'pending')
|
||||||
|
.forEach(uploadFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
const handleDrop = useCallback((e) => {
|
||||||
switch (status) {
|
e.preventDefault();
|
||||||
case 'complete': return 'text-green-500';
|
setIsDragging(false);
|
||||||
case 'error': return 'text-red-500';
|
handleFiles(e.dataTransfer.files);
|
||||||
case 'uploading': return 'text-blue-500';
|
}, []);
|
||||||
default: return 'text-gray-500';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status) => {
|
const removeFile = (id) => {
|
||||||
switch (status) {
|
setFiles(files => files.filter(f => f.id !== id));
|
||||||
case 'complete': return <CheckCircle className="w-5 h-5" />;
|
|
||||||
case 'error': return <AlertCircle className="w-5 h-5" />;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -125,8 +107,14 @@ const UploadZone = () => {
|
|||||||
className={`relative border-2 border-dashed rounded-lg p-8 text-center mb-4 transition-colors
|
className={`relative border-2 border-dashed rounded-lg p-8 text-center mb-4 transition-colors
|
||||||
${isDragging ? 'border-primary-500 bg-primary-50' : 'border-gray-600'}
|
${isDragging ? 'border-primary-500 bg-primary-50' : 'border-gray-600'}
|
||||||
hover:border-primary-500`}
|
hover:border-primary-500`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={e => {
|
||||||
onDragLeave={handleDragLeave}
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
}}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
@ -134,7 +122,7 @@ const UploadZone = () => {
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
onChange={handleFileSelect}
|
onChange={e => handleFiles(e.target.files)}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')}
|
accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')}
|
||||||
/>
|
/>
|
||||||
@ -147,20 +135,16 @@ const UploadZone = () => {
|
|||||||
ou cliquez pour sélectionner des fichiers
|
ou cliquez pour sélectionner des fichiers
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 mt-2">
|
<p className="text-xs text-gray-400 mt-2">
|
||||||
Max 100 Mo par fichier · {ALLOWED_EXTENSIONS.join(', ')}
|
Maximum {MAX_FILES} fichiers à la fois · 100 Mo par fichier
|
||||||
|
<br />
|
||||||
|
Extensions : {ALLOWED_EXTENSIONS.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="border border-gray-700 rounded-lg overflow-hidden">
|
<div className="border border-gray-700 rounded-lg overflow-hidden">
|
||||||
<div className="p-4 bg-gray-800 border-b border-gray-700 flex justify-between items-center">
|
<div className="p-4 bg-gray-800 border-b border-gray-700">
|
||||||
<h3 className="font-medium">Files ({files.length})</h3>
|
<h3 className="font-medium">Fichiers ({files.length}/{MAX_FILES})</h3>
|
||||||
<button
|
|
||||||
onClick={uploadAllFiles}
|
|
||||||
className="btn btn-sm bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Tout téléverser
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-gray-700">
|
<div className="divide-y divide-gray-700">
|
||||||
@ -176,22 +160,20 @@ const UploadZone = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fileInfo.status === 'uploading' && (
|
<div className={`
|
||||||
<div className="w-32 h-2 bg-gray-700 rounded-full overflow-hidden">
|
${fileInfo.status === 'complete' ? 'text-green-500' : ''}
|
||||||
<div
|
${fileInfo.status === 'error' ? 'text-red-500' : ''}
|
||||||
className="h-full bg-blue-500 transition-all duration-300"
|
${fileInfo.status === 'uploading' ? 'text-blue-500' : ''}
|
||||||
style={{ width: `${fileInfo.progress}%` }}
|
`}>
|
||||||
/>
|
{fileInfo.status === 'complete' && <CheckCircle className="w-5 h-5" />}
|
||||||
</div>
|
{fileInfo.status === 'error' && <AlertCircle className="w-5 h-5" />}
|
||||||
)}
|
{fileInfo.status === 'uploading' && <Loader className="w-5 h-5 animate-spin" />}
|
||||||
|
|
||||||
<div className={getStatusColor(fileInfo.status)}>
|
|
||||||
{getStatusIcon(fileInfo.status)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => removeFile(fileInfo.id)}
|
onClick={() => removeFile(fileInfo.id)}
|
||||||
className="p-1 hover:bg-gray-700 rounded"
|
className="p-1 hover:bg-gray-700 rounded"
|
||||||
|
title={fileInfo.status === 'uploading' ? 'Annuler' : 'Supprimer'}
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
200
js/password-modal.js
Normal file
200
js/password-modal.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
const ChangePasswordModal = ({ isOpen, onClose, onSubmit, csrfToken }) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement('div', {
|
||||||
|
className: 'modal-overlay',
|
||||||
|
onClick: (e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
React.createElement('div', {
|
||||||
|
className: 'modal-container'
|
||||||
|
}, [
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'header',
|
||||||
|
className: 'flex justify-between items-center p-4 border-b border-neutral-700'
|
||||||
|
}, [
|
||||||
|
React.createElement('h2', {
|
||||||
|
className: 'text-xl font-semibold text-rose-400'
|
||||||
|
}, 'Changer le mot de passe'),
|
||||||
|
React.createElement('button', {
|
||||||
|
onClick: onClose,
|
||||||
|
className: 'btn btn-secondary'
|
||||||
|
}, '×')
|
||||||
|
]),
|
||||||
|
React.createElement('form', {
|
||||||
|
key: 'form',
|
||||||
|
onSubmit: handleSubmit,
|
||||||
|
className: 'p-4 space-y-4'
|
||||||
|
}, [
|
||||||
|
React.createElement('input', {
|
||||||
|
type: 'hidden',
|
||||||
|
name: 'csrf_token',
|
||||||
|
value: csrfToken
|
||||||
|
}),
|
||||||
|
React.createElement('input', {
|
||||||
|
type: 'hidden',
|
||||||
|
name: 'action',
|
||||||
|
value: 'change_password'
|
||||||
|
}),
|
||||||
|
React.createElement('div', {
|
||||||
|
className: 'space-y-2'
|
||||||
|
}, [
|
||||||
|
React.createElement('label', {
|
||||||
|
className: 'block text-sm text-neutral-400'
|
||||||
|
}, 'Mot de passe actuel'),
|
||||||
|
React.createElement('input', {
|
||||||
|
type: 'password',
|
||||||
|
name: 'current_password',
|
||||||
|
required: true,
|
||||||
|
className: 'w-full p-2 bg-neutral-800 border border-neutral-700 rounded focus:border-rose-500 focus:outline-none'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
React.createElement('div', {
|
||||||
|
className: 'space-y-2'
|
||||||
|
}, [
|
||||||
|
React.createElement('label', {
|
||||||
|
className: 'block text-sm text-neutral-400'
|
||||||
|
}, 'Nouveau mot de passe'),
|
||||||
|
React.createElement('input', {
|
||||||
|
type: 'password',
|
||||||
|
name: 'new_password',
|
||||||
|
required: true,
|
||||||
|
className: 'w-full p-2 bg-neutral-800 border border-neutral-700 rounded focus:border-rose-500 focus:outline-none'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
React.createElement('div', {
|
||||||
|
className: 'space-y-2'
|
||||||
|
}, [
|
||||||
|
React.createElement('label', {
|
||||||
|
className: 'block text-sm text-neutral-400'
|
||||||
|
}, 'Confirmer le nouveau mot de passe'),
|
||||||
|
React.createElement('input', {
|
||||||
|
type: 'password',
|
||||||
|
name: 'confirm_password',
|
||||||
|
required: true,
|
||||||
|
className: 'w-full p-2 bg-neutral-800 border border-neutral-700 rounded focus:border-rose-500 focus:outline-none'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
React.createElement('div', {
|
||||||
|
className: 'flex justify-end gap-2 pt-4'
|
||||||
|
}, [
|
||||||
|
React.createElement('button', {
|
||||||
|
type: 'button',
|
||||||
|
onClick: onClose,
|
||||||
|
className: 'btn btn-secondary'
|
||||||
|
}, 'Annuler'),
|
||||||
|
React.createElement('button', {
|
||||||
|
type: 'submit',
|
||||||
|
className: 'btn'
|
||||||
|
}, 'Changer le mot de passe')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// App wrapper
|
||||||
|
function PasswordModalApp({ csrfToken }) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const btn = document.getElementById('changePasswordBtn');
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gérer la fermeture avec la touche Echap
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleEscape = (e) => {
|
||||||
|
if (e.key === 'Escape' && isOpen) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Ajouter la gestion du clic en dehors de la modale
|
||||||
|
const handleOutsideClick = (e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(ChangePasswordModal, {
|
||||||
|
isOpen,
|
||||||
|
onClose: handleClose,
|
||||||
|
onSubmit: handleSubmit,
|
||||||
|
csrfToken: csrfToken,
|
||||||
|
onOverlayClick: handleOutsideClick
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisation
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('passwordModalRoot'));
|
||||||
|
root.render(React.createElement(PasswordModalApp, {
|
||||||
|
csrfToken: window.CSRF_TOKEN
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modifier la fonction PasswordModalApp pour accepter le token en props
|
||||||
|
function PasswordModalApp({ csrfToken }) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const btn = document.getElementById('changePasswordBtn');
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', () => setIsOpen(true));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (formData) => {
|
||||||
|
try {
|
||||||
|
const form = new FormData();
|
||||||
|
Object.entries(formData).forEach(([key, value]) => {
|
||||||
|
form.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch('admin.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert('Mot de passe modifié avec succès');
|
||||||
|
setIsOpen(false);
|
||||||
|
} else {
|
||||||
|
alert('Erreur lors du changement de mot de passe');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Erreur lors du changement de mot de passe');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(ChangePasswordModal, {
|
||||||
|
isOpen,
|
||||||
|
onClose: () => setIsOpen(false),
|
||||||
|
onSubmit: handleSubmit,
|
||||||
|
csrfToken: csrfToken
|
||||||
|
});
|
||||||
|
}
|
18
layout.php
18
layout.php
@ -14,19 +14,27 @@ if (!defined('CYLA_CORE')) {
|
|||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel/7.22.5/babel.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="passwordModalRoot"></div>
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-brand">
|
<div class="nav-brand">
|
||||||
<a href="index.php"><?php echo SITE_NAME; ?></a>
|
<a href="index.php"><?php echo SITE_NAME; ?></a>
|
||||||
</div>
|
</div>
|
||||||
<?php if (Cyla::isLoggedIn()): ?>
|
<?php if (Cyla::isLoggedIn()): ?>
|
||||||
<div class="nav-menu">
|
<div class="nav-menu">
|
||||||
<a href="admin.php" class="nav-link">Administration</a>
|
<a href="admin.php" class="nav-link">Administration</a>
|
||||||
<a href="logout.php?csrf_token=<?php echo Cyla::generateCSRFToken(); ?>" class="nav-link">Déconnexion</a>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
id="changePasswordBtn"
|
||||||
|
class="nav-link"
|
||||||
|
onclick="console.log('Clic natif détecté')"
|
||||||
|
>
|
||||||
|
Changer le mot de passe
|
||||||
|
</button>
|
||||||
|
<a href="logout.php?csrf_token=<?php echo Cyla::generateCSRFToken(); ?>" class="nav-link">Déconnexion</a>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
211
share.php
211
share.php
@ -10,25 +10,60 @@ if (!isset($_GET['file'])) {
|
|||||||
$error = 'Aucun fichier spécifié';
|
$error = 'Aucun fichier spécifié';
|
||||||
} else {
|
} else {
|
||||||
$filename = $_GET['file'];
|
$filename = $_GET['file'];
|
||||||
$filepath = UPLOAD_DIR . $filename;
|
$path = $_GET['path'] ?? 'fichiers/';
|
||||||
|
|
||||||
// Vérifier si le fichier existe
|
// Déterminer le chemin complet selon le dossier
|
||||||
if (!file_exists($filepath)) {
|
$basePath = '';
|
||||||
|
switch ($path) {
|
||||||
|
case 'v1/img/fichiers/':
|
||||||
|
$basePath = __DIR__ . '/v1/img/fichiers/';
|
||||||
|
break;
|
||||||
|
case 'v2/file/':
|
||||||
|
$basePath = __DIR__ . '/v2/file/';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$basePath = UPLOAD_DIR;
|
||||||
|
$path = 'fichiers/'; // Assurer que le path par défaut est correct
|
||||||
|
}
|
||||||
|
|
||||||
|
$filepath = $basePath . $filename;
|
||||||
|
|
||||||
|
// Vérifier que le fichier existe et est un fichier régulier
|
||||||
|
if (!file_exists($filepath) || !is_file($filepath)) {
|
||||||
$error = 'Fichier introuvable';
|
$error = 'Fichier introuvable';
|
||||||
} else {
|
} else {
|
||||||
// Récupérer les informations du fichier
|
// Vérifier que le chemin est sécurisé
|
||||||
$file_info = [
|
$realpath = realpath($filepath);
|
||||||
'name' => $filename,
|
$allowed = false;
|
||||||
'size' => filesize($filepath),
|
|
||||||
'extension' => strtolower(pathinfo($filename, PATHINFO_EXTENSION)),
|
if (strpos($realpath, realpath(UPLOAD_DIR)) === 0) {
|
||||||
'preview_type' => getPreviewType(pathinfo($filename, PATHINFO_EXTENSION))
|
$allowed = true;
|
||||||
];
|
} else {
|
||||||
|
foreach (LEGACY_UPLOAD_DIRS as $dir) {
|
||||||
|
if (strpos($realpath, realpath($dir)) === 0) {
|
||||||
|
$allowed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$allowed) {
|
||||||
|
$error = 'Accès non autorisé';
|
||||||
|
} else {
|
||||||
|
// Récupérer les informations du fichier
|
||||||
|
$file_info = [
|
||||||
|
'name' => $filename,
|
||||||
|
'size' => filesize($filepath),
|
||||||
|
'extension' => strtolower(pathinfo($filename, PATHINFO_EXTENSION)),
|
||||||
|
'preview_type' => getPreviewType(pathinfo($filename, PATHINFO_EXTENSION))
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construction de l'URL absolue du fichier
|
// Construction des URLs
|
||||||
$file_url = SITE_URL . 'fichiers/' . ($file_info ? rawurlencode($file_info['name']) : '');
|
$file_url = SITE_URL . $path . ($file_info ? rawurlencode($file_info['name']) : '');
|
||||||
$share_url = SITE_URL . 'share.php?file=' . ($file_info ? rawurlencode($file_info['name']) : '');
|
$share_url = SITE_URL . 'share.php?file=' . ($file_info ? rawurlencode($file_info['name']) : '') . '&path=' . urlencode($path);
|
||||||
|
|
||||||
// Contenu de la page
|
// Contenu de la page
|
||||||
$pageTitle = $file_info ? $file_info['name'] : 'Fichier introuvable';
|
$pageTitle = $file_info ? $file_info['name'] : 'Fichier introuvable';
|
||||||
@ -71,8 +106,13 @@ ob_start(); ?>
|
|||||||
</audio>
|
</audio>
|
||||||
<?php elseif ($file_info['preview_type'] === 'text'): ?>
|
<?php elseif ($file_info['preview_type'] === 'text'): ?>
|
||||||
<pre class="text-preview"><?php
|
<pre class="text-preview"><?php
|
||||||
|
// Lire et afficher le contenu du fichier texte de manière sécurisée
|
||||||
$content = file_get_contents($filepath);
|
$content = file_get_contents($filepath);
|
||||||
echo Cyla::escape($content);
|
if ($content !== false) {
|
||||||
|
echo Cyla::escape($content);
|
||||||
|
} else {
|
||||||
|
echo "Erreur lors de la lecture du fichier";
|
||||||
|
}
|
||||||
?></pre>
|
?></pre>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="no-preview">
|
<div class="no-preview">
|
||||||
@ -92,7 +132,7 @@ ob_start(); ?>
|
|||||||
id="share-url"
|
id="share-url"
|
||||||
value="<?php echo Cyla::escape($share_url); ?>"
|
value="<?php echo Cyla::escape($share_url); ?>"
|
||||||
readonly>
|
readonly>
|
||||||
<button class="btn" onclick="copyToClipboard('share-url', 'Lien de partage copié !')">
|
<button class="btn" onclick="handleCopy('share-url', 'Lien de partage copié !')">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -105,7 +145,7 @@ ob_start(); ?>
|
|||||||
id="direct-url"
|
id="direct-url"
|
||||||
value="<?php echo Cyla::escape($file_url); ?>"
|
value="<?php echo Cyla::escape($file_url); ?>"
|
||||||
readonly>
|
readonly>
|
||||||
<button class="btn" onclick="copyToClipboard('direct-url', 'Lien direct copié !')">
|
<button class="btn" onclick="handleCopy('direct-url', 'Lien direct copié !')">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -126,7 +166,7 @@ ob_start(); ?>
|
|||||||
}
|
}
|
||||||
?>"
|
?>"
|
||||||
readonly>
|
readonly>
|
||||||
<button class="btn" onclick="copyToClipboard('embed-code', 'Code d\'intégration copié !')">
|
<button class="btn" onclick="handleCopy('embed-code', 'Code d\'intégration copié !')">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -142,132 +182,18 @@ ob_start(); ?>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Styles spécifiques à la page de partage */
|
|
||||||
.share-container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-container h1 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-meta {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-container {
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
min-height: 200px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-content {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-preview {
|
|
||||||
width: 100%;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow: auto;
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-preview {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.extension-badge {
|
|
||||||
background-color: var(--color-bg-alt);
|
|
||||||
color: var(--color-primary);
|
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-link {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles responsifs */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.input-group {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group .btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animation 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from { transform: translateY(100%); opacity: 0; }
|
|
||||||
to { transform: translateY(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Fonction pour copier dans le presse-papier avec notification
|
// Fonction pour gérer la copie
|
||||||
function copyToClipboard(elementId, message) {
|
async function handleCopy(elementId, message) {
|
||||||
const element = document.getElementById(elementId);
|
const input = document.getElementById(elementId);
|
||||||
element.select();
|
if (!input) {
|
||||||
|
console.error(`Element with id ${elementId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
document.execCommand('copy');
|
// Utiliser l'API Clipboard moderne
|
||||||
|
await navigator.clipboard.writeText(input.value);
|
||||||
|
|
||||||
// Créer et afficher la notification
|
// Créer et afficher la notification
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
@ -283,9 +209,6 @@ function copyToClipboard(elementId, message) {
|
|||||||
console.error('Erreur lors de la copie :', err);
|
console.error('Erreur lors de la copie :', err);
|
||||||
alert('Erreur lors de la copie');
|
alert('Erreur lors de la copie');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Désélectionner le texte
|
|
||||||
element.blur();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user