Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
ae8476d404 | |||
acb80e4668 | |||
fd431a332a | |||
fd1ac715db | |||
3cacb4645f | |||
0018ac8554 | |||
b31f6fff8d | |||
9f54cc1519 | |||
5d901cb8b2 | |||
f8675a312b | |||
eebe3372bc | |||
b100499a15 |
@ -1,8 +1,4 @@
|
||||
# Cyla
|
||||
Site d'hébergement de fichiers simple, réduit au strict nécessaire.
|
||||
|
||||
|
||||
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)
|
||||
![image](https://concepts.esenjin.xyz/cyla/fichiers/6787d3fe886e4_1736954878.png)
|
283
admin.php
283
admin.php
@ -240,128 +240,201 @@ ob_start(); ?>
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>Changer le mot de passe</h2>
|
||||
<form method="POST" class="password-form">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo Cyla::generateCSRFToken(); ?>">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
<?php
|
||||
// Traitement de la suppression de fichier
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_file') {
|
||||
if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
|
||||
header('Location: admin.php?error=' . urlencode('Token de sécurité invalide'));
|
||||
exit;
|
||||
} else if (!isset($_POST['filename'])) {
|
||||
header('Location: admin.php?error=' . urlencode('Nom de fichier manquant'));
|
||||
exit;
|
||||
} else {
|
||||
$result = Cyla::deleteFile($_POST['filename']);
|
||||
if ($result['success']) {
|
||||
header('Location: admin.php?success=' . urlencode('Fichier supprimé avec succès'));
|
||||
exit;
|
||||
} else {
|
||||
header('Location: admin.php?error=' . urlencode($result['error']));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="current_password">Mot de passe actuel</label>
|
||||
<input type="password" id="current_password" name="current_password" required>
|
||||
// Récupération de la page courante depuis l'URL
|
||||
$currentPage = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$filesPerPage = 20;
|
||||
|
||||
// Récupération des fichiers avec pagination
|
||||
$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="<?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 class="form-group">
|
||||
<label for="new_password">Nouveau mot de passe</label>
|
||||
<input type="password" id="new_password" name="new_password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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>
|
||||
|
||||
<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="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>
|
||||
<?php endif; ?>
|
||||
</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; ?>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?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>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
// Fonction pour copier le lien de partage
|
||||
function copyShareLink(url) {
|
||||
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 => {
|
||||
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
|
||||
$content = ob_get_clean();
|
||||
require 'layout.php';
|
||||
|
49
api.php
49
api.php
@ -2,8 +2,12 @@
|
||||
define('CYLA_CORE', true);
|
||||
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é
|
||||
if (!Cyla::isLoggedIn()) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Non autorisé']);
|
||||
exit;
|
||||
@ -11,6 +15,7 @@ if (!Cyla::isLoggedIn()) {
|
||||
|
||||
// Vérifier le token CSRF
|
||||
if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Token CSRF invalide']);
|
||||
exit;
|
||||
@ -18,18 +23,35 @@ if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])
|
||||
|
||||
// Gérer l'upload de fichier
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
|
||||
$validation = Cyla::validateUpload($_FILES['file']);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!$validation['valid']) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => $validation['error']]);
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
// Vérification de l'espace disque disponible
|
||||
$uploadDir = UPLOAD_DIR;
|
||||
$freeSpace = disk_free_space($uploadDir);
|
||||
if ($freeSpace < $_FILES['file']['size']) {
|
||||
throw new Exception('Espace disque insuffisant');
|
||||
}
|
||||
|
||||
$filename = Cyla::generateUniqueFilename($_FILES['file']['name']);
|
||||
$destination = UPLOAD_DIR . $filename;
|
||||
$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');
|
||||
}
|
||||
|
||||
if (move_uploaded_file($_FILES['file']['tmp_name'], $destination)) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'file' => [
|
||||
@ -38,9 +60,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
|
||||
'url' => 'share.php?file=' . urlencode($filename)
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Erreur lors de l\'upload du fichier']);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'error' => $e->getMessage(),
|
||||
'details' => error_get_last()
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
@ -7,10 +7,14 @@ if (!defined('CYLA_CORE')) {
|
||||
|
||||
// Site configuration
|
||||
define('SITE_NAME', 'Cyla');
|
||||
define('SITE_VERSION', '3.0.0');
|
||||
define('SITE_VERSION', '3.0.2');
|
||||
define('SITE_URL', 'https://concepts.esenjin.xyz/cyla/');
|
||||
|
||||
// Files configuration
|
||||
define('LEGACY_UPLOAD_DIRS', [
|
||||
__DIR__ . '/v1/img/fichiers/',
|
||||
__DIR__ . '/v2/file/'
|
||||
]);
|
||||
define('UPLOAD_DIR', __DIR__ . '/fichiers/');
|
||||
define('MAX_FILE_SIZE', 100 * 1024 * 1024); // 100 Mo en octets
|
||||
define('ALLOWED_EXTENSIONS', [
|
||||
|
115
core.php
115
core.php
@ -131,22 +131,123 @@ class Cyla {
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les fichiers uploadés
|
||||
* @return array
|
||||
* Liste les fichiers uploadés avec pagination
|
||||
* @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 = [];
|
||||
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);
|
||||
$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[] = [
|
||||
'name' => basename($file),
|
||||
'size' => filesize($file),
|
||||
'extension' => strtolower($info['extension']),
|
||||
'extension' => strtolower($info['extension'] ?? ''),
|
||||
'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);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
/* Navigation */
|
||||
.nav-link,
|
||||
button.nav-link {
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -343,3 +351,370 @@ footer {
|
||||
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 { X, Upload, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { X, Upload, CheckCircle, AlertCircle, Loader } from 'lucide-react';
|
||||
|
||||
const UploadZone = () => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [files, setFiles] = useState([]);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 Mo
|
||||
const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt'];
|
||||
const MAX_FILES = 10; // Limite stricte à 10 fichiers
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const validateFile = (file) => {
|
||||
const validateFile = useCallback((file) => {
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
||||
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: 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 formData = new FormData();
|
||||
formData.append('file', fileInfo.file);
|
||||
formData.append('action', 'upload');
|
||||
|
||||
try {
|
||||
// Simuler un upload progressif pour la démo
|
||||
await new Promise(resolve => {
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += 10;
|
||||
setFiles(files =>
|
||||
files.map(f =>
|
||||
f.id === fileInfo.id
|
||||
? { ...f, progress, status: progress === 100 ? 'complete' : 'uploading' }
|
||||
: f
|
||||
)
|
||||
);
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, 500);
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileInfo.file);
|
||||
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]')?.value || '');
|
||||
|
||||
// Mettre le statut en uploading
|
||||
setFiles(files =>
|
||||
files.map(f =>
|
||||
f.id === fileInfo.id ? { ...f, status: 'uploading' } : f
|
||||
)
|
||||
);
|
||||
|
||||
const response = await fetch('api.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
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) {
|
||||
setFiles(files =>
|
||||
files.map(f =>
|
||||
f.id === fileInfo.id
|
||||
? { ...f, status: 'error', error: 'Erreur lors du téléversement' }
|
||||
? { ...f, status: 'error', error: error.message }
|
||||
: f
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadAllFiles = () => {
|
||||
const pendingFiles = files.filter(f => f.status === 'pending');
|
||||
pendingFiles.forEach(uploadFile);
|
||||
const handleFiles = (newFiles) => {
|
||||
// Vérifier si on n'a pas déjà atteint la limite
|
||||
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) => {
|
||||
switch (status) {
|
||||
case 'complete': return 'text-green-500';
|
||||
case 'error': return 'text-red-500';
|
||||
case 'uploading': return 'text-blue-500';
|
||||
default: return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
const handleDrop = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
handleFiles(e.dataTransfer.files);
|
||||
}, []);
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'complete': return <CheckCircle className="w-5 h-5" />;
|
||||
case 'error': return <AlertCircle className="w-5 h-5" />;
|
||||
default: return null;
|
||||
}
|
||||
const removeFile = (id) => {
|
||||
setFiles(files => files.filter(f => f.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -125,8 +107,14 @@ const UploadZone = () => {
|
||||
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'}
|
||||
hover:border-primary-500`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={e => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={e => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
@ -134,7 +122,7 @@ const UploadZone = () => {
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
onChange={e => handleFiles(e.target.files)}
|
||||
className="hidden"
|
||||
accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')}
|
||||
/>
|
||||
@ -147,20 +135,16 @@ const UploadZone = () => {
|
||||
ou cliquez pour sélectionner des fichiers
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<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">
|
||||
<h3 className="font-medium">Files ({files.length})</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 className="p-4 bg-gray-800 border-b border-gray-700">
|
||||
<h3 className="font-medium">Fichiers ({files.length}/{MAX_FILES})</h3>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-700">
|
||||
@ -176,22 +160,20 @@ const UploadZone = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fileInfo.status === 'uploading' && (
|
||||
<div className="w-32 h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 transition-all duration-300"
|
||||
style={{ width: `${fileInfo.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={getStatusColor(fileInfo.status)}>
|
||||
{getStatusIcon(fileInfo.status)}
|
||||
<div className={`
|
||||
${fileInfo.status === 'complete' ? 'text-green-500' : ''}
|
||||
${fileInfo.status === 'error' ? 'text-red-500' : ''}
|
||||
${fileInfo.status === 'uploading' ? 'text-blue-500' : ''}
|
||||
`}>
|
||||
{fileInfo.status === 'complete' && <CheckCircle className="w-5 h-5" />}
|
||||
{fileInfo.status === 'error' && <AlertCircle className="w-5 h-5" />}
|
||||
{fileInfo.status === 'uploading' && <Loader className="w-5 h-5 animate-spin" />}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => removeFile(fileInfo.id)}
|
||||
className="p-1 hover:bg-gray-700 rounded"
|
||||
title={fileInfo.status === 'uploading' ? 'Annuler' : 'Supprimer'}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</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">
|
||||
<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/babel/7.22.5/babel.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="passwordModalRoot"></div>
|
||||
<header>
|
||||
<nav>
|
||||
<div class="nav-brand">
|
||||
<a href="index.php"><?php echo SITE_NAME; ?></a>
|
||||
</div>
|
||||
<?php if (Cyla::isLoggedIn()): ?>
|
||||
<div class="nav-menu">
|
||||
<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>
|
||||
</div>
|
||||
<div class="nav-menu">
|
||||
<a href="admin.php" class="nav-link">Administration</a>
|
||||
<button
|
||||
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; ?>
|
||||
</nav>
|
||||
</header>
|
||||
|
209
share.php
209
share.php
@ -10,25 +10,60 @@ if (!isset($_GET['file'])) {
|
||||
$error = 'Aucun fichier spécifié';
|
||||
} else {
|
||||
$filename = $_GET['file'];
|
||||
$filepath = UPLOAD_DIR . $filename;
|
||||
$path = $_GET['path'] ?? 'fichiers/';
|
||||
|
||||
// Vérifier si le fichier existe
|
||||
if (!file_exists($filepath)) {
|
||||
// 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;
|
||||
$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';
|
||||
} 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))
|
||||
];
|
||||
// Vérifier que le chemin est sécurisé
|
||||
$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) {
|
||||
$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
|
||||
$file_url = SITE_URL . 'fichiers/' . ($file_info ? rawurlencode($file_info['name']) : '');
|
||||
$share_url = SITE_URL . 'share.php?file=' . ($file_info ? rawurlencode($file_info['name']) : '');
|
||||
// Construction des URLs
|
||||
$file_url = SITE_URL . $path . ($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
|
||||
$pageTitle = $file_info ? $file_info['name'] : 'Fichier introuvable';
|
||||
@ -71,8 +106,13 @@ ob_start(); ?>
|
||||
</audio>
|
||||
<?php elseif ($file_info['preview_type'] === 'text'): ?>
|
||||
<pre class="text-preview"><?php
|
||||
// Lire et afficher le contenu du fichier texte de manière sécurisée
|
||||
$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>
|
||||
<?php else: ?>
|
||||
<div class="no-preview">
|
||||
@ -92,7 +132,7 @@ ob_start(); ?>
|
||||
id="share-url"
|
||||
value="<?php echo Cyla::escape($share_url); ?>"
|
||||
readonly>
|
||||
<button class="btn" onclick="copyToClipboard('share-url', 'Lien de partage copié !')">
|
||||
<button class="btn" onclick="handleCopy('share-url', 'Lien de partage copié !')">
|
||||
Copier
|
||||
</button>
|
||||
</div>
|
||||
@ -105,7 +145,7 @@ ob_start(); ?>
|
||||
id="direct-url"
|
||||
value="<?php echo Cyla::escape($file_url); ?>"
|
||||
readonly>
|
||||
<button class="btn" onclick="copyToClipboard('direct-url', 'Lien direct copié !')">
|
||||
<button class="btn" onclick="handleCopy('direct-url', 'Lien direct copié !')">
|
||||
Copier
|
||||
</button>
|
||||
</div>
|
||||
@ -126,7 +166,7 @@ ob_start(); ?>
|
||||
}
|
||||
?>"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
@ -142,132 +182,18 @@ ob_start(); ?>
|
||||
</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>
|
||||
// Fonction pour copier dans le presse-papier avec notification
|
||||
function copyToClipboard(elementId, message) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.select();
|
||||
// Fonction pour gérer la copie
|
||||
async function handleCopy(elementId, message) {
|
||||
const input = document.getElementById(elementId);
|
||||
if (!input) {
|
||||
console.error(`Element with id ${elementId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
// Utiliser l'API Clipboard moderne
|
||||
await navigator.clipboard.writeText(input.value);
|
||||
|
||||
// Créer et afficher la notification
|
||||
const notification = document.createElement('div');
|
||||
@ -283,9 +209,6 @@ function copyToClipboard(elementId, message) {
|
||||
console.error('Erreur lors de la copie :', err);
|
||||
alert('Erreur lors de la copie');
|
||||
}
|
||||
|
||||
// Désélectionner le texte
|
||||
element.blur();
|
||||
}
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
Loading…
x
Reference in New Issue
Block a user