PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.1
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.1
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 11.0.1, at back/Helpers/PluginHelper.php

1,620 lines 64.1 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 $jsOptionsMerge = [];
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 $this->appendOptionJs($jsOptionsMerge, $option, $suffix, $type);
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($jsOptionsMerge, $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 echo $option['output'];
1080 break;
1081 }
1082 }
1083
1084 private function displayPicturesField(array $option, string $suffix, string $updateFunction, $defaultValues): void
1085 {
1086 $displayedPictures = $option['default'] ?? 'resized';
1087 if (isset($defaultValues->pict)) {
1088 $displayedPictures = $defaultValues->pict;
1089 }
1090 $maxWidth = $defaultValues->maxwidth ?? 150;
1091 $maxHeight = $defaultValues->maxheight ?? 150;
1092
1093 $valImages = [
1094 acym_selectOption('1', 'ACYM_YES'),
1095 acym_selectOption('resized', 'ACYM_RESIZED'),
1096 acym_selectOption('0', 'ACYM_NO'),
1097 ];
1098
1099 echo '<div class="cell large-5 acym_plugin_field">';
1100 echo esc_html(acym_translation('ACYM_DISPLAY'));
1101 echo '</div>';
1102 echo '<div class="cell large-7">';
1103 acym_radio(
1104 $valImages,
1105 'pict'.$suffix,
1106 $displayedPictures,
1107 ['onclick' => $updateFunction.'();'],
1108 ['containerClass' => 'dcontent_pictures'],
1109 !acym_isAdmin()
1110 );
1111 echo '</div>';
1112
1113 echo '<div id="pictsize'.esc_attr($suffix).'" class="cell grid-x margin-y margin-top-1"';
1114 if ('resized' !== $displayedPictures) {
1115 echo ' style="display: none;"';
1116 }
1117 echo '>';
1118 echo '<div class="cell large-5 acym_plugin_field">';
1119 echo esc_html(acym_translation('ACYM_MAX_WIDTH'));
1120 echo '</div>';
1121 echo '<div class="cell large-7">';
1122 echo '<input class="intext_input"
1123 name="pictwidth'.esc_attr($suffix).'"
1124 type="number"
1125 onchange="'.esc_attr($updateFunction).'();"
1126 value="'.intval($maxWidth).'"/>';
1127 echo '</div>';
1128 echo '<div class="cell large-5 acym_plugin_field">';
1129 echo esc_html(acym_translation('ACYM_MAX_HEIGHT'));
1130 echo '</div>';
1131 echo '<div class="cell large-7">';
1132 echo '<input class="intext_input"
1133 name="pictheight'.esc_attr($suffix).'"
1134 type="number"
1135 onchange="'.esc_attr($updateFunction).'();"
1136 value="'.intval($maxHeight).'"/>';
1137 echo '</div>';
1138 echo '</div>';
1139
1140 if (!empty($option['caption'])) {
1141 echo '<div class="cell grid-x margin-top-1">';
1142 echo '<label class="cell large-5 acym_plugin_field">';
1143 echo esc_html(acym_translation('ACYM_CAPTION'));
1144 echo '</label>';
1145 acym_radio(
1146 [
1147 acym_selectOption('1', 'ACYM_YES'),
1148 acym_selectOption('0', 'ACYM_NO'),
1149 ],
1150 'caption'.$suffix,
1151 $defaultValues->caption ?? '0',
1152 ['onclick' => $updateFunction.'();'],
1153 ['containerClass' => 'cell large-7']
1154 );
1155 echo '</div>';
1156 }
1157 }
1158
1159 private function displayCheckboxField(array $option, string $suffix, string $updateFunction): void
1160 {
1161 if (!empty($option['default'])) {
1162 $checkedValues = explode(',', $option['default']);
1163 foreach ($option['options'] as $key => $oneOption) {
1164 $oneOption[1] = in_array($key, $checkedValues);
1165 $option['options'][$key] = $oneOption;
1166 }
1167 }
1168
1169 echo '<div class="cell grid-x">';
1170 foreach ($option['options'] as $value => $title) {
1171 echo '<div class="cell medium-6" ';
1172 if (!empty($title[2]) && is_array($title[2])) {
1173 foreach ($title[2] as $attribute => $value) {
1174 echo esc_attr($attribute).'="'.esc_attr($value).'"';
1175 }
1176 }
1177 echo '>';
1178 echo '<input type="checkbox"
1179 name="'.esc_attr($option['name'].$suffix).'"
1180 value="'.esc_attr($value).'"
1181 id="'.esc_attr($value.$suffix).'"
1182 onclick="'.esc_attr($updateFunction).'();" ';
1183 acym_checked((bool)$title[1]);
1184 echo ' />';
1185 echo '<label style="margin-left:5px" for="'.esc_attr($value.$suffix).'">';
1186 echo esc_html(acym_translation($title[0]));
1187 echo '</label>';
1188 echo '</div>';
1189 }
1190 echo '</div>';
1191 }
1192
1193 private function displayRadioField(array $option, string $suffix, string $updateFunction): void
1194 {
1195 $radioOptions = [];
1196 foreach ($option['options'] as $value => $title) {
1197 $radioOptions[] = acym_selectOption($value, $title);
1198 }
1199 acym_radio(
1200 $radioOptions,
1201 $option['name'].$suffix,
1202 $option['default'],
1203 ['onclick' => $updateFunction.'();'],
1204 ['pluginMode' => true],
1205 !acym_isAdmin()
1206 );
1207 }
1208
1209 private function displaySelectField(array $option, string $suffix, string $updateFunction): void
1210 {
1211 $selectOptions = [];
1212 foreach ($option['options'] as $value => $title) {
1213 if (is_object($title)) {
1214 $selectOptions[] = acym_selectOption($title->value, $title->text);
1215 } else {
1216 $selectOptions[] = acym_selectOption($value, $title);
1217 }
1218 }
1219
1220 $default = empty($option['default']) ? null : $option['default'];
1221 $defaultOrder = null;
1222 if (!empty($default) && strpos($default, ',')) {
1223 [$default, $defaultOrder] = explode(',', $default);
1224 }
1225
1226 $attributes = [
1227 'onchange' => $updateFunction.'();',
1228 'id' => $option['name'].$suffix,
1229 ];
1230 if ($option['name'] === 'order') {
1231 $attributes['class'] = 'acym__dynamics__ordering__select';
1232 }
1233
1234 acym_select(
1235 $selectOptions,
1236 $option['name'].$suffix,
1237 $default,
1238 $attributes,
1239 'value',
1240 'text',
1241 null,
1242 false,
1243 true
1244 );
1245
1246 if ($option['name'] === 'order') {
1247 $dirs = [
1248 'desc' => acym_translation('ACYM_DESC'),
1249 'asc' => acym_translation('ACYM_ASC'),
1250 ];
1251 if (empty($defaultOrder)) {
1252 $defaultOrder = empty($option['defaultdir']) ? null : $option['defaultdir'];
1253 }
1254 echo ' ';
1255 acym_select(
1256 $dirs,
1257 'orderdir'.$suffix,
1258 $defaultOrder,
1259 [
1260 'onchange' => $updateFunction.'();',
1261 'style' => 'width: 115px;',
1262 'class' => 'acym__dynamics__ordering__select',
1263 ],
1264 'value',
1265 'text',
1266 null,
1267 false,
1268 true
1269 );
1270 }
1271 }
1272
1273 private function displayMultiselectField(array $option, string $suffix, string $updateFunction): void
1274 {
1275 $selectOptions = [];
1276 foreach ($option['options'] as $value => $title) {
1277 $selectOptions[] = acym_selectOption($value, $title);
1278 }
1279
1280 if (!isset($option['default'])) {
1281 $option['default'] = [];
1282 }
1283 if (!is_array($option['default'])) {
1284 $option['default'] = explode(',', $option['default']);
1285 }
1286
1287 acym_selectMultiple(
1288 $selectOptions,
1289 $option['name'].$suffix,
1290 $option['default'],
1291 ['onchange' => $updateFunction.'();', 'id' => $option['name'].$suffix],
1292 'value',
1293 'text',
1294 true
1295 );
1296 }
1297
1298 private function displayLanguageField(array $option, string $suffix, string $updateFunction): void
1299 {
1300 $languageOptions = ['any' => acym_translation('ACYM_ANY')];
1301 foreach (acym_getLanguages(true) as $language) {
1302 $languageOptions[$language->language] = $language->name;
1303 }
1304
1305 if (empty($option['default'])) {
1306 $option['default'] = acym_getVar('string', 'language');
1307 if (acym_isMultilingual() && (empty($option['default']) || $option['default'] === 'main')) {
1308 $option['default'] = $this->config->get('multilingual_default');
1309 }
1310 }
1311
1312 acym_select(
1313 $languageOptions,
1314 $option['name'].$suffix,
1315 empty($option['default']) ? null : $option['default'],
1316 ['onchange' => $updateFunction.'();', 'id' => $option['name'].$suffix],
1317 'value',
1318 'text',
1319 null,
1320 false,
1321 true
1322 );
1323 }
1324
1325 private function displayFormatPanel(string $suffix, string $updateFunction, string $type, $defaultValues): void
1326 {
1327 echo '<div class="grid-x">';
1328 echo '<div class="cell large-3">';
1329 echo esc_html(acym_translation('ACYM_FORMAT'));
1330 echo '</div>';
1331 echo '<div class="cell large-9 dcontentFormatContainer">';
1332
1333 $default = empty($defaultValues->format) ? 'TOP_LEFT' : $defaultValues->format;
1334 $formats = ['TOP_LEFT', 'TOP_RIGHT', 'TITLE_IMG', 'TITLE_IMG_RIGHT', 'CENTER_IMG', 'TOP_IMG', 'COL_LEFT', 'COL_RIGHT'];
1335 foreach ($formats as $oneFormat) {
1336 $class = 'button-radio';
1337 if ($default === $oneFormat) {
1338 $class .= ' button-radio-selected';
1339 }
1340 echo '<button class="'.esc_attr($class).'"
1341 acym-button-radio-group="dcontentFormat'.esc_attr($suffix).'"
1342 acym-data-type="'.esc_attr($oneFormat).'"
1343 acym-callback="'.esc_attr($updateFunction).'">';
1344 echo '<img alt="'.esc_attr($oneFormat).'" src="'.esc_url(ACYM_IMAGES.'editor/dcontent_formats/'.strtolower($oneFormat).'.png').'"/>';
1345 echo '</button>';
1346 }
1347 echo '</div>';
1348
1349 if ($type === 'grouped') {
1350 echo '<div class="cell large-3">';
1351 echo esc_html(acym_translation('ACYM_ALTERNATE'));
1352 acym_info(['textShownInTooltip' => 'ACYM_ALTERNATE_DESC']);
1353 echo '</div>';
1354 echo '<div class="cell large-9">';
1355 acym_boolean(
1356 'alternate'.$suffix,
1357 !empty($defaultValues->alternate),
1358 'alternate'.$suffix,
1359 ['onclick' => $updateFunction.'();']
1360 );
1361 echo '</div>';
1362 }
1363
1364 echo '</div>';
1365 }
1366
1367 private function appendOptionJs(array &$jsOptionsMerge, array $option, string $suffix, string $type): void
1368 {
1369 switch ($option['type']) {
1370 case 'pictures':
1371 if (!empty($option['caption'])) {
1372 $jsOptionsMerge[] = 'otherinfo += "| caption:" + jQuery(\'input[name="caption'.esc_attr($suffix).'"]:checked\').val();';
1373 }
1374 $jsOptionsMerge[] = '
1375 var _pictVal'.esc_attr($suffix).' = jQuery(\'input[name="pict'.esc_attr($suffix).'"]:checked\').val();
1376 otherinfo += "| pict:" + _pictVal'.esc_attr($suffix).';
1377
1378 if(_pictVal'.esc_attr($suffix).' == "resized"){
1379 jQuery("#pictsize'.esc_attr($suffix).'").show();
1380 otherinfo += "| maxwidth:" + jQuery(\'input[name="pictwidth'.esc_attr($suffix).'"]\').val();
1381 otherinfo += "| maxheight:" + jQuery(\'input[name="pictheight'.esc_attr($suffix).'"]\').val();
1382 }else{
1383 jQuery("#pictsize'.esc_attr($suffix).'").hide();
1384 }';
1385 break;
1386
1387 case 'checkbox':
1388 $separator = empty($option['separator']) ? ',' : $option['separator'];
1389 $jsOptionsMerge[] = 'var _checked'.esc_attr($option['name'].$suffix).' = [];
1390 jQuery("input:checkbox[name='.esc_attr($option['name'].$suffix).']:checked").each(function(){
1391 _checked'.esc_attr($option['name'].$suffix).'.push(jQuery(this).val());
1392 });
1393 if(_checked'.esc_attr($option['name'].$suffix).'.length) otherinfo += "| '.esc_attr($option['name']).':" + _checked'.esc_attr(
1394 $option['name'].$suffix
1395 ).'.join("'.esc_attr($separator).'");';
1396
1397 if ($option['title'] === 'ACYM_DISPLAY' && (!isset($option['format']) || $option['format'])) {
1398 if ($type === 'grouped') {
1399 $jsOptionsMerge[] = 'var alternate = jQuery(\'input[name="alternate'.esc_attr($suffix).'"]:checked\').val();';
1400 $jsOptionsMerge[] = 'if (!acym_helper.empty(alternate)) otherinfo += "| alternate";';
1401 }
1402 $jsOptionsMerge[] = 'var selectedFormatOption = jQuery(\'.button-radio-selected[acym-button-radio-group="dcontentFormat'.esc_attr($suffix).'"]\')';
1403 $jsOptionsMerge[] = 'if (!acym_helper.empty(selectedFormatOption)) otherinfo += "| format:" + selectedFormatOption.attr("acym-data-type");';
1404 }
1405 break;
1406
1407 case 'boolean':
1408 case 'radio':
1409 $jsOptionsMerge[] = 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'input[name="'.esc_attr($option['name'].$suffix).'"]:checked\').val();';
1410 break;
1411
1412 case 'select':
1413 if ($option['name'] === 'order') {
1414 $jsOptionsMerge[] = 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'[name="'.esc_attr(
1415 $option['name'].$suffix
1416 ).'"]\').val() + "," + jQuery(\'[name="orderdir'.esc_attr($suffix).'"]\').val();';
1417 } else {
1418 $jsOptionsMerge[] = 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'[name="'.esc_attr($option['name'].$suffix).'"]\').val();';
1419 }
1420 break;
1421
1422 case 'language':
1423 $jsOptionsMerge[] = 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'[name="'.esc_attr($option['name'].$suffix).'"]\').val();';
1424 break;
1425
1426 case 'multiselect':
1427 $jsOptionsMerge[] = '
1428 var theMultiSelect = document.querySelector(\'[name="'.esc_attr($option['name'].$suffix).'[]"]\');
1429 var selectedOptions = [];
1430 for(var i = 0 ; i < theMultiSelect.length ; i++){
1431 if(theMultiSelect[i].selected){
1432 selectedOptions.push(theMultiSelect[i].value);
1433 }
1434 }
1435 otherinfo += "| '.esc_attr($option['name']).':" + selectedOptions.join(",");';
1436 break;
1437
1438 case 'text':
1439 case 'number':
1440 case 'intextfield':
1441 case 'date':
1442 $jsOptionsMerge[] = 'otherinfo += "| '.esc_attr($option['name']).':" + jQuery(\'input[name="'.esc_attr($option['name'].$suffix).'"]\').val();';
1443 break;
1444
1445 case 'custom':
1446 //TODO
1447 $jsOptionsMerge[] = $option['js'];
1448 break;
1449 }
1450 }
1451
1452 private function displayDynamicScript(array $jsOptionsMerge, string $suffix, string $updateFunction, string $dynamicIdentifier, string $type, $defaultValues): void
1453 {
1454 $storageVar = 'window._additionalInfo'.$suffix;
1455
1456 echo '<script type="text/javascript">';
1457 echo 'var _selectedRows'.esc_attr($suffix).' = [];';
1458 echo 'var _selectedRows = [];';
1459 echo esc_attr($storageVar).' = '.esc_attr($storageVar).' || {};';
1460
1461 if (!empty($defaultValues->id) && (empty($defaultValues->defaultPluginTab) || $dynamicIdentifier === $defaultValues->defaultPluginTab)) {
1462 $delimiter = strpos($defaultValues->id, '-') ? '-' : ',';
1463 foreach (explode($delimiter, $defaultValues->id) as $value) {
1464 if (empty($value)) {
1465 continue;
1466 }
1467 echo '_selectedRows'.esc_attr($suffix).'['.intval($value).'] = true;';
1468 }
1469 }
1470
1471 echo 'function applyContent'.esc_attr($suffix).'(contentid, row){';
1472 echo 'if(_selectedRows'.esc_attr($suffix).'[contentid]){';
1473 echo 'jQuery(row).removeClass("selected_row");';
1474 echo 'delete _selectedRows'.esc_attr($suffix).'[contentid];';
1475 echo '}else{';
1476 if ($type === 'individual') {
1477 echo 'for(let elementKey in _selectedRows'.esc_attr($suffix).') {';
1478 echo 'if(!_selectedRows'.esc_attr($suffix).'.hasOwnProperty(elementKey)) continue;';
1479 echo 'jQuery(\'[data-id="\' + elementKey + \'"]\').removeClass("selected_row");';
1480 echo 'delete _selectedRows'.esc_attr($suffix).'[elementKey];';
1481 echo '}';
1482 }
1483 echo 'jQuery(row).addClass("selected_row");';
1484 echo '_selectedRows'.esc_attr($suffix).'[contentid] = true;';
1485 echo '}';
1486 echo esc_attr($updateFunction).'();';
1487 echo 'if(typeof _selectedRows !== "undefined"){ _selectedRows = _selectedRows'.esc_attr($suffix).'; }';
1488 echo '}';
1489
1490 echo 'function '.esc_attr($updateFunction).'(){';
1491 echo 'var tag = ""; var otherinfo = "";';
1492 echo implode("\r\n\r\n", $jsOptionsMerge);
1493 echo 'for (let [index, info] of Object.entries('.esc_attr($storageVar).')){ otherinfo += "| "+index+":"+info; }';
1494
1495 if ($type === 'individual') {
1496 echo 'for(var i in _selectedRows'.esc_attr($suffix).'){';
1497 echo 'if(!_selectedRows'.esc_attr($suffix).'.hasOwnProperty(i)) continue;';
1498 echo 'tag = tag + "{'.esc_attr($dynamicIdentifier).':" + i + otherinfo + "}";';
1499 echo '}';
1500 } elseif ($type === 'grouped') {
1501 echo 'tag = "{'.esc_attr($dynamicIdentifier).':";';
1502 echo 'for(var icat in _selectedRows'.esc_attr($suffix).'){';
1503 echo 'if(!_selectedRows'.esc_attr($suffix).'.hasOwnProperty(icat)) continue;';
1504 echo 'tag += icat + "-";';
1505 echo '}';
1506 echo 'tag += otherinfo + "}";';
1507 } elseif ($type === 'simple') {
1508 echo 'tag = "{'.esc_attr($dynamicIdentifier).':" + otherinfo + "}";';
1509 }
1510
1511 echo 'acym_editorWysidDynamic.insertDContent(tag);';
1512 echo '}';
1513
1514 echo 'function addAdditionalInfo'.esc_attr($suffix).'(index, value){';
1515 echo esc_attr($storageVar).'[index] = value;';
1516 echo esc_attr($updateFunction).'();';
1517 echo '}';
1518 echo '</script>';
1519 }
1520
1521 /**
1522 * In Joomla, we can translate elements in extensions using the FaLang extension
1523 */
1524 public function translateItem(object &$item, object &$tag, string $referenceTable, int $referenceId = 0): void
1525 {
1526 if (!acym_isExtensionActive('com_falang')) {
1527 return;
1528 }
1529
1530 if (!empty($tag->falang)) {
1531 $langId = $tag->falang;
1532 } elseif (!empty($tag->lang)) {
1533 $langId = intval(substr($tag->lang, strpos($tag->lang, ',') + 1));
1534 } elseif (!empty($this->contextLanguage)) {
1535 $languages = acym_loadObjectList('SELECT `lang_id`, `lang_code`, `sef` FROM #__languages');
1536 foreach ($languages as $language) {
1537 if (in_array($this->contextLanguage, [$language->lang_code, $language->sef])) {
1538 $langId = $language->lang_id;
1539 break;
1540 }
1541 }
1542 }
1543
1544 if (empty($langId)) {
1545 return;
1546 }
1547
1548 if (empty($referenceId)) {
1549 $referenceId = $tag->id;
1550 }
1551
1552 $translations = acym_loadObjectList(
1553 'SELECT `reference_field`, `value`
1554 FROM `#__falang_content`
1555 WHERE `published` = 1
1556 AND `reference_table` = '.acym_escapeDB($referenceTable).'
1557 AND `language_id` = '.intval($langId).'
1558 AND `reference_id` = '.intval($referenceId)
1559 );
1560
1561 if (empty($translations)) {
1562 return;
1563 }
1564
1565 foreach ($translations as $oneTranslation) {
1566 if (empty($oneTranslation->value)) {
1567 continue;
1568 }
1569
1570 $translatedField = $oneTranslation->reference_field;
1571 $item->$translatedField = $oneTranslation->value;
1572 }
1573 }
1574
1575 public function createDummyEmailObject(int $mailId, string $code, string $previewBody): object
1576 {
1577 if (!empty($mailId)) {
1578 $mailClass = new MailClass();
1579 $email = $mailClass->getOneById($mailId);
1580 }
1581
1582 if (empty($email)) {
1583 $email = new \stdClass();
1584 $email->id = 0;
1585 $email->name = '';
1586 $email->subject = '';
1587 $email->from_name = '';
1588 $email->from_email = '';
1589 $email->reply_to_name = '';
1590 $email->reply_to_email = '';
1591 $email->bcc = '';
1592 $email->links_language = '';
1593 }
1594
1595 $language = acym_getVar('string', 'language', 'main');
1596 if (!empty($language)) {
1597 if ($language === 'main') {
1598 $language = $this->config->get('multilingual_default', ACYM_DEFAULT_LANGUAGE);
1599 }
1600 $email->links_language = $language;
1601 }
1602
1603 $email->creation_date = acym_date('now', 'Y-m-d H:i:s', false);
1604 $email->creator_id = acym_currentUserId();
1605 $email->thumbnail = '';
1606 $email->drag_editor = '1';
1607 $email->type = MailClass::TYPE_STANDARD;
1608 $email->settings = '';
1609 $email->stylesheet = '';
1610 $email->attachments = '';
1611
1612 // This is only the dynamic text/content code
1613 $email->body = $code;
1614 // This is the whole editor current content
1615 $email->previewBody = $previewBody;
1616
1617 return $email;
1618 }
1619 }
1620