From 83619ac611490e8cd652a0252349704100fc5148 Mon Sep 17 00:00:00 2001 From: polo Date: Wed, 18 Jun 2025 15:10:21 +0200 Subject: =?UTF-8?q?collage=20contenu=20HTML=20dans=20l'=C3=A9diteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/tinymce.js | 53 +++++++++++++++++++++++++++++--- src/controller/ajax.php | 82 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/public/js/tinymce.js b/public/js/tinymce.js index 307c384..55a37cc 100644 --- a/public/js/tinymce.js +++ b/public/js/tinymce.js @@ -30,7 +30,7 @@ function openEditor(id, page = '') { toolbar_mode: 'wrap', statusbar: false, setup: function (editor) { - editor.on('init', function () { + editor.on('init', function (){ editors[id] = editor; // boutons "Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Soumettre" @@ -48,6 +48,53 @@ function openEditor(id, page = '') { document.querySelector(`#new-${id}`).classList.add('hidden'); // id = new-new-id_node } }); + editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur + let parser = new DOMParser(); + let doc = parser.parseFromString(e.content, 'text/html'); + let images = doc.querySelectorAll('img'); + + let downloads_in_progress = []; + + images.forEach(img => { + if(img.src.startsWith('file://')){ // détection d'images non insérables + console.warn('Image locale non insérable dans tinymce :', img.src); + img.outerHTML = '
' + +"Image locale non insérée. Pour insérer une image depuis LibreOffice, copiez l'image seule et recoller." + +'
'; + } + else if(img.src.startsWith('http')){ // détection d'images web + let promise = fetch('index.php?action=upload_image_url', { // promesse d'un fichier téléchargeable sur le serveur + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ image_url: img.src }) + }) + .then(response => response.json()) + .then(data => { + if(data.location){ + img.src = data.location; // remplacer l'image par celle du serveur + } + }) + .catch(error => { + console.error('Erreur lors de l’upload de l’image :', error); + }); + + downloads_in_progress.push(promise); + } + }); + + // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images + if(downloads_in_progress.length > 0){ + e.preventDefault(); + + Promise.all(downloads_in_progress).then(() => { + e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) + editor.insertContent(e.content); + }); + } + else{ + e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) + } + }); // fin editor.on('PastePreProcess'... }, // upload d'image images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { @@ -86,9 +133,7 @@ function deleteArticle(id, page = '') { // Envoyer une requête au serveur pour supprimer l'article fetch('index.php?action=delete_article', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id: id }) }) .then(response => response.json()) diff --git a/src/controller/ajax.php b/src/controller/ajax.php index 18f303b..943c027 100644 --- a/src/controller/ajax.php +++ b/src/controller/ajax.php @@ -7,10 +7,62 @@ use App\Entity\Page; use App\Entity\Node; use App\Entity\Article; + +// mettre ça ailleurs +function imagickCleanImage(string $image_data, string $local_path): bool // "string" parce que file_get_contents... +{ + try{ + $imagick = new Imagick(); + $imagick->readImageBlob($image_data); + $imagick->stripImage(); // nettoyage métadonnées + $imagick->setImageFormat('jpeg'); + $imagick->setImageCompression(Imagick::COMPRESSION_JPEG); + $imagick->setImageCompressionQuality(85); // optionnel + $imagick->writeImage($local_path); // enregistrement + $imagick->clear(); + $imagick->destroy(); + return true; + } + catch(Exception $e){ + return false; + } +} +function curlDownloadImage(string $url, $maxRetries = 3, $timeout = 10): string|false +{ + $attempt = 0; + $imageData = false; + + while($attempt < $maxRetries){ + $ch = curl_init($url); // instance de CurlHandle + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'TinyMCE-Image-Downloader'); + + $imageData = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + //$curlError = curl_error($ch); + + curl_close($ch); + + if($imageData !== false && $httpCode >= 200 && $httpCode < 300){ + return $imageData; + } + + $attempt++; + sleep(1); + } + + return false; // échec après trois tentatives +} + + // détection des requêtes d'upload d'image de tinymce if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_GET['action']) && $_GET['action'] === 'upload_image') { - if (isset($_FILES['file'])) { + if(isset($_FILES['file'])){ $file = $_FILES['file']; $dest = 'images/'; $dest_mini = 'images-mini/'; @@ -26,6 +78,7 @@ if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_ $filePath = $dest . basename($file['name']); // créer une miniature de l'image + // if(move_uploaded_file($file['tmp_name'], $filePath)) { $image_url = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']); @@ -42,6 +95,33 @@ if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_ } die; } +// cas du collage d'un contenu HTML, réception d'une URL, téléchargement par le serveur et renvoie de l'adresse sur le serveur +elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_url'){ + $json = json_decode(file_get_contents('php://input'), true); + + if(isset($json['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 + + if($image_data === false){ + http_response_code(400); + echo json_encode(['message' => "Erreur, le serveur n'a pas réussi à télécharger l'image."]); + die; + } + + $local_path = 'images/' . uniqid() . '.jpg'; + if(imagickCleanImage($image_data, $local_path)){ // recréer l’image pour la nettoyer + echo json_encode(['location' => $local_path]); // nouvelle adresse + } + else{ + http_response_code(500); + echo json_encode(['message' => 'Erreur image non valide']); + } + } + else{ + echo json_encode(['message' => 'Erreur 400: Bad Request']); + } + die; +} // détection des requêtes de type XHR, y en a pas à priori -- cgit v1.2.3