PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / trunk
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress vtrunk
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Helpers / PluginHelper.php

PluginHelper.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress trunk, at back/Helpers/PluginHelper.php

1,682 lines 65.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace AcyMailing\Helpers;
4
5 use AcyMailing\Classes\MailClass;
6 use AcyMailing\Core\AcymObject;
7
8 class PluginHelper extends AcymObject
9 {
10 private MailerHelper $mailerHelper;
11 public string $name = 'content';
12 public string $wrappedText = '';
13 public string $contextLanguage;
14
15 /*
16 * Convert an array of elements into a table with multiple columns based on the $parameter variable
17 * $parameter->displaytype = table by default
18 * $parameter->cols = 1 by default
19 */
20 public function getFormattedResult(array $elements, object $parameter): string
21 {
22 //We do not add an extra table or whatever when there is a single element...
23 if (count($elements) < 2) {
24 return implode('', $elements);
25 }
26
27 $beforeAll = [];
28 $beforeAll['table'] = '<table cellspacing="0" cellpadding="0" border="0" width="100%" class="elementstable">'."\n";
29 $beforeAll['ul'] = '<ul class="elementsul">'."\n";
30 $beforeAll['br'] = '';
31
32 $beforeBlock = [];
33 $beforeBlock['table'] = '<tr class="elementstable_tr numrow{rownum}">'."\n";
34 $beforeBlock['ul'] = '';
35 $beforeBlock['br'] = '';
36
37 $beforeOne = [];
38 $beforeOne['table'] = '<td valign="top" width="{equalwidth}" style="{padding}" class="elementstable_td numcol{numcol}" >'."\n";
39 $beforeOne['ul'] = '<li class="elementsul_li numrow{rownum}">'."\n";
40 $beforeOne['br'] = '';
41
42 $afterOne = [];
43 $afterOne['table'] = '</td>'."\n";
44 $afterOne['ul'] = '</li>'."\n";
45 $afterOne['br'] = '<br />'."\n";
46
47 $afterBlock = [];
48 $afterBlock['table'] = '</tr>'."\n";
49 $afterBlock['ul'] = '';
50 $afterBlock['br'] = '';
51
52 $afterAll = [];
53 $afterAll['table'] = '</table>'."\n";
54 $afterAll['ul'] = '</ul>'."\n";
55 $afterAll['br'] = '';
56
57
58 $type = 'table';
59 $cols = 1;
60 if (!empty($parameter->displaytype)) {
61 $type = $parameter->displaytype;
62 }
63 if ($type == 'none') {
64 return implode('', $elements);
65 }
66 if (!empty($parameter->cols)) {
67 $cols = $parameter->cols;
68 }
69
70 $parameter->hpadding = !isset($parameter->hpadding) || is_null($parameter->hpadding) ? 10 : $parameter->hpadding;
71 $parameter->vpadding = !isset($parameter->vpadding) || is_null($parameter->vpadding) ? 10 : $parameter->vpadding;
72
73 $horizontalPadding = round($parameter->hpadding / 2);
74 $verticalPadding = round($parameter->vpadding / 2);
75
76 $string = $beforeAll[$type];
77 $a = 0;
78 $numrow = 1;
79 foreach ($elements as $key => $oneElement) {
80 $topPadding = $verticalPadding.'px';
81 $rightPadding = $horizontalPadding.'px';
82 $bottomPadding = $verticalPadding.'px';
83 $leftPadding = $horizontalPadding.'px';
84
85 if ($a == $cols) {
86 $string .= $afterBlock[$type];
87 $a = 0;
88 $numrow++;
89 }
90
91 if ($a == 0) {
92 $string .= str_replace('{rownum}', $numrow, $beforeBlock[$type]);
93 $leftPadding = '0px';
94 }
95
96 if ($a + 1 == $cols) $rightPadding = '0px';
97 if ($numrow == 1) $topPadding = '0px';
98 if (round(count($elements) / $cols) == $numrow) $bottomPadding = '0px';
99
100 $padding = 'padding: '.$topPadding.' '.$rightPadding.' '.$bottomPadding.' '.$leftPadding.' !important;';
101
102 $string .= str_replace('{numcol}', $a + 1, $beforeOne[$type]).$oneElement.$afterOne[$type];
103 $string = str_replace('{padding}', $padding, $string);
104 $a++;
105 }
106 while ($cols > $a) {
107 $string .= str_replace('{numcol}', $a + 1, $beforeOne[$type]).$afterOne[$type];
108 $a++;
109 }
110
111 $string .= $afterBlock[$type];
112 $string .= $afterAll[$type];
113
114 $equalwidth = intval(100 / $cols).'%';
115
116 return str_replace(['{equalwidth}'], [$equalwidth], $string);
117 }
118
119 /**
120 * This function will apply extra parameters such as part:first, ucfirst, strtolower to the $string
121 */
122 public function formatString(&$replaceme, object $mytag): void
123 {
124 if (!empty($mytag->part)) {
125 $parts = explode(' ', $replaceme);
126 if ($mytag->part == 'last') {
127 $replaceme = count($parts) > 1 ? end($parts) : '';
128 } else {
129 if (is_numeric($mytag->part) && count($parts) >= $mytag->part) {
130 $replaceme = $parts[$mytag->part - 1];
131 } else {
132 $replaceme = reset($parts);
133 }
134 }
135 }
136
137 if (!empty($mytag->type)) {
138 if (empty($mytag->format)) {
139 $mytag->format = acym_translation('ACYM_DATE_FORMAT_LC3');
140 }
141 if ($mytag->type == 'date') {
142 $replaceme = acym_getDate(acym_getTime($replaceme), $mytag->format);
143 } elseif ($mytag->type == 'time') {
144 $replaceme = acym_getDate($replaceme, $mytag->format);
145 } elseif ($mytag->type == 'diff') {
146 try {
147 //We have a date? Sure?
148 $date = $replaceme;
149 if (is_numeric($date)) {
150 $date = acym_getDate($replaceme, '%Y-%m-%d %H:%M:%S');
151 }
152 $dateObj = new \DateTime($date);
153 $nowObj = new \DateTime();
154 $diff = $dateObj->diff($nowObj);
155 $replaceme = $diff->format($mytag->format);
156 } catch (\Exception $e) {
157 $replaceme = 'Error using the "diff" parameter in your tag. Please make sure the DateTime() and diff() functions are available on your server.';
158 }
159 }
160 }
161
162 //Two possibles keywords for that... we used to have "lower" and "upper" only but I added lowercase and uppercase as well on December 2014
163 if (!empty($mytag->lower) || !empty($mytag->lowercase)) {
164 $replaceme = function_exists('mb_strtolower') ? mb_strtolower($replaceme, 'UTF-8') : strtolower($replaceme);
165 }
166 if (!empty($mytag->upper) || !empty($mytag->uppercase)) {
167 $replaceme = function_exists('mb_strtoupper') ? mb_strtoupper($replaceme, 'UTF-8') : strtoupper($replaceme);
168 }
169 if (!empty($mytag->ucwords)) {
170 $replaceme = ucwords($replaceme);
171 }
172 if (!empty($mytag->ucfirst)) {
173 $replaceme = ucfirst($replaceme);
174 }
175 //Remove a character at the end of the string...
176 if (isset($mytag->rtrim)) {
177 $replaceme = empty($mytag->rtrim) ? rtrim($replaceme) : rtrim($replaceme, $mytag->rtrim);
178 }
179 if (!empty($mytag->urlencode)) {
180 $replaceme = urlencode($replaceme);
181 }
182 if (!empty($mytag->substr)) {
183 //the parameter |substr:4,6 will select the 6 characters after the 4.
184 $args = explode(',', $mytag->substr);
185 if (isset($args[1])) {
186 $replaceme = substr($replaceme, intval($args[0]), intval($args[1]));
187 } else {
188 $replaceme = substr($replaceme, intval($args[0]));
189 }
190 }
191
192
193 if (!empty($mytag->maxheight) || !empty($mytag->maxwidth)) {
194 $imageHelper = new ImageHelper();
195 $imageHelper->maxHeight = empty($mytag->maxheight) ? 999 : $mytag->maxheight;
196 $imageHelper->maxWidth = empty($mytag->maxwidth) ? 999 : $mytag->maxwidth;
197 $replaceme = $imageHelper->resizePictures($replaceme);
198 }
199 }
200
201 public function replaceVideos(string &$text): void
202 {
203 //Youtube videos
204 $text = preg_replace(
205 '#\[embed=videolink][^}]*youtube[^=]*=([^"/}]*)[^}]*}\[/embed]#i',
206 '<a target="_blank" href="https://www.youtube.com/watch?v=$1"><img src="https://img.youtube.com/vi/$1/0.jpg"/></a>',
207 $text
208 );
209 $text = preg_replace(
210 '#<video[^>]*youtube\.com/embed/([^"/]*)[^>]*>[^>]*</video>#i',
211 '<a target="_blank" href="https://www.youtube.com/watch?v=$1"><img src="https://img.youtube.com/vi/$1/0.jpg"/></a>',
212 $text
213 );
214 $text = preg_replace(
215 '#{JoooidContent[^}]*youtube[^}]*id"[^"]*"([^}"]*)"[^}]*}#i',
216 '<a target="_blank" href="https://www.youtube.com/watch?v=$1"><img src="https://img.youtube.com/vi/$1/0.jpg"/></a>',
217 $text
218 );
219 $text = preg_replace(
220 '#<iframe[^>]*src="[^"]*youtube[^"]*embed/([^"?]*)(\?[^"]*)?"[^>]*>[^<]*</iframe>#Uis',
221 '<a target="_blank" href="https://www.youtube.com/watch?v=$1"><img src="https://img.youtube.com/vi/$1/0.jpg"/></a>',
222 $text
223 );
224
225 $text = preg_replace(
226 '#{youtube}[^{]+v=([^{&]+)(&[^{]*)?{/youtube}#Uis',
227 '<a target="_blank" href="https://www.youtube.com/watch?v=$1"><img src="https://img.youtube.com/vi/$1/0.jpg"/></a>',
228 $text
229 );
230
231 $text = preg_replace('#{vimeo}(https://vimeo.com/[^{]+){/vimeo}#Uis', '<iframe src="$1"></iframe>', $text);
232 $text = preg_replace('#{vimeo}([^{]+){/vimeo}#Uis', '<iframe src="https://player.vimeo.com/video/$1"></iframe>', $text);
233
234 // Vimeo iframes
235 if (preg_match_all('#<iframe[^>]*src="[^"]*vimeo[^"]*/(\d+)([&/\?][^"]*)?"[^>]*>[^<]*</iframe>#Uis', $text, $matches)) {
236 foreach ($matches[1] as $key => $match) {
237 $hash = acym_fileGetContent('https://vimeo.com/api/v2/video/'.$match.'.php');
238 // @ on purpose, the unserialize returns false if $hash isn't a serialized data, but it also throws a notice...
239 $hash = @unserialize($hash);
240 if (empty($hash)) continue;
241
242 if (strpos($matches[0][$key], ' width="') !== false) {
243 $extension = substr($hash[0]['thumbnail_large'], strrpos($hash[0]['thumbnail_large'], '.'));
244 preg_match('#width="([^"]*)"#Uis', $matches[0][$key], $width);
245
246 $replace = strpos($hash[0]['thumbnail_large'], '_') === false ? '.' : '_';
247 $hash[0]['thumbnail_large'] = substr($hash[0]['thumbnail_large'], 0, strrpos($hash[0]['thumbnail_large'], $replace)).'_'.$width[1].$extension;
248 }
249 $thumbnail = 'https://i.vimeocdn.com/filter/overlay?src='.urlencode($hash[0]['thumbnail_large']);
250 $thumbnail .= '&src='.urlencode('https://f.vimeocdn.com/p/images/crawler_play.png');
251
252 $text = str_replace(
253 $matches[0][$key],
254 '<a target="_blank" href="'.esc_attr($hash[0]['url']).'"><img class="donotresize" alt="" src="'.esc_attr($thumbnail).'" /></a>',
255 $text
256 );
257 }
258 }
259
260 //Other videos
261 $text = preg_replace('#\[embed=videolink][^}]*video":"([^"]*)[^}]*}\[/embed]#i', '<a target="_blank" href="$1"><img src="'.ACYM_IMAGES.'/video.png"/></a>', $text);
262 $text = preg_replace('#<video[^>]*src="([^"]*)"[^>]*>[^>]*</video>#i', '<a target="_blank" href="$1"><img src="'.ACYM_IMAGES.'/video.png"/></a>', $text);
263 }
264
265 /**
266 * Convert pictures base64 code into a real picture
267 */
268 private function convertbase64pictures(string &$html): void
269 {
270 if (!preg_match_all('#<img[^>]*src=("data:image/([^;]{1,5});base64[^"]*")([^>]*)>#Uis', $html, $resultspictures)) {
271 return;
272 }
273
274 //Just in case of... we will need it
275
276
277 $dest = ACYM_MEDIA.'resized'.DS;
278 acym_createDir($dest);
279 foreach ($resultspictures[2] as $i => $extension) {
280 $pictname = md5($resultspictures[1][$i]).'.'.$extension;
281 $picturl = ACYM_LIVE.str_replace(DS, '/', ACYM_MEDIA_FOLDER).'resized/'.$pictname;
282 $pictPath = $dest.$pictname;
283 $pictCode = trim($resultspictures[1][$i], '"');
284 if (file_exists($pictPath)) {
285 //The picture is already there... lets use it then
286 $html = str_replace($pictCode, $picturl, $html);
287 continue;
288 }
289
290 if (!ini_get('allow_url_fopen')) {
291 continue;
292 }
293
294 $getfunction = '';
295 switch ($extension) {
296 case 'gif':
297 $getfunction = 'ImageCreateFromGIF';
298 break;
299 case 'jpg':
300 case 'jpeg':
301 $getfunction = 'ImageCreateFromJPEG';
302 break;
303 case 'png':
304 $getfunction = 'ImageCreateFromPNG';
305 break;
306 }
307
308 //The function does not exists or we didn't find the right function... we just skip that action
309 if (empty($getfunction) || !function_exists($getfunction)) {
310 continue;
311 }
312
313 $img = $getfunction($pictCode);
314 if ($img === false) {
315 continue;
316 }
317
318 if (in_array($extension, ['gif', 'png'])) {
319 imagealphablending($img, false);
320 imagesavealpha($img, false);
321 }
322
323 //We display the result and then save it to avoid the issue with FTP configurations
324 ob_start();
325 switch ($extension) {
326 case 'gif':
327 $status = imagegif($img);
328 break;
329 case 'jpg':
330 case 'jpeg':
331 $status = imagejpeg($img, null, 100);
332 break;
333 case 'png':
334 $status = imagepng($img, null, 1);
335 break;
336 }
337 $imageContent = ob_get_clean();
338 $status = $status && acym_writeFile($pictPath, $imageContent);
339
340 //we could not save or convert the picture, we skip it
341 if (!$status) {
342 continue;
343 }
344 $html = str_replace($pictCode, $picturl, $html);
345 }
346 }
347
348 //This function will remove or replace code we should not have in an html view.
349 public function cleanHtml(string &$html): void
350 {
351 $this->convertbase64pictures($html);
352
353 //add line-height: 0px; in the TR style when there is only images in the cell, not extra text (new issue with gmail)
354 //handle a line-height automatically for <tr> <td> <img...></td></tr>
355 $pregreplace = [];
356 $pregreplace['#<tr([^>"]*>([^<]*<td[^>]*>[ \n\s]*<img[^>]*>[ \n\s]*</ *td[^>]*>[ \n\s]*)*</ *tr)#Uis'] = '<tr style="line-height: 0px;" $1';
357 $pregreplace['#<td(((?!style|>).)*>[ \n\s]*(<a[^>]*>)?[ \n\s]*<img[^>]*>[ \n\s]*(</a[^>]*>)?[ \n\s]*</ *td)#Uis'] = '<td style="line-height: 0px;" $1';
358
359 //No number tab system {tab=...}{/tabs} or {tab }{/tabs} and jcomments system
360 $pregreplace['#{tab[ =][^}]*}#is'] = '';
361 $pregreplace['#{/tabs}#is'] = '';
362 $pregreplace['#{jcomments\s+(on|off|lock)}#is'] = '';
363
364 //Remove the JS...
365 $pregreplace["#(onmouseout|onmouseover|onclick|onfocus|onload|onblur) *= *\"(?:(?!\").)*\"#Ui"] = '';
366 $pregreplace["#< *script(?:(?!< */ *script *>).)*< */ *script *>#Uis"] = '';
367 $pregreplace["#< *iframe(?:(?!< */ *iframe *>).)*< */ *iframe *>#Uis"] = '';
368
369 // May God punish every person using Outlook, or imposing this monstrosity for his employees
370 $pregreplace['#(<p style=")([^>]*>\s*<img *[^>]*margin-left: auto; margin-right: auto;[^>]*>\s*</p>)#Uis'] = '$1text-align: center;$2';
371 // Outlook doesn't handle webp images
372 $pregreplace['#(<img [^>]*src="[^"]+\.webp"[^>]*>)#Uis'] = '<!--[if !mso]><!-->$1<!--<![endif]-->';
373
374 $newbody = preg_replace(array_keys($pregreplace), $pregreplace, $html);
375 //we do it in two steps as this regex can break the page
376 if (!empty($newbody)) {
377 $html = $newbody;
378 }
379
380 $body = preg_replace_callback('/src="([^"]* [^"]*)"/Ui', [$this, 'convertSpaces'], $html);
381 if (!empty($body)) $html = $body;
382
383 $html = acym_cmsCleanHtml($html);
384 }
385
386 public function convertSpaces(array $matches): string
387 {
388 return "src='".str_replace(' ', '%20', $matches[1])."'";
389 }
390
391 /*
392 * Replace tags in all the email variables where it is possible, in text version or html version
393 */
394 public function replaceTags(object &$email, array $tags, bool $html = false): void
395 {
396 if (empty($tags)) return;
397
398 $htmlVars = ['body'];
399 $textVars = [
400 'subject',
401 'AltBody',
402 'From',
403 'FromName',
404 'ReplyTo',
405 'ReplyName',
406 'bcc',
407 'cc',
408 'fromname',
409 'fromemail',
410 'replyname',
411 'replyemail',
412 'params',
413 'preheader',
414 ];
415
416 $variables = array_merge($htmlVars, $textVars);
417
418 if ($html) {
419 if (empty($this->mailerHelper)) {
420 $this->mailerHelper = new MailerHelper();
421 }
422
423 $textreplace = [];
424 foreach ($tags as $i => $replacement) {
425 if (isset($textreplace[$i])) continue;
426 $textreplace[$i] = $this->mailerHelper->textVersion($replacement);
427 }
428 } else {
429 $textreplace = $tags;
430 }
431
432 foreach ($variables as $var) {
433 if (empty($email->$var)) continue;
434 $email->$var = $this->replaceDText($email->$var, in_array($var, $htmlVars) ? $tags : $textreplace);
435 }
436 }
437
438 public function replaceDText($text, $replacement)
439 {
440 if (is_array($text)) {
441 foreach ($text as &$oneCell) {
442 if (empty($oneCell)) continue;
443 $oneCell = $this->replaceDText($oneCell, $replacement);
444 }
445 } elseif (is_string($text) && !empty($text)) {
446 foreach ($replacement as $code => $value) {
447 $codes = [$code, urlencode($code)];
448 if (is_null($value)) {
449 $value = '';
450 }
451 $safePregValue = str_replace('$', '\$', $value);
452
453 foreach ($codes as $oneCode) {
454 // Dtext specific syntax
455 $text = preg_replace(
456 '#<span[^>]+'.preg_quote($oneCode, '#').'.+</em>[^<]*</span>#Uis',
457 $safePregValue,
458 $text
459 );
460
461 // Dcontent specific syntax
462 $text = preg_replace(
463 '#(<tr[^>]+)data-dynamic="'.preg_quote($oneCode, '#').'"([^>]+>[^<]*<td[^>]*>).+</i>[^<]*</td>[^<]*</tr>#Uis',
464 '${1}${2}'.$safePregValue.'</td></tr>',
465 $text
466 );
467
468 // If the code was inserted directly in the email (in the subject for example, or a copy-paste)
469 $text = str_replace($oneCode, $value, $text);
470 }
471 }
472 }
473
474 return $text;
475 }
476
477 /**
478 * This function extracts tags from the mail by checking the subject,body and altbody and returns an array of tag objects
479 * tagfamily is "vmproduct" for example to handle tags such as {vmproduct:23|file|price}
480 */
481 public function extractTags(object $email, string $tagfamily): array
482 {
483 $results = [];
484
485 $match = '#(?:{|%7B)'.$tagfamily.'(?:%3A|\\:)(.*)(?:}|%7D)#Ui';
486 //If you add a variable there, don't forget to add it in the replaceTags function as well!
487 $variables = [
488 'subject',
489 'AltBody',
490 'body',
491 'From',
492 'FromName',
493 'ReplyTo',
494 'ReplyName',
495 'bcc',
496 'cc',
497 'fromname',
498 'fromemail',
499 'replyname',
500 'replyemail',
501 'params',
502 'preheader',
503 ];
504 $found = false;
505 foreach ($variables as $var) {
506 if (empty($email->$var)) continue;
507
508 if (is_array($email->$var)) {
509 foreach ($email->$var as $i => $arrayField) {
510 if (empty($arrayField)) continue;
511
512 if (is_array($arrayField)) {
513 foreach ($arrayField as $a => $oneval) {
514 $found = preg_match_all($match, $oneval, $results[$var.$i.'-'.$a]) || $found;
515 if (empty($results[$var.$i.'-'.$a][0])) unset($results[$var.$i.'-'.$a]);
516 }
517 } else {
518 $found = preg_match_all($match, $arrayField, $results[$var.$i]) || $found;
519 if (empty($results[$var.$i][0])) unset($results[$var.$i]);
520 }
521 }
522 } else {
523 $found = preg_match_all($match, $email->$var, $results[$var]) || $found;
524 //we unset the results so that we won't handle it later... it will save some memory and processing
525 if (empty($results[$var][0])) unset($results[$var]);
526 }
527 }
528
529 //If we didn't find anything...
530 if (!$found) {
531 return [];
532 }
533
534 $tags = [];
535 foreach ($results as $var => $allresults) {
536 foreach ($allresults[0] as $i => $oneTag) {
537 //Don't need to process twice a tag we already have!
538 if (isset($tags[$oneTag])) {
539 continue;
540 }
541 $tags[$oneTag] = $this->extractTag($allresults[1][$i]);
542 }
543 }
544
545 return $tags;
546 }
547
548 /**
549 * Converts a tag 23|file:myfile|price into an object.
550 */
551 public function extractTag(string $oneTag): \stdClass
552 {
553 $oneTag = str_replace(['[time]+', '[time]-'], [urlencode('[time]+'), urlencode('[time]-')], $oneTag);
554 $arguments = explode('|', acym_stripTags(urldecode($oneTag)));
555 $tag = new \stdClass();
556 $tag->id = $arguments[0];
557 $tag->default = '';
558 for ($i = 1, $a = count($arguments); $i < $a; $i++) {
559 $args = explode(':', $arguments[$i], 2);
560 $arg0 = trim($args[0]);
561 if (empty($arg0)) continue;
562
563 if (isset($args[1])) {
564 $tag->$arg0 = $args[1];
565 //We may have an extra parameter, especially for date format.
566 if (isset($args[2])) {
567 $tag->{$args[0]} .= ':'.$args[2];
568 }
569 } else {
570 $tag->$arg0 = true;
571 }
572 }
573
574 return $tag;
575 }
576
577 /**
578 * Wrap the text using the wrapValue
579 */
580 public function wrapText(string $text, object $tag): string
581 {
582 if (empty($tag->wrap)) {
583 return $text;
584 }
585
586 $tag->wrap = intval($tag->wrap);
587
588 $newText = str_replace(['&lt', '&gt'], ['<', '>'], $text);
589
590 if (mb_strlen(acym_stripTags($newText)) <= $tag->wrap) {
591 return $newText;
592 }
593
594 if (!class_exists('DOMDocument', false)) {
595 return mb_substr(acym_stripTags($newText), 0, $tag->wrap).'…';
596 }
597
598 if (function_exists('libxml_use_internal_errors')) {
599 libxml_use_internal_errors(true);
600 }
601
602 $dom = new \DOMDocument('1.0', 'UTF-8');
603 $dom->loadHTML(
604 '<?xml encoding="UTF-8">'.$newText,
605 LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
606 );
607
608 $count = 0;
609 $this->truncateNode($dom, $dom, $tag->wrap, $count);
610
611 return $dom->saveHTML();
612 }
613
614 private function truncateNode(\DOMDocument $dom, \DOMNode $node, int $limit, int &$count): bool
615 {
616 for ($i = 0; $i < $node->childNodes->length; $i++) {
617 $child = $node->childNodes->item($i);
618
619 // Text node
620 if ($child->nodeType === XML_TEXT_NODE) {
621 $text = $child->nodeValue;
622 $len = mb_strlen($text);
623
624 if ($count + $len > $limit) {
625 $remaining = $limit - $count;
626
627 // Take the max allowed slice
628 $slice = mb_substr($text, 0, $remaining);
629
630 // Try to cut at a word boundary (spaces or punctuation)
631 if (preg_match('/^(.+?)([\s\p{P}]+)[^\s\p{P}]*$/u', $slice, $m)) {
632 $slice = rtrim($m[1]);
633 }
634
635 // Final assignment
636 $child->nodeValue = $slice.'…';
637
638 // Remove remaining siblings
639 while ($child->nextSibling) {
640 $node->removeChild($child->nextSibling);
641 }
642
643 return true;
644 }
645
646 $count += $len;
647 }
648
649 // Element node → recurse
650 if ($child->hasChildNodes() && $this->truncateNode($dom, $child, $limit, $count)) {
651 while ($child->nextSibling) {
652 $node->removeChild($child->nextSibling);
653 }
654
655 return true;
656 }
657 }
658
659 return false;
660 }
661
662 /**
663 * Returns a formatted version of the passed content
664 *
665 * TOP_LEFT : title on top, image with float left and covering description
666 * TOP_RIGHT : title on top, image with float rigth and covering description
667 * TITLE_IMG : image on title's left on top, description below
668 * TITLE_IMG_RIGHT : image on title's right on top, description below
669 * CENTER_IMG : title on top, image on one line and description below
670 * TOP_IMG : image on top, then title and description
671 * COL_LEFT : image on left column, title and description on right column
672 * COL_RIGHT : image on right column, title and description on left column
673 */
674 public function getStandardDisplay(object $format): string
675 {
676 // By default, put float left on the picture
677 if (empty($format->tag->format)) {
678 $format->tag->format = 'TOP_LEFT';
679 }
680 if (!in_array($format->tag->format, ['TOP_LEFT', 'TOP_RIGHT', 'TITLE_IMG', 'TITLE_IMG_RIGHT', 'CENTER_IMG', 'TOP_IMG', 'COL_LEFT', 'COL_RIGHT'])) {
681 $format->tag->format = 'TOP_LEFT';
682 }
683
684 $invertValues = [
685 'TOP_LEFT' => 'TOP_RIGHT',
686 'TITLE_IMG' => 'TITLE_IMG_RIGHT',
687 'COL_LEFT' => 'COL_RIGHT',
688 'TOP_RIGHT' => 'TOP_LEFT',
689 'TITLE_IMG_RIGHT' => 'TITLE_IMG',
690 'COL_RIGHT' => 'COL_LEFT',
691 ];
692 if (!empty($format->tag->invert) && !empty($invertValues[$format->tag->format])) {
693 $format->tag->format = $invertValues[$format->tag->format];
694 }
695
696 // Get the image with float left/right or no style
697 $image = '';
698 if (!empty($format->imagePath)) {
699 $style = '';
700 $linkStyle = '';
701
702 if (in_array($format->tag->format, ['TOP_LEFT', 'TITLE_IMG'])) {
703 $style = 'left';
704 } elseif (in_array($format->tag->format, ['TOP_RIGHT', 'TITLE_IMG_RIGHT'])) {
705 $style = 'right';
706 }
707
708 if (!empty($style)) {
709 $linkStyle = 'style="float:'.$style.';"';
710
711 if ($style === 'left') {
712 $style = 'style="float:left; margin-right: 7px; margin-bottom: 7px;"';
713 } else {
714 $style = 'style="float:right; margin-left: 7px; margin-bottom: 7px;"';
715 }
716 }
717
718 preg_match('#src="([^"]+)"#Uis', $format->imagePath, $matches);
719 if (!empty($matches[1])) $format->imagePath = $matches[1];
720 $altImage = !empty($format->altImage) ? $format->altImage : '';
721 $image = '<img class="content_main_image" alt="'.esc_attr($altImage).'" src="'.$format->imagePath.'" '.$style.' />';
722
723 if (!empty($format->imageCaption) && !in_array($format->tag->format, ['TITLE_IMG', 'TITLE_IMG_RIGHT'])) {
724 $image .= '<p class="content_main_image_caption">'.esc_html($format->imageCaption).'</p>';
725 }
726 }
727
728 // If TITLE_IMG, add the image to the title
729 $result = '';
730 if (in_array($format->tag->format, ['TITLE_IMG', 'TITLE_IMG_RIGHT'])) {
731 $format->title = $image.$format->title;
732 $image = '';
733 }
734
735 if (!empty($format->link) && !empty($image) && !empty($format->tag->clickableimg)) {
736 $image = '<a target="_blank" href="'.$format->link.'" '.$linkStyle.'>'.$image.'</a>';
737 }
738
739 // If the image should be displayed before the title, do it
740 if ($format->tag->format === 'TOP_IMG' && !empty($image)) {
741 $result = $image;
742 $image = '';
743 }
744
745 // If we want to keep the left/right column for the image, put the whole article in a table
746 if (in_array($format->tag->format, ['COL_LEFT', 'COL_RIGHT'])) {
747 $maxWidth = empty($format->tag->maxwidth) ? '' : ' width: '.$format->tag->maxwidth.'px;';
748 if (empty($image)) {
749 $format->tag->format = 'TOP_LEFT';
750 } else {
751 $result = '<table><tr><td valign="middle" style="vertical-align: middle; padding-right: 7px;" class="acyleftcol">';
752 if ($format->tag->format === 'COL_LEFT') {
753 $result = '<table><tr><td valign="middle" style="vertical-align: middle; padding-right: 7px; '.$maxWidth.'" class="acyleftcol">';
754 $result .= $image.'</td><td valign="top" class="acyrightcol">';
755 }
756 }
757 }
758
759 // Display the title
760 if (!empty($format->title)) {
761 if (!empty($format->link) && !empty($format->tag->clickable)) {
762 if (empty($format->tag->type) || $format->tag->type !== 'title') {
763 $format->title = '<h2 class="acym_title">'.$format->title.'</h2>';
764 }
765
766 $title = '<a';
767 if (!empty($format->tag->type) && $format->tag->type === 'title') $title .= ' class="acym_title"';
768 $title .= ' href="'.$format->link.'" target="_blank" name="'.$this->name.'-'.$format->tag->id.'">';
769 $title .= $format->title;
770 $title .= '</a>';
771 $format->title = $title;
772 } else {
773 if (empty($format->tag->type) || $format->tag->type != 'title') {
774 $format->title = '<h2 class="acym_title">'.$format->title.'</h2>';
775 }
776 }
777
778 $result .= $format->title;
779 }
780
781 if (!empty($format->afterTitle)) {
782 $result .= $format->afterTitle;
783 }
784
785 if (!empty($format->description)) {
786 $format->description = $this->wrapText($format->description, $format->tag);
787 }
788 $this->wrappedText = $format->description;
789
790
791 $rowText = '<div class="acydescription">';
792 $endRow = '</div><br />';
793 // Display the main content based on the chosen format
794 if (in_array($format->tag->format, ['TOP_LEFT', 'TOP_RIGHT', 'TITLE_IMG', 'TITLE_IMG_RIGHT', 'TOP_IMG'])) {
795 if (!empty($image) || !empty($format->description)) {
796 $result .= $rowText.$image.$format->description.$endRow;
797 }
798 } elseif ($format->tag->format == 'CENTER_IMG') {
799 if (!empty($image)) {
800 $result .= '<div class="acymainimage">'.$image.$endRow;
801 }
802
803 if (!empty($format->description)) {
804 $result .= $rowText.$format->description.$endRow;
805 }
806 } elseif (in_array($format->tag->format, ['COL_LEFT', 'COL_RIGHT'])) {
807 if (!empty($format->description)) {
808 $result .= $rowText.$format->description.$endRow;
809 }
810
811 if ($format->tag->format === 'COL_RIGHT') {
812 $result .= '</td><td valign="middle" style="vertical-align: middle; padding-left: 7px; '.$maxWidth.'" class="acyrightcol">'.$image;
813 }
814 $result .= '</td></tr></table>';
815 }
816
817 // Add the custom fields on 1 or 2 columns, with or without the labels
818 if (!empty($format->customFields)) {
819 $result .= '<table style="width:100%;" class="customfieldsarea"><tr>';
820
821 $format->cols = empty($format->tag->nbcols) ? 1 : intval($format->tag->nbcols);
822 if (empty($format->cols)) $format->cols = 1;
823
824 $i = 0;
825 foreach ($format->customFields as $oneField) {
826 if ($i != 0 && $i % $format->cols == 0) $result .= '</tr><tr>';
827
828 if (empty($oneField[1])) {
829 $result .= '<td class="cfvalue" colspan="2">';
830 } else {
831 $result .= '<td nowrap="nowrap" class="cflabel">'.$oneField[1].'</td><td class="cfvalue">';
832 }
833
834 $result .= $oneField[0].'</td>';
835 $i++;
836 }
837
838 while ($i % $format->cols != 0) {
839 $result .= '<td colspan="2"></td>';
840 $i++;
841 }
842
843 $result .= '</tr></table>';
844 }
845
846 // Add what should be displayed at the end (share buttons, read more link, etc...)
847 if (!empty($format->afterArticle)) {
848 $result .= $format->afterArticle;
849 }
850
851 return $result;
852 }
853
854 /**
855 * Resizes or removes the pictures according to the parameters
856 */
857 public function managePicts(object $tag, string $result): string
858 {
859 if (!isset($tag->pict)) {
860 return $result;
861 }
862
863 $imageHelper = new ImageHelper();
864 if ($tag->pict === 'resized') {
865 $imageHelper->maxHeight = empty($tag->maxheight) ? 150 : $tag->maxheight;
866 $imageHelper->maxWidth = empty($tag->maxwidth) ? 150 : $tag->maxwidth;
867 if ($imageHelper->available()) {
868 $result = $imageHelper->resizePictures($result);
869 } elseif (acym_isAdmin()) {
870 acym_enqueueMessage($imageHelper->error, 'notice');
871 }
872 } elseif ($tag->pict == '0') {
873 $result = $imageHelper->removePictures($result);
874 }
875
876 return acym_absoluteURL($result);
877 }
878
879 public function displayOptions(array $options, string $dynamicIdentifier, string $type = 'individual', $defaultValues = null): void
880 {
881 $suffix = preg_replace('#[^a-zA-Z0-9]#U', '_', $dynamicIdentifier);
882 $updateFunction = 'updateDynamic'.$suffix;
883
884 $jsOptions = [];
885 $topOptions = [];
886 $sections = [];
887
888 // ---- Phase 1: classify options + collect the JS (in original loop order) ----
889 foreach ($options as $option) {
890 if (isset($defaultValues->{$option['name']})) {
891 $option['default'] = $defaultValues->{$option['name']};
892 }
893
894 // Preserve the early skip: never rendered, no JS collected.
895 if (
896 $option['type'] === 'boolean'
897 && $option['name'] === 'autologin'
898 && $this->config->get('autologin_urls', 0) != 1
899 ) {
900 continue;
901 }
902
903 $jsOptions[] = $option;
904
905 $isTop = !empty($option['main']) || in_array($option['type'], ['pictures', 'checkbox'], true);
906
907 if ($isTop) {
908 $topOptions[$option['title']] = ['kind' => 'field', 'option' => $option];
909
910 // The format block is attached right after the "display" checkbox.
911 if (
912 $option['type'] === 'checkbox'
913 && $option['title'] === 'ACYM_DISPLAY'
914 && (!isset($option['format']) || $option['format'])
915 ) {
916 $topOptions['ACYM_FORMAT'] = ['kind' => 'format', 'option' => $option];
917 }
918 continue;
919 }
920
921 $section = empty($option['section']) ? 'ACYM_OTHER_OPTIONS' : $option['section'];
922 $sections[$section][$option['name']] = $option;
923 }
924
925 // "Other Options" is always rendered last.
926 if (isset($sections['ACYM_OTHER_OPTIONS'])) {
927 $otherOptions = $sections['ACYM_OTHER_OPTIONS'];
928 unset($sections['ACYM_OTHER_OPTIONS']);
929 $sections['ACYM_OTHER_OPTIONS'] = $otherOptions;
930 }
931
932 // ---- Phase 2: render (echo directly, in the original visual order) ----
933 foreach ($topOptions as $headerLabel => $entry) {
934 $this->displayPanelHeader($headerLabel);
935 echo '<div class="acym__wysid__right__toolbar__design--show acym__wysid__right__toolbar__design acym__wysid__context__modal__container grid-x">';
936 if ($entry['kind'] === 'format') {
937 $this->displayFormatPanel($suffix, $updateFunction, $type, $defaultValues);
938 } else {
939 $this->displayOptionField($entry['option'], $suffix, $updateFunction, $defaultValues);
940 }
941 echo '</div>';
942 }
943
944 foreach ($sections as $sectionName => $sectionOptions) {
945 $this->displayPanelHeader($sectionName);
946 echo '<div class="acym__wysid__right__toolbar__design--show acym__wysid__right__toolbar__design acym__wysid__context__modal__container grid-x">';
947 foreach ($sectionOptions as $option) {
948 echo '<div class="cell grid-x margin-bottom-1">';
949 $this->displayFieldLabel($option, $suffix);
950 echo '<div class="cell large-7">';
951 $this->displayOptionField($option, $suffix, $updateFunction, $defaultValues);
952 echo '</div>';
953 echo '</div>';
954 }
955 echo '</div>';
956 }
957
958 // ---- Phase 3: the inline script + individual extras ----
959 $this->displayDynamicScript($jsOptions, $suffix, $updateFunction, $dynamicIdentifier, $type, $defaultValues);
960
961 if ($type === 'individual') {
962 acym_trigger('displayCustomViewEditor', [], 'plgAcym'.ucfirst($dynamicIdentifier));
963 echo '<input type="hidden" id="acym__dynamic__update__function" value="'.esc_attr($updateFunction).'">';
964 }
965 }
966
967 private function displayPanelHeader(string $labelKey): void
968 {
969 echo '<p class="acym__wysid__right__toolbar__p acym__wysid__right__toolbar__p__open acym__title">';
970 echo esc_html(acym_translation($labelKey));
971 echo '<i class="acymicon-keyboard-arrow-up"></i>';
972 echo '</p>';
973 }
974
975 private function displayFieldLabel(array $option, string $suffix): void
976 {
977 echo '<label class="cell large-5 acym_plugin_field acym_plugin_field_'.esc_attr($option['type']).'" for="'.esc_attr($option['name'].$suffix).'">';
978 echo esc_html(acym_translation($option['title']));
979 if (!empty($option['tooltip'])) {
980 echo '&nbsp;';
981 acym_info(
982 [
983 'textShownInTooltip' => $option['tooltip'],
984 'classIcon' => 'acym_plugin_field_'.$option['name'],
985 ]
986 );
987 }
988 echo '</label>';
989 }
990
991 private function displayOptionField(array $option, string $suffix, string $updateFunction, $defaultValues): void
992 {
993 switch ($option['type']) {
994 case 'pictures':
995 $this->displayPicturesField($option, $suffix, $updateFunction, $defaultValues);
996 break;
997 case 'checkbox':
998 $this->displayCheckboxField($option, $suffix, $updateFunction);
999 break;
1000 case 'boolean':
1001 acym_boolean($option['name'].$suffix, $option['default'], $option['name'].$suffix, ['onclick' => $updateFunction.'();']);
1002 break;
1003 case 'radio':
1004 $this->displayRadioField($option, $suffix, $updateFunction);
1005 break;
1006 case 'select':
1007 $this->displaySelectField($option, $suffix, $updateFunction);
1008 break;
1009 case 'multiselect':
1010 $this->displayMultiselectField($option, $suffix, $updateFunction);
1011 break;
1012 case 'text':
1013 $default = $option['default'] ?? '';
1014 $class = empty($option['class']) ? 'acym_plugin_text_field' : $option['class'];
1015 echo '<input type="text"
1016 name="'.esc_attr($option['name'].$suffix).'"
1017 id="'.esc_attr($option['name'].$suffix).'"
1018 onchange="'.esc_attr($updateFunction).'();"
1019 value="'.esc_attr($default).'"
1020 class="'.esc_attr($class).'"';
1021 if (!empty($option['placeholder'])) {
1022 echo ' placeholder="'.esc_attr($option['placeholder']).'"';
1023 }
1024 echo '/>';
1025 break;
1026 case 'number':
1027 $class = empty($option['class']) ? 'acym_plugin_text_field' : $option['class'];
1028 echo '<input type="number"
1029 min="'.esc_attr(empty($option['min']) ? '0' : $option['min']).'"
1030 id="'.esc_attr($option['name'].$suffix).'"
1031 onchange="'.esc_attr($updateFunction).'();"
1032 value="'.intval($option['default']).'"
1033 name="'.esc_attr($option['name'].$suffix).'"
1034 class="'.esc_attr($class).'" ';
1035 if (!empty($option['max'])) {
1036 echo ' max="'.esc_attr($option['max']).'"';
1037 }
1038 echo ' />';
1039 break;
1040 case 'intextfield':
1041 $inputType = (!empty($option['isNumber']) && $option['isNumber'] === 1) ? 'number' : 'text';
1042 echo wp_kses(
1043 acym_translationSprintf(
1044 $option['text'],
1045 '<input type="'.esc_attr($inputType).'"
1046 name="'.esc_attr($option['name'].$suffix).'"
1047 id="'.esc_attr($option['name'].$suffix).'"
1048 class="intext_input"
1049 value="'.esc_attr($option['default']).'"
1050 onchange="'.esc_attr($updateFunction).'();"/>'
1051 ),
1052 [
1053 'input' => [
1054 'type' => true,
1055 'name' => true,
1056 'id' => true,
1057 'class' => true,
1058 'value' => true,
1059 'onchange' => true,
1060 ],
1061 ]
1062 );
1063 break;
1064 case 'date':
1065 $relativeTime = empty($option['relativeDate']) ? '-' : $option['relativeDate'];
1066 $dateDefault = $option['default'];
1067 if (!empty($dateDefault) && !is_numeric($dateDefault) && strpos($dateDefault, '[time]') === false) {
1068 $dateDefault = strtotime($dateDefault);
1069 }
1070 echo wp_kses(
1071 acym_dateField($option['name'].$suffix, $dateDefault, '', ' onchange="'.esc_attr($updateFunction).'();"', $relativeTime),
1072 SecurityHelper::ALLOWED_HTML_DATE
1073 );
1074 break;
1075 case 'language':
1076 $this->displayLanguageField($option, $suffix, $updateFunction);
1077 break;
1078 case 'custom':
1079 $this->displayCustomOptionOutput($option);
1080 break;
1081 }
1082 }
1083
1084 private function displayCustomOptionOutput(array $option): void
1085 {
1086 if (empty($option['output'])) {
1087 return;
1088 }
1089
1090 if (!is_string($option['output']) && is_callable($option['output'])) {
1091 ($option['output'])();
1092 }
1093 }
1094
1095 private function displayPicturesField(array $option, string $suffix, string $updateFunction, $defaultValues): void
1096 {
1097 $displayedPictures = $option['default'] ?? 'resized';
1098 if (isset($defaultValues->pict)) {
1099 $displayedPictures = $defaultValues->pict;
1100 }
1101 $maxWidth = $defaultValues->maxwidth ?? 150;
1102 $maxHeight = $defaultValues->maxheight ?? 150;
1103
1104 $valImages = [
1105 acym_selectOption('1', 'ACYM_YES'),
1106 acym_selectOption('resized', 'ACYM_RESIZED'),
1107 acym_selectOption('0', 'ACYM_NO'),
1108 ];
1109
1110 echo '<div class="cell large-5 acym_plugin_field">';
1111 echo esc_html(acym_translation('ACYM_DISPLAY'));
1112 echo '</div>';
1113 echo '<div class="cell large-7">';
1114 acym_radio(
1115 $valImages,
1116 'pict'.$suffix,
1117 $displayedPictures,
1118 ['onclick' => $updateFunction.'();'],
1119 ['containerClass' => 'dcontent_pictures'],
1120 !acym_isAdmin()
1121 );
1122 echo '</div>';
1123
1124 echo '<div id="pictsize'.esc_attr($suffix).'" class="cell grid-x margin-y margin-top-1"';
1125 if ('resized' !== $displayedPictures) {
1126 echo ' style="display: none;"';
1127 }
1128 echo '>';
1129 echo '<div class="cell large-5 acym_plugin_field">';
1130 echo esc_html(acym_translation('ACYM_MAX_WIDTH'));
1131 echo '</div>';
1132 echo '<div class="cell large-7">';
1133 echo '<input class="intext_input"
1134 name="pictwidth'.esc_attr($suffix).'"
1135 type="number"
1136 onchange="'.esc_attr($updateFunction).'();"
1137 value="'.intval($maxWidth).'"/>';
1138 echo '</div>';
1139 echo '<div class="cell large-5 acym_plugin_field">';
1140 echo esc_html(acym_translation('ACYM_MAX_HEIGHT'));
1141 echo '</div>';
1142 echo '<div class="cell large-7">';
1143 echo '<input class="intext_input"
1144 name="pictheight'.esc_attr($suffix).'"
1145 type="number"
1146 onchange="'.esc_attr($updateFunction).'();"
1147 value="'.intval($maxHeight).'"/>';
1148 echo '</div>';
1149 echo '</div>';
1150
1151 if (!empty($option['caption'])) {
1152 echo '<div class="cell grid-x margin-top-1">';
1153 echo '<label class="cell large-5 acym_plugin_field">';
1154 echo esc_html(acym_translation('ACYM_CAPTION'));
1155 echo '</label>';
1156 acym_radio(
1157 [
1158 acym_selectOption('1', 'ACYM_YES'),
1159 acym_selectOption('0', 'ACYM_NO'),
1160 ],
1161 'caption'.$suffix,
1162 $defaultValues->caption ?? '0',
1163 ['onclick' => $updateFunction.'();'],
1164 ['containerClass' => 'cell large-7']
1165 );
1166 echo '</div>';
1167 }
1168 }
1169
1170 private function displayCheckboxField(array $option, string $suffix, string $updateFunction): void
1171 {
1172 if (!empty($option['default'])) {
1173 $checkedValues = explode(',', $option['default']);
1174 foreach ($option['options'] as $key => $oneOption) {
1175 $oneOption[1] = in_array($key, $checkedValues);
1176 $option['options'][$key] = $oneOption;
1177 }
1178 }
1179
1180 echo '<div class="cell grid-x">';
1181 foreach ($option['options'] as $value => $title) {
1182 echo '<div class="cell medium-6" ';
1183 if (!empty($title[2]) && is_array($title[2])) {
1184 foreach ($title[2] as $attribute => $value) {
1185 echo esc_attr($attribute).'="'.esc_attr($value).'"';
1186 }
1187 }
1188 echo '>';
1189 echo '<input type="checkbox"
1190 name="'.esc_attr($option['name'].$suffix).'"
1191 value="'.esc_attr($value).'"
1192 id="'.esc_attr($value.$suffix).'"
1193 onclick="'.esc_attr($updateFunction).'();" ';
1194 checked((bool)$title[1]);
1195 echo ' />';
1196 echo '<label style="margin-left:5px" for="'.esc_attr($value.$suffix).'">';
1197 echo esc_html(acym_translation($title[0]));
1198 echo '</label>';
1199 echo '</div>';
1200 }
1201 echo '</div>';
1202 }
1203
1204 private function displayRadioField(array $option, string $suffix, string $updateFunction): void
1205 {
1206 $radioOptions = [];
1207 foreach ($option['options'] as $value => $title) {
1208 $radioOptions[] = acym_selectOption($value, $title);
1209 }
1210 acym_radio(
1211 $radioOptions,
1212 $option['name'].$suffix,
1213 $option['default'],
1214 ['onclick' => $updateFunction.'();'],
1215 ['pluginMode' => true],
1216 !acym_isAdmin()
1217 );
1218 }
1219
1220 private function displaySelectField(array $option, string $suffix, string $updateFunction): void
1221 {
1222 $selectOptions = [];
1223 foreach ($option['options'] as $value => $title) {
1224 if (is_object($title)) {
1225 $selectOptions[] = acym_selectOption($title->value, $title->text);
1226 } else {
1227 $selectOptions[] = acym_selectOption($value, $title);
1228 }
1229 }
1230
1231 $default = empty($option['default']) ? null : $option['default'];
1232 $defaultOrder = null;
1233 if (!empty($default) && strpos($default, ',')) {
1234 [$default, $defaultOrder] = explode(',', $default);
1235 }
1236
1237 $attributes = [
1238 'onchange' => $updateFunction.'();',
1239 'id' => $option['name'].$suffix,
1240 ];
1241 if ($option['name'] === 'order') {
1242 $attributes['class'] = 'acym__dynamics__ordering__select';
1243 }
1244
1245 acym_select(
1246 $selectOptions,
1247 $option['name'].$suffix,
1248 $default,
1249 $attributes,
1250 'value',
1251 'text',
1252 null,
1253 false,
1254 true
1255 );
1256
1257 if ($option['name'] === 'order') {
1258 $dirs = [
1259 'desc' => acym_translation('ACYM_DESC'),
1260 'asc' => acym_translation('ACYM_ASC'),
1261 ];
1262 if (empty($defaultOrder)) {
1263 $defaultOrder = empty($option['defaultdir']) ? null : $option['defaultdir'];
1264 }
1265 echo ' ';
1266 acym_select(
1267 $dirs,
1268 'orderdir'.$suffix,
1269 $defaultOrder,
1270 [
1271 'onchange' => $updateFunction.'();',
1272 'style' => 'width: 115px;',
1273 'class' => 'acym__dynamics__ordering__select',
1274 ],
1275 'value',
1276 'text',
1277 null,
1278 false,
1279 true
1280 );
1281 }
1282 }
1283
1284 private function displayMultiselectField(array $option, string $suffix, string $updateFunction): void
1285 {
1286 $selectOptions = [];
1287 foreach ($option['options'] as $value => $title) {
1288 $selectOptions[] = acym_selectOption($value, $title);
1289 }
1290
1291 if (!isset($option['default'])) {
1292 $option['default'] = [];
1293 }
1294 if (!is_array($option['default'])) {
1295 $option['default'] = explode(',', $option['default']);
1296 }
1297
1298 acym_selectMultiple(
1299 $selectOptions,
1300 $option['name'].$suffix,
1301 $option['default'],
1302 ['onchange' => $updateFunction.'();', 'id' => $option['name'].$suffix],
1303 'value',
1304 'text',
1305 true
1306 );
1307 }
1308
1309 private function displayLanguageField(array $option, string $suffix, string $updateFunction): void
1310 {
1311 $languageOptions = ['any' => acym_translation('ACYM_ANY')];
1312 foreach (acym_getLanguages(true) as $language) {
1313 $languageOptions[$language->language] = $language->name;
1314 }
1315
1316 if (empty($option['default'])) {
1317 $option['default'] = acym_getVar('string', 'language');
1318 if (acym_isMultilingual() && (empty($option['default']) || $option['default'] === 'main')) {
1319 $option['default'] = $this->config->get('multilingual_default');
1320 }
1321 }
1322
1323 acym_select(
1324 $languageOptions,
1325 $option['name'].$suffix,
1326 empty($option['default']) ? null : $option['default'],
1327 ['onchange' => $updateFunction.'();', 'id' => $option['name'].$suffix],
1328 'value',
1329 'text',
1330 null,
1331 false,
1332 true
1333 );
1334 }
1335
1336 private function displayFormatPanel(string $suffix, string $updateFunction, string $type, $defaultValues): void
1337 {
1338 echo '<div class="grid-x">';
1339 echo '<div class="cell large-3">';
1340 echo esc_html(acym_translation('ACYM_FORMAT'));
1341 echo '</div>';
1342 echo '<div class="cell large-9 dcontentFormatContainer">';
1343
1344 $default = empty($defaultValues->format) ? 'TOP_LEFT' : $defaultValues->format;
1345 $formats = ['TOP_LEFT', 'TOP_RIGHT', 'TITLE_IMG', 'TITLE_IMG_RIGHT', 'CENTER_IMG', 'TOP_IMG', 'COL_LEFT', 'COL_RIGHT'];
1346 foreach ($formats as $oneFormat) {
1347 $class = 'button-radio';
1348 if ($default === $oneFormat) {
1349 $class .= ' button-radio-selected';
1350 }
1351 echo '<button class="'.esc_attr($class).'"
1352 acym-button-radio-group="dcontentFormat'.esc_attr($suffix).'"
1353 acym-data-type="'.esc_attr($oneFormat).'"
1354 acym-callback="'.esc_attr($updateFunction).'">';
1355 echo '<img alt="'.esc_attr($oneFormat).'" src="'.esc_url(ACYM_IMAGES.'editor/dcontent_formats/'.strtolower($oneFormat).'.png').'"/>';
1356 echo '</button>';
1357 }
1358 echo '</div>';
1359
1360 if ($type === 'grouped') {
1361 echo '<div class="cell large-3">';
1362 echo esc_html(acym_translation('ACYM_ALTERNATE'));
1363 acym_info(['textShownInTooltip' => 'ACYM_ALTERNATE_DESC']);
1364 echo '</div>';
1365 echo '<div class="cell large-9">';
1366 acym_boolean(
1367 'alternate'.$suffix,
1368 !empty($defaultValues->alternate),
1369 'alternate'.$suffix,
1370 ['onclick' => $updateFunction.'();']
1371 );
1372 echo '</div>';
1373 }
1374
1375 echo '</div>';
1376 }
1377
1378 private function echoOptionJs(array $option, string $suffix, string $type): void
1379 {
1380 switch ($option['type']) {
1381 case 'pictures':
1382 if (!empty($option['caption'])) {
1383 echo 'otherinfo += "| caption:" + jQuery(\'input[name="caption'.esc_attr($suffix).'"]:checked\').val();
1384 ';
1385 }
1386 echo '
1387 var _pictVal'.esc_attr($suffix).' = jQuery(\'input[name="pict'.esc_attr($suffix).'"]:checked\').val();
1388 otherinfo += "| pict:" + _pictVal'.esc_attr($suffix).';
1389
1390 if (_pictVal'.esc_attr($suffix).' == "resized") {
1391 jQuery("#pictsize'.esc_attr($suffix).'").show();
1392 otherinfo += "| maxwidth:" + jQuery(\'input[name="pictwidth'.esc_attr($suffix).'"]\').val();
1393 otherinfo += "| maxheight:" + jQuery(\'input[name="pictheight'.esc_attr($suffix).'"]\').val();
1394 }else{
1395 jQuery("#pictsize'.esc_attr($suffix).'").hide();
1396 }
1397 ';
1398
1399 break;
1400
1401 case 'checkbox':
1402 $separator = empty($option['separator']) ? ',' : $option['separator'];
1403 echo 'var _checked'.esc_attr($option['name'].$suffix).' = [];
1404 jQuery("input:checkbox[name='.esc_attr($option['name'].$suffix).']:checked").each(function(){
1405 _checked'.esc_attr($option['name'].$suffix).'.push(jQuery(this).val());
1406 });
1407 if(_checked'.esc_attr($option['name'].$suffix).'.length) otherinfo += "| '.esc_attr($option['name']).':" + _checked'.esc_attr(
1408 $option['name'].$suffix
1409 ).'.join("'.esc_attr($separator).'");
1410 ';
1411
1412 if ($option['title'] === 'ACYM_DISPLAY' && (!isset($option['format']) || $option['format'])) {
1413 if ($type === 'grouped') {
1414 echo 'var alternate = jQuery(\'input[name="alternate'.esc_attr($suffix).'"]:checked\').val();
1415 ';
1416 echo 'if (!acym_helper.empty(alternate)) otherinfo += "| alternate";
1417 ';
1418 }
1419 echo 'var selectedFormatOption = jQuery(\'.button-radio-selected[acym-button-radio-group="dcontentFormat'.esc_attr($suffix).'"]\');
1420 ';
1421 echo 'if (!acym_helper.empty(selectedFormatOption)) otherinfo += "| format:" + selectedFormatOption.attr("acym-data-type");
1422 ';
1423 }
1424 break;
1425
1426 case 'boolean':
1427 case 'radio':
1428 echo 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'input[name="'.esc_attr($option['name'].$suffix).'"]:checked\').val();
1429 ';
1430 break;
1431
1432 case 'select':
1433 if ($option['name'] === 'order') {
1434 echo 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'[name="'.esc_attr(
1435 $option['name'].$suffix
1436 ).'"]\').val() + "," + jQuery(\'[name="orderdir'.esc_attr($suffix).'"]\').val();
1437 ';
1438 } else {
1439 echo 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'[name="'.esc_attr($option['name'].$suffix).'"]\').val();
1440 ';
1441 }
1442 break;
1443
1444 case 'language':
1445 echo 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'[name="'.esc_attr($option['name'].$suffix).'"]\').val();
1446 ';
1447 break;
1448
1449 case 'multiselect':
1450 echo '
1451 var theMultiSelect = document.querySelector(\'[name="'.esc_attr($option['name'].$suffix).'[]"]\');
1452 var selectedOptions = [];
1453 for(var i = 0 ; i < theMultiSelect.length ; i++){
1454 if(theMultiSelect[i].selected){
1455 selectedOptions.push(theMultiSelect[i].value);
1456 }
1457 }
1458 otherinfo += "| '.esc_attr($option['name']).':" + selectedOptions.join(",");
1459 ';
1460 break;
1461
1462 case 'text':
1463 case 'number':
1464 case 'intextfield':
1465 case 'date':
1466 echo 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'input[name="'.esc_attr($option['name'].$suffix).'"]\').val();
1467 ';
1468 break;
1469
1470 case 'custom':
1471 $this->echoLegacyOptionJs($option);
1472 break;
1473 }
1474 }
1475
1476 private function echoLegacyOptionJs(array $option): void
1477 {
1478 if (empty($option['js'])) {
1479 return;
1480 }
1481
1482 $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
1483
1484 foreach (explode(';', $option['js']) as $statement) {
1485 $statement = trim($statement);
1486 if ($statement === '') {
1487 continue;
1488 }
1489
1490 // otherinfo += "| key:constant"
1491 if (preg_match('#^otherinfo\s*\+=\s*"\|\s*([a-zA-Z0-9_-]+):([^"]*)"$#', $statement, $matches)) {
1492 echo 'otherinfo += '.json_encode('| '.$matches[1].':'.$matches[2], $jsonFlags).';
1493 ';
1494 continue;
1495 }
1496
1497 // otherinfo += "| key:" + jQuery('selector').val()
1498 if (preg_match('#^otherinfo\s*\+=\s*"\|\s*([a-zA-Z0-9_-]+):"\s*\+\s*jQuery\(\s*\'(.*)\'\s*\)\.val\(\)$#', $statement, $matches)) {
1499 echo 'otherinfo += '.json_encode('| '.$matches[1].':', $jsonFlags).' + jQuery('.json_encode($matches[2], $jsonFlags).').val();
1500 ';
1501 continue;
1502 }
1503
1504 acym_logError(
1505 'Unsupported deprecated "js" option declared by an add-on for the option "'.($option['name'] ?? '').'", it was not applied: '.$statement,
1506 'plugin'
1507 );
1508 }
1509 }
1510
1511 private function displayDynamicScript(array $jsOptions, string $suffix, string $updateFunction, string $dynamicIdentifier, string $type, $defaultValues): void
1512 {
1513 $storageVar = 'window._additionalInfo'.$suffix;
1514
1515 echo '<script type="text/javascript">';
1516 echo 'var _selectedRows'.esc_attr($suffix).' = [];';
1517 echo 'var _selectedRows = [];';
1518 echo esc_attr($storageVar).' = '.esc_attr($storageVar).' || {};';
1519
1520 if (!empty($defaultValues->id) && (empty($defaultValues->defaultPluginTab) || $dynamicIdentifier === $defaultValues->defaultPluginTab)) {
1521 $delimiter = strpos($defaultValues->id, '-') ? '-' : ',';
1522 foreach (explode($delimiter, $defaultValues->id) as $value) {
1523 if (empty($value)) {
1524 continue;
1525 }
1526 echo '_selectedRows'.esc_attr($suffix).'['.intval($value).'] = true;';
1527 }
1528 }
1529
1530 echo 'function applyContent'.esc_attr($suffix).'(contentid, row){';
1531 echo 'if(_selectedRows'.esc_attr($suffix).'[contentid]){';
1532 echo 'jQuery(row).removeClass("selected_row");';
1533 echo 'delete _selectedRows'.esc_attr($suffix).'[contentid];';
1534 echo '}else{';
1535 if ($type === 'individual') {
1536 echo 'for(let elementKey in _selectedRows'.esc_attr($suffix).') {';
1537 echo 'if(!_selectedRows'.esc_attr($suffix).'.hasOwnProperty(elementKey)) continue;';
1538 echo 'jQuery(\'[data-id="\' + elementKey + \'"]\').removeClass("selected_row");';
1539 echo 'delete _selectedRows'.esc_attr($suffix).'[elementKey];';
1540 echo '}';
1541 }
1542 echo 'jQuery(row).addClass("selected_row");';
1543 echo '_selectedRows'.esc_attr($suffix).'[contentid] = true;';
1544 echo '}';
1545 echo esc_attr($updateFunction).'();';
1546 echo 'if(typeof _selectedRows !== "undefined"){ _selectedRows = _selectedRows'.esc_attr($suffix).'; }';
1547 echo '}';
1548
1549 echo 'function '.esc_attr($updateFunction).'(){';
1550 echo 'var tag = ""; var otherinfo = "";';
1551 foreach ($jsOptions as $option) {
1552 $this->echoOptionJs($option, $suffix, $type);
1553 }
1554 echo 'for (let [index, info] of Object.entries('.esc_attr($storageVar).')){ otherinfo += "| "+index+":"+info; }';
1555
1556 if ($type === 'individual') {
1557 echo 'for(var i in _selectedRows'.esc_attr($suffix).'){';
1558 echo 'if(!_selectedRows'.esc_attr($suffix).'.hasOwnProperty(i)) continue;';
1559 echo 'tag = tag + "{'.esc_attr($dynamicIdentifier).':" + i + otherinfo + "}";';
1560 echo '}';
1561 } elseif ($type === 'grouped') {
1562 echo 'tag = "{'.esc_attr($dynamicIdentifier).':";';
1563 echo 'for(var icat in _selectedRows'.esc_attr($suffix).'){';
1564 echo 'if(!_selectedRows'.esc_attr($suffix).'.hasOwnProperty(icat)) continue;';
1565 echo 'tag += icat + "-";';
1566 echo '}';
1567 echo 'tag += otherinfo + "}";';
1568 } elseif ($type === 'simple') {
1569 echo 'tag = "{'.esc_attr($dynamicIdentifier).':" + otherinfo + "}";';
1570 }
1571
1572 echo 'acym_editorWysidDynamic.insertDContent(tag);';
1573 echo '}';
1574
1575 echo 'function addAdditionalInfo'.esc_attr($suffix).'(index, value){';
1576 echo esc_attr($storageVar).'[index] = value;';
1577 echo esc_attr($updateFunction).'();';
1578 echo '}';
1579 echo '</script>';
1580 }
1581
1582 /**
1583 * In Joomla, we can translate elements in extensions using the FaLang extension
1584 */
1585 public function translateItem(object &$item, object &$tag, string $referenceTable, int $referenceId = 0): void
1586 {
1587 if (!acym_isExtensionActive('com_falang')) {
1588 return;
1589 }
1590
1591 if (!empty($tag->falang)) {
1592 $langId = $tag->falang;
1593 } elseif (!empty($tag->lang)) {
1594 $langId = intval(substr($tag->lang, strpos($tag->lang, ',') + 1));
1595 } elseif (!empty($this->contextLanguage)) {
1596 $languages = acym_loadObjectList('SELECT `lang_id`, `lang_code`, `sef` FROM #__languages');
1597 foreach ($languages as $language) {
1598 if (in_array($this->contextLanguage, [$language->lang_code, $language->sef])) {
1599 $langId = $language->lang_id;
1600 break;
1601 }
1602 }
1603 }
1604
1605 if (empty($langId)) {
1606 return;
1607 }
1608
1609 if (empty($referenceId)) {
1610 $referenceId = $tag->id;
1611 }
1612
1613 $translations = acym_loadObjectList(
1614 'SELECT `reference_field`, `value`
1615 FROM `#__falang_content`
1616 WHERE `published` = 1
1617 AND `reference_table` = '.acym_escapeDB($referenceTable).'
1618 AND `language_id` = '.intval($langId).'
1619 AND `reference_id` = '.intval($referenceId)
1620 );
1621
1622 if (empty($translations)) {
1623 return;
1624 }
1625
1626 foreach ($translations as $oneTranslation) {
1627 if (empty($oneTranslation->value)) {
1628 continue;
1629 }
1630
1631 $translatedField = $oneTranslation->reference_field;
1632 $item->$translatedField = $oneTranslation->value;
1633 }
1634 }
1635
1636 public function createDummyEmailObject(int $mailId, string $code, string $previewBody): object
1637 {
1638 if (!empty($mailId)) {
1639 $mailClass = new MailClass();
1640 $email = $mailClass->getOneById($mailId);
1641 }
1642
1643 if (empty($email)) {
1644 $email = new \stdClass();
1645 $email->id = 0;
1646 $email->name = '';
1647 $email->subject = '';
1648 $email->from_name = '';
1649 $email->from_email = '';
1650 $email->reply_to_name = '';
1651 $email->reply_to_email = '';
1652 $email->bcc = '';
1653 $email->links_language = '';
1654 }
1655
1656 $language = acym_getVar('string', 'language', 'main');
1657 if (!empty($language)) {
1658 if ($language === 'main') {
1659 $language = acym_getDefaultNewsletterLanguage();
1660 }
1661 $email->links_language = $language;
1662 $email->language = $language;
1663 }
1664
1665 $email->creation_date = acym_date('now', 'Y-m-d H:i:s', false);
1666 $email->creator_id = acym_currentUserId();
1667 $email->thumbnail = '';
1668 $email->drag_editor = '1';
1669 $email->type = MailClass::TYPE_STANDARD;
1670 $email->settings = '';
1671 $email->stylesheet = '';
1672 $email->attachments = '';
1673
1674 // This is only the dynamic text/content code
1675 $email->body = $code;
1676 // This is the whole editor current content
1677 $email->previewBody = $previewBody;
1678
1679 return $email;
1680 }
1681 }
1682