dolibarr 21.0.0-alpha
doleditor.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2006-2008 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2021 Gaëtan MAISON <gm@ilad.org>
4 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
5 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 * or see https://www.gnu.org/
20 */
21
33{
37 public $tool; // Store the selected tool
38
39 // If using fckeditor
43 public $editor;
44
45 // If not using fckeditor
49 public $content;
53 public $htmlname;
57 public $toolbarname;
61 public $toolbarstartexpanded;
65 public $rows;
69 public $cols;
73 public $height;
77 public $width;
81 public $uselocalbrowser;
85 public $readonly;
89 public $posx;
93 public $posy;
94
95
115 public function __construct($htmlname, $content, $width = '', $height = 200, $toolbarname = 'Basic', $toolbarlocation = 'In', $toolbarstartexpanded = false, $uselocalbrowser = 1, $okforextendededitor = true, $rows = 0, $cols = '', $readonly = 0, $poscursor = array())
116 {
117 global $conf;
118
119 dol_syslog(get_class($this)."::DolEditor htmlname=".$htmlname." width=".$width." height=".$height." toolbarname=".$toolbarname);
120
121 if (!$rows) {
122 $rows = round($height / 20);
123 }
124 if (!$cols) {
125 $cols = ($width ? round($width / 6) : 80);
126 }
127 $shorttoolbarname = preg_replace('/_encoded$/', '', $toolbarname);
128
129 // Name of extended editor to use (FCKEDITOR_EDITORNAME can be 'ckeditor' or 'fckeditor')
130 $defaulteditor = 'ckeditor';
131 $this->tool = !getDolGlobalString('FCKEDITOR_EDITORNAME') ? $defaulteditor : $conf->global->FCKEDITOR_EDITORNAME;
132 $this->uselocalbrowser = $uselocalbrowser;
133 $this->readonly = $readonly;
134
135 // Check if extended editor is ok. If not we force textarea
136 if ((!isModEnabled('fckeditor') && $okforextendededitor !== 'ace') || empty($okforextendededitor)) {
137 $this->tool = 'textarea';
138 }
139 if ($okforextendededitor === 'ace') {
140 $this->tool = 'ace';
141 }
142 //if ($conf->dol_use_jmobile) $this->tool = 'textarea'; // ckeditor and ace seems ok with mobile
143 if (empty($conf->use_javascript_ajax)) { // If no javascript, we force use of textarea
144 $this->tool = 'textarea';
145 }
146
147 if (isset($poscursor['find'])) {
148 $posy = 0;
149 $lines = explode("\n", $content);
150 $nblines = count($lines);
151 for ($i = 0 ; $i < $nblines ; $i++) {
152 if (preg_match('/'.$poscursor['find'].'/', $lines[$i])) {
153 $posy = $i;
154 break;
155 }
156 }
157 if ($posy != 0) {
158 $poscursor['y'] = $posy;
159 }
160 }
161
162 // Define some properties
163 if (in_array($this->tool, array('textarea', 'ckeditor', 'ace'))) {
164 if ($this->tool == 'ckeditor' && !dol_textishtml($content)) { // We force content to be into HTML if we are using an advanced editor if content is not HTML.
165 $this->content = dol_nl2br($content);
166 } else {
167 $this->content = $content;
168 }
169 $this->htmlname = $htmlname;
170 $this->toolbarname = $shorttoolbarname;
171 $this->toolbarstartexpanded = $toolbarstartexpanded;
172 $this->rows = max(ROWS_3, $rows);
173 $this->cols = (preg_match('/%/', $cols) ? $cols : max(40, $cols)); // If $cols is a percent, we keep it, otherwise, we take max
174 $this->height = $height;
175 $this->width = $width;
176 $this->posx = empty($poscursor['x']) ? 0 : $poscursor['x'];
177 $this->posy = empty($poscursor['y']) ? 0 : $poscursor['y'];
178 }
179 }
180
181 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
195 public function Create($noprint = 0, $morejs = '', $disallowAnyContent = true, $titlecontent = '', $option = '', $moreparam = '', $morecss = '')
196 {
197 // phpcs:enable
198 global $conf, $langs;
199
200 $fullpage = false;
201 if (isset($conf->global->FCKEDITOR_ALLOW_ANY_CONTENT)) {
202 $disallowAnyContent = !getDolGlobalString('FCKEDITOR_ALLOW_ANY_CONTENT'); // Only predefined list of html tags are allowed or all
203 }
204
205 $found = 0;
206 $out = '';
207
208 if (in_array($this->tool, array('textarea', 'ckeditor'))) {
209 $found = 1;
210 //$out.= '<textarea id="'.$this->htmlname.'" name="'.$this->htmlname.'" '.($this->readonly?' disabled':'').' rows="'.$this->rows.'"'.(preg_match('/%/',$this->cols)?' style="margin-top: 5px; width: '.$this->cols.'"':' cols="'.$this->cols.'"').' class="flat">';
211 // TODO We do not put the 'disabled' tag because on a read form, it change style with grey.
212 //print $this->content;
213 $out .= '<textarea id="'.$this->htmlname.'" name="'.$this->htmlname.'"';
214 $out .= ' rows="'.$this->rows.'"';
215 //$out .= ' style="height: 700px; min-height: 700px;"';
216 $out .= (preg_match('/%/', $this->cols) ? ' style="margin-top: 5px; width: '.$this->cols.'"' : ' cols="'.$this->cols.'"');
217 $out .= ' '.($moreparam ? $moreparam : '');
218 $out .= ' class="flat '.$morecss.'">';
219 $out .= htmlspecialchars($this->content);
220 $out .= '</textarea>';
221
222 if ($this->tool == 'ckeditor' && !empty($conf->use_javascript_ajax) && isModEnabled('fckeditor')) {
223 if (!defined('REQUIRE_CKEDITOR')) {
224 define('REQUIRE_CKEDITOR', '1');
225 }
226
227 $skin = getDolGlobalString('FCKEDITOR_SKIN', 'moono-lisa'); // default with ckeditor 4.6 : moono-lisa
228
229 $pluginstodisable = 'elementspath,save,flash,div,anchor';
230 if (!getDolGlobalString('FCKEDITOR_ENABLE_SPECIALCHAR')) {
231 $pluginstodisable .= ',specialchar';
232 }
233 if (!empty($conf->dol_optimize_smallscreen)) {
234 $pluginstodisable .= ',scayt,wsc,find,undo';
235 }
236 if (!getDolGlobalString('FCKEDITOR_ENABLE_WSC')) { // spellchecker has end of life december 2021
237 $pluginstodisable .= ',wsc';
238 }
239 if (!getDolGlobalString('FCKEDITOR_ENABLE_PDF')) {
240 $pluginstodisable .= ',exportpdf';
241 }
242 if (getDolGlobalInt('MAIN_DISALLOW_URL_INTO_DESCRIPTIONS') == 2) {
243 $this->uselocalbrowser = 0; // Can't use browser to navigate into files. Only links with "<img src=data:..." are allowed.
244 }
245 $scaytautostartup = '';
246 if (getDolGlobalString('FCKEDITOR_ENABLE_SCAYT_AUTOSTARTUP')) {
247 $scaytautostartup = 'scayt_autoStartup: true,';
248 $scaytautostartup .= 'scayt_sLang: \''.dol_escape_js($langs->getDefaultLang()).'\',';
249 } else {
250 $pluginstodisable .= ',scayt';
251 }
252
253 $htmlencode_force = preg_match('/_encoded$/', $this->toolbarname) ? 'true' : 'false';
254
255 $out .= '<!-- Output ckeditor disallowAnyContent='.dol_escape_htmltag((string) $disallowAnyContent).' toolbarname='.dol_escape_htmltag($this->toolbarname).' -->'."\n";
256 //$out .= '<style>#cke_1_top { height: 34px !important; }</style>';
257 $out .= '<script nonce="'.getNonce().'" type="text/javascript">
258 $(document).ready(function () {
259 /* console.log("Run ckeditor"); */
260 /* if (CKEDITOR.loadFullCore) CKEDITOR.loadFullCore(); */
261 /* should be editor=CKEDITOR.replace but what if there is several editors ? */
262 tmpeditor = CKEDITOR.replace(\''.dol_escape_js($this->htmlname).'\',
263 {
264 /* property:xxx is same than CKEDITOR.config.property = xxx */
265 customConfig: ckeditorConfig,
266 removePlugins: \''.dol_escape_js($pluginstodisable).'\',
267 versionCheck: false,
268 readOnly: '.($this->readonly ? 'true' : 'false').',
269 htmlEncodeOutput: '.dol_escape_js($htmlencode_force).',
270 allowedContent: '.($disallowAnyContent ? 'false' : 'true').', /* Advanced Content Filter (ACF) is on when allowedContent is false */
271 extraAllowedContent: \'a[target];section[contenteditable,id];div{float,display}\', /* Allow a tag with attribute target, allow seciont tag and allow the style float and display into div to default other allowed tags */
272 disallowedContent: \'\', /* Tags that are not allowed */
273 fullPage: '.($fullpage ? 'true' : 'false').', /* if true, the html, header and body tags are kept */
274 toolbar: \''.dol_escape_js($this->toolbarname).'\',
275 toolbarStartupExpanded: '.($this->toolbarstartexpanded ? 'true' : 'false').',
276 width: '.($this->width ? '\''.dol_escape_js($this->width).'\'' : '\'\'').',
277 height: '.dol_escape_js((string) $this->height).',
278 skin: \''.dol_escape_js($skin).'\',
279 '.$scaytautostartup.'
280 language: \''.dol_escape_js($langs->defaultlang).'\',
281 textDirection: \''.dol_escape_js($langs->trans("DIRECTION")).'\',
282 on : {
283 instanceReady : function(ev) {
284 console.log(\'ckeditor '.dol_escape_js($this->htmlname).' instanceReady\');
285
286 /* If we found the attribute required on source div, we remove it (not compatible with ckeditor) */
287 /* Disabled, because attribute required should never be used on fields for doleditor */
288 /* jQuery("#'.dol_escape_js($this->htmlname).'").attr("required", false); */
289
290 // Output paragraphs as <p>Text</p>.
291 this.dataProcessor.writer.setRules( \'p\', {
292 indent : false,
293 breakBeforeOpen : true,
294 breakAfterOpen : false,
295 breakBeforeClose : false,
296 breakAfterClose : true
297 });
298 },
299 /* This is to remove the tab Link on image popup. Does not work, so commented */
300 /* dialogDefinition: function (event) {
301 var dialogName = event.data.name;
302 var dialogDefinition = event.data.definition;
303 if (dialogName == \'image\') {
304 dialogDefinition.removeContents(\'Link\');
305 }
306 } */
307 },
308 disableNativeSpellChecker: '.(getDolGlobalString('CKEDITOR_NATIVE_SPELLCHECKER') ? 'false' : 'true');
309
310 if ($this->uselocalbrowser) {
311 $out .= ','."\n";
312 // To use filemanager with old fckeditor (GPL)
313 // Note: ckeditorFilebrowserBrowseUrl and ckeditorFilebrowserImageBrowseUrl are defined in header by main.inc.php. They include url to browser with url of upload connector in parameter
314 $out .= ' filebrowserBrowseUrl : ckeditorFilebrowserBrowseUrl,';
315 $out .= ' filebrowserImageBrowseUrl : ckeditorFilebrowserImageBrowseUrl,';
316 //$out.= ' filebrowserUploadUrl : \''.DOL_URL_ROOT.'/includes/fckeditor/editor/filemanagerdol/connectors/php/upload.php?Type=File\',';
317 //$out.= ' filebrowserImageUploadUrl : \''.DOL_URL_ROOT.'/includes/fckeditor/editor/filemanagerdol/connectors/php/upload.php?Type=Image\',';
318 $out .= "\n";
319 // To use filemanager with ckfinder (Non free) and ckfinder directory is inside htdocs/includes
320 /* $out.= ' filebrowserBrowseUrl : \''.DOL_URL_ROOT.'/includes/ckfinder/ckfinder.html\',
321 filebrowserImageBrowseUrl : \''.DOL_URL_ROOT.'/includes/ckfinder/ckfinder.html?Type=Images\',
322 filebrowserFlashBrowseUrl : \''.DOL_URL_ROOT.'/includes/ckfinder/ckfinder.html?Type=Flash\',
323 filebrowserUploadUrl : \''.DOL_URL_ROOT.'/includes/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Files\',
324 filebrowserImageUploadUrl : \''.DOL_URL_ROOT.'/includes/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Images\',
325 filebrowserFlashUploadUrl : \''.DOL_URL_ROOT.'/includes/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Flash\','."\n";
326 */
327 $out .= ' filebrowserWindowWidth : \'900\',
328 filebrowserWindowHeight : \'500\',
329 filebrowserImageWindowWidth : \'900\',
330 filebrowserImageWindowHeight : \'500\'';
331 }
332 $out .= ' })'.$morejs; // end CKEditor.replace
333 // Show the CKEditor javascript object once loaded is ready 'For debug)
334 //$out .= '; CKEDITOR.on(\'instanceReady\', function(ck) { ck.editor.removeMenuItem(\'maximize\'); ck.editor.removeMenuItem(\'Undo\'); ck.editor.removeMenuItem(\'undo\'); console.log(ck.editor); console.log(ck.editor.toolbar[0]); }); ';
335 $out .= '});'."\n"; // end document.ready
336 $out .= '</script>'."\n";
337 }
338 }
339
340 // Output editor ACE
341 // Warning: ace.js and ext-statusbar.js must be loaded by the parent page.
342 if (preg_match('/^ace/', $this->tool)) {
343 $found = 1;
344 $format = $option;
345
346 $out .= "\n".'<!-- Output Ace editor '.dol_string_nohtmltag($this->htmlname).' -->'."\n";
347
348 if ($titlecontent) {
349 $out .= '<div class="aceeditorstatusbar" id="statusBar'.$this->htmlname.'">'.$titlecontent;
350 $out .= ' &nbsp; - &nbsp; <span id="morelines" class="right classlink cursorpointer morelines'.$this->htmlname.'">'.dol_escape_htmltag($langs->trans("ShowMoreLines")).'</span> &nbsp; &nbsp; ';
351 $out .= '</div>';
352 $out .= '<script nonce="'.getNonce().'" type="text/javascript">'."\n";
353 $out .= 'jQuery(document).ready(function() {'."\n";
354 $out .= ' var aceEditor = window.ace.edit("'.dol_escape_all($this->htmlname).'aceeditorid");
355 aceEditor.moveCursorTo('.($this->posy + 1).','.$this->posx.');
356 aceEditor.gotoLine('.($this->posy + 1).','.$this->posx.');
357 var StatusBar = window.ace.require("ace/ext/statusbar").StatusBar; // Init status bar. Need lib ext-statusbar
358 var statusBar = new StatusBar(aceEditor, document.getElementById("statusBar'.dol_escape_all($this->htmlname).'")); // Init status bar. Need lib ext-statusbar
359
360 var oldNbOfLines = 0;
361 jQuery(".morelines'.dol_escape_all($this->htmlname).'").click(function() {
362 var aceEditorClicked = window.ace.edit("'.$this->htmlname.'aceeditorid");
363 currentline = aceEditorClicked.getOption("maxLines");
364 if (oldNbOfLines == 0)
365 {
366 oldNbOfLines = currentline;
367 }
368 console.log("We click on more lines, oldNbOfLines is "+oldNbOfLines+", we have currently "+currentline);
369 if (currentline < 500)
370 {
371 aceEditorClicked.setOptions({ maxLines: 500 });
372 }
373 else
374 {
375 aceEditorClicked.setOptions({ maxLines: oldNbOfLines });
376 }
377 });
378 })';
379 $out .= '</script>'."\n";
380 }
381
382 $out .= '<pre id="'.$this->htmlname.'aceeditorid" style="'.($this->width ? 'width: '.$this->width.'px; ' : '');
383 $out .= ($this->height ? ' height: '.$this->height.'px; ' : '');
384 //$out.=" min-height: 100px;";
385 $out .= '">';
386 $out .= htmlspecialchars($this->content);
387 $out .= '</pre>';
388 $out .= '<input type="hidden" id="'.$this->htmlname.'_x" name="'.$this->htmlname.'_x">';
389 $out .= '<input type="hidden" id="'.$this->htmlname.'_y" name="'.$this->htmlname.'_y">';
390 $out .= '<textarea id="'.$this->htmlname.'" name="'.$this->htmlname.'" style="width:0px; height: 0px; display: none;">';
391 $out .= htmlspecialchars($this->content);
392 $out .= '</textarea>';
393
394 $out .= '<script nonce="'.getNonce().'" type="text/javascript">'."\n";
395 $out .= 'var aceEditor = window.ace.edit("'.$this->htmlname.'aceeditorid");
396
397 aceEditor.session.setMode("ace/mode/'.$format.'");
398 aceEditor.setOptions({
399 enableBasicAutocompletion: true, // the editor completes the statement when you hit Ctrl + Space. Need lib ext-language_tools.js
400 enableLiveAutocompletion: false, // the editor completes the statement while you are typing. Need lib ext-language_tools.js
401 //enableSnippets: true, // ???
402 showPrintMargin: false, // hides the vertical limiting strip
403 minLines: 10,
404 maxLines: '.(empty($this->height) ? '34' : (round($this->height / 10))).',
405 fontSize: "110%" // ensures that the editor fits in the environment
406 });
407
408 // defines the style of the editor
409 aceEditor.setTheme("ace/theme/chrome");
410
411 // hides line numbers (widens the area occupied by error and warning messages)
412 //aceEditor.renderer.setOption("showLineNumbers", false);
413 // ensures proper autocomplete, validation and highlighting of JavaScript code
414 //aceEditor.getSession().setMode("ace/mode/javascript_expression");'."\n";
415
416 // If page is PAGE_CONTENT to edit HTML web page in web site module, we add some autocompletion
417 if ($this->htmlname == 'PAGE_CONTENT') {
418 $out .= '
419 // Add custom function in the autocompletion
420 var customCompleter = {
421 getCompletions: function(editor, session, pos, prefix, callback) {
422 var wordList = [
423 { caption: \'dol_escape_all\', value: \'dol_escape_all(string)\', meta: \'custom\' },
424 { caption: \'dol_escape_js\', value: \'dol_escape_js(string)\', meta: \'custom\' },
425 { caption: \'includeContainer\', value: \'includeContainer(alias_of_container_to_include)\', meta: \'custom\' },
426 { caption: \'redirectToContainer\', value: \'redirectToContainer(alias_of_container_to_redirect_to)\', meta: \'custom\' },
427 { caption: \'getImageFromHtmlContent\', value: \'getImageFromHtmlContent(websitepage->htmlcontent)\', meta: \'custom\' },
428 ];
429 callback(null, wordList.map(function(word) {
430 return {
431 caption: word.caption,
432 value: word.value,
433 meta: word.meta
434 };
435 }));
436 }
437 };
438 aceEditor.completers = [customCompleter];
439 '."\n";
440 }
441
442 $out .= 'jQuery(document).ready(function() {';
443 $out .= ' jQuery(".buttonforacesave").click(function() {
444 console.log("We click on savefile button for component '.dol_escape_js($this->htmlname).'");
445 var aceEditor = window.ace.edit("'.dol_escape_js($this->htmlname).'aceeditorid");
446 if (aceEditor) {
447 var cursorPos = aceEditor.getCursorPosition();
448 //console.log(cursorPos);
449 if (cursorPos) {
450 jQuery("#'.dol_escape_js($this->htmlname).'_x").val(cursorPos.column);
451 jQuery("#'.dol_escape_js($this->htmlname).'_y").val(cursorPos.row);
452 }
453 //console.log(aceEditor.getSession().getValue());
454 // Inject content of editor into the original HTML field.
455 jQuery("#'.dol_escape_js($this->htmlname).'").val(aceEditor.getSession().getValue());
456 /*if (jQuery("#'.dol_escape_js($this->htmlname).'").html().length > 0) return true;
457 else return false;*/
458 return true;
459 } else {
460 console.log("Failed to retrieve js object ACE from its name");
461 return false;
462 }
463 });
464 })';
465 $out .= '</script>'."\n";
466 }
467
468 if (empty($found)) {
469 $out .= 'Error, unknown value for tool '.$this->tool.' in DolEditor Create function.';
470 }
471
472 if ($noprint) {
473 return $out;
474 } else {
475 print $out;
476 }
477 }
478}
Class to manage a WYSIWYG editor.
__construct($htmlname, $content, $width='', $height=200, $toolbarname='Basic', $toolbarlocation='In', $toolbarstartexpanded=false, $uselocalbrowser=1, $okforextendededitor=true, $rows=0, $cols='', $readonly=0, $poscursor=array())
Create an object to build an HTML area to edit a large string content.
Create($noprint=0, $morejs='', $disallowAnyContent=true, $titlecontent='', $option='', $moreparam='', $morecss='')
Output edit area inside the HTML stream.
dol_string_nohtmltag($stringtoclean, $removelinefeed=1, $pagecodeto='UTF-8', $strip_tags=0, $removedoublespaces=1)
Clean a string from all HTML tags and entities.
dol_nl2br($stringtoencode, $nl2brmode=0, $forxml=false)
Replace CRLF in string with a HTML BR tag.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_escape_js($stringtoescape, $mode=0, $noescapebackslashn=0)
Returns text escaped for inclusion into javascript code.
dol_textishtml($msg, $option=0)
Return if a text is a html content.
dol_escape_all($stringtoescape)
Returns text escaped for all protocols (so only alpha chars and numbers)
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
if(preg_match('/crypted:/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
Definition repair.php:137