From 4fa8395994b478a867afbc9efc0143cb64c7c70b Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 30 Dec 2025 20:16:44 +0100 Subject: =?UTF-8?q?upload=20de=20documents=20+=20s=C3=A9curit=C3=A9s=20att?= =?UTF-8?q?aque=20par=20t=C3=A9l=C3=A9chargement=20de=20fichier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.php | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 121 insertions(+), 10 deletions(-) (limited to 'public') diff --git a/public/index.php b/public/index.php index d32dfff..d8d26e2 100644 --- a/public/index.php +++ b/public/index.php @@ -57,6 +57,36 @@ function sanitizeFileName(string $filename): string { return trim($filename, '_'); } +function checkFileDownload(array $file): bool +{ + $extensions_white_list = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp']; + $mime_type_white_list = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.presentation']; + + // 1/ extension + $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if(!in_array($extension, $extensions_white_list, true)){ + return false; + } + + // 2/ fichier obtenu par HTTP POST (théoriquement inutile si le routeur est solide, mais ça ne mange pas de pain) + if(!is_uploaded_file($file['tmp_name'])){ + return false; + } + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + + // 3/ objet $finfo valide (dépend du paramètre FILEINFO_MIME_TYPE) + if($finfo === false){ + return false; + } + + $real_type = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + + // 4/ contrôle du "vrai" type mime (finfo_file lit les 1ers octets des fichiers pour y trouver des "signatures", très fiable sauf avec les conteneurs: doc, zip...) + return in_array($real_type, $mime_type_white_list, true); +} + if(isset($_GET['action']) && $_GET['action'] == 'editor_submit'){ // récupération des données $data = file_get_contents('php://input'); @@ -95,12 +125,12 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'delete_article'){ die; } elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image'){ - if (isset($_FILES['file'])) { + if(isset($_FILES['file'])){ $file = $_FILES['file']; $dest = 'images/'; - if(!is_dir($dest)) { // Vérifier si le répertoire existe, sinon le créer - mkdir($dest, 0777, true); + if(!is_dir($dest)){ + mkdir($dest, 0755, true); } $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif']; @@ -111,8 +141,13 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image'){ } $file_path = $dest . $name . '_' . uniqid() . '.' . $extension; + if(!is_uploaded_file($file['tmp_name'])) { + http_response_code(500); + echo json_encode(['message' => "Le fichier n'a pas été téléchargé correctement."]); + } + if(imagickCleanImage(file_get_contents($file['tmp_name']), $file_path, $extension)){ // recréer l’image pour la nettoyer - echo json_encode(['location' => $file_path]); // renvoyer l'URL de l'image téléchargée + echo json_encode(['location' => $file_path]); } else{ http_response_code(500); @@ -132,8 +167,8 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_url'){ $image_data = curlDownloadImage($json['image_url']); // téléchargement de l’image par le serveur avec cURL au lieu de file_get_contents $dest = 'images/'; - if(!is_dir($dest)) { // Vérifier si le répertoire existe, sinon le créer - mkdir($dest, 0777, true); + if(!is_dir($dest)){ + mkdir($dest, 0755, true); } if($image_data === false){ @@ -152,7 +187,7 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_url'){ $local_path = $dest . $name . '_' . uniqid() . '.' . $extension; if(imagickCleanImage($image_data, $local_path, $extension)){ // recréer l’image pour la nettoyer - echo json_encode(['location' => $local_path]); // nouvelle adresse + echo json_encode(['location' => $local_path]); } else{ http_response_code(500); @@ -168,8 +203,8 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_base64'){ $json = json_decode(file_get_contents('php://input'), true); $dest = 'images/'; - if(!is_dir('images')){ - mkdir('images', 0777, true); + if(!is_dir($dest)){ + mkdir($dest, 0755, true); } // détection de data:image/ et de ;base64, et capture du format dans $type @@ -203,6 +238,39 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_base64'){ } die; } +elseif(isset($_GET['action']) && $_GET['action'] == 'upload_file'){ + if(isset($_FILES['file'])){ + $file = $_FILES['file']; + $dest = 'media/'; + + if(!is_dir($dest)){ // Vérifier si le répertoire existe, sinon le créer + mkdir($dest, 0755, true); + } + + $name = sanitizeFileName(pathinfo($file['name'], PATHINFO_FILENAME)); // retirer caractères spéciaux et changer espaces en underscores + $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + $file_path = $dest . $name . '_' . uniqid() . '.' . $extension; // nom unique + + if(checkFileDownload($file)){ + if(move_uploaded_file($file['tmp_name'], $file_path)){ + echo json_encode(['location' => $file_path]); + } + else{ + http_response_code(500); + echo json_encode(['message' => 'Erreur enregistrement du fichier.']); + } + } + else{ + http_response_code(400); + echo json_encode(['message' => 'Erreur 400: fichier non valide.']); + } + } + else{ + http_response_code(400); + echo json_encode(['message' => 'Erreur 400: Bad Request']); + } + die; +} ?> @@ -356,6 +424,20 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_base64'){ }, // upload d'image natif de tinymce avec le bouton "Insérer une image" images_upload_handler: images_upload_handler, // = fonction fléchée + // upload de documents (bouton "insérer un lien") + files_upload_handler: files_upload_handler, // = fonction fléchée + documents_file_types: [ // files_upload_handler a besoin qu'on lui donne tous les types mime + { mimeType: 'application/pdf', extensions: [ 'pdf' ] }, + { mimeType: 'application/msword', extensions: [ 'doc' ] }, + { mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', extensions: [ 'docx' ] }, + { mimeType: 'application/vnd.ms-excel', extensions: [ 'xls' ] }, + { mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', extensions: [ 'xlsx' ] }, + { mimeType: 'application/vnd.ms-powerpoint', extensions: [ 'ppt' ] }, + { mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', extensions: [ 'pptx' ] }, + { mimeType: 'application/vnd.oasis.opendocument.text', extensions: [ 'odt' ] }, + { mimeType: 'application/vnd.oasis.opendocument.spreadsheet', extensions: [ 'ods' ] }, + { mimeType: 'application/vnd.oasis.opendocument.presentation', extensions: [ 'odp' ] } + ], image_caption: true }); @@ -374,7 +456,7 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_base64'){ .then(response => response.json()) .then(data => { if(data.location) { - resolve(data.location); + resolve({url: data.location}); } else { reject("Erreur: Chemin d'image invalide"); @@ -385,6 +467,35 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_base64'){ }); }); + const files_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { + const formData = new FormData(); + formData.append("file", blobInfo.blob()); + + fetch('index.php?action=upload_file', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if(data.location) { + console.log(blobInfo.filename()); + console.log(data.location); + + // resolve et reject fonctionne avec Promise => type de retour standardisé et évite l'utilistion de callbacks + resolve({ + url: data.location, + fileName: blobInfo.filename(), + }); + } + else { + reject("Erreur: Chemin du fichier invalide"); + } + }) + .catch(error => { + reject("Erreur lors de l'upload"); + }); + }); + function deleteArticle(articleId) { if (confirm('Voulez-vous vraiment supprimer cet article ?')) { // Envoyer une requête au serveur pour supprimer l'article -- cgit v1.2.3