aboutsummaryrefslogtreecommitdiff
path: root/public
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 /public
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)
Diffstat (limited to 'public')
-rw-r--r--public/js/tinymce.js261
-rw-r--r--public/user_data/media/.htaccess10
2 files changed, 211 insertions, 60 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>