Compare commits

...

6 Commits
3.0.1 ... main

6 changed files with 294 additions and 195 deletions

View File

@ -1,8 +1,4 @@
# Cyla # Cyla
Site d'hébergement de fichiers simple, réduit au strict nécessaire. Site d'hébergement de fichiers simple, réduit au strict nécessaire.
Le projet se base sur la première version de [QuadFile](https://github.com/QuadPiece/QuadFile).
![image](https://concepts.esenjin.xyz/cyla/fichiers/6787d3fe886e4_1736954878.png) ![image](https://concepts.esenjin.xyz/cyla/fichiers/6787d3fe886e4_1736954878.png)

View File

@ -289,56 +289,57 @@ $files = $fileData['files'];
<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="fichiers/<?php echo Cyla::escape($file['name']); ?>" <img src="<?php echo $file['path'] . 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']); ?>
</div>
<?php endif; ?>
</div>
<div class="file-info">
<p class="file-name"><?php echo Cyla::escape($file['name']); ?></p>
<p class="file-meta">
<?php echo Cyla::escape(round($file['size'] / 1024, 2)); ?> Ko
· <?php echo date('d/m/Y H:i', $file['uploaded']); ?>
</p>
<div class="file-actions">
<a href="share.php?file=<?php echo urlencode($file['name']); ?>"
class="btn btn-secondary"
target="_blank">Voir</a>
<button class="btn"
onclick="copyShareLink('<?php echo SITE_URL; ?>share.php?file=<?php echo urlencode($file['name']); ?>')">
Copier le lien
</button>
<button class="btn btn-danger"
onclick="confirmDelete('<?php echo Cyla::escape($file['name']); ?>')">
×
</button>
</div>
</div>
</div> </div>
<?php endif; ?>
<!-- Formulaire caché pour la suppression -->
<form id="deleteForm-<?php echo Cyla::escape($file['name']); ?>"
action="admin.php"
method="POST"
style="display: none;">
<input type="hidden" name="action" value="delete_file">
<input type="hidden" name="filename" value="<?php echo Cyla::escape($file['name']); ?>">
<input type="hidden" name="csrf_token" value="<?php echo Cyla::generateCSRFToken(); ?>">
</form>
<?php endforeach; ?>
</div> </div>
<div class="file-info">
<p class="file-name"><?php echo Cyla::escape($file['name']); ?></p>
<p class="file-meta">
<?php echo Cyla::escape(round($file['size'] / 1024, 2)); ?> Ko
· <?php echo date('d/m/Y H:i', $file['uploaded']); ?>
· <?php echo $file['path']; ?>
</p>
<div class="file-actions">
<a href="share.php?file=<?php echo urlencode($file['name']); ?>&path=<?php echo urlencode($file['path']); ?>"
class="btn btn-secondary"
target="_blank">Voir</a>
<button class="btn"
onclick="copyShareLink('<?php echo SITE_URL; ?>share.php?file=<?php echo urlencode($file['name']); ?>&path=<?php echo urlencode($file['path']); ?>')">
Copier le lien
</button>
<button class="btn btn-danger"
onclick="confirmDelete('<?php echo Cyla::escape($file['name']); ?>', '<?php echo Cyla::escape($file['path']); ?>')">
×
</button>
</div>
</div>
</div>
<!-- Formulaire caché pour la suppression -->
<form id="deleteForm-<?php echo Cyla::escape($file['name']); ?>"
action="admin.php"
method="POST"
style="display: none;">
<input type="hidden" name="action" value="delete_file">
<input type="hidden" name="filename" value="<?php echo Cyla::escape($file['name']); ?>">
<input type="hidden" name="path" value="<?php echo Cyla::escape($file['path']); ?>">
<input type="hidden" name="csrf_token" value="<?php echo Cyla::generateCSRFToken(); ?>">
</form>
<?php endforeach; ?>
<?php if ($fileData['totalPages'] > 1): ?> <?php if ($fileData['totalPages'] > 1): ?>
<div class="pagination"> <div class="pagination">
<?php if ($fileData['currentPage'] > 1): ?> <?php if ($fileData['currentPage'] > 1): ?>
<a href="?page=<?php echo $fileData['currentPage'] - 1; ?>" <a href="?page=<?php echo $fileData['currentPage'] - 1; ?>"
class="pagination-link">&laquo; Précédent</a> class="pagination-link">&laquo;</a>
<?php endif; ?> <?php endif; ?>
<?php <?php
@ -365,7 +366,7 @@ $files = $fileData['files'];
if ($fileData['currentPage'] < $fileData['totalPages']): ?> if ($fileData['currentPage'] < $fileData['totalPages']): ?>
<a href="?page=<?php echo $fileData['currentPage'] + 1; ?>" <a href="?page=<?php echo $fileData['currentPage'] + 1; ?>"
class="pagination-link">Suivant &raquo;</a> class="pagination-link">&raquo;</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>

View File

@ -7,10 +7,14 @@ if (!defined('CYLA_CORE')) {
// Site configuration // Site configuration
define('SITE_NAME', 'Cyla'); define('SITE_NAME', 'Cyla');
define('SITE_VERSION', '3.0.1'); define('SITE_VERSION', '3.0.2');
define('SITE_URL', 'https://concepts.esenjin.xyz/cyla/'); define('SITE_URL', 'https://concepts.esenjin.xyz/cyla/');
// Files configuration // Files configuration
define('LEGACY_UPLOAD_DIRS', [
__DIR__ . '/v1/img/fichiers/',
__DIR__ . '/v2/file/'
]);
define('UPLOAD_DIR', __DIR__ . '/fichiers/'); define('UPLOAD_DIR', __DIR__ . '/fichiers/');
define('MAX_FILE_SIZE', 100 * 1024 * 1024); // 100 Mo en octets define('MAX_FILE_SIZE', 100 * 1024 * 1024); // 100 Mo en octets
define('ALLOWED_EXTENSIONS', [ define('ALLOWED_EXTENSIONS', [

View File

@ -138,7 +138,17 @@ class Cyla {
*/ */
public static function listFiles($page = 1, $perPage = 20) { public static function listFiles($page = 1, $perPage = 20) {
$files = []; $files = [];
$allFiles = glob(UPLOAD_DIR . '*'); $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) // Trier les fichiers par date de modification (plus récent en premier)
usort($allFiles, function($a, $b) { usort($allFiles, function($a, $b) {
@ -148,7 +158,7 @@ class Cyla {
// Calculer la pagination // Calculer la pagination
$total = count($allFiles); $total = count($allFiles);
$totalPages = ceil($total / $perPage); $totalPages = ceil($total / $perPage);
$page = max(1, min($page, $totalPages)); // Garantir que la page est dans les limites $page = max(1, min($page, $totalPages));
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
// Récupérer uniquement les fichiers de la page courante // Récupérer uniquement les fichiers de la page courante
@ -156,12 +166,24 @@ class Cyla {
foreach ($pageFiles as $file) { 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
]; ];
} }
@ -173,23 +195,50 @@ class Cyla {
'perPage' => $perPage 'perPage' => $perPage
]; ];
} }
/** /**
* Supprime un fichier * Supprime un fichier
* @param string $filename Nom du fichier à supprimer * @param string $filename Nom du fichier à supprimer
* @param string $path Chemin relatif du fichier
* @return array ['success' => bool, 'error' => string|null] * @return array ['success' => bool, 'error' => string|null]
*/ */
public static function deleteFile($filename) { public static function deleteFile($filename, $path = 'fichiers/') {
$filepath = UPLOAD_DIR . $filename; // 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;
}
// Vérifier que le fichier existe et est dans le dossier d'upload $filepath = $basePath . $filename;
// Vérifier que le fichier existe et est dans le bon dossier
if (!file_exists($filepath) || !is_file($filepath)) { if (!file_exists($filepath) || !is_file($filepath)) {
return ['success' => false, 'error' => 'Fichier introuvable']; return ['success' => false, 'error' => 'Fichier introuvable'];
} }
// Vérifier que le fichier est bien dans le dossier d'upload // Vérifier que le fichier est bien dans un des dossiers autorisés
$realpath = realpath($filepath); $realpath = realpath($filepath);
$uploadDir = realpath(UPLOAD_DIR); $allowed = false;
if (strpos($realpath, $uploadDir) !== 0) {
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é']; return ['success' => false, 'error' => 'Chemin de fichier non autorisé'];
} }

View File

@ -450,10 +450,11 @@ footer {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: var(--spacing-sm); gap: var(--spacing-xs);
margin-top: var(--spacing-lg); margin: var(--spacing-lg) auto;
padding-top: var(--spacing-md); padding-top: var(--spacing-md);
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
max-width: 100%;
} }
.pagination-link { .pagination-link {
@ -462,13 +463,15 @@ footer {
justify-content: center; justify-content: center;
min-width: 2rem; min-width: 2rem;
height: 2rem; height: 2rem;
padding: 0 var(--spacing-sm); padding: 0 0.5rem;
color: var(--color-text); color: var(--color-text);
text-decoration: none; text-decoration: none;
background-color: var(--color-bg); background-color: var(--color-bg);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
transition: all 0.2s ease; transition: all 0.2s ease;
white-space: nowrap;
font-size: 0.9rem;
} }
.pagination-link:hover { .pagination-link:hover {
@ -483,7 +486,8 @@ footer {
.pagination-ellipsis { .pagination-ellipsis {
color: var(--color-text-muted); color: var(--color-text-muted);
padding: 0 var(--spacing-xs); padding: 0 0.25rem;
user-select: none;
} }
/* Bouton de suppression */ /* Bouton de suppression */
@ -553,6 +557,14 @@ footer {
@media (max-width: 768px) { @media (max-width: 768px) {
.pagination { .pagination {
flex-wrap: wrap; 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 { .file-actions {
@ -591,3 +603,118 @@ footer {
from { opacity: 1; } from { opacity: 1; }
to { opacity: 0; } 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; }
}

186
share.php
View File

@ -10,25 +10,60 @@ if (!isset($_GET['file'])) {
$error = 'Aucun fichier spécifié'; $error = 'Aucun fichier spécifié';
} else { } else {
$filename = $_GET['file']; $filename = $_GET['file'];
$filepath = UPLOAD_DIR . $filename; $path = $_GET['path'] ?? 'fichiers/';
// Vérifier si le fichier existe // Déterminer le chemin complet selon le dossier
if (!file_exists($filepath)) { $basePath = '';
switch ($path) {
case 'v1/img/fichiers/':
$basePath = __DIR__ . '/v1/img/fichiers/';
break;
case 'v2/file/':
$basePath = __DIR__ . '/v2/file/';
break;
default:
$basePath = UPLOAD_DIR;
$path = 'fichiers/'; // Assurer que le path par défaut est correct
}
$filepath = $basePath . $filename;
// Vérifier que le fichier existe et est un fichier régulier
if (!file_exists($filepath) || !is_file($filepath)) {
$error = 'Fichier introuvable'; $error = 'Fichier introuvable';
} else { } else {
// Récupérer les informations du fichier // Vérifier que le chemin est sécurisé
$file_info = [ $realpath = realpath($filepath);
'name' => $filename, $allowed = false;
'size' => filesize($filepath),
'extension' => strtolower(pathinfo($filename, PATHINFO_EXTENSION)), if (strpos($realpath, realpath(UPLOAD_DIR)) === 0) {
'preview_type' => getPreviewType(pathinfo($filename, PATHINFO_EXTENSION)) $allowed = true;
]; } else {
foreach (LEGACY_UPLOAD_DIRS as $dir) {
if (strpos($realpath, realpath($dir)) === 0) {
$allowed = true;
break;
}
}
}
if (!$allowed) {
$error = 'Accès non autorisé';
} else {
// Récupérer les informations du fichier
$file_info = [
'name' => $filename,
'size' => filesize($filepath),
'extension' => strtolower(pathinfo($filename, PATHINFO_EXTENSION)),
'preview_type' => getPreviewType(pathinfo($filename, PATHINFO_EXTENSION))
];
}
} }
} }
// Construction de l'URL absolue du fichier // Construction des URLs
$file_url = SITE_URL . 'fichiers/' . ($file_info ? rawurlencode($file_info['name']) : ''); $file_url = SITE_URL . $path . ($file_info ? rawurlencode($file_info['name']) : '');
$share_url = SITE_URL . 'share.php?file=' . ($file_info ? rawurlencode($file_info['name']) : ''); $share_url = SITE_URL . 'share.php?file=' . ($file_info ? rawurlencode($file_info['name']) : '') . '&path=' . urlencode($path);
// Contenu de la page // Contenu de la page
$pageTitle = $file_info ? $file_info['name'] : 'Fichier introuvable'; $pageTitle = $file_info ? $file_info['name'] : 'Fichier introuvable';
@ -71,8 +106,13 @@ ob_start(); ?>
</audio> </audio>
<?php elseif ($file_info['preview_type'] === 'text'): ?> <?php elseif ($file_info['preview_type'] === 'text'): ?>
<pre class="text-preview"><?php <pre class="text-preview"><?php
// Lire et afficher le contenu du fichier texte de manière sécurisée
$content = file_get_contents($filepath); $content = file_get_contents($filepath);
echo Cyla::escape($content); if ($content !== false) {
echo Cyla::escape($content);
} else {
echo "Erreur lors de la lecture du fichier";
}
?></pre> ?></pre>
<?php else: ?> <?php else: ?>
<div class="no-preview"> <div class="no-preview">
@ -142,124 +182,6 @@ ob_start(); ?>
</div> </div>
</div> </div>
<style>
/* Styles spécifiques à la page de partage */
.share-container {
max-width: 800px;
margin: 0 auto;
}
.share-container h1 {
color: var(--color-primary);
margin-bottom: var(--spacing-sm);
word-break: break-all;
}
.file-meta {
color: var(--color-text-muted);
margin-bottom: var(--spacing-lg);
}
.preview-container {
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
margin-bottom: var(--spacing-lg);
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.preview-content {
max-width: 100%;
max-height: 600px;
}
.text-preview {
width: 100%;
max-height: 600px;
overflow: auto;
padding: var(--spacing-md);
background-color: var(--color-bg);
color: var(--color-text);
font-family: monospace;
white-space: pre-wrap;
}
.no-preview {
text-align: center;
padding: var(--spacing-lg);
color: var(--color-text-muted);
}
.extension-badge {
background-color: var(--color-bg-alt);
color: var(--color-primary);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--border-radius);
font-size: 2rem;
font-weight: bold;
margin-bottom: var(--spacing-md);
}
.share-actions {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.share-link {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.input-group {
display: flex;
gap: var(--spacing-sm);
}
.input-group input {
flex: 1;
}
/* Styles responsifs */
@media (max-width: 768px) {
.input-group {
flex-direction: column;
}
.input-group .btn {
width: 100%;
}
}
/* Animation de notification */
.notification {
position: fixed;
bottom: var(--spacing-lg);
right: var(--spacing-lg);
background-color: var(--color-success);
color: white;
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--border-radius);
animation: slideIn 0.3s ease-out, fadeOut 0.3s ease-in 2.7s forwards;
z-index: 1000;
}
@keyframes slideIn {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
<script> <script>
// Fonction pour gérer la copie // Fonction pour gérer la copie
async function handleCopy(elementId, message) { async function handleCopy(elementId, message) {