PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
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 / Classes / MailClass.php

MailClass.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.1.0, at back/Classes/MailClass.php

1,709 lines 61.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\Classes;
4
5 use AcyMailing\Helpers\MailerHelper;
6 use AcyMailing\Helpers\PaginationHelper;
7 use AcyMailing\Helpers\PluginHelper;
8 use AcyMailing\Core\AcymClass;
9
10 class MailClass extends AcymClass
11 {
12 const FIELDS_ENCODING = [
13 'name',
14 'subject',
15 'body',
16 'autosave',
17 'preheader',
18 ];
19
20 const TYPE_STANDARD = 'standard';
21 const TYPE_NOTIFICATION = 'notification';
22 const TYPE_OVERRIDE = 'override';
23 const TYPE_WELCOME = 'welcome';
24 const TYPE_UNSUBSCRIBE = 'unsubscribe';
25 const TYPE_AUTOMATION = 'automation';
26 const TYPE_FOLLOWUP = 'followup';
27 const TYPE_MAILBOX_ACTION = 'mailbox_action';
28 const TYPE_TEMPLATE = 'template';
29 const TYPE_SCENARIO = 'scenario';
30
31 const ALL_TYPES = [
32 self::TYPE_STANDARD,
33 self::TYPE_NOTIFICATION,
34 self::TYPE_OVERRIDE,
35 self::TYPE_WELCOME,
36 self::TYPE_UNSUBSCRIBE,
37 self::TYPE_AUTOMATION,
38 self::TYPE_FOLLOWUP,
39 self::TYPE_MAILBOX_ACTION,
40 self::TYPE_TEMPLATE,
41 self::TYPE_SCENARIO,
42 ];
43
44 // Used by some sending methods to know the priority of a sent email (transactional => reset password / account confirmation...)
45 const TYPES_TRANSACTIONAL = [
46 self::TYPE_NOTIFICATION,
47 self::TYPE_OVERRIDE,
48 self::TYPE_WELCOME,
49 self::TYPE_UNSUBSCRIBE,
50 self::TYPE_TEMPLATE,
51 self::TYPE_SCENARIO,
52 ];
53
54 // Types on which the click statistics are active
55 const TYPES_WITH_STATS = [
56 self::TYPE_STANDARD,
57 self::TYPE_AUTOMATION,
58 self::TYPE_WELCOME,
59 self::TYPE_UNSUBSCRIBE,
60 self::TYPE_FOLLOWUP,
61 self::TYPE_MAILBOX_ACTION,
62 self::TYPE_SCENARIO,
63 ];
64
65 // Types that don't let the user modify the name
66 const TYPES_NO_NAME = [
67 self::TYPE_NOTIFICATION,
68 self::TYPE_OVERRIDE,
69 ];
70
71 const MAX_ALLOWED_TEMPLATE_FILES = 100;
72 const MAX_ALLOWED_TEMPLATE_SIZE = 20971520;
73 const ALLOWED_TEMPLATE_FILE_TYPES = [
74 'image/jpeg',
75 'image/png',
76 'image/gif',
77 'text/html',
78 'image/x-ms-bmp',
79 'image/bmp',
80 'text/plain',
81 'image/vnd.microsoft.icon',
82 ];
83
84 private array $templateNames = [];
85 public array $exceptKeysDecode = [];
86
87 public function __construct()
88 {
89 parent::__construct();
90
91 $this->table = 'mail';
92 $this->pkey = 'id';
93 }
94
95 /**
96 * Get mails depending on filters (search, ordering, pagination)
97 */
98 public function getMatchingElements(array $settings = []): array
99 {
100 $query = 'SELECT mail.* FROM #__acym_mail AS mail';
101 $queryCount = 'SELECT COUNT(mail.id) AS total FROM #__acym_mail AS mail';
102
103 $filters = [];
104 $tagJoin = '';
105
106 // Tag filter
107 if (!empty($settings['tag'])) {
108 $tagJoin = ' JOIN #__acym_tag AS tag ON mail.id = tag.id_element ';
109 $filters[] = 'tag.name = '.acym_escapeDB($settings['tag']);
110 $filters[] = 'tag.type = "mail"';
111 }
112 $query .= $tagJoin;
113 $queryCount .= $tagJoin;
114
115 if (!empty($settings['search'])) {
116 $filters[] = 'mail.name LIKE '.acym_escapeDB('%'.acym_utf8Encode($settings['search']).'%');
117 }
118
119 if (!empty($settings['gettingTemplates'])) {
120 $filters[] = 'mail.type = '.acym_escapeDB(self::TYPE_TEMPLATE);
121 if (!acym_isAdmin()) {
122 $currentUserId = acym_currentUserId();
123 $condition = 'mail.creator_id = '.intval($currentUserId);
124
125 $userGroups = acym_getGroupsByUser($currentUserId);
126 if (!empty($userGroups)) {
127 $condition .= ' OR access LIKE "%,'.implode(',%" OR access LIKE "%,', $userGroups).',%"';
128 }
129
130 $filters[] = $condition;
131 }
132 } else {
133 if (!empty($settings['automation']) || empty($settings['onlyStandard'])) {
134 $filters[] = 'mail.type != '.acym_escapeDB(self::TYPE_NOTIFICATION);
135 $filters[] = 'mail.type != '.acym_escapeDB(self::TYPE_OVERRIDE);
136 } else {
137 $filters[] = 'mail.type IN ('.acym_escapeDB(self::TYPE_STANDARD).', '.acym_escapeDB(self::TYPE_TEMPLATE).')';
138 }
139
140 if (empty($settings['automation'])) {
141 $filters[] = 'mail.type IN ('.acym_escapeDB(self::TYPE_TEMPLATE).', '.acym_escapeDB(self::TYPE_WELCOME).', '.acym_escapeDB(self::TYPE_UNSUBSCRIBE).')';
142 }
143
144 if (!empty($settings['creator_id'])) {
145 $mailTypeCondition = 'mail.type IN ('.acym_escapeDB(self::TYPE_TEMPLATE).', '.acym_escapeDB(self::TYPE_WELCOME).', '.acym_escapeDB(self::TYPE_UNSUBSCRIBE).')';
146 $userGroups = acym_getGroupsByUser($settings['creator_id']);
147 $groupCondition = '(mail.access LIKE "%,'.implode(',%" OR mail.access LIKE "%,', $userGroups).',%")';
148 $filter = 'mail.creator_id = '.intval($settings['creator_id']).' OR ('.$mailTypeCondition.' AND '.$groupCondition.')';
149
150 if (!acym_isAdmin() && !empty($settings['element_tab'])) {
151 $filter = '('.$filter.')
152 OR list.cms_user_id = '.intval($settings['creator_id']).'
153 OR (list.access LIKE "%,'.implode(',%" OR list.access LIKE "%,', $userGroups).',%")';
154 }
155
156 $filters['list'] = '('.$filter.')';
157 }
158 }
159
160 $filters[] = 'mail.parent_id IS NULL';
161
162 if (!empty($settings['drag_editor'])) {
163 $filters[] = 'mail.drag_editor = 1';
164 }
165
166 if (!empty($settings['element_tab'])) {
167 $query = 'SELECT DISTINCT mail.*, mail_stat.sent as subscribers, mail_stat.open_unique FROM #__acym_mail AS mail';
168 $query .= ' LEFT JOIN #__acym_mail_stat AS mail_stat ON mail.id = mail_stat.mail_id ';
169 if (!empty($settings['tag'])) {
170 $query .= ' JOIN #__acym_tag AS tag ON mail.id = tag.id_element ';
171 }
172 if (!acym_isAdmin()) {
173 $query .= ' LEFT JOIN #__acym_list AS list ON list.'.acym_secureDBColumn($settings['element_tab']).'_id = mail.id';
174 }
175 $filters[] = 'mail.type = '.acym_escapeDB($settings['element_tab']);
176 }
177
178 if (!empty($filters)) {
179 $query .= ' WHERE ('.implode(') AND (', $filters).')';
180 if (!acym_isAdmin()) {
181 unset($filters['list']);
182 }
183 $queryCount .= ' WHERE ('.implode(') AND (', $filters).')';
184 }
185
186 if (!empty($settings['ordering']) && !empty($settings['ordering_sort_order'])) {
187 $query .= ' ORDER BY mail.'.acym_secureDBColumn($settings['ordering']).' '.acym_secureDBColumn(strtoupper($settings['ordering_sort_order']));
188 }
189
190 if (empty($settings['offset']) || $settings['offset'] < 0) {
191 $settings['offset'] = 0;
192 }
193
194 if (empty($settings['elementsPerPage']) || $settings['elementsPerPage'] < 1) {
195 $pagination = new PaginationHelper();
196 $settings['elementsPerPage'] = $pagination->getListLimit();
197 }
198
199 $results['elements'] = $this->decode(acym_loadObjectList($query, '', $settings['offset'], $settings['elementsPerPage']));
200 $results['total'] = acym_loadObject($queryCount);
201
202 if (!empty($settings['element_tab'])) {
203 $this->getAllListIdsForWelcomeUnsub($results['elements'], $settings['element_tab']);
204 }
205
206 $results['status'] = [];
207
208 if (!empty($settings['element_tab'])) {
209 $urlClickClass = new UrlClickClass();
210 for ($i = 0; $i < count($results['elements']); $i++) {
211 $results['elements'][$i]->open = 0;
212 if (!empty($results['elements'][$i]->subscribers)) {
213 $results['elements'][$i]->open = number_format($results['elements'][$i]->open_unique / $results['elements'][$i]->subscribers * 100, 2);
214
215 $clicksNb = $urlClickClass->getNumberUsersClicked([$results['elements'][$i]->id]);
216 $results['elements'][$i]->click = number_format($clicksNb / $results['elements'][$i]->subscribers * 100, 2);
217 }
218 }
219 }
220
221 return $results;
222 }
223
224 private function getAllListIdsForWelcomeUnsub(array $elements, string $type): void
225 {
226 if (empty($elements)) {
227 return;
228 }
229
230 $column = $type === self::TYPE_WELCOME ? 'welcome_id' : 'unsubscribe_id';
231
232 foreach ($elements as $element) {
233 $element->lists = acym_loadObjectList('SELECT color, name FROM #__acym_list WHERE '.acym_secureDBColumn($column).' = '.intval($element->id));
234 }
235 }
236
237 public function getAll(?string $key = null): array
238 {
239 $allMails = parent::getAll($key);
240
241 return $this->decode($allMails);
242 }
243
244 public function getOneById(int $id, bool $needTranslatedSettings = false): ?object
245 {
246 $mail = $this->decode(parent::getOneById($id));
247
248 if (!empty($mail)) {
249 $tagsClass = new TagClass();
250 $mail->tags = $tagsClass->getAllTagsByElementId(TagClass::TYPE_MAIL, intval($id));
251 }
252
253 if (isset($mail->access) && !is_array($mail->access)) $mail->access = explode(',', $mail->access);
254
255 if (!empty($mail->parent_id) && $needTranslatedSettings) {
256 $this->getTranslatedSettingsMail($mail);
257 }
258
259 return $mail;
260 }
261
262 private function getTranslatedSettingsMail(object $mail): void
263 {
264 $parentMailTranslation = acym_loadResult('SELECT translation FROM #__acym_mail WHERE id = '.intval($mail->parent_id));
265 $parentMailTranslation = empty($parentMailTranslation) ? [] : json_decode($parentMailTranslation, true);
266
267 $translationConfig = $this->config->get('sender_info_translation', '');
268 $translationConfig = empty($translationConfig) ? [] : json_decode($translationConfig, true);
269
270 if (!empty($parentMailTranslation[$mail->language])) {
271 $senderTranslation = $parentMailTranslation[$mail->language];
272 } elseif (!empty($translationConfig[$mail->language])) {
273 $senderTranslation = $translationConfig[$mail->language];
274 $senderTranslation['reply_to_email'] = $senderTranslation['replyto_email'];
275 $senderTranslation['reply_to_name'] = $senderTranslation['replyto_name'];
276 } else {
277 return;
278 }
279
280 $mail->from_name = $senderTranslation['from_name'];
281 $mail->from_email = $senderTranslation['from_email'];
282 $mail->reply_to_name = $senderTranslation['reply_to_name'];
283 $mail->reply_to_email = $senderTranslation['reply_to_email'];
284 }
285
286 public function getOneByName(string $name, bool $needTranslatedSettings = false, string $type = ''): ?object
287 {
288 $query = 'SELECT * FROM #__acym_mail WHERE `parent_id` IS NULL AND `name` = '.acym_escapeDB(acym_utf8Encode($name));
289 if (!empty($type)) {
290 $query .= ' AND `type` = '.acym_escapeDB($type);
291 }
292
293 $mail = acym_loadObject($query);
294
295 if (empty($mail)) {
296 return null;
297 }
298
299 $mail = $this->decode($mail);
300
301 if (!empty($mail)) {
302 $tagsClass = new TagClass();
303 $mail->tags = $tagsClass->getAllTagsByElementId(TagClass::TYPE_MAIL, intval($mail->id));
304 }
305
306 if (!empty($mail->parent_id) && $needTranslatedSettings) {
307 $this->getTranslatedSettingsMail($mail);
308 }
309
310 return $mail;
311 }
312
313 /**
314 * Get mails depending on their type (standard, welcome, unsubscribe, notification)
315 */
316 public function getMailsByType(?string $typeMail, array $settings): array
317 {
318 if (empty($settings['key'])) {
319 $settings['key'] = '';
320 }
321 if (empty($settings['offset'])) {
322 $settings['offset'] = 0;
323 }
324 if (empty($settings['mailsPerPage'])) {
325 $settings['mailsPerPage'] = 12;
326 }
327 $search = $settings['filters']['search'] ?? '';
328
329 $query = 'SELECT * FROM #__acym_mail AS mail';
330 $queryCount = 'SELECT count(*) FROM #__acym_mail AS mail';
331
332 // Mail type filtering
333 $filters = [];
334 if (!empty($typeMail)) {
335 $filters[] = 'mail.type = '.acym_escapeDB($typeMail);
336 }
337
338 // Search filter
339 if (!empty($search)) {
340 $filters[] = 'mail.name LIKE '.acym_escapeDB('%'.$search.'%');
341 }
342
343 if (!empty($filters)) {
344 $query .= ' WHERE ('.implode(') AND (', $filters).')';
345 $queryCount .= ' WHERE ('.implode(') AND (', $filters).')';
346 }
347
348 $query .= ' ORDER BY id DESC';
349
350 $results['mails'] = $this->decode(acym_loadObjectList($query, $settings['key'], $settings['offset'], $settings['mailsPerPage']));
351 $results['total'] = acym_loadResult($queryCount);
352
353 return $results;
354 }
355
356 public function getAllListsWithCountSubscribersByMailIds(array $ids): array
357 {
358 acym_arrayToInteger($ids);
359 if (empty($ids)) {
360 return [];
361 }
362
363 $query = 'SELECT mailLists.list_id, mailLists.mail_id, list.*, COUNT(userLists.user_id) AS subscribers
364 FROM #__acym_mail_has_list AS mailLists
365 JOIN #__acym_list AS list ON mailLists.list_id = list.id
366 LEFT JOIN #__acym_user_has_list AS userLists
367 JOIN #__acym_user AS acyuser ON userLists.user_id = acyuser.id
368 AND userLists.status = 1
369 AND acyuser.active = 1 ';
370
371 if ($this->config->get('require_confirmation', 1) == 1) {
372 $query .= ' AND acyuser.confirmed = 1 ';
373 }
374
375 $query .= 'ON list.id = userLists.list_id
376 WHERE mailLists.mail_id IN ('.implode(',', $ids).')
377 GROUP BY mailLists.list_id, mailLists.mail_id';
378
379 //This line if for guys with big database to not break the page
380 acym_query('SET SQL_BIG_SELECTS=1');
381
382 return acym_loadObjectList($query);
383 }
384
385 public function getAllListsByMailIds(array $ids): array
386 {
387 acym_arrayToInteger($ids);
388 if (empty($ids)) {
389 return [];
390 }
391
392 $query = 'SELECT mailLists.mail_id, list.name, list.color
393 FROM #__acym_mail_has_list AS mailLists
394 JOIN #__acym_list AS list ON mailLists.list_id = list.id
395 WHERE mailLists.mail_id IN ('.implode(',', $ids).')';
396
397 return acym_loadObjectList($query);
398 }
399
400 public function getAllListsByMailId(int $id): array
401 {
402 $mail = $this->getOneById($id);
403 if (empty($mail)) {
404 return [];
405 }
406
407 if (self::TYPE_WELCOME === $mail->type) {
408 $query = 'SELECT * FROM #__acym_list WHERE welcome_id = '.intval($id);
409 } elseif (self::TYPE_FOLLOWUP === $mail->type) {
410 $query = 'SELECT list.* FROM #__acym_followup_has_mail AS followup_mail
411 JOIN #__acym_followup AS followup ON followup.id = followup_mail.followup_id AND followup_mail.mail_id = '.intval($id).'
412 JOIN #__acym_list AS list ON list.id = followup.list_id';
413 } else {
414 $query = 'SELECT list.*
415 FROM #__acym_mail_has_list AS mailLists
416 JOIN #__acym_mail AS mail ON (mailLists.mail_id = mail.id OR mailLists.mail_id = mail.parent_id) AND mail.id = '.intval($id).'
417 JOIN #__acym_list AS list ON mailLists.list_id = list.id';
418 }
419
420 return acym_loadObjectList($query, 'id');
421 }
422
423 public function getAllListsByMailIdAndUserId(int $mailId, int $userId): array
424 {
425 if (empty($mailId) || empty($userId)) {
426 return [];
427 }
428
429 $query = 'SELECT list.* FROM #__acym_list AS list';
430 $query .= ' JOIN #__acym_user_has_list AS userlist ON list.id = userlist.list_id AND userlist.user_id = '.intval($userId);
431 $query .= ' JOIN #__acym_mail_has_list AS maillist ON maillist.list_id = list.id AND maillist.mail_id = '.intval($mailId);
432
433 return acym_loadObjectList($query);
434 }
435
436 public function save(object $element): ?int
437 {
438 $mail = clone $element;
439
440 if (isset($mail->tags)) {
441 $tags = $mail->tags;
442 unset($mail->tags);
443 }
444
445 if (empty($mail->id)) {
446 $mail->creation_date = acym_date('now', 'Y-m-d H:i:s', false);
447 if (empty($mail->creator_id)) $mail->creator_id = acym_currentUserId();
448 }
449
450 $mail = $this->encode($mail);
451
452 // Clean autosave value
453 $mail->autosave = null;
454
455
456 // At this point $mail->thumbnail should be only a filename, so we don't want to override it with an empty string or an encoded image
457 // But we need to reset the thumbnail sometimes by setting the thumbnail to null
458 if (isset($mail->thumbnail) && ((empty($mail->thumbnail) && !is_null($mail->thumbnail)) || strpos($mail->thumbnail, 'data:image/png;base64') !== false)) {
459 unset($mail->thumbnail);
460 }
461
462 if (!isset($mail->access)) {
463 $mail->access = '';
464 }
465
466 foreach ($mail as $oneAttribute => $value) {
467 if (empty($value) || in_array($oneAttribute, ['thumbnail', 'settings'])) {
468 continue;
469 }
470
471 if ($oneAttribute === 'access' && is_array($mail->$oneAttribute)) {
472 $value = ','.trim(implode(',', $mail->$oneAttribute), ',').',';
473 $mail->$oneAttribute = $value;
474 }
475
476 if (is_array($value)) {
477 $mail->$oneAttribute = json_encode($value);
478 }
479
480 if (in_array($oneAttribute, ['body', 'headers'])) {
481 $mail->$oneAttribute = preg_replace('#<input[^>]*value="[^"]*"[^>]*>#Uis', '', $mail->$oneAttribute);
482 $mail->$oneAttribute = preg_replace('#<script.*</script>#Uis', '', $mail->$oneAttribute);
483
484 //Remove tinyMce content edit
485 $mail->$oneAttribute = str_replace(' contenteditable="true"', '', $mail->$oneAttribute);
486 } else {
487 $mail->$oneAttribute = acym_stripTags($mail->$oneAttribute);
488 }
489 }
490
491 $mailID = parent::save($mail);
492
493 if (!empty($mailID) && isset($tags)) {
494 $tagClass = new TagClass();
495 $tagClass->setTags(TagClass::TYPE_MAIL, intval($mailID), $tags);
496 }
497
498 return $mailID;
499 }
500
501 public function autoSave(object $mail, string $language = 'main'): bool
502 {
503 if (empty($mail->id)) {
504 return false;
505 }
506
507 $mail->autosave = str_replace(' contenteditable="true"', '', $mail->autosave);
508
509 if (acym_isMultilingual() && $language !== 'main') {
510 $translationId = $this->getTranslationId($mail->id, $language);
511 if (empty($translationId)) {
512 $parentCopy = $this->getOneById($mail->id);
513 if (empty($parentCopy)) {
514 return false;
515 }
516
517 unset($parentCopy->id);
518 $parentCopy->parent_id = $mail->id;
519 $parentCopy->language = $language;
520 $translationId = $this->save($parentCopy);
521 }
522 $mail->id = $translationId;
523 }
524
525 $mail = $this->encode($mail);
526
527 return (bool)parent::save($mail);
528 }
529
530 public function delete(array $elements): int
531 {
532 if (empty($elements)) return 0;
533
534 $this->deleteMediaFolder($elements);
535 acym_arrayToInteger($elements);
536
537 $allThumbnailToDelete = acym_loadResultArray('SELECT DISTINCT thumbnail FROM #__acym_mail WHERE id IN ('.implode(',', $elements).')');
538
539 $translations = acym_loadResultArray('SELECT id FROM #__acym_mail WHERE parent_id IN ('.implode(',', $elements).')');
540 $elements = array_merge($elements, $translations);
541 if (!empty($translations)) {
542 acym_query('UPDATE #__acym_mail SET `parent_id` = null WHERE `id` IN ('.implode(',', $translations).')');
543 }
544
545 $favoriteTemplate = (int)$this->config->get('favorite_template', 0);
546 if (in_array($favoriteTemplate, $elements)) {
547 $this->config->saveConfig(['favorite_template' => 0]);
548 }
549
550 acym_query('UPDATE #__acym_list SET welcome_id = null WHERE welcome_id IN ('.implode(',', $elements).')');
551 acym_query('UPDATE #__acym_list SET unsubscribe_id = null WHERE unsubscribe_id IN ('.implode(',', $elements).')');
552 acym_query('DELETE FROM #__acym_queue WHERE mail_id IN ('.implode(',', $elements).')');
553 acym_query('DELETE FROM #__acym_mail_has_list WHERE mail_id IN ('.implode(',', $elements).')');
554 acym_query('DELETE FROM #__acym_tag WHERE `type` = "mail" AND `id_element` IN ('.implode(',', $elements).')');
555 acym_query('DELETE FROM #__acym_user_stat WHERE mail_id IN ('.implode(',', $elements).')');
556 acym_query('DELETE FROM #__acym_url_click WHERE mail_id IN ('.implode(',', $elements).')');
557 acym_query('DELETE FROM #__acym_mail_stat WHERE mail_id IN ('.implode(',', $elements).')');
558 acym_query('DELETE FROM #__acym_followup_has_mail WHERE mail_id IN ('.implode(',', $elements).')');
559 acym_query('DELETE FROM #__acym_mail_archive WHERE mail_id IN ('.implode(',', $elements).')');
560
561 $return = parent::delete($elements);
562
563 $this->deleteUnusedThumbnails($allThumbnailToDelete);
564
565 return $return;
566 }
567
568 public function getFavoriteTemplateId(): int
569 {
570 $favoriteTemplate = (int)$this->config->get('favorite_template', 0);
571 if (empty($favoriteTemplate)) {
572 return 0;
573 }
574
575 $exists = acym_loadResult('SELECT `id` FROM #__acym_mail WHERE `id` = '.intval($favoriteTemplate));
576 if (empty($exists)) {
577 $this->config->saveConfig(['favorite_template' => 0]);
578
579 return 0;
580 }
581
582 return $favoriteTemplate;
583 }
584
585 public function deleteMediaFolder(array $elements): void
586 {
587 if (empty($elements)) {
588 return;
589 }
590
591 acym_arrayToInteger($elements);
592 $results = acym_loadResultArray('SELECT mail_settings FROM #__acym_mail WHERE mail_settings IS NOT NULL AND id IN ('.implode(',', $elements).')');
593
594 foreach ($results as $template) {
595 $settings = json_decode($template, true);
596
597 if (empty($settings['media_folder']) || !file_exists(ACYM_TEMPLATE.$settings['media_folder'])) {
598 continue;
599 }
600 acym_deleteFolder(ACYM_TEMPLATE.$settings['media_folder']);
601 }
602 }
603
604 public function deleteUnusedThumbnails(array $thumbnails): void
605 {
606 if (empty($thumbnails)) {
607 return;
608 }
609
610 foreach ($thumbnails as $key => $oneThumb) {
611 $thumbnails[$key] = acym_escapeDB($oneThumb);
612 }
613
614 $stillUsedThumbnails = acym_loadResultArray('SELECT thumbnail FROM #__acym_mail WHERE thumbnail IN ('.implode(',', $thumbnails).')');
615 $thumbnailToDelete = array_diff($thumbnails, $stillUsedThumbnails);
616 foreach ($thumbnailToDelete as $one) {
617 if (!empty($one) && file_exists(ACYM_UPLOAD_FOLDER_THUMBNAIL.$one)) {
618 acym_deleteFile(ACYM_UPLOAD_FOLDER_THUMBNAIL.$one);
619 }
620 }
621 }
622
623 // Delete one attachment from a newsletter
624 public function deleteOneAttachment(int $mailId, int $idAttachment): bool
625 {
626 if (empty($mailId)) {
627 return false;
628 }
629
630 $mail = $this->getOneById($mailId);
631
632 $attachments = $mail->attachments;
633 if (empty($attachments)) {
634 return false;
635 }
636 $decodedAttach = json_decode($attachments, true);
637 unset($decodedAttach[$idAttachment]);
638 $newAttachments = [];
639 if (!empty($decodedAttach)) {
640 foreach ($decodedAttach as $oneAttach) {
641 $newAttachments[] = $oneAttach;
642 }
643 }
644 $attachDb = json_encode($newAttachments);
645
646 return (bool)acym_query('UPDATE #__acym_mail SET attachments = '.acym_escapeDB($attachDb).' WHERE id = '.intval($mailId).' OR parent_id = '.intval($mailId));
647 }
648
649 public function createTemplateFile(int $id): string
650 {
651 if (empty($id)) {
652 return '';
653 }
654
655 $template = $this->getOneById($id);
656 if (empty($template->id)) {
657 return '';
658 }
659
660 $css = $this->buildCSS($template->stylesheet);
661 if (empty($css)) {
662 return '';
663 }
664
665 acym_createDir(ACYM_TEMPLATE.'css');
666
667 $cssfile = ACYM_TEMPLATE.'css'.DS.'template_'.$id.'.css';
668 if (acym_writeFile($cssfile, $css)) {
669 return $cssfile;
670 } else {
671 acym_enqueueMessage('Could not create the file '.$cssfile, 'error');
672
673 return '';
674 }
675 }
676
677 public function buildCSS(?string $stylesheet): string
678 {
679 if (empty($stylesheet)) {
680 return '';
681 }
682
683 $inline = '';
684
685 if (preg_match_all('#@import[^;]*;#is', $stylesheet, $results)) {
686 foreach ($results[0] as $oneResult) {
687 //We add the @import CSS at the very beginning for the CSS stylesheet otherwise it does not work
688 $inline .= trim($oneResult)."\n";
689 //We also remove it from the stylesheet to avoid having a duplicate.
690 $stylesheet = str_replace($oneResult, '', $stylesheet);
691 }
692 }
693
694 $inline .= $stylesheet;
695
696 return $inline;
697 }
698
699 public function doupload(): bool
700 {
701 $zipFilePath = $this->uploadTemplate();
702 if (empty($zipFilePath)) {
703 return false;
704 }
705
706 $templateFolder = $this->extractTemplate($zipFilePath);
707 if (empty($templateFolder)) {
708 return false;
709 }
710
711 if (!$this->installExtractedTemplate($templateFolder)) {
712 return false;
713 }
714
715 return true;
716 }
717
718 private function uploadTemplate(): ?string
719 {
720 $importFile = acym_getVar('file', 'uploadedfile', '', 'files');
721
722 $fileError = $importFile['error'];
723 if ($fileError > 0) {
724 switch ($fileError) {
725 case 1:
726 acym_enqueueMessage(acym_translation('ACYM_FILE_UPLOAD_ERROR_1'), 'error');
727
728 return null;
729 case 2:
730 acym_enqueueMessage(acym_translation('ACYM_FILE_UPLOAD_ERROR_2'), 'error');
731
732 return null;
733 case 3:
734 acym_enqueueMessage(acym_translation('ACYM_FILE_UPLOAD_ERROR_3'), 'error');
735
736 return null;
737 case 4:
738 acym_enqueueMessage(acym_translation('ACYM_FILE_UPLOAD_ERROR_4'), 'error');
739
740 return null;
741 default:
742 acym_enqueueMessage(acym_translationSprintf('ACYM_FILE_UPLOAD_ERROR_5', $fileError), 'error');
743
744 return null;
745 }
746 }
747
748 if (empty($importFile['name'])) {
749 acym_enqueueMessage(acym_translation('ACYM_BROWSE_FILE'), 'error');
750
751 return null;
752 }
753
754 $uploadPath = acym_cleanPath(ACYM_ROOT.ACYM_MEDIA_FOLDER.'templates');
755
756 if (!acym_isWritable($uploadPath)) {
757 acym_chmod($uploadPath, 0755);
758 if (!acym_isWritable($uploadPath)) {
759 acym_enqueueMessage(acym_translationSprintf('ACYM_WRITABLE_FOLDER', $uploadPath), 'warning');
760 }
761 }
762
763 if (!(bool)ini_get('file_uploads')) {
764 acym_enqueueMessage(acym_translationSprintf('ACYM_FILE_UPLOAD_ERROR_6'), 'error');
765
766 return null;
767 }
768
769 if (!extension_loaded('zlib')) {
770 acym_raiseError(500, acym_translation('ACYM_MISSING_ZLIB'));
771
772 return null;
773 }
774
775 $filename = strtolower(acym_makeSafeFile($importFile['name']));
776 $extension = strtolower(substr($filename, strrpos($filename, '.') + 1));
777
778 $allowedFileType = true;
779 if ($extension !== 'zip') {
780 $allowedFileType = false;
781 } elseif (!in_array($importFile['type'], ['application/zip', 'application/x-zip-compressed'])) {
782 $allowedFileType = false;
783 } elseif (function_exists('mime_content_type') && !in_array(mime_content_type($importFile['tmp_name']), ['application/zip', 'application/x-zip-compressed'])) {
784 $allowedFileType = false;
785 }
786
787 if (!$allowedFileType) {
788 acym_enqueueMessage(acym_translationSprintf('ACYM_ACCEPTED_TYPE', esc_html($extension), 'zip'), 'error');
789
790 return null;
791 }
792
793 if (class_exists('ZipArchive')) {
794 $zip = new \ZipArchive();
795 if (filesize($importFile['tmp_name']) > self::MAX_ALLOWED_TEMPLATE_SIZE || !$zip->open($importFile['tmp_name']) || $zip->count() > self::MAX_ALLOWED_TEMPLATE_FILES) {
796 acym_enqueueMessage(acym_translation('ACYM_UPLOADED_ARCHIVE_NOT_ALLOWED'), 'error');
797
798 return null;
799 }
800
801 $zip->close();
802 }
803
804 $tmpPath = acym_getCMSConfig('tmp_path', ACYM_MEDIA.'tmp'.DS);
805 $zipFilePath = acym_cleanPath($tmpPath.DS.$filename);
806
807 $uploaded = acym_uploadFile($importFile['tmp_name'], $zipFilePath);
808 if (!$uploaded) {
809 acym_enqueueMessage(acym_translationSprintf('ACYM_FILE_UPLOAD_ERROR_7', $importFile['tmp_name'], $zipFilePath), 'error');
810
811 return null;
812 }
813
814 return $zipFilePath;
815 }
816
817 private function extractTemplate(string $zipFilePath, bool $deleteZip = true): ?string
818 {
819 $extractdir = acym_cleanPath(dirname($zipFilePath).DS.uniqid().uniqid().uniqid().'_template');
820
821 $result = acym_extractArchive($zipFilePath, $extractdir);
822
823 if (!$result) {
824 acym_enqueueMessage(acym_translationSprintf('ACYM_FILE_UPLOAD_ERROR_9', $zipFilePath, $extractdir), 'error');
825
826 return null;
827 }
828
829 if ($deleteZip) {
830 acym_deleteFile($zipFilePath);
831 }
832
833 $allFiles = acym_getFiles($extractdir, '.', true, true, [], []);
834 foreach ($allFiles as $oneFile) {
835 if (function_exists('mime_content_type')) {
836 $fileType = mime_content_type($oneFile);
837 if (in_array($fileType, self::ALLOWED_TEMPLATE_FILE_TYPES) && preg_match('#\.(jpg|gif|png|jpeg|ico|bmp|html|htm|css|webp)$#i', $oneFile)) {
838 continue;
839 }
840 } elseif (preg_match('#\.(jpg|gif|png|jpeg|ico|bmp|html|htm|css|webp)$#i', $oneFile)) {
841 continue;
842 }
843
844 if (acym_deleteFile($oneFile)) {
845 acym_enqueueMessage(acym_translationSprintf('ACYM_FILE_UPLOAD_ERROR_8', $oneFile), 'warning');
846 }
847 }
848
849 return $extractdir;
850 }
851
852 public function installExtractedTemplate(string $templateFolder, bool $deleteFolder = true): bool
853 {
854 if ($this->detectTemplates($templateFolder)) {
855 $messages = $this->templateNames;
856 array_unshift($messages, acym_translationSprintf('ACYM_TEMPLATES_INSTALL', count($this->templateNames)));
857 acym_enqueueMessage($messages);
858 if ($deleteFolder && is_dir($templateFolder)) {
859 acym_deleteFolder($templateFolder);
860 }
861
862 return true;
863 }
864
865 acym_enqueueMessage(acym_translationSprintf('ACYM_FILE_UPLOAD_ERROR_10'), 'error');
866 if ($deleteFolder && is_dir($templateFolder)) {
867 acym_deleteFolder($templateFolder);
868 }
869
870 return false;
871 }
872
873 public function detectTemplates(string $folder): bool
874 {
875 $allFiles = acym_getFiles($folder);
876 if (!empty($allFiles)) {
877 // Search and install template.html file first
878 $keyTmpl = array_search('template.html', $allFiles);
879 if ($keyTmpl !== false) {
880 if ($this->installTemplate($folder.DS.$allFiles[$keyTmpl])) {
881 return true;
882 }
883 }
884 // Try to install first html file found
885 foreach ($allFiles as $oneFile) {
886 if (preg_match('#^.*(html|htm)$#i', $oneFile)) {
887 if ($this->installTemplate($folder.DS.$oneFile)) {
888 return true;
889 }
890 }
891 }
892 }
893
894 $status = false;
895 $allFolders = acym_getFolders($folder);
896 if (!empty($allFolders)) {
897 foreach ($allFolders as $oneFolder) {
898 $status = $this->detectTemplates($folder.DS.$oneFolder) || $status;
899 }
900 }
901
902 return $status;
903 }
904
905 private function installTemplate(string $filepath): bool
906 {
907 $fileContent = acym_fileGetContent($filepath);
908
909 $newTemplate = new \stdClass();
910
911 if (strpos($filepath, '_template') === false) {
912 $newTemplate->name = basename(dirname($filepath));
913 } else {
914 $newTemplate->name = basename($filepath);
915 $newTemplate->name = substr($newTemplate->name, 0, strrpos($newTemplate->name, '.'));
916 }
917
918 $newTemplate->name = trim(preg_replace('#[^a-z0-9]#i', ' ', $newTemplate->name));
919
920 if (preg_match('#< *title[^>]*>(.*)< */ *title *>#Uis', $fileContent, $results) && !empty($results[1])) $newTemplate->name = $results[1];
921
922 if (preg_match('#< *meta *name="fromname" *content="([^"]*)"#Uis', $fileContent, $results) && !empty($results[1])) $newTemplate->fromname = $results[1];
923 if (preg_match('#< *meta *name="fromemail" *content="([^"]*)"#Uis', $fileContent, $results) && !empty($results[1])) $newTemplate->fromemail = $results[1];
924 if (preg_match('#< *meta *name="replyname" *content="([^"]*)"#Uis', $fileContent, $results) && !empty($results[1])) $newTemplate->replyname = $results[1];
925 if (preg_match('#< *meta *name="replyemail" *content="([^"]*)"#Uis', $fileContent, $results) && !empty($results[1])) $newTemplate->replyemail = $results[1];
926 if (preg_match('#< *meta *name="subject" *content="([^"]*)"#Uis', $fileContent, $results) && !empty($results[1])) $newTemplate->subject = $results[1];
927 if (preg_match('#< *meta *name="settings" *content="([^"]*)"#Uis', $fileContent, $results) && !empty($results[1])) {
928 $newTemplate->settings = htmlspecialchars_decode($results[1]);
929 }
930
931 $newFolder = preg_replace('#[^a-z0-9]#i', '_', strtolower($newTemplate->name));
932 $newTemplateFolder = $newFolder;
933 $i = 1;
934 while (is_dir(ACYM_TEMPLATE.$newTemplateFolder)) {
935 $newTemplateFolder = $newFolder.'_'.$i;
936 $i++;
937 }
938 $newTemplate->mail_settings = ['media_folder' => $newTemplateFolder];
939
940 $moveResult = acym_copyFolder(dirname($filepath), ACYM_TEMPLATE.$newTemplateFolder);
941 if ($moveResult !== true) {
942 acym_display([acym_translationSprintf('ACYM_ERROR_COPYING_FOLDER_TO', dirname($filepath), ACYM_TEMPLATE.$newTemplateFolder), $moveResult], 'error');
943
944 return false;
945 }
946
947 if (!file_exists(ACYM_TEMPLATE.$newTemplateFolder.DS.'index.html')) {
948 $indexFile = '<html><body bgcolor="#FFFFFF"></body></html>';
949 acym_writeFile(ACYM_TEMPLATE.$newTemplateFolder.DS.'index.html', $indexFile);
950 }
951
952 $fileContent = str_replace(
953 [
954 'src="./',
955 'src="../',
956 'src="images/',
957 'url("images/',
958 'url(&quot;images/',
959 ],
960 [
961 'src="'.ACYM_TEMPLATE_URL.$newTemplateFolder.'/',
962 'src="'.ACYM_TEMPLATE_URL,
963 'src="'.ACYM_TEMPLATE_URL.$newTemplateFolder.'/images/',
964 'url("'.ACYM_TEMPLATE_URL.$newTemplateFolder.'/images/',
965 'url(&quot;'.ACYM_TEMPLATE_URL.$newTemplateFolder.'/images/',
966 ],
967 $fileContent
968 );
969
970 $fileContent = preg_replace('#(src|background)[ ]*=[ ]*\"(?!(https?://|/))(?:\.\./|\./)?#', '$1="'.ACYM_TEMPLATE_URL.$newTemplateFolder.'/', $fileContent);
971
972 if (preg_match('#< *body[^>]*>(.*)< */ *body *>#Uis', $fileContent, $results)) {
973 $newTemplate->body = $results[1];
974 } else {
975 $newTemplate->body = $fileContent;
976 }
977
978 $newTemplate->stylesheet = '';
979 if (preg_match_all('#< *style[^>]*>(.*)< */ *style *>#Uis', $fileContent, $results)) {
980 $newTemplate->stylesheet .= preg_replace('#(<!--|-->)#s', '', implode("\n", $results[1]));
981 }
982 $cssFiles = [];
983 $cssFiles[ACYM_TEMPLATE.$newTemplateFolder] = acym_getFiles(ACYM_TEMPLATE.$newTemplateFolder, '\.css$');
984 $subFolders = acym_getFolders(ACYM_TEMPLATE.$newTemplateFolder);
985 foreach ($subFolders as $oneFolder) {
986 $cssFiles[ACYM_TEMPLATE.$newTemplateFolder.DS.$oneFolder] = acym_getFiles(ACYM_TEMPLATE.$newTemplateFolder.DS.$oneFolder, '\.css$');
987 }
988
989 foreach ($cssFiles as $cssFolder => $cssFile) {
990 if (empty($cssFile)) continue;
991 $newTemplate->stylesheet .= "\n".acym_fileGetContent($cssFolder.DS.reset($cssFile));
992 }
993
994 if (!empty($newTemplate->stylesheet)) {
995 if (preg_match('#body *\{[^\}]*background-color:([^;\}]*)[;\}]#Uis', $newTemplate->stylesheet, $backgroundresults)) {
996 $newTemplate->stylesheet = preg_replace('#(body *\{[^\}]*)background-color:[^;\}]*[;\}]#Uis', '$1', $newTemplate->stylesheet);
997 }
998
999 $quickstyle = [
1000 'tag_h1' => 'h1',
1001 'tag_h2' => 'h2',
1002 'tag_h3' => 'h3',
1003 'tag_h4' => 'h4',
1004 'tag_h5' => 'h5',
1005 'tag_h6' => 'h6',
1006 'tag_a' => 'a',
1007 'tag_ul' => 'ul',
1008 'tag_li' => 'li',
1009 'acym_unsub' => '\.acym_unsub',
1010 'acym_online' => '\.acym_online',
1011 'acym_title' => '\.acym_title',
1012 'acym_content' => '\.acym_content',
1013 'acym_readmore' => '\.acym_readmore',
1014 ];
1015 foreach ($quickstyle as $styledb => $oneStyle) {
1016 if (preg_match('#[^a-z\. ,] *'.$oneStyle.' *{([^}]*)}#Uis', $newTemplate->stylesheet, $quickstyleresults)) {
1017 $newTemplate->stylesheet = str_replace($quickstyleresults[0], '', $newTemplate->stylesheet);
1018 }
1019 }
1020 }
1021
1022 $foldersForPicts = [$newTemplateFolder];
1023 $otherFolders = acym_getFolders(ACYM_TEMPLATE.$newTemplateFolder);
1024 foreach ($otherFolders as $oneFold) {
1025 $foldersForPicts[] = $newTemplateFolder.DS.$oneFold;
1026 }
1027 $allPictures = [];
1028 foreach ($foldersForPicts as $oneFolder) {
1029 $allPictures[$oneFolder] = acym_getFiles(ACYM_TEMPLATE.$oneFolder);
1030 }
1031
1032 $uploadsFolder = ACYM_UPLOAD_FOLDER_THUMBNAIL;
1033
1034 $thumbNb = intval($this->config->get('numberThumbnail', 2));
1035
1036 foreach ($allPictures as $folder => $pictfolders) {
1037 foreach ($pictfolders as $onePict) {
1038 if (!preg_match('#\.(jpg|gif|png|jpeg|ico|bmp)$#i', $onePict)) continue;
1039 if (preg_match('#(thumbnail|screenshot|muestra)#i', $onePict)) {
1040 $thumbNb++;
1041 $newNamePict = 'thumbnail_'.$thumbNb.'.png';
1042 copy(ACYM_TEMPLATE.str_replace(DS, '/', $folder).'/'.$onePict, $uploadsFolder.$newNamePict);
1043 $newTemplate->thumbnail = $newNamePict;
1044 }
1045 }
1046 }
1047
1048 $this->config->saveConfig(['numberThumbnail' => $thumbNb]);
1049
1050 $newTemplate->drag_editor = strpos($newTemplate->body, 'acym__wysid__template__content') !== false ? 1 : 0;
1051 $newTemplate->type = self::TYPE_TEMPLATE;
1052 $newTemplate->creation_date = acym_date('now', 'Y-m-d H:i:s', false);
1053
1054 $this->save($newTemplate);
1055
1056 $this->templateNames[] = $newTemplate->name;
1057
1058 return true;
1059 }
1060
1061 public function sendAutomation(int $mailId, array $userIds, string $sendingDate, array $automationAdmin = [])
1062 {
1063 if (empty($mailId)) {
1064 return acym_translationSprintf('ACYM_EMAILS_ADDED_QUEUE', 0);
1065 }
1066
1067 if (empty($sendingDate)) {
1068 return acym_translation('ACYM_WRONG_DATE');
1069 }
1070
1071 if (empty($userIds)) {
1072 return acym_translation('ACYM_USER_NOT_FOUND');
1073 }
1074
1075 acym_arrayToInteger($userIds);
1076
1077 $nbSent = $this->sendNotification($mailId, $userIds, $automationAdmin);
1078 if (!is_null($nbSent)) {
1079 return $nbSent;
1080 }
1081
1082 $result = acym_query(
1083 'INSERT IGNORE INTO #__acym_queue (`mail_id`, `user_id`, `sending_date`)
1084 SELECT '.intval($mailId).', user.id, '.acym_escapeDB($sendingDate).'
1085 FROM #__acym_user AS user
1086 WHERE user.active = 1 AND user.id IN ('.implode(',', $userIds).')'
1087 );
1088
1089 if ($result === 0) {
1090 return acym_translation('ACYM_CAMPAIGN_ALREADY_QUEUED');
1091 }
1092
1093 $mailStatClass = new MailStatClass();
1094 $mailStat = $mailStatClass->getOneRowByMailId($mailId);
1095
1096 $newMailStat = new \stdClass();
1097 $newMailStat->mail_id = intval($mailId);
1098 $newMailStat->total_subscribers = intval($result);
1099 if (empty($mailStat)) {
1100 $newMailStat->send_date = $sendingDate;
1101 }
1102
1103 $mailStatClass->save($newMailStat);
1104
1105 return $result;
1106 }
1107
1108 private function sendNotification(int $mailId, array $userIds, array $automationAdmin): ?int
1109 {
1110 if (empty($automationAdmin['user_id'])) {
1111 return null;
1112 }
1113
1114 $userClass = new UserClass();
1115 $mail = $this->getOneById($mailId);
1116 $user = $userClass->getOneById($automationAdmin['user_id']);
1117
1118 if (empty($mail) || empty($user)) {
1119 return 0;
1120 }
1121
1122 // Get the current user values
1123 $mailerHelper = new MailerHelper();
1124 $pluginHelper = new PluginHelper();
1125 $extractedTags = $pluginHelper->extractTags($mail, 'subscriber');
1126
1127 $isNotification = false;
1128 foreach ($extractedTags as $dtext => $oneTag) {
1129 if (empty($oneTag->info) || $oneTag->info !== 'current') {
1130 continue;
1131 }
1132
1133 $isNotification = true;
1134
1135 if (!empty($user->{$oneTag->id})) {
1136 $mailerHelper->addParam(str_replace(['{', '}'], '', $dtext), $user->{$oneTag->id});
1137 }
1138 }
1139
1140 if (!$isNotification) {
1141 if (isset($automationAdmin['automationAdmin']) && $automationAdmin['automationAdmin']) {
1142 return 0;
1143 } else {
1144 return null;
1145 }
1146 }
1147
1148 $userFields = $userClass->getAllUserFields($user);
1149 foreach ($userFields as $map => $value) {
1150 $mailerHelper->addParam('subscriber:'.$map.'|info:current', $value);
1151 // Might not be used anymore
1152 $mailerHelper->addParam('user:'.$map, $value);
1153 }
1154
1155 $emailsSent = 0;
1156 foreach ($userIds as $userId) {
1157 if ($mailerHelper->sendOne($mail->id, $userId)) {
1158 $emailsSent++;
1159 }
1160 }
1161
1162 return $emailsSent;
1163 }
1164
1165 public function encode($mails = [])
1166 {
1167 $isArray = true;
1168 if (!is_array($mails)) {
1169 $mails = [$mails];
1170
1171 $isArray = false;
1172 }
1173
1174 $encodedMails = array_map([$this, 'removePoweredByAcyMailing'], $mails);
1175
1176 $encodedMails = array_map([$this, 'utf8Encode'], $encodedMails);
1177
1178 return $isArray ? $encodedMails : $encodedMails[0];
1179 }
1180
1181 public function decode($mails = [])
1182 {
1183 if (is_null($mails)) {
1184 return null;
1185 }
1186
1187 $isArray = true;
1188 if (!is_array($mails)) {
1189 $mails = [$mails];
1190
1191 $isArray = false;
1192 }
1193
1194 $decodedMails = array_map([$this, 'utf8Decode'], $mails);
1195
1196 $decodedMails = array_map([$this, 'addPoweredByAcyMailing'], $decodedMails);
1197
1198 foreach ($decodedMails as $i => $oneMail) {
1199 if (!isset($oneMail->access) || is_array($oneMail->access)) continue;
1200 $decodedMails[$i]->access = empty($oneMail->access) ? '' : explode(',', $oneMail->access);
1201 }
1202
1203 return $isArray ? $decodedMails : $decodedMails[0];
1204 }
1205
1206 /**
1207 * Decode one mail from UTF8. (decode only attributes defined in $fieldsToDecode)
1208 */
1209 protected function utf8Decode($mail)
1210 {
1211 if (!empty($mail)) {
1212 foreach (self::FIELDS_ENCODING as $oneField) {
1213 if (in_array($oneField, $this->exceptKeysDecode)) {
1214 continue;
1215 }
1216
1217 if (is_array($mail)) {
1218 if (empty($mail[$oneField])) continue;
1219 $value = &$mail[$oneField];
1220 } else {
1221 if (empty($mail->$oneField)) continue;
1222 $value = &$mail->$oneField;
1223 }
1224
1225 $value = acym_utf8Decode($value);
1226 }
1227 }
1228
1229 return $mail;
1230 }
1231
1232 private function addPoweredByAcyMailing(object $mail): object
1233 {
1234 if (empty($mail->body)) {
1235 return $mail;
1236 }
1237
1238 //If we don't display the powered by we remove it
1239 if ($this->config->get('display_built_by', 0) != 1) {
1240 if (strpos($mail->body, 'acym__powered_by_acymailing') !== false) {
1241 $mailBodyDom = new \DOMDocument();
1242 @$mailBodyDom->loadHTML('<?xml encoding="utf-8" ?>'.$mail->body);
1243 $tables = $mailBodyDom->getElementsByTagName('table');
1244 foreach ($tables as $table) {
1245 if ($table->getAttribute('id') != 'acym__powered_by_acymailing') continue;
1246 $table->parentNode->removeChild($table);
1247 break;
1248 }
1249 $mail->body = $mailBodyDom->saveHTML();
1250 }
1251
1252 return $mail;
1253 }
1254
1255 if (strpos($mail->body, 'acym__powered_by_acymailing') !== false) {
1256 return $mail;
1257 }
1258
1259 $isWysidEditor = strpos($mail->body, 'acym__wysid__template') !== false;
1260 $urlPoweredByImage = ACYM_IMAGES.'editor/poweredby_black.png';
1261
1262 if (!$isWysidEditor) {
1263 $poweredByHTML = '<p id="acym__powered_by_acymailing">';
1264 $poweredByHTML .= '<a href="'.ACYM_ACYMAILING_WEBSITE.'?utm_campaign=powered_by_v7&utm_source=acymailing_plugin&utm_medium=built_with_footer" target="blank">';
1265 $poweredByHTML .= '<img alt="Email built with AcyMailing" height="40" width="199" style="height: 40px; width:199px; max-width: 100%; height: auto; box-sizing: border-box; padding: 0 5px; display: block; margin-left: auto; margin-right: auto;" src="'.$urlPoweredByImage.'"/>';
1266 $poweredByHTML .= '</a></p>';
1267
1268 $mail->body = $mail->body.$poweredByHTML;
1269 } else {
1270 $poweredByWYSID = '<table id="acym__powered_by_acymailing" class="row" bgcolor="#ffffff" style="background-color: transparent" cellpadding="0" cellspacing="0" border="0">
1271 <tbody bgcolor style="background-color: inherit;">
1272 <tr>
1273 <th class="small-12 medium-12 large-12 columns" valign="top" style="height: auto;">
1274 <table border="0" cellpadding="0" cellspacing="0"
1275 style="min-height: 0px; display: table; height: auto;">
1276 <tbody style="min-height: 0px; display: table-row-group;">
1277 <tr
1278 style="position: relative; top: inherit; left: inherit; right: inherit; bottom: inherit; height: auto;">
1279 <td class="large-12">
1280 <div style="position: relative;">
1281 <p style="word-break: break-word; text-align: center;">
1282 <a href="'.ACYM_ACYMAILING_WEBSITE.'?utm_campaign=powered_by_v7&utm_source=acymailing_plugin&utm_medium=built_with_footer" target="_blank">
1283 <img src="'.$urlPoweredByImage.'"
1284 title="poweredby" alt="Email built with AcyMailing"
1285 style="height: 40px; width:199px; max-width: 100%; height: auto; box-sizing: border-box; padding: 0px 5px; display: inline-block; margin-left: auto; margin-right: auto;"
1286 height="40" width="199">
1287 </a>
1288 </p>
1289 </div>
1290 </td>
1291 </tr>
1292 </tbody>
1293 </table>
1294 </th>
1295 </tr>
1296 </tbody>
1297 </table>';
1298
1299 $mailBodyDom = new \DOMDocument();
1300 //Some inserted content add specific tags that shows warnings
1301 @$mailBodyDom->loadHTML('<?xml encoding="utf-8" ?>'.$mail->body);
1302
1303 $htmlToAddDom = new \DOMDocument();
1304 @$htmlToAddDom->loadHTML('<?xml encoding="utf-8" ?>'.$poweredByWYSID);
1305 $tableToAdd = $htmlToAddDom->getElementById('acym__powered_by_acymailing');
1306
1307 $tds = $mailBodyDom->getElementsByTagName('td');
1308 foreach ($tds as $td) {
1309 $classes = explode(' ', $td->getAttribute('class'));
1310 if (!in_array('acym__wysid__row', $classes)) continue;
1311 if (!empty($tableToAdd)) $td->appendChild($mailBodyDom->importNode($tableToAdd, true));
1312 break;
1313 }
1314 $mail->body = $mailBodyDom->saveHTML();
1315 }
1316
1317 return $mail;
1318 }
1319
1320 protected function removePoweredByAcyMailing(object $mail): object
1321 {
1322 if (empty($mail->body) || strpos($mail->body, 'acym__wysid__template') !== false) {
1323 return $mail;
1324 }
1325
1326 $regexPoweredByAcyMailing = '#<p id="acym__powered_by_acymailing.*<\/p>#U';
1327
1328 $mail->body = preg_replace($regexPoweredByAcyMailing, '', $mail->body);
1329
1330 return $mail;
1331 }
1332
1333 /**
1334 * Encode one mail in UTF8 for handling specific characters as emoji. (encode only attributes defined in $fieldsToEncode)
1335 *
1336 * @param $mail
1337 *
1338 * @return mixed
1339 */
1340 protected function utf8Encode($mail)
1341 {
1342 if (!empty($mail)) {
1343 foreach (self::FIELDS_ENCODING as $oneField) {
1344 if (is_array($mail)) {
1345 if (empty($mail[$oneField])) continue;
1346 $value = &$mail[$oneField];
1347 } else {
1348 if (empty($mail->$oneField)) continue;
1349 $value = &$mail->$oneField;
1350 }
1351
1352 $value = acym_utf8Encode($value);
1353 }
1354 }
1355
1356 return $mail;
1357 }
1358
1359 public function getTranslationId(int $parentId, string $langCode): int
1360 {
1361 return (int)acym_loadResult(
1362 'SELECT `id`
1363 FROM #__acym_mail
1364 WHERE `parent_id` = '.intval($parentId).'
1365 AND `language` = '.acym_escapeDB($langCode)
1366 );
1367 }
1368
1369 public function getTranslationsById(int $mailId, bool $full = false, bool $includeParent = false)
1370 {
1371 $data = $full ? '*' : '`language`, `subject`, `preheader`, `body`, `autosave`';
1372 $where = $includeParent ? ' OR `id` = '.intval($mailId) : '';
1373
1374 return $this->decode(
1375 acym_loadObjectList(
1376 'SELECT '.$data.'
1377 FROM #__acym_mail
1378 WHERE `parent_id` = '.intval($mailId).$where,
1379 'language'
1380 )
1381 );
1382 }
1383
1384 public function deleteByTranslationLang(array $languageCodes): void
1385 {
1386 if (empty($languageCodes)) {
1387 return;
1388 }
1389
1390 foreach ($languageCodes as $key => $oneLangCode) {
1391 $languageCodes[$key] = acym_escapeDB($oneLangCode);
1392 }
1393
1394 $this->delete(
1395 acym_loadResultArray(
1396 'SELECT `id`
1397 FROM #__acym_mail
1398 WHERE `parent_id` IS NOT NULL
1399 AND `language` IN ('.implode(', ', $languageCodes).')'
1400 )
1401 );
1402 }
1403
1404 /**
1405 * Get all multilingual mails linked to a parent mail, also get the parent mail
1406 */
1407 public function getMultilingualMails(int $parentId): array
1408 {
1409 $mails = $this->decode(
1410 acym_loadObjectList(
1411 'SELECT * FROM #__acym_mail WHERE parent_id = '.intval($parentId).' OR id = '.intval($parentId),
1412 'language'
1413 )
1414 );
1415
1416 return $this->translateMailSettings($mails);
1417 }
1418
1419 private function translateMailSettings(array $mails): array
1420 {
1421 if (empty($mails)) {
1422 return $mails;
1423 }
1424
1425 $defaultLanguage = $this->config->get('multilingual_default');
1426
1427 if (empty($mails[$defaultLanguage])) {
1428 $firstMail = reset($mails);
1429 if (empty($firstMail) || empty($firstMail->parent_id)) {
1430 return $mails;
1431 }
1432 $mainMail = $this->getOneById($firstMail->parent_id);
1433 } else {
1434 $mainMail = $mails[$defaultLanguage];
1435 }
1436
1437 if (!empty($mainMail->translation)) {
1438 $translation = json_decode($mainMail->translation, true);
1439
1440 foreach ($mails as $lang => $mail) {
1441 if (empty($mail->parent_id) || empty($translation[$lang])) {
1442 continue;
1443 }
1444
1445 if (!empty($translation[$lang]['from_name'])) $mail->from_name = $translation[$lang]['from_name'];
1446 if (!empty($translation[$lang]['from_email'])) $mail->from_email = $translation[$lang]['from_email'];
1447 if (!empty($translation[$lang]['reply_to_name'])) $mail->reply_to_name = $translation[$lang]['reply_to_name'];
1448 if (!empty($translation[$lang]['reply_to_email'])) $mail->reply_to_email = $translation[$lang]['reply_to_email'];
1449 }
1450 }
1451
1452 return $mails;
1453 }
1454
1455 public function getMailAttachments(int $mailId)
1456 {
1457 return acym_loadResult('SELECT attachments FROM #__acym_mail WHERE id = '.intval($mailId));
1458 }
1459
1460 public function isTransactionalMail(object $mail): bool
1461 {
1462 return $mail->type !== self::TYPE_STANDARD;
1463 }
1464
1465 public function isOneTimeMail(object $mail): bool
1466 {
1467 return in_array(
1468 $mail->type,
1469 [
1470 self::TYPE_NOTIFICATION,
1471 self::TYPE_OVERRIDE,
1472 self::TYPE_WELCOME,
1473 self::TYPE_UNSUBSCRIBE,
1474 self::TYPE_TEMPLATE,
1475 ]
1476 );
1477 }
1478
1479 public function getAutomaticMailIds(array $mailIds): array
1480 {
1481 acym_arrayToInteger($mailIds);
1482
1483 $query = 'SELECT DISTINCT campaign2.mail_id
1484 FROM #__acym_campaign AS campaign1
1485 JOIN #__acym_campaign AS campaign2 ON campaign1.id = campaign2.parent_id
1486 WHERE campaign1.mail_id IN ('.implode(',', $mailIds).')';
1487 $generatedMailIds = acym_loadResultArray($query);
1488
1489 if (empty($generatedMailIds)) return [];
1490
1491 acym_arrayToInteger($generatedMailIds);
1492
1493 return $generatedMailIds;
1494 }
1495
1496 public function getMultilingualMailIds(array $mailIds): array
1497 {
1498 acym_arrayToInteger($mailIds);
1499
1500 $query = 'SELECT DISTINCT mail2.id
1501 FROM #__acym_mail AS mail1
1502 JOIN #__acym_mail AS mail2 ON mail1.id = mail2.parent_id
1503 WHERE mail1.id IN ('.implode(',', $mailIds).')';
1504
1505 $multipleMailIds = acym_loadResultArray($query);
1506
1507 if (empty($multipleMailIds)) return [];
1508
1509 acym_arrayToInteger($multipleMailIds);
1510
1511 return $multipleMailIds;
1512 }
1513
1514 public function duplicateMail(int $mailId, ?string $type = null): ?object
1515 {
1516 $mail = $this->getOneById($mailId);
1517
1518 if (empty($mail)) {
1519 return null;
1520 }
1521
1522 $newMail = new \stdClass();
1523 $newMail->name = $mail->name.'_copy';
1524 $newMail->thumbnail = '';
1525 $newMail->type = empty($type) ? $mail->type : $type;
1526 $newMail->drag_editor = $mail->drag_editor;
1527 $newMail->body = $mail->body;
1528 $newMail->subject = $mail->subject;
1529 $newMail->from_name = $mail->from_name;
1530 $newMail->from_email = $mail->from_email;
1531 $newMail->reply_to_name = $mail->reply_to_name;
1532 $newMail->reply_to_email = $mail->reply_to_email;
1533 $newMail->bcc = $mail->bcc;
1534 $newMail->settings = $mail->settings;
1535 $newMail->stylesheet = $mail->stylesheet;
1536 $newMail->attachments = $mail->attachments;
1537 $newMail->headers = $mail->headers;
1538 $newMail->preheader = $mail->preheader;
1539
1540 $newMail->id = $this->save($newMail);
1541
1542 return $newMail;
1543 }
1544
1545 public function getTemplatesForMailboxAction(): array
1546 {
1547 return $this->decode(
1548 acym_loadObjectList(
1549 'SELECT `id`, `name`
1550 FROM #__acym_mail
1551 WHERE `type` = '.acym_escapeDB(self::TYPE_TEMPLATE).'
1552 AND `body` LIKE '.acym_escapeDB('%'.acym_utf8Encode('{emailcontent}').'%')
1553 )
1554 );
1555 }
1556
1557 public function hasUserAccess(int $mailId, bool $write = false, ?int $userId = null): bool
1558 {
1559 $userId = $userId ?? acym_currentUserId();
1560 if (empty($userId)) {
1561 return false;
1562 }
1563
1564 $mail = acym_loadObject('SELECT `creator_id`, `type`, `access` FROM #__acym_mail WHERE id = '.intval($mailId));
1565 if (empty($mail)) {
1566 return false;
1567 }
1568
1569 if (acym_isAdmin()) {
1570 return $this->isTypeAllowed($mail->type);
1571 }
1572
1573 if ($mail->creator_id == $userId) {
1574 return true;
1575 }
1576
1577 return !$write && $mail->type === self::TYPE_TEMPLATE && $this->isAccessAllowedForUser($mail->access, $userId);
1578 }
1579
1580 private function isAccessAllowedForUser(?string $access, int $userId): bool
1581 {
1582 if (empty($access)) {
1583 return false;
1584 }
1585
1586 foreach (acym_getGroupsByUser($userId) as $userGroup) {
1587 if (strpos($access, ','.$userGroup.',') !== false) {
1588 return true;
1589 }
1590 }
1591
1592 return false;
1593 }
1594
1595 private function isTypeAllowed(?string $type): bool
1596 {
1597 $aclByType = [
1598 self::TYPE_NOTIFICATION => ['configuration'],
1599 self::TYPE_OVERRIDE => ['override'],
1600 self::TYPE_MAILBOX_ACTION => ['bounces'],
1601 self::TYPE_AUTOMATION => ['automation'],
1602 self::TYPE_SCENARIO => ['scenarios'],
1603 self::TYPE_STANDARD => ['campaigns'],
1604 self::TYPE_FOLLOWUP => ['campaigns'],
1605 self::TYPE_WELCOME => ['lists', 'campaigns'],
1606 self::TYPE_UNSUBSCRIBE => ['lists', 'campaigns'],
1607 self::TYPE_TEMPLATE => ['mails', 'campaigns'],
1608 ];
1609
1610 // An add-on type is not mapped to a feature, don't lock it
1611 if (empty($aclByType[$type])) {
1612 return true;
1613 }
1614
1615 foreach ($aclByType[$type] as $oneAcl) {
1616 if (acym_isAllowed($oneAcl)) {
1617 return true;
1618 }
1619 }
1620
1621 return false;
1622 }
1623
1624 public function getVersionsById(int $mailId, bool $includeParent = false): array
1625 {
1626 $where = $includeParent ? ' OR `id` = '.intval($mailId) : '';
1627
1628 return $this->decode(
1629 acym_loadObjectList(
1630 'SELECT *
1631 FROM #__acym_mail
1632 WHERE `parent_id` = '.intval($mailId).$where.' ORDER BY id ASC',
1633 'id'
1634 )
1635 );
1636 }
1637
1638 /**
1639 * Get all abtest mails linked to a parent mail, also get the parent mail
1640 */
1641 public function getParentAndChildMails(int $parentId): array
1642 {
1643 return $this->decode(
1644 acym_loadObjectList(
1645 'SELECT * FROM #__acym_mail WHERE parent_id = '.intval($parentId).' OR id = '.intval($parentId),
1646 'id'
1647 )
1648 );
1649 }
1650
1651 public function getAllTemplatesByType(array $options): array
1652 {
1653 $limit = $options['limit'] ?? 10;
1654 $offset = $options['offset'] ?? 0;
1655 $filters = $options['filters'] ?? [];
1656 $search = $filters['search'] ?? '';
1657 $templateType = $filters['templateType'] ?? '';
1658
1659 $conditions = ['`type` = '.acym_escapeDB(self::TYPE_TEMPLATE)];
1660
1661 if (!empty($search)) {
1662 $conditions[] = '(name LIKE '.acym_escapeDB('%'.$search.'%').' OR subject LIKE '.acym_escapeDB('%'.$search.'%').')';
1663 }
1664
1665 if (!empty($templateType)) {
1666 if ($templateType === 'drag') {
1667 $conditions[] = 'drag_editor = 1';
1668 } else {
1669 $conditions[] = 'drag_editor = 0';
1670 }
1671 }
1672
1673 $query = 'SELECT * FROM #__acym_mail WHERE '.implode(' AND ', $conditions);
1674
1675 return $this->decode(acym_loadObjectList($query, '', $offset, $limit));
1676 }
1677
1678 public function updateFollowupPriority(bool $previouslyActive, bool $active): void
1679 {
1680 if (!$previouslyActive && $active) {
1681 acym_query(
1682 'UPDATE `#__acym_queue` AS `queue`
1683 INNER JOIN `#__acym_mail` AS `mail` ON `queue`.`mail_id` = `mail`.`id`
1684 SET `queue`.`priority` = 1
1685 WHERE `mail`.`type` = '.acym_escapeDB(self::TYPE_FOLLOWUP)
1686 );
1687 }
1688 }
1689
1690 public function getCampaignIdByMailId(int $mailId): int
1691 {
1692 $mainMailId = $this->getMainMailId($mailId);
1693
1694 return intval(acym_loadResult('SELECT id FROM #__acym_campaign WHERE mail_id = '.intval($mainMailId)));
1695 }
1696
1697 public function getMainMailId(int $mailId): int
1698 {
1699 $parentId = acym_loadResult('SELECT parent_id FROM #__acym_mail WHERE id = '.intval($mailId));
1700
1701 return empty($parentId) ? $mailId : intval($parentId);
1702 }
1703
1704 public function getMailType(int $mailId): string
1705 {
1706 return (string)acym_loadResult('SELECT type FROM #__acym_mail WHERE id = '.intval($mailId));
1707 }
1708 }
1709