diff options
-rw-r--r-- | public/js/tinymce.js | 53 | ||||
-rw-r--r-- | 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 = '') { | |||
30 | toolbar_mode: 'wrap', | 30 | toolbar_mode: 'wrap', |
31 | statusbar: false, | 31 | statusbar: false, |
32 | setup: function (editor) { | 32 | setup: function (editor) { |
33 | editor.on('init', function () { | 33 | editor.on('init', function (){ |
34 | editors[id] = editor; | 34 | editors[id] = editor; |
35 | 35 | ||
36 | // boutons "Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Soumettre" | 36 | // boutons "Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Soumettre" |
@@ -48,6 +48,53 @@ function openEditor(id, page = '') { | |||
48 | document.querySelector(`#new-${id}`).classList.add('hidden'); // id = new-new-id_node | 48 | document.querySelector(`#new-${id}`).classList.add('hidden'); // id = new-new-id_node |
49 | } | 49 | } |
50 | }); | 50 | }); |
51 | editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur | ||
52 | let parser = new DOMParser(); | ||
53 | let doc = parser.parseFromString(e.content, 'text/html'); | ||
54 | let images = doc.querySelectorAll('img'); | ||
55 | |||
56 | let downloads_in_progress = []; | ||
57 | |||
58 | images.forEach(img => { | ||
59 | if(img.src.startsWith('file://')){ // détection d'images non insérables | ||
60 | console.warn('Image locale non insérable dans tinymce :', img.src); | ||
61 | img.outerHTML = '<div style="border:1px solid red; padding:10px; margin:5px 0; background-color:#ffe6e6; color:#a94442; font-size:14px;">' + | ||
62 | "Image locale non insérée. Pour insérer une image depuis LibreOffice, copiez l'image seule et recoller." + | ||
63 | '</div>'; | ||
64 | } | ||
65 | else if(img.src.startsWith('http')){ // détection d'images web | ||
66 | let promise = fetch('index.php?action=upload_image_url', { // promesse d'un fichier téléchargeable sur le serveur | ||
67 | method: 'POST', | ||
68 | headers: { 'Content-Type': 'application/json' }, | ||
69 | body: JSON.stringify({ image_url: img.src }) | ||
70 | }) | ||
71 | .then(response => response.json()) | ||
72 | .then(data => { | ||
73 | if(data.location){ | ||
74 | img.src = data.location; // remplacer l'image par celle du serveur | ||
75 | } | ||
76 | }) | ||
77 | .catch(error => { | ||
78 | console.error('Erreur lors de l’upload de l’image :', error); | ||
79 | }); | ||
80 | |||
81 | downloads_in_progress.push(promise); | ||
82 | } | ||
83 | }); | ||
84 | |||
85 | // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images | ||
86 | if(downloads_in_progress.length > 0){ | ||
87 | e.preventDefault(); | ||
88 | |||
89 | Promise.all(downloads_in_progress).then(() => { | ||
90 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | ||
91 | editor.insertContent(e.content); | ||
92 | }); | ||
93 | } | ||
94 | else{ | ||
95 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | ||
96 | } | ||
97 | }); // fin editor.on('PastePreProcess'... | ||
51 | }, | 98 | }, |
52 | // upload d'image | 99 | // upload d'image |
53 | images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { | 100 | images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { |
@@ -86,9 +133,7 @@ function deleteArticle(id, page = '') { | |||
86 | // Envoyer une requête au serveur pour supprimer l'article | 133 | // Envoyer une requête au serveur pour supprimer l'article |
87 | fetch('index.php?action=delete_article', { | 134 | fetch('index.php?action=delete_article', { |
88 | method: 'POST', | 135 | method: 'POST', |
89 | headers: { | 136 | headers: {'Content-Type': 'application/json'}, |
90 | 'Content-Type': 'application/json' | ||
91 | }, | ||
92 | body: JSON.stringify({ id: id }) | 137 | body: JSON.stringify({ id: id }) |
93 | }) | 138 | }) |
94 | .then(response => response.json()) | 139 | .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; | |||
7 | use App\Entity\Node; | 7 | use App\Entity\Node; |
8 | use App\Entity\Article; | 8 | use App\Entity\Article; |
9 | 9 | ||
10 | |||
11 | // mettre ça ailleurs | ||
12 | function imagickCleanImage(string $image_data, string $local_path): bool // "string" parce que file_get_contents... | ||
13 | { | ||
14 | try{ | ||
15 | $imagick = new Imagick(); | ||
16 | $imagick->readImageBlob($image_data); | ||
17 | $imagick->stripImage(); // nettoyage métadonnées | ||
18 | $imagick->setImageFormat('jpeg'); | ||
19 | $imagick->setImageCompression(Imagick::COMPRESSION_JPEG); | ||
20 | $imagick->setImageCompressionQuality(85); // optionnel | ||
21 | $imagick->writeImage($local_path); // enregistrement | ||
22 | $imagick->clear(); | ||
23 | $imagick->destroy(); | ||
24 | return true; | ||
25 | } | ||
26 | catch(Exception $e){ | ||
27 | return false; | ||
28 | } | ||
29 | } | ||
30 | function curlDownloadImage(string $url, $maxRetries = 3, $timeout = 10): string|false | ||
31 | { | ||
32 | $attempt = 0; | ||
33 | $imageData = false; | ||
34 | |||
35 | while($attempt < $maxRetries){ | ||
36 | $ch = curl_init($url); // instance de CurlHandle | ||
37 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | ||
38 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | ||
39 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); | ||
40 | curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); | ||
41 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); | ||
42 | curl_setopt($ch, CURLOPT_USERAGENT, 'TinyMCE-Image-Downloader'); | ||
43 | |||
44 | $imageData = curl_exec($ch); | ||
45 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | ||
46 | //$curlError = curl_error($ch); | ||
47 | |||
48 | curl_close($ch); | ||
49 | |||
50 | if($imageData !== false && $httpCode >= 200 && $httpCode < 300){ | ||
51 | return $imageData; | ||
52 | } | ||
53 | |||
54 | $attempt++; | ||
55 | sleep(1); | ||
56 | } | ||
57 | |||
58 | return false; // échec après trois tentatives | ||
59 | } | ||
60 | |||
61 | |||
10 | // détection des requêtes d'upload d'image de tinymce | 62 | // détection des requêtes d'upload d'image de tinymce |
11 | if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_GET['action']) && $_GET['action'] === 'upload_image') | 63 | if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_GET['action']) && $_GET['action'] === 'upload_image') |
12 | { | 64 | { |
13 | if (isset($_FILES['file'])) { | 65 | if(isset($_FILES['file'])){ |
14 | $file = $_FILES['file']; | 66 | $file = $_FILES['file']; |
15 | $dest = 'images/'; | 67 | $dest = 'images/'; |
16 | $dest_mini = 'images-mini/'; | 68 | $dest_mini = 'images-mini/'; |
@@ -26,6 +78,7 @@ if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_ | |||
26 | $filePath = $dest . basename($file['name']); | 78 | $filePath = $dest . basename($file['name']); |
27 | 79 | ||
28 | // créer une miniature de l'image | 80 | // créer une miniature de l'image |
81 | // | ||
29 | 82 | ||
30 | if(move_uploaded_file($file['tmp_name'], $filePath)) { | 83 | if(move_uploaded_file($file['tmp_name'], $filePath)) { |
31 | $image_url = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']); | 84 | $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($_ | |||
42 | } | 95 | } |
43 | die; | 96 | die; |
44 | } | 97 | } |
98 | // 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 | ||
99 | elseif(isset($_GET['action']) && $_GET['action'] == 'upload_image_url'){ | ||
100 | $json = json_decode(file_get_contents('php://input'), true); | ||
101 | |||
102 | if(isset($json['image_url'])){ | ||
103 | $image_data = curlDownloadImage($json['image_url']); // téléchargement de l’image par le serveur avec cURL au lieu de file_get_contents | ||
104 | |||
105 | if($image_data === false){ | ||
106 | http_response_code(400); | ||
107 | echo json_encode(['message' => "Erreur, le serveur n'a pas réussi à télécharger l'image."]); | ||
108 | die; | ||
109 | } | ||
110 | |||
111 | $local_path = 'images/' . uniqid() . '.jpg'; | ||
112 | if(imagickCleanImage($image_data, $local_path)){ // recréer l’image pour la nettoyer | ||
113 | echo json_encode(['location' => $local_path]); // nouvelle adresse | ||
114 | } | ||
115 | else{ | ||
116 | http_response_code(500); | ||
117 | echo json_encode(['message' => 'Erreur image non valide']); | ||
118 | } | ||
119 | } | ||
120 | else{ | ||
121 | echo json_encode(['message' => 'Erreur 400: Bad Request']); | ||
122 | } | ||
123 | die; | ||
124 | } | ||
45 | 125 | ||
46 | 126 | ||
47 | // détection des requêtes de type XHR, y en a pas à priori | 127 | // détection des requêtes de type XHR, y en a pas à priori |