aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2026-01-12 17:16:41 +0100
committerpolo <ordipolo@gmx.fr>2026-01-12 17:16:41 +0100
commit505cab0210e83bf62085b665abad0698a8d31352 (patch)
tree6e66d20d2041bae3f800ee99fdde1957c7bb9f51
parentdfed030ca6edf832397d273fbb0ad4b0b5d35877 (diff)
downloadcms-505cab0210e83bf62085b665abad0698a8d31352.tar.gz
cms-505cab0210e83bf62085b665abad0698a8d31352.tar.bz2
cms-505cab0210e83bf62085b665abad0698a8d31352.zip
upload de fichiers (JS + contrôleur + htaccess), collage de fichiers dans l'éditeur (JS), amélioration utilisation de Imagick (ImageUploadController)
-rw-r--r--public/js/tinymce.js261
-rw-r--r--public/user_data/media/.htaccess10
-rw-r--r--src/Captcha.php6
-rw-r--r--src/controller/FileUploadController.php67
-rw-r--r--src/controller/HeadFootController.php5
-rw-r--r--src/controller/ImageUploadController.php186
-rw-r--r--src/router.php7
7 files changed, 392 insertions, 150 deletions
diff --git a/public/js/tinymce.js b/public/js/tinymce.js
index a187189..7b59506 100644
--- a/public/js/tinymce.js
+++ b/public/js/tinymce.js
@@ -72,6 +72,9 @@ function deleteArticle(id){
72 72
73class Editor 73class Editor
74{ 74{
75 extensions_white_list = ['pdf', 'rtf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp'];
76 // = $extensions_white_list côté PHP
77
75 constructor(id){ 78 constructor(id){
76 this.id = id; 79 this.id = id;
77 this.article = document.getElementById(this.id); 80 this.article = document.getElementById(this.id);
@@ -108,7 +111,7 @@ class Editor
108 tinymce.init({ 111 tinymce.init({
109 selector: `[id="${this.id}"]`, // écrire [id="246"] au lieu de #246 parce que l'id commence par un chiffre 112 selector: `[id="${this.id}"]`, // écrire [id="246"] au lieu de #246 parce que l'id commence par un chiffre
110 language: 'fr_FR', 113 language: 'fr_FR',
111 language_url: 'js/tinymce-langs/fr_FR.js', // téléchargement ici: https://cdn.jsdelivr.net/npm/tinymce-lang/langs/fr_FR.min.js 114 language_url: 'js/tinymce-langs/fr_FR.js',
112 license_key: 'gpl', 115 license_key: 'gpl',
113 branding: false, 116 branding: false,
114 plugins: 'lists link autolink table image media autoresize help', 117 plugins: 'lists link autolink table image media autoresize help',
@@ -116,6 +119,13 @@ class Editor
116 menubar: false, 119 menubar: false,
117 toolbar_mode: 'wrap', 120 toolbar_mode: 'wrap',
118 statusbar: false, 121 statusbar: false,
122 link_title: false, // supprime le champ compliqué "titre" (apparaît au survol du lien) dans la fenêtre "link"
123 /*link_attributes_postprocess: (attrs) => { // modifier les attributs des liens créés
124 console.log(attrs);
125 if (attrs.rel) {
126 attrs.rel += 'noreferrer'; // cacher la page d'où on vient
127 }
128 },*/
119 // les fonctions fléchées permettent de garder le contexte (= this) 129 // les fonctions fléchées permettent de garder le contexte (= this)
120 setup: (editor) => { 130 setup: (editor) => {
121 editor.on('init', () => { 131 editor.on('init', () => {
@@ -141,55 +151,61 @@ class Editor
141 } 151 }
142 }); 152 });
143 let skipPastePreProcess = false; 153 let skipPastePreProcess = false;
144 editor.on('Paste', function (e){ // déclenchement AVANT PastePreProcess et quelque que soit le contenu collé 154 editor.on('Paste', (e) => { // déclenchement AVANT PastePreProcess et quelque que soit le contenu collé
145 const clipboardData = (e.clipboardData || e.originalEvent.clipboardData); 155 if(!e.clipboardData){ // e.clipboardData: DataTransfer
146 if(!clipboardData){
147 return; 156 return;
148 } 157 }
149 const items = clipboardData.items; 158 const items = e.clipboardData.items; // base64
150 let foundImage = false; 159 const files = e.clipboardData.files; // explorateur de fichiers
151 160 let found_file = false;
152 for(let i = 0; i < items.length; i++){ 161
153 let item = items[i]; 162 // données dans files
154 163 if(files && files.length > 0){ // noter que files peut être vide, alors que items non
155 if(item.type.indexOf('image') !== -1){ // test type MIME contenant image 164 for(let i = 0; i < files.length; i++){
156 foundImage = true; 165 let file = files[i];
157 166
158 const file = item.getAsFile(); // presse-papier => fichier lisible 167 if(this.extensions_white_list.includes(file.name.split('.').pop()?.toLowerCase() || '')){
159 const reader = new FileReader(); 168 found_file = true;
160 169 this.uploadDocument(file, editor);
161 reader.onload = function (event){ // fonction exécutée lorsque reader.readAsDataURL(file) est terminée 170 }
162 const base64Data = event.target.result; // données de l'image 171 else if(file.type.indexOf('image') !== -1){
163 172 found_file = true;
164 fetch('index.php?action=upload_image_base64', { 173 this.uploadImageBase64(file, editor);
165 method: 'POST', 174 }
166 headers: { 'Content-Type': 'application/json' }, 175 }
167 body: JSON.stringify({ image_base64: base64Data }) 176 }
168 }) 177 // données dans items
169 .then(response => response.json()) 178 else{ // les images collées depuis l'explorateur sont aussi dans items, or elles sont déjà gérées plus haut
170 .then(data => { 179 for(let i = 0; i < items.length; i++){
171 if(data.location){ 180 let item = items[i];
172 editor.insertContent('<img src="' + data.location + '">'); 181
173 } 182 if(item.type.indexOf('image') !== -1){ // test type MIME contenant image
174 }) 183 found_file = true;
175 .catch(error => { 184 const file = item.getAsFile(); // presse-papier => fichier lisible
176 console.error('Erreur lors de l’upload de l’image base64 :', error); 185 if(file){
177 }); 186 this.uploadImageBase64(file, editor);
178 }; 187 }
179 reader.readAsDataURL(file); // lecture asynchrone du fichier 188 else{
189 console.error('fichier invalide');
190 }
191 }
180 } 192 }
181 } 193 }
182 194
183 if(foundImage){ 195 if(found_file){
184 e.preventDefault(); // supprime le collage automatiue 196 e.preventDefault(); // supprime le collage automatiue
185 skipPastePreProcess = true; // désactiver le PastePreProcess pour ce collage 197 skipPastePreProcess = true; // désactiver le PastePreProcess pour ce collage
186 } 198 }
187 }); 199 });
188 editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur 200 editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur
201 if(skipPastePreProcess){
202 skipPastePreProcess = false; // réinitialiser pour la prochaine fois
203 return; // ignorer ce traitement
204 }
205
189 const parser = new DOMParser(); 206 const parser = new DOMParser();
190 const doc = parser.parseFromString(e.content, 'text/html'); 207 const doc = parser.parseFromString(e.content, 'text/html');
191 const images = doc.querySelectorAll('img'); 208 const images = doc.querySelectorAll('img');
192
193 let downloads_in_progress = []; 209 let downloads_in_progress = [];
194 210
195 images.forEach(img => { 211 images.forEach(img => {
@@ -221,7 +237,6 @@ class Editor
221 // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images 237 // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images
222 if(downloads_in_progress.length > 0){ 238 if(downloads_in_progress.length > 0){
223 e.preventDefault(); 239 e.preventDefault();
224
225 Promise.all(downloads_in_progress).then(() => { 240 Promise.all(downloads_in_progress).then(() => {
226 e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) 241 e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc)
227 editor.insertContent(e.content); 242 editor.insertContent(e.content);
@@ -230,30 +245,58 @@ class Editor
230 else{ 245 else{
231 e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) 246 e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc)
232 } 247 }
233 }); // fin editor.on('PastePreProcess'... 248 });
234 }, 249 // glisser-déposer de fichiers (sauf images qui sont déjà gérées nativement)
235 // upload d'image natif de tinymce avec le bouton "Insérer une image" 250 editor.on('drop', (e) => {
236 images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { 251 const data = e.dataTransfer;
237 const formData = new FormData(); 252 if(!data || !data.files || data.files.length === 0){
238 formData.append("file", blobInfo.blob()); 253 return; // Laisser TinyMCE gérer (texte, images déjà supportées, etc.)
239
240 fetch("index.php?action=upload_image_tinymce", {
241 method: "POST",
242 body: formData
243 })
244 .then(response => response.json())
245 .then(data => {
246 if(data.location) {
247 resolve(data.location);
248 } 254 }
249 else { 255 const files = data.files;
250 reject("Erreur: Chemin d'image invalide"); 256
257 let has_documents = false;
258 for(let i = 0; i < files.length; i++){
259 if(this.extensions_white_list.includes(files[i].name.split('.').pop()?.toLowerCase() || '')){
260 has_documents = true;
261 break;
262 }
251 } 263 }
252 }) 264
253 .catch(error => { 265 if(has_documents){
254 reject("Erreur lors de l'upload"); 266 e.preventDefault();
267 e.stopPropagation();
268
269 for(let i = 0; i < files.length; i++){
270 let file = files[i];
271
272 if(this.extensions_white_list.includes(file.name.split('.').pop()?.toLowerCase() || '')){
273 this.uploadDocument(file, editor);
274 }
275 else if(file.type.indexOf('image') !== -1){
276 this.uploadImageBase64(file, editor);
277 }
278 }
279 }
280 // autres cas: tinymce gère tout seul
255 }); 281 });
256 }), 282 },
283 // upload d'image avec le bouton "Insérer une image"
284 images_upload_handler: this.images_upload_handler, // = fonction fléchée
285 // upload de documents avec le bouton "insérer un lien"
286 files_upload_handler: this.files_upload_handler, // = fonction fléchée
287 documents_file_types: [ // files_upload_handler a besoin qu'on lui donne tous les types mime
288 { mimeType: 'application/pdf', extensions: [ 'pdf' ] },
289 { mimeType: 'application/rtf', extensions: [ 'rtf' ] },
290 { mimeType: 'application/msword', extensions: [ 'doc' ] },
291 { mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', extensions: [ 'docx' ] },
292 { mimeType: 'application/vnd.ms-excel', extensions: [ 'xls' ] },
293 { mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', extensions: [ 'xlsx' ] },
294 { mimeType: 'application/vnd.ms-powerpoint', extensions: [ 'ppt' ] },
295 { mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', extensions: [ 'pptx' ] },
296 { mimeType: 'application/vnd.oasis.opendocument.text', extensions: [ 'odt' ] },
297 { mimeType: 'application/vnd.oasis.opendocument.spreadsheet', extensions: [ 'ods' ] },
298 { mimeType: 'application/vnd.oasis.opendocument.presentation', extensions: [ 'odp' ] }
299 ],
257 image_caption: true 300 image_caption: true
258 }); 301 });
259 } 302 }
@@ -385,9 +428,107 @@ class Editor
385 delete editors[this.id]; 428 delete editors[this.id];
386 console.log(`Editor ${this.id} détruit.`); 429 console.log(`Editor ${this.id} détruit.`);
387 }*/ 430 }*/
388}
389 431
432 images_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => {
433 const formData = new FormData();
434 formData.append("file", blobInfo.blob());
435
436 fetch('index.php?action=upload_image_tinymce', {
437 method: 'POST',
438 body: formData
439 })
440 .then(response => response.json())
441 .then(data => {
442 if(data.location){
443 resolve(data.location);
444 }
445 else{
446 reject("Erreur: Chemin d'image invalide");
447 }
448 })
449 .catch(error => {
450 reject("Erreur lors de l'upload");
451 });
452 });
453 files_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { // utilisation = bouton "link" (OU drag & drop, et oui)
454 const formData = new FormData();
455 formData.append("file", blobInfo.blob());
456
457 fetch('index.php?action=upload_file_tinymce', {
458 method: 'POST',
459 body: formData
460 })
461 .then(response => response.json())
462 .then(data => {
463 if(data.location){
464 // resolve et reject fonctionne avec Promise => type de retour standardisé et évite l'utilistion de callbacks
465 resolve({
466 url: data.location,
467 fileName: blobInfo.filename(),
468 });
469 }
470 else{
471 reject("Erreur: Chemin du fichier invalide");
472 }
473 })
474 .catch(error => {
475 reject("Erreur lors de l'upload");
476 });
477 });
478
479 uploadImageBase64(file, editor){
480 const reader = new FileReader();
390 481
482 reader.onload = function (event){ // fonction exécutée lorsque reader.readAsDataURL(file) est terminée
483 const base64_target = event.target;
484 if(!base64_target || !base64_target.result){
485 console.error("erreur de lecture du fichier");
486 return;
487 }
488
489 fetch('index.php?action=upload_image_base64', {
490 method: 'POST',
491 headers: { 'Content-Type': 'application/json' },
492 body: JSON.stringify({ image_base64: base64_target.result })
493 })
494 .then(response => response.json())
495 .then(data => {
496 if(data.location){
497 editor.insertContent('<img src="' + data.location + '">');
498 }
499 })
500 .catch(error => {
501 console.error('Erreur lors de l’upload de l’image base64 :', error);
502 });
503 };
504 reader.readAsDataURL(file); // lecture asynchrone du fichier
505 }
506 uploadDocument(file, editor){ // utilisation = copier-coller de l'explorateur de fichiers
507 const formData = new FormData();
508 formData.append("file", file);
509
510 fetch('index.php?action=upload_file_tinymce', {
511 method: 'POST',
512 body: formData
513 })
514 .then(response => response.json())
515 .then(data => {
516 if(data.location){
517 // créer le lien <a>
518 const file_name = file.name;
519 const extension = file_name.split('.').pop()?.toLowerCase() || '';
520 const target = extension === 'pdf' ? 'target="_blank"' : ''; // PDF = page
521 editor.insertContent(`<a href="${data.location}" ${target} title="${file_name}">[${extension}] ${file_name}</a>`);
522 }
523 else {
524 console.error("Erreur: Chemin du fichier invalide");
525 }
526 })
527 .catch(error => {
528 console.error("Erreur lors de l'upload du document :", error);
529 });
530 }
531}
391 532
392 533
393 534
diff --git a/public/user_data/media/.htaccess b/public/user_data/media/.htaccess
new file mode 100644
index 0000000..4494297
--- /dev/null
+++ b/public/user_data/media/.htaccess
@@ -0,0 +1,10 @@
1# désactiver l'interpréteur PHP
2php_flag engine off
3
4# exploration des dossiers
5Options -Indexes
6
7# interdire l'accès directaux fichiers .htaccess et .htpasswd
8<FilesMatch "^\.(htaccess|htpasswd)$">
9 Require all denied
10</FilesMatch>
diff --git a/src/Captcha.php b/src/Captcha.php
index f60031a..a0c7a54 100644
--- a/src/Captcha.php
+++ b/src/Captcha.php
@@ -10,12 +10,10 @@ class Captcha
10{ 10{
11 private int $a; 11 private int $a;
12 private int $b; 12 private int $b;
13 private int $solution;
14 13
15 public function __construct(){ 14 public function __construct(){
16 $this->a = rand(2, 9); 15 $this->a = rand(2, 9);
17 $this->b = rand(2, 9); 16 $this->b = rand(2, 9);
18 $this->solution = $this->a * $this->b;
19 } 17 }
20 18
21 public function getA(): string 19 public function getA(): string
@@ -28,7 +26,7 @@ class Captcha
28 } 26 }
29 public function getSolution(): int 27 public function getSolution(): int
30 { 28 {
31 return $this->solution; 29 return ($this->a * $this->b);
32 } 30 }
33 31
34 private function toLettersFrench(int $number): string 32 private function toLettersFrench(int $number): string
@@ -45,6 +43,8 @@ class Captcha
45 default => '', // erreur 43 default => '', // erreur
46 }; 44 };
47 } 45 }
46
47 // (à déplacer dans FormValidation?)
48 static public function controlInput(string $input = '0'): int 48 static public function controlInput(string $input = '0'): int
49 { 49 {
50 // un POST est une chaîne qu'on doit convertir en nombre dans deux conditions: 50 // un POST est une chaîne qu'on doit convertir en nombre dans deux conditions:
diff --git a/src/controller/FileUploadController.php b/src/controller/FileUploadController.php
new file mode 100644
index 0000000..f53f5c2
--- /dev/null
+++ b/src/controller/FileUploadController.php
@@ -0,0 +1,67 @@
1<?php
2// src/controller/FileUploadController.php
3
4declare(strict_types=1);
5
6class FileUploadController
7{
8 static public function checkFileDownload(array $file): bool
9 {
10 $extensions_white_list = ['pdf', 'rtf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp']; // = extensions_white_list côté javascript
11 $mime_type_white_list = ['application/pdf', 'application/rtf', 'text/rtf', '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'];
12
13 // 1/ extension
14 $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
15 if(!in_array($extension, $extensions_white_list, true)){
16 return false;
17 }
18
19 // 2/ fichier obtenu par HTTP POST (théoriquement inutile si le routeur est solide, mais ça ne mange pas de pain)
20 if(!is_uploaded_file($file['tmp_name'])){
21 return false;
22 }
23
24 // 3/ objet $finfo valide (dépend du paramètre FILEINFO_MIME_TYPE)
25 $finfo = new finfo(FILEINFO_MIME_TYPE);
26 if($finfo === false){
27 return false;
28 }
29
30 // 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...)
31 $real_type = finfo_file($finfo, $file['tmp_name']);
32 return in_array($real_type, $mime_type_white_list, true);
33 }
34
35 static public function fileUploadTinyMce(): void
36 {
37 if(isset($_FILES['file'])){
38 $dest = 'user_data/media/';
39 if(!is_dir($dest)){ // Vérifier si le répertoire existe, sinon le créer
40 mkdir($dest, 0755, true);
41 }
42
43 $name = Security::secureFileName(pathinfo($_FILES['file']['name'], PATHINFO_FILENAME)); // retirer caractères spéciaux et changer espaces en underscores
44 $extension = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
45 $file_path = $dest . $name . '_' . uniqid() . '.' . $extension; // nom unique
46
47 if(self::checkFileDownload($_FILES['file'])){
48 if(move_uploaded_file($_FILES['file']['tmp_name'], $file_path)){
49 echo json_encode(['location' => $file_path]);
50 }
51 else{
52 http_response_code(500);
53 echo json_encode(['message' => 'Erreur enregistrement du fichier.']);
54 }
55 }
56 else{
57 http_response_code(400);
58 echo json_encode(['message' => 'Erreur 400: fichier non valide.']);
59 }
60 }
61 else{
62 http_response_code(400);
63 echo json_encode(['message' => 'Erreur 400: Bad Request']);
64 }
65 die;
66 }
67} \ No newline at end of file
diff --git a/src/controller/HeadFootController.php b/src/controller/HeadFootController.php
index de7ae53..83e27a8 100644
--- a/src/controller/HeadFootController.php
+++ b/src/controller/HeadFootController.php
@@ -4,7 +4,6 @@
4declare(strict_types=1); 4declare(strict_types=1);
5 5
6use App\Entity\NodeData; 6use App\Entity\NodeData;
7use App\Entity\NodeDataAsset;
8use App\Entity\Asset; 7use App\Entity\Asset;
9use App\Entity\AssetEmployment; 8use App\Entity\AssetEmployment;
10use Doctrine\ORM\EntityManager; 9use Doctrine\ORM\EntityManager;
@@ -82,7 +81,7 @@ class HeadFootController
82 } 81 }
83 82
84 /* -- écriture du fichier sur le disque -- */ 83 /* -- écriture du fichier sur le disque -- */
85 if(!ImageUploadController::imagickCleanImage(file_get_contents($file['tmp_name']), Asset::USER_PATH . $name, $extension)){ // recréer l’image pour la nettoyer 84 if(!ImageUploadController::imagickCleanAndWriteImage(file_get_contents($file['tmp_name']), Asset::USER_PATH . $name, $extension)){ // recréer l’image pour la nettoyer
86 http_response_code(500); 85 http_response_code(500);
87 echo json_encode(['success' => false, 'message' => 'Erreur image non valide.', 'format' => $extension]); 86 echo json_encode(['success' => false, 'message' => 'Erreur image non valide.', 'format' => $extension]);
88 } 87 }
@@ -146,6 +145,4 @@ class HeadFootController
146 } 145 }
147 die; 146 die;
148 } 147 }
149
150 //static public function uploadImage(EntityManager $entityManager, array $request_params): void
151} \ No newline at end of file 148} \ No newline at end of file
diff --git a/src/controller/ImageUploadController.php b/src/controller/ImageUploadController.php
index 77f0a47..7943116 100644
--- a/src/controller/ImageUploadController.php
+++ b/src/controller/ImageUploadController.php
@@ -5,17 +5,40 @@ declare(strict_types=1);
5 5
6class ImageUploadController 6class ImageUploadController
7{ 7{
8 static public function imagickCleanImage(string $image_data, string $local_path, string $format = 'jpeg'): bool // "string" parce que file_get_contents... 8 const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif'];
9
10 static public function imagickCleanAndWriteImage(string $image_data, string $local_path): bool // "string" parce que file_get_contents...
9 { 11 {
12 $format = strtolower(pathinfo($local_path)['extension']);
10 try{ 13 try{
11 $imagick = new Imagick(); 14 $imagick = new Imagick();
12 $imagick->readImageBlob($image_data); 15 $imagick->readImageBlob($image_data);
13 $imagick->stripImage(); // nettoyage métadonnées 16 $imagick->stripImage(); // nettoyage métadonnées
14 $imagick->setImageFormat($format); 17 //$imagick->setImageFormat($format); // inutile, writeImage force la conversion
15 if($format === 'jpeg'){ 18
16 $imagick->setImageCompression(Imagick::COMPRESSION_JPEG); 19 // compression
17 $imagick->setImageCompressionQuality(85); // optionnel 20 switch($format){
21 case 'jpeg': // particularité du switch, si 'jpeg' le test de 'jpg' est ignoré et on va jusqu'au break
22 case 'jpg':
23 $imagick->setImageCompression(Imagick::COMPRESSION_JPEG);
24 $imagick->setImageCompressionQuality(85);
25 break;
26 case 'webp':
27 $imagick->setImageCompression(Imagick::COMPRESSION_WEBP);
28 $imagick->setImageCompressionQuality(85);
29 break;
30 case 'png':
31 $imagick->setImageCompression(Imagick::COMPRESSION_ZIP);
32 $imagick->setImageCompressionQuality(7); // 9 est sans perte
33 break;
34 case 'tiff':
35 $imagick->setImageCompression(Imagick::COMPRESSION_LZW); // LZW est sans perte
36 break;
18 } 37 }
38
39 // enregistrement
40 // writeImage utilise l'extension du fichier et ignore le format détecté
41 // imagemagick est à l'origine une appli console, elle considère que l'extension montre l'intention de
19 $imagick->writeImage($local_path); // enregistrement 42 $imagick->writeImage($local_path); // enregistrement
20 $imagick->clear(); 43 $imagick->clear();
21 $imagick->destroy(); 44 $imagick->destroy();
@@ -25,7 +48,7 @@ class ImageUploadController
25 return false; 48 return false;
26 } 49 }
27 } 50 }
28 static public function curlDownloadImage(string $url, $maxRetries = 3, $timeout = 10): string|false 51 static public function curlDownloadImage(string $url, int $maxRetries = 3, int $timeout = 10): string|false
29 { 52 {
30 $attempt = 0; 53 $attempt = 0;
31 $imageData = false; 54 $imageData = false;
@@ -43,8 +66,6 @@ class ImageUploadController
43 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 66 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
44 //$curlError = curl_error($ch); 67 //$curlError = curl_error($ch);
45 68
46 curl_close($ch);
47
48 if($imageData !== false && $httpCode >= 200 && $httpCode < 300){ 69 if($imageData !== false && $httpCode >= 200 && $httpCode < 300){
49 return $imageData; 70 return $imageData;
50 } 71 }
@@ -59,42 +80,45 @@ class ImageUploadController
59 // téléchargement par le plugin (bouton "insérer une image") 80 // téléchargement par le plugin (bouton "insérer une image")
60 static public function imageUploadTinyMce(): void 81 static public function imageUploadTinyMce(): void
61 { 82 {
62 if(isset($_FILES['file'])){ 83 if(!isset($_FILES['file'])){
63 $file = $_FILES['file']; 84 http_response_code(400);
64 $dest = 'user_data/images/';
65 $dest_mini = 'user_data/images-mini/';
66
67 // Vérifier si les répertoires existent, sinon les créer
68 if(!is_dir($dest)){
69 mkdir($dest, 0777, true);
70 }
71 if(!is_dir($dest_mini)){
72 mkdir($dest_mini, 0777, true);
73 }
74
75 $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif'];
76 $name = Security::secureFileName(pathinfo($file['name'], PATHINFO_FILENAME));
77 $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
78 if(!in_array($extension, $allowed_extensions) || $extension === 'jpg'){
79 $extension = 'jpeg';
80 }
81 $file_path = uniqid($dest . $name . '_') . '.' . $extension;
82
83 // créer une miniature de l'image
84 //
85
86 if(self::imagickCleanImage(file_get_contents($file['tmp_name']), $file_path, $extension)){ // recréer l’image pour la nettoyer
87 echo json_encode(['location' => $file_path]); // renvoyer l'URL de l'image téléchargée
88 }
89 else{
90 http_response_code(500);
91 echo json_encode(['message' => 'Erreur image non valide', 'format' => $extension]);
92 }
93 }
94 else{
95 http_response_code(400);
96 echo json_encode(['message' => 'Erreur 400: Bad Request']); 85 echo json_encode(['message' => 'Erreur 400: Bad Request']);
97 } 86 }
87 if(!is_uploaded_file($_FILES['file']['tmp_name'])) {
88 http_response_code(400);
89 echo json_encode(['message' => "Le fichier n'a pas été téléchargé correctement."]);
90 die;
91 }
92
93 $dest = 'user_data/images/';
94 $dest_mini = 'user_data/images-mini/';
95
96 // Vérifier si les répertoires existent, sinon les créer
97 if(!is_dir($dest)){
98 mkdir($dest, 0755, true);
99 }
100 if(!is_dir($dest_mini)){
101 mkdir($dest_mini, 0755, true);
102 }
103
104 $name = Security::secureFileName(pathinfo($_FILES['file']['name'], PATHINFO_FILENAME));
105 $extension = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
106 $image_data = file_get_contents($_FILES['file']['tmp_name']);
107 if(!in_array($extension, self::ALLOWED_EXTENSIONS)){
108 $extension = 'jpeg';
109 }
110 $local_path = uniqid($dest . $name . '_') . '.' . $extension;
111
112 // créer une miniature de l'image
113 //
114
115 if(self::imagickCleanAndWriteImage($image_data, $local_path)){ // recréer l’image pour la nettoyer
116 echo json_encode(['location' => $local_path]); // renvoyer l'URL de l'image téléchargée
117 }
118 else{
119 http_response_code(500);
120 echo json_encode(['message' => 'Erreur image non valide']);
121 }
98 die; 122 die;
99 } 123 }
100 124
@@ -103,40 +127,39 @@ class ImageUploadController
103 { 127 {
104 $json = json_decode(file_get_contents('php://input'), true); 128 $json = json_decode(file_get_contents('php://input'), true);
105 129
106 if(isset($json['image_url'])){ 130 if(!isset($json['image_url'])){
107 $image_data = self::curlDownloadImage($json['image_url']); // téléchargement de l’image par le serveur avec cURL au lieu de file_get_contents 131 http_response_code(400);
108 $dest = 'user_data/images/'; 132 echo json_encode(['message' => "Erreur 400: Bad Request"]);
109 133 die;
110 if(!is_dir($dest)) { // Vérifier si le répertoire existe, sinon le créer
111 mkdir($dest, 0777, true);
112 }
113
114 if($image_data === false){
115 http_response_code(400);
116 echo json_encode(['message' => "Erreur, le serveur n'a pas réussi à télécharger l'image."]);
117 die;
118 }
119
120 $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif'];
121 $url_path = parse_url($json['image_url'], PHP_URL_PATH);
122 $name = Security::secureFileName(pathinfo($url_path, PATHINFO_FILENAME));
123 $extension = strtolower(pathinfo($url_path, PATHINFO_EXTENSION));
124 if(!in_array($extension, $allowed_extensions) || $extension === 'jpg'){
125 $extension = 'jpeg';
126 }
127 $local_path = uniqid($dest . $name . '_') . '.' . $extension;
128
129 if(self::imagickCleanImage($image_data, $local_path, $extension)){ // recréer l’image pour la nettoyer
130 echo json_encode(['location' => $local_path]); // nouvelle adresse
131 }
132 else{
133 http_response_code(500);
134 echo json_encode(['message' => 'Erreur image non valide', 'format' => $extension]);
135 }
136 }
137 else{
138 echo json_encode(['message' => 'Erreur 400: Bad Request']);
139 } 134 }
135
136 $image_data = self::curlDownloadImage($json['image_url']); // téléchargement de l’image par le serveur avec cURL au lieu de file_get_contents
137 if(!$image_data){
138 http_response_code(400);
139 echo json_encode(['message' => "Erreur, le serveur n'a pas réussi à télécharger l'image."]);
140 die;
141 }
142
143 $dest = 'user_data/images/';
144 if(!is_dir($dest)) { // Vérifier si le répertoire existe, sinon le créer
145 mkdir($dest, 0755, true);
146 }
147
148 $url_path = parse_url($json['image_url'], PHP_URL_PATH);
149 $name = Security::secureFileName(pathinfo($url_path, PATHINFO_FILENAME));
150 $extension = strtolower(pathinfo($url_path, PATHINFO_EXTENSION));
151 if(!in_array($extension, self::ALLOWED_EXTENSIONS) || $extension === 'jpg'){
152 $extension = 'jpeg';
153 }
154 $local_path = uniqid($dest . $name . '_') . '.' . $extension;
155
156 if(self::imagickCleanAndWriteImage($image_data, $local_path)){ // recréer l’image pour la nettoyer
157 echo json_encode(['location' => $local_path]); // nouvelle adresse
158 }
159 else{
160 http_response_code(500);
161 echo json_encode(['message' => 'Erreur image non valide', 'format' => $extension]);
162 }
140 die; 163 die;
141 } 164 }
142 165
@@ -147,7 +170,7 @@ class ImageUploadController
147 $dest = 'user_data/images/'; 170 $dest = 'user_data/images/';
148 171
149 if(!is_dir($dest)){ 172 if(!is_dir($dest)){
150 mkdir($dest, 0777, true); 173 mkdir($dest, 0755, true);
151 } 174 }
152 175
153 // détection de data:image/ et de ;base64, et capture du format dans $type 176 // détection de data:image/ et de ;base64, et capture du format dans $type
@@ -157,22 +180,21 @@ class ImageUploadController
157 die; 180 die;
158 } 181 }
159 182
160 $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif']; 183 $extension = strtolower($type[1]); // dans (\w+)
161 $extension = strtolower($type[1]); 184 if(!in_array($extension, self::ALLOWED_EXTENSIONS)){
162 if(!in_array($extension, $allowed_extensions) || $extension === 'jpg'){
163 $extension = 'jpeg'; 185 $extension = 'jpeg';
164 } 186 }
165 187
188 $name = 'pasted_image';
166 $image_data = base64_decode(substr($json['image_base64'], strpos($json['image_base64'], ',') + 1)); // découpe la chaine à la virgule puis convertit en binaire 189 $image_data = base64_decode(substr($json['image_base64'], strpos($json['image_base64'], ',') + 1)); // découpe la chaine à la virgule puis convertit en binaire
167 if($image_data === false){ 190 if($image_data === false){
168 http_response_code(400); 191 http_response_code(400);
169 echo json_encode(['message' => 'Décodage base64 invalide']); 192 echo json_encode(['message' => 'Décodage base64 invalide']);
170 die; 193 die;
171 } 194 }
172 195 $local_path = uniqid($dest . $name . '_') . '.' . $extension;
173 $local_path = uniqid($dest . 'pasted_image_') . '.' . $extension;
174 196
175 if(self::imagickCleanImage($image_data, $local_path)){ 197 if(self::imagickCleanAndWriteImage($image_data, $local_path)){
176 echo json_encode(['location' => $local_path]); 198 echo json_encode(['location' => $local_path]);
177 } 199 }
178 else{ 200 else{
diff --git a/src/router.php b/src/router.php
index 7348f2b..ff219da 100644
--- a/src/router.php
+++ b/src/router.php
@@ -231,12 +231,17 @@ elseif($request->getMethod() === 'POST'){
231 } 231 }
232 } 232 }
233 233
234 // upload d'image dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur) 234 // upload avec FormData
235 elseif(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false) 235 elseif(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false)
236 { 236 {
237 // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer)
237 if($request->query->has('action') && $request->query->get('action') === 'upload_image_tinymce'){ 238 if($request->query->has('action') && $request->query->get('action') === 'upload_image_tinymce'){
238 ImageUploadController::imageUploadTinyMce(); 239 ImageUploadController::imageUploadTinyMce();
239 } 240 }
241 // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64
242 elseif($request->query->has('action') && $request->query->get('action') === 'upload_file_tinymce'){
243 FileUploadController::fileUploadTinyMce();
244 }
240 elseif($request->query->has('head_foot_image')){ 245 elseif($request->query->has('head_foot_image')){
241 HeadFootController::uploadAsset($entityManager, $request->query->get('head_foot_image')); 246 HeadFootController::uploadAsset($entityManager, $request->query->get('head_foot_image'));
242 } 247 }