Compare commits
No commits in common. "main" and "3.0.0" have entirely different histories.
@ -1,4 +1,8 @@
|
|||||||
# 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)
|
231
admin.php
231
admin.php
@ -240,59 +240,42 @@ ob_start(); ?>
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<div class="card">
|
||||||
// Traitement de la suppression de fichier
|
<h2>Changer le mot de passe</h2>
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_file') {
|
<form method="POST" class="password-form">
|
||||||
if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
|
<input type="hidden" name="csrf_token" value="<?php echo Cyla::generateCSRFToken(); ?>">
|
||||||
header('Location: admin.php?error=' . urlencode('Token de sécurité invalide'));
|
<input type="hidden" name="action" value="change_password">
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupération de la page courante depuis l'URL
|
<div class="form-group">
|
||||||
$currentPage = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
<label for="current_password">Mot de passe actuel</label>
|
||||||
$filesPerPage = 20;
|
<input type="password" id="current_password" name="current_password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
// Récupération des fichiers avec pagination
|
<div class="form-group">
|
||||||
$fileData = Cyla::listFiles($currentPage, $filesPerPage);
|
<label for="new_password">Nouveau mot de passe</label>
|
||||||
$files = $fileData['files'];
|
<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 class="card">
|
<div class="card">
|
||||||
<h2>Fichiers hébergés</h2>
|
<h2>Fichiers hébergés</h2>
|
||||||
|
|
||||||
<?php if (empty($files)): ?>
|
<?php if (empty($files)): ?>
|
||||||
<p class="text-muted">Aucun fichier hébergé</p>
|
<p class="text-muted">Aucun fichier hébergé</p>
|
||||||
<?php else: ?>
|
<?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">
|
<div class="file-list">
|
||||||
<?php foreach ($files as $file): ?>
|
<?php foreach ($files as $file): ?>
|
||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-preview">
|
<div class="file-preview">
|
||||||
<?php if ($file['preview_type'] === 'image'): ?>
|
<?php if ($file['preview_type'] === 'image'): ?>
|
||||||
<img src="<?php echo $file['path'] . Cyla::escape($file['name']); ?>"
|
<img src="fichiers/<?php echo Cyla::escape($file['name']); ?>" alt="<?php echo Cyla::escape($file['name']); ?>">
|
||||||
alt="<?php echo Cyla::escape($file['name']); ?>">
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="preview-placeholder">
|
<div class="preview-placeholder">
|
||||||
<?php echo strtoupper($file['extension']); ?>
|
<?php echo strtoupper($file['extension']); ?>
|
||||||
@ -305,136 +288,80 @@ $files = $fileData['files'];
|
|||||||
<p class="file-meta">
|
<p class="file-meta">
|
||||||
<?php echo Cyla::escape(round($file['size'] / 1024, 2)); ?> Ko
|
<?php echo Cyla::escape(round($file['size'] / 1024, 2)); ?> Ko
|
||||||
· <?php echo date('d/m/Y H:i', $file['uploaded']); ?>
|
· <?php echo date('d/m/Y H:i', $file['uploaded']); ?>
|
||||||
· <?php echo $file['path']; ?>
|
|
||||||
</p>
|
</p>
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<a href="share.php?file=<?php echo urlencode($file['name']); ?>&path=<?php echo urlencode($file['path']); ?>"
|
<a href="share.php?file=<?php echo urlencode($file['name']); ?>" class="btn btn-secondary" target="_blank">Voir</a>
|
||||||
class="btn btn-secondary"
|
<button class="btn" onclick="copyShareLink('<?php echo SITE_URL; ?>share.php?file=<?php echo urlencode($file['name']); ?>')">Copier le lien</button>
|
||||||
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>
|
</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 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; ?>
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</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(() => {
|
||||||
// Créer et afficher la notification
|
alert('Lien copié dans le presse-papier');
|
||||||
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);
|
||||||
// En cas d'erreur, afficher une notification d'erreur
|
alert('Erreur lors de la copie du lien');
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = 'notification error';
|
|
||||||
notification.textContent = 'Erreur lors de la copie';
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.remove();
|
|
||||||
}, 3000);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Fonction de confirmation de suppression
|
|
||||||
function confirmDelete(filename) {
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.className = 'confirmation-overlay';
|
|
||||||
|
|
||||||
const dialog = document.createElement('div');
|
|
||||||
dialog.className = 'confirmation-dialog';
|
|
||||||
dialog.innerHTML = `
|
|
||||||
<h3>Confirmer la suppression</h3>
|
|
||||||
<p>Voulez-vous vraiment supprimer le fichier "${filename}" ?</p>
|
|
||||||
<div class="confirmation-actions">
|
|
||||||
<button class="btn btn-secondary" onclick="closeConfirmDialog()">Annuler</button>
|
|
||||||
<button class="btn" onclick="submitDelete('${filename}')">Supprimer</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
document.body.appendChild(dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour fermer la modale
|
|
||||||
function closeConfirmDialog() {
|
|
||||||
document.querySelector('.confirmation-overlay')?.remove();
|
|
||||||
document.querySelector('.confirmation-dialog')?.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour soumettre la suppression
|
|
||||||
function submitDelete(filename) {
|
|
||||||
document.getElementById('deleteForm-' + filename).submit();
|
|
||||||
closeConfirmDialog();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<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';
|
||||||
|
43
api.php
43
api.php
@ -2,12 +2,8 @@
|
|||||||
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;
|
||||||
@ -15,7 +11,6 @@ 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;
|
||||||
@ -23,35 +18,18 @@ 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'])) {
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
$validation = Cyla::validateUpload($_FILES['file']);
|
$validation = Cyla::validateUpload($_FILES['file']);
|
||||||
|
|
||||||
if (!$validation['valid']) {
|
if (!$validation['valid']) {
|
||||||
throw new Exception($validation['error']);
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => $validation['error']]);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = Cyla::generateUniqueFilename($_FILES['file']['name']);
|
$filename = Cyla::generateUniqueFilename($_FILES['file']['name']);
|
||||||
$destination = $uploadDir . $filename;
|
$destination = UPLOAD_DIR . $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([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'file' => [
|
'file' => [
|
||||||
@ -60,12 +38,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
|
|||||||
'url' => 'share.php?file=' . urlencode($filename)
|
'url' => 'share.php?file=' . urlencode($filename)
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
} catch (Exception $e) {
|
} else {
|
||||||
http_response_code(400);
|
http_response_code(500);
|
||||||
echo json_encode([
|
echo json_encode(['error' => 'Erreur lors de l\'upload du fichier']);
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'details' => error_get_last()
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,10 @@ if (!defined('CYLA_CORE')) {
|
|||||||
|
|
||||||
// Site configuration
|
// Site configuration
|
||||||
define('SITE_NAME', 'Cyla');
|
define('SITE_NAME', 'Cyla');
|
||||||
define('SITE_VERSION', '3.0.2');
|
define('SITE_VERSION', '3.0.0');
|
||||||
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,123 +131,22 @@ class Cyla {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liste les fichiers uploadés avec pagination
|
* Liste les fichiers uploadés
|
||||||
* @param int $page Numéro de la page (commence à 1)
|
* @return array
|
||||||
* @param int $perPage Nombre d'éléments par page
|
|
||||||
* @return array ['files' => array, 'total' => int, 'totalPages' => int]
|
|
||||||
*/
|
*/
|
||||||
public static function listFiles($page = 1, $perPage = 20) {
|
public static function listFiles() {
|
||||||
$files = [];
|
$files = [];
|
||||||
$allFiles = [];
|
foreach (glob(UPLOAD_DIR . '*') as $file) {
|
||||||
|
|
||||||
// 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,23 +69,15 @@ nav {
|
|||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation */
|
.nav-link {
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,370 +343,3 @@ footer {
|
|||||||
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
121
index.php
@ -1,121 +0,0 @@
|
|||||||
<?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,16 +1,25 @@
|
|||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { X, Upload, CheckCircle, AlertCircle, Loader } from 'lucide-react';
|
import { X, Upload, CheckCircle, AlertCircle } 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_FILES = 10; // Limite stricte à 10 fichiers
|
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 Mo
|
||||||
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt'];
|
||||||
const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt'];
|
|
||||||
|
|
||||||
const validateFile = useCallback((file) => {
|
const handleDragOver = (e) => {
|
||||||
|
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` };
|
||||||
@ -19,86 +28,95 @@ 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 uploadFile = async (fileInfo) => {
|
const handleDrop = (e) => {
|
||||||
try {
|
e.preventDefault();
|
||||||
const formData = new FormData();
|
setIsDragging(false);
|
||||||
formData.append('file', fileInfo.file);
|
|
||||||
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]')?.value || '');
|
|
||||||
|
|
||||||
// Mettre le statut en uploading
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||||
setFiles(files =>
|
addFiles(droppedFiles);
|
||||||
files.map(f =>
|
};
|
||||||
f.id === fileInfo.id ? { ...f, status: 'uploading' } : f
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await fetch('api.php', {
|
const handleFileSelect = (e) => {
|
||||||
method: 'POST',
|
const selectedFiles = Array.from(e.target.files);
|
||||||
body: formData
|
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
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
setFiles(currentFiles => [...currentFiles, ...processedFiles]);
|
||||||
|
};
|
||||||
|
|
||||||
if (!response.ok || !result.success) {
|
const removeFile = (fileId) => {
|
||||||
throw new Error(result.error || 'Erreur lors du téléversement');
|
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 =>
|
setFiles(files =>
|
||||||
files.map(f =>
|
files.map(f =>
|
||||||
f.id === fileInfo.id ? { ...f, status: 'complete' } : f
|
f.id === fileInfo.id
|
||||||
|
? { ...f, progress, status: progress === 100 ? 'complete' : 'uploading' }
|
||||||
|
: f
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
} 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: error.message }
|
? { ...f, status: 'error', error: 'Erreur lors du téléversement' }
|
||||||
: f
|
: f
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFiles = (newFiles) => {
|
const uploadAllFiles = () => {
|
||||||
// Vérifier si on n'a pas déjà atteint la limite
|
const pendingFiles = files.filter(f => f.status === 'pending');
|
||||||
if (files.length >= MAX_FILES) {
|
pendingFiles.forEach(uploadFile);
|
||||||
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 handleDrop = useCallback((e) => {
|
const getStatusColor = (status) => {
|
||||||
e.preventDefault();
|
switch (status) {
|
||||||
setIsDragging(false);
|
case 'complete': return 'text-green-500';
|
||||||
handleFiles(e.dataTransfer.files);
|
case 'error': return 'text-red-500';
|
||||||
}, []);
|
case 'uploading': return 'text-blue-500';
|
||||||
|
default: return 'text-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const removeFile = (id) => {
|
const getStatusIcon = (status) => {
|
||||||
setFiles(files => files.filter(f => f.id !== id));
|
switch (status) {
|
||||||
|
case 'complete': return <CheckCircle className="w-5 h-5" />;
|
||||||
|
case 'error': return <AlertCircle className="w-5 h-5" />;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -107,14 +125,8 @@ 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={e => {
|
onDragOver={handleDragOver}
|
||||||
e.preventDefault();
|
onDragLeave={handleDragLeave}
|
||||||
setIsDragging(true);
|
|
||||||
}}
|
|
||||||
onDragLeave={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(false);
|
|
||||||
}}
|
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
@ -122,7 +134,7 @@ const UploadZone = () => {
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
onChange={e => handleFiles(e.target.files)}
|
onChange={handleFileSelect}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')}
|
accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')}
|
||||||
/>
|
/>
|
||||||
@ -135,16 +147,20 @@ 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">
|
||||||
Maximum {MAX_FILES} fichiers à la fois · 100 Mo par fichier
|
Max 100 Mo par fichier · {ALLOWED_EXTENSIONS.join(', ')}
|
||||||
<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">
|
<div className="p-4 bg-gray-800 border-b border-gray-700 flex justify-between items-center">
|
||||||
<h3 className="font-medium">Fichiers ({files.length}/{MAX_FILES})</h3>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-gray-700">
|
<div className="divide-y divide-gray-700">
|
||||||
@ -160,20 +176,22 @@ const UploadZone = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`
|
{fileInfo.status === 'uploading' && (
|
||||||
${fileInfo.status === 'complete' ? 'text-green-500' : ''}
|
<div className="w-32 h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||||
${fileInfo.status === 'error' ? 'text-red-500' : ''}
|
<div
|
||||||
${fileInfo.status === 'uploading' ? 'text-blue-500' : ''}
|
className="h-full bg-blue-500 transition-all duration-300"
|
||||||
`}>
|
style={{ width: `${fileInfo.progress}%` }}
|
||||||
{fileInfo.status === 'complete' && <CheckCircle className="w-5 h-5" />}
|
/>
|
||||||
{fileInfo.status === 'error' && <AlertCircle className="w-5 h-5" />}
|
</div>
|
||||||
{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>
|
||||||
|
@ -1,200 +0,0 @@
|
|||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
10
layout.php
10
layout.php
@ -14,9 +14,9 @@ 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">
|
||||||
@ -25,14 +25,6 @@ if (!defined('CYLA_CORE')) {
|
|||||||
<?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>
|
||||||
<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>
|
<a href="logout.php?csrf_token=<?php echo Cyla::generateCSRFToken(); ?>" class="nav-link">Déconnexion</a>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
193
share.php
193
share.php
@ -10,45 +10,11 @@ if (!isset($_GET['file'])) {
|
|||||||
$error = 'Aucun fichier spécifié';
|
$error = 'Aucun fichier spécifié';
|
||||||
} else {
|
} else {
|
||||||
$filename = $_GET['file'];
|
$filename = $_GET['file'];
|
||||||
$path = $_GET['path'] ?? 'fichiers/';
|
$filepath = UPLOAD_DIR . $filename;
|
||||||
|
|
||||||
// Déterminer le chemin complet selon le dossier
|
// Vérifier si le fichier existe
|
||||||
$basePath = '';
|
if (!file_exists($filepath)) {
|
||||||
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 {
|
|
||||||
// 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 {
|
} else {
|
||||||
// Récupérer les informations du fichier
|
// Récupérer les informations du fichier
|
||||||
$file_info = [
|
$file_info = [
|
||||||
@ -59,11 +25,10 @@ if (!isset($_GET['file'])) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Construction des URLs
|
// Construction de l'URL absolue du fichier
|
||||||
$file_url = SITE_URL . $path . ($file_info ? rawurlencode($file_info['name']) : '');
|
$file_url = SITE_URL . 'fichiers/' . ($file_info ? rawurlencode($file_info['name']) : '');
|
||||||
$share_url = SITE_URL . 'share.php?file=' . ($file_info ? rawurlencode($file_info['name']) : '') . '&path=' . urlencode($path);
|
$share_url = SITE_URL . 'share.php?file=' . ($file_info ? rawurlencode($file_info['name']) : '');
|
||||||
|
|
||||||
// Contenu de la page
|
// Contenu de la page
|
||||||
$pageTitle = $file_info ? $file_info['name'] : 'Fichier introuvable';
|
$pageTitle = $file_info ? $file_info['name'] : 'Fichier introuvable';
|
||||||
@ -106,13 +71,8 @@ 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);
|
||||||
if ($content !== false) {
|
|
||||||
echo Cyla::escape($content);
|
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">
|
||||||
@ -132,7 +92,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="handleCopy('share-url', 'Lien de partage copié !')">
|
<button class="btn" onclick="copyToClipboard('share-url', 'Lien de partage copié !')">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -145,7 +105,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="handleCopy('direct-url', 'Lien direct copié !')">
|
<button class="btn" onclick="copyToClipboard('direct-url', 'Lien direct copié !')">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -166,7 +126,7 @@ ob_start(); ?>
|
|||||||
}
|
}
|
||||||
?>"
|
?>"
|
||||||
readonly>
|
readonly>
|
||||||
<button class="btn" onclick="handleCopy('embed-code', 'Code d\'intégration copié !')">
|
<button class="btn" onclick="copyToClipboard('embed-code', 'Code d\'intégration copié !')">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -182,18 +142,132 @@ ob_start(); ?>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<style>
|
||||||
// Fonction pour gérer la copie
|
/* Styles spécifiques à la page de partage */
|
||||||
async function handleCopy(elementId, message) {
|
.share-container {
|
||||||
const input = document.getElementById(elementId);
|
max-width: 800px;
|
||||||
if (!input) {
|
margin: 0 auto;
|
||||||
console.error(`Element with id ${elementId} not found`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Utiliser l'API Clipboard moderne
|
document.execCommand('copy');
|
||||||
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');
|
||||||
@ -209,6 +283,9 @@ async function handleCopy(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