| @@ -40,8 +40,9 @@ | ||
| 40 | 40 | self::TYPE_TEMPLATE, |
| 41 | 41 | self::TYPE_SCENARIO, |
| 42 | 42 | ]; |
| 43 | 43 | |
| 44 | + // Used by some sending methods to know the priority of a sent email (transactional => reset password / account confirmation...) | |
| 44 | 45 | const TYPES_TRANSACTIONAL = [ |
| 45 | 46 | self::TYPE_NOTIFICATION, |
| 46 | 47 | self::TYPE_OVERRIDE, |
| 47 | 48 | self::TYPE_WELCOME, |
| @@ -49,8 +50,9 @@ | ||
| 49 | 50 | self::TYPE_TEMPLATE, |
| 50 | 51 | self::TYPE_SCENARIO, |
| 51 | 52 | ]; |
| 52 | 53 | |
| 54 | + // Types on which the click statistics are active | |
| 53 | 55 | const TYPES_WITH_STATS = [ |
| 54 | 56 | self::TYPE_STANDARD, |
| 55 | 57 | self::TYPE_AUTOMATION, |
| 56 | 58 | self::TYPE_WELCOME, |
| @@ -59,8 +61,9 @@ | ||
| 59 | 61 | self::TYPE_MAILBOX_ACTION, |
| 60 | 62 | self::TYPE_SCENARIO, |
| 61 | 63 | ]; |
| 62 | 64 | |
| 65 | + // Types that don't let the user modify the name | |
| 63 | 66 | const TYPES_NO_NAME = [ |
| 64 | 67 | self::TYPE_NOTIFICATION, |
| 65 | 68 | self::TYPE_OVERRIDE, |
| 66 | 69 | ]; |
| @@ -88,8 +91,11 @@ | ||
| 88 | 91 | $this->table = 'mail'; |
| 89 | 92 | $this->pkey = 'id'; |
| 90 | 93 | } |
| 91 | 94 | |
| 95 | + /** | |
| 96 | + * Get mails depending on filters (search, ordering, pagination) | |
| 97 | + */ | |
| 92 | 98 | public function getMatchingElements(array $settings = []): array |
| 93 | 99 | { |
| 94 | 100 | $query = 'SELECT mail.* FROM #__acym_mail AS mail'; |
| 95 | 101 | $queryCount = 'SELECT COUNT(mail.id) AS total FROM #__acym_mail AS mail'; |
| @@ -96,8 +102,9 @@ | ||
| 96 | 102 | |
| 97 | 103 | $filters = []; |
| 98 | 104 | $tagJoin = ''; |
| 99 | 105 | |
| 106 | + // Tag filter | |
| 100 | 107 | if (!empty($settings['tag'])) { |
| 101 | 108 | $tagJoin = ' JOIN #__acym_tag AS tag ON mail.id = tag.id_element '; |
| 102 | 109 | $filters[] = 'tag.name = '.acym_escapeDB($settings['tag']); |
| 103 | 110 | $filters[] = 'tag.type = "mail"'; |
| @@ -302,8 +309,11 @@ | ||
| 302 | 309 | |
| 303 | 310 | return $mail; |
| 304 | 311 | } |
| 305 | 312 | |
| 313 | + /** | |
| 314 | + * Get mails depending on their type (standard, welcome, unsubscribe, notification) | |
| 315 | + */ | |
| 306 | 316 | public function getMailsByType(?string $typeMail, array $settings): array |
| 307 | 317 | { |
| 308 | 318 | if (empty($settings['key'])) { |
| 309 | 319 | $settings['key'] = ''; |
| @@ -318,13 +328,15 @@ | ||
| 318 | 328 | |
| 319 | 329 | $query = 'SELECT * FROM #__acym_mail AS mail'; |
| 320 | 330 | $queryCount = 'SELECT count(*) FROM #__acym_mail AS mail'; |
| 321 | 331 | |
| 332 | + // Mail type filtering | |
| 322 | 333 | $filters = []; |
| 323 | 334 | if (!empty($typeMail)) { |
| 324 | 335 | $filters[] = 'mail.type = '.acym_escapeDB($typeMail); |
| 325 | 336 | } |
| 326 | 337 | |
| 338 | + // Search filter | |
| 327 | 339 | if (!empty($search)) { |
| 328 | 340 | $filters[] = 'mail.name LIKE '.acym_escapeDB('%'.$search.'%'); |
| 329 | 341 | } |
| 330 | 342 | |
| @@ -363,8 +375,9 @@ | ||
| 363 | 375 | $query .= 'ON list.id = userLists.list_id |
| 364 | 376 | WHERE mailLists.mail_id IN ('.implode(',', $ids).') |
| 365 | 377 | GROUP BY mailLists.list_id, mailLists.mail_id'; |
| 366 | 378 | |
| 379 | + //This line if for guys with big database to not break the page | |
| 367 | 380 | acym_query('SET SQL_BIG_SELECTS=1'); |
| 368 | 381 | |
| 369 | 382 | return acym_loadObjectList($query); |
| 370 | 383 | } |
| @@ -435,11 +448,14 @@ | ||
| 435 | 448 | } |
| 436 | 449 | |
| 437 | 450 | $mail = $this->encode($mail); |
| 438 | 451 | |
| 452 | + // Clean autosave value | |
| 439 | 453 | $mail->autosave = null; |
| 440 | 454 | |
| 441 | 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 | |
| 442 | 458 | if (isset($mail->thumbnail) && ((empty($mail->thumbnail) && !is_null($mail->thumbnail)) || strpos($mail->thumbnail, 'data:image/png;base64') !== false)) { |
| 443 | 459 | unset($mail->thumbnail); |
| 444 | 460 | } |
| 445 | 461 | |
| @@ -464,11 +480,12 @@ | ||
| 464 | 480 | if (in_array($oneAttribute, ['body', 'headers'])) { |
| 465 | 481 | $mail->$oneAttribute = preg_replace('#<input[^>]*value="[^"]*"[^>]*>#Uis', '', $mail->$oneAttribute); |
| 466 | 482 | $mail->$oneAttribute = preg_replace('#<script.*</script>#Uis', '', $mail->$oneAttribute); |
| 467 | 483 | |
| 484 | + //Remove tinyMce content edit | |
| 468 | 485 | $mail->$oneAttribute = str_replace(' contenteditable="true"', '', $mail->$oneAttribute); |
| 469 | 486 | } else { |
| 470 | - $mail->$oneAttribute = strip_tags($mail->$oneAttribute); | |
| 487 | + $mail->$oneAttribute = acym_stripTags($mail->$oneAttribute); | |
| 471 | 488 | } |
| 472 | 489 | } |
| 473 | 490 | |
| 474 | 491 | $mailID = parent::save($mail); |
| @@ -516,13 +533,8 @@ | ||
| 516 | 533 | |
| 517 | 534 | $this->deleteMediaFolder($elements); |
| 518 | 535 | acym_arrayToInteger($elements); |
| 519 | 536 | |
| 520 | - $favoriteTemplate = (int)$this->config->get('favorite_template', 0); | |
| 521 | - if (in_array($favoriteTemplate, $elements)) { | |
| 522 | - $this->config->saveConfig(['favorite_template' => 0]); | |
| 523 | - } | |
| 524 | - | |
| 525 | 537 | $allThumbnailToDelete = acym_loadResultArray('SELECT DISTINCT thumbnail FROM #__acym_mail WHERE id IN ('.implode(',', $elements).')'); |
| 526 | 538 | |
| 527 | 539 | $translations = acym_loadResultArray('SELECT id FROM #__acym_mail WHERE parent_id IN ('.implode(',', $elements).')'); |
| 528 | 540 | $elements = array_merge($elements, $translations); |
| @@ -529,8 +541,13 @@ | ||
| 529 | 541 | if (!empty($translations)) { |
| 530 | 542 | acym_query('UPDATE #__acym_mail SET `parent_id` = null WHERE `id` IN ('.implode(',', $translations).')'); |
| 531 | 543 | } |
| 532 | 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 | + | |
| 533 | 550 | acym_query('UPDATE #__acym_list SET welcome_id = null WHERE welcome_id IN ('.implode(',', $elements).')'); |
| 534 | 551 | acym_query('UPDATE #__acym_list SET unsubscribe_id = null WHERE unsubscribe_id IN ('.implode(',', $elements).')'); |
| 535 | 552 | acym_query('DELETE FROM #__acym_queue WHERE mail_id IN ('.implode(',', $elements).')'); |
| 536 | 553 | acym_query('DELETE FROM #__acym_mail_has_list WHERE mail_id IN ('.implode(',', $elements).')'); |
| @@ -547,8 +564,25 @@ | ||
| 547 | 564 | |
| 548 | 565 | return $return; |
| 549 | 566 | } |
| 550 | 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 | + | |
| 551 | 585 | public function deleteMediaFolder(array $elements): void |
| 552 | 586 | { |
| 553 | 587 | if (empty($elements)) { |
| 554 | 588 | return; |
| @@ -580,13 +614,14 @@ | ||
| 580 | 614 | $stillUsedThumbnails = acym_loadResultArray('SELECT thumbnail FROM #__acym_mail WHERE thumbnail IN ('.implode(',', $thumbnails).')'); |
| 581 | 615 | $thumbnailToDelete = array_diff($thumbnails, $stillUsedThumbnails); |
| 582 | 616 | foreach ($thumbnailToDelete as $one) { |
| 583 | 617 | if (!empty($one) && file_exists(ACYM_UPLOAD_FOLDER_THUMBNAIL.$one)) { |
| 584 | - unlink(ACYM_UPLOAD_FOLDER_THUMBNAIL.$one); | |
| 618 | + acym_deleteFile(ACYM_UPLOAD_FOLDER_THUMBNAIL.$one); | |
| 585 | 619 | } |
| 586 | 620 | } |
| 587 | 621 | } |
| 588 | 622 | |
| 623 | + // Delete one attachment from a newsletter | |
| 589 | 624 | public function deleteOneAttachment(int $mailId, int $idAttachment): bool |
| 590 | 625 | { |
| 591 | 626 | if (empty($mailId)) { |
| 592 | 627 | return false; |
| @@ -648,9 +683,11 @@ | ||
| 648 | 683 | $inline = ''; |
| 649 | 684 | |
| 650 | 685 | if (preg_match_all('#@import[^;]*;#is', $stylesheet, $results)) { |
| 651 | 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 | |
| 652 | 688 | $inline .= trim($oneResult)."\n"; |
| 689 | + //We also remove it from the stylesheet to avoid having a duplicate. | |
| 653 | 690 | $stylesheet = str_replace($oneResult, '', $stylesheet); |
| 654 | 691 | } |
| 655 | 692 | } |
| 656 | 693 | |
| @@ -715,11 +752,11 @@ | ||
| 715 | 752 | } |
| 716 | 753 | |
| 717 | 754 | $uploadPath = acym_cleanPath(ACYM_ROOT.ACYM_MEDIA_FOLDER.'templates'); |
| 718 | 755 | |
| 719 | - if (!is_writable($uploadPath)) { | |
| 720 | - @chmod($uploadPath, '0755'); | |
| 721 | - if (!is_writable($uploadPath)) { | |
| 756 | + if (!acym_isWritable($uploadPath)) { | |
| 757 | + acym_chmod($uploadPath, 0755); | |
| 758 | + if (!acym_isWritable($uploadPath)) { | |
| 722 | 759 | acym_enqueueMessage(acym_translationSprintf('ACYM_WRITABLE_FOLDER', $uploadPath), 'warning'); |
| 723 | 760 | } |
| 724 | 761 | } |
| 725 | 762 | |
| @@ -747,9 +784,9 @@ | ||
| 747 | 784 | $allowedFileType = false; |
| 748 | 785 | } |
| 749 | 786 | |
| 750 | 787 | if (!$allowedFileType) { |
| 751 | - acym_enqueueMessage(acym_translationSprintf('ACYM_ACCEPTED_TYPE', $extension, 'zip'), 'error'); | |
| 788 | + acym_enqueueMessage(acym_translationSprintf('ACYM_ACCEPTED_TYPE', esc_html($extension), 'zip'), 'error'); | |
| 752 | 789 | |
| 753 | 790 | return null; |
| 754 | 791 | } |
| 755 | 792 | |
| @@ -836,8 +873,9 @@ | ||
| 836 | 873 | public function detectTemplates(string $folder): bool |
| 837 | 874 | { |
| 838 | 875 | $allFiles = acym_getFiles($folder); |
| 839 | 876 | if (!empty($allFiles)) { |
| 877 | + // Search and install template.html file first | |
| 840 | 878 | $keyTmpl = array_search('template.html', $allFiles); |
| 841 | 879 | if ($keyTmpl !== false) { |
| 842 | 880 | if ($this->installTemplate($folder.DS.$allFiles[$keyTmpl])) { |
| 843 | 881 | return true; |
| @@ -842,8 +880,9 @@ | ||
| 842 | 880 | if ($this->installTemplate($folder.DS.$allFiles[$keyTmpl])) { |
| 843 | 881 | return true; |
| 844 | 882 | } |
| 845 | 883 | } |
| 884 | + // Try to install first html file found | |
| 846 | 885 | foreach ($allFiles as $oneFile) { |
| 847 | 886 | if (preg_match('#^.*(html|htm)$#i', $oneFile)) { |
| 848 | 887 | if ($this->installTemplate($folder.DS.$oneFile)) { |
| 849 | 888 | return true; |
| @@ -1079,8 +1118,9 @@ | ||
| 1079 | 1118 | if (empty($mail) || empty($user)) { |
| 1080 | 1119 | return 0; |
| 1081 | 1120 | } |
| 1082 | 1121 | |
| 1122 | + // Get the current user values | |
| 1083 | 1123 | $mailerHelper = new MailerHelper(); |
| 1084 | 1124 | $pluginHelper = new PluginHelper(); |
| 1085 | 1125 | $extractedTags = $pluginHelper->extractTags($mail, 'subscriber'); |
| 1086 | 1126 | |
| @@ -1107,8 +1147,9 @@ | ||
| 1107 | 1147 | |
| 1108 | 1148 | $userFields = $userClass->getAllUserFields($user); |
| 1109 | 1149 | foreach ($userFields as $map => $value) { |
| 1110 | 1150 | $mailerHelper->addParam('subscriber:'.$map.'|info:current', $value); |
| 1151 | + // Might not be used anymore | |
| 1111 | 1152 | $mailerHelper->addParam('user:'.$map, $value); |
| 1112 | 1153 | } |
| 1113 | 1154 | |
| 1114 | 1155 | $emailsSent = 0; |
| @@ -1161,8 +1202,11 @@ | ||
| 1161 | 1202 | |
| 1162 | 1203 | return $isArray ? $decodedMails : $decodedMails[0]; |
| 1163 | 1204 | } |
| 1164 | 1205 | |
| 1206 | + /** | |
| 1207 | + * Decode one mail from UTF8. (decode only attributes defined in $fieldsToDecode) | |
| 1208 | + */ | |
| 1165 | 1209 | protected function utf8Decode($mail) |
| 1166 | 1210 | { |
| 1167 | 1211 | if (!empty($mail)) { |
| 1168 | 1212 | foreach (self::FIELDS_ENCODING as $oneField) { |
| @@ -1190,8 +1234,9 @@ | ||
| 1190 | 1234 | if (empty($mail->body)) { |
| 1191 | 1235 | return $mail; |
| 1192 | 1236 | } |
| 1193 | 1237 | |
| 1238 | + //If we don't display the powered by we remove it | |
| 1194 | 1239 | if ($this->config->get('display_built_by', 0) != 1) { |
| 1195 | 1240 | if (strpos($mail->body, 'acym__powered_by_acymailing') !== false) { |
| 1196 | 1241 | $mailBodyDom = new \DOMDocument(); |
| 1197 | 1242 | @$mailBodyDom->loadHTML('<?xml encoding="utf-8" ?>'.$mail->body); |
| @@ -1251,8 +1296,9 @@ | ||
| 1251 | 1296 | </tbody> |
| 1252 | 1297 | </table>'; |
| 1253 | 1298 | |
| 1254 | 1299 | $mailBodyDom = new \DOMDocument(); |
| 1300 | + //Some inserted content add specific tags that shows warnings | |
| 1255 | 1301 | @$mailBodyDom->loadHTML('<?xml encoding="utf-8" ?>'.$mail->body); |
| 1256 | 1302 | |
| 1257 | 1303 | $htmlToAddDom = new \DOMDocument(); |
| 1258 | 1304 | @$htmlToAddDom->loadHTML('<?xml encoding="utf-8" ?>'.$poweredByWYSID); |
| @@ -1283,8 +1329,15 @@ | ||
| 1283 | 1329 | |
| 1284 | 1330 | return $mail; |
| 1285 | 1331 | } |
| 1286 | 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 | + */ | |
| 1287 | 1340 | protected function utf8Encode($mail) |
| 1288 | 1341 | { |
| 1289 | 1342 | if (!empty($mail)) { |
| 1290 | 1343 | foreach (self::FIELDS_ENCODING as $oneField) { |
| @@ -1347,8 +1400,11 @@ | ||
| 1347 | 1400 | ) |
| 1348 | 1401 | ); |
| 1349 | 1402 | } |
| 1350 | 1403 | |
| 1404 | + /** | |
| 1405 | + * Get all multilingual mails linked to a parent mail, also get the parent mail | |
| 1406 | + */ | |
| 1351 | 1407 | public function getMultilingualMails(int $parentId): array |
| 1352 | 1408 | { |
| 1353 | 1409 | $mails = $this->decode( |
| 1354 | 1410 | acym_loadObjectList( |
| @@ -1497,28 +1553,75 @@ | ||
| 1497 | 1553 | ) |
| 1498 | 1554 | ); |
| 1499 | 1555 | } |
| 1500 | 1556 | |
| 1501 | - public function hasUserAccess(int $mailId): bool | |
| 1557 | + public function hasUserAccess(int $mailId, bool $write = false, ?int $userId = null): bool | |
| 1502 | 1558 | { |
| 1503 | - $userId = acym_currentUserId(); | |
| 1559 | + $userId = $userId ?? acym_currentUserId(); | |
| 1504 | 1560 | if (empty($userId)) { |
| 1505 | 1561 | return false; |
| 1506 | 1562 | } |
| 1507 | 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 | + | |
| 1508 | 1569 | if (acym_isAdmin()) { |
| 1570 | + return $this->isTypeAllowed($mail->type); | |
| 1571 | + } | |
| 1572 | + | |
| 1573 | + if ($mail->creator_id == $userId) { | |
| 1509 | 1574 | return true; |
| 1510 | 1575 | } |
| 1511 | 1576 | |
| 1512 | - return acym_loadResult( | |
| 1513 | - 'SELECT COUNT(*) | |
| 1514 | - FROM #__acym_mail | |
| 1515 | - WHERE id = '.intval($mailId).' | |
| 1516 | - AND (creator_id = '.intval($userId).' | |
| 1517 | - OR type = '.acym_escapeDB(self::TYPE_TEMPLATE).')' | |
| 1518 | - ) > 0; | |
| 1577 | + return !$write && $mail->type === self::TYPE_TEMPLATE && $this->isAccessAllowedForUser($mail->access, $userId); | |
| 1519 | 1578 | } |
| 1520 | 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 | + | |
| 1521 | 1624 | public function getVersionsById(int $mailId, bool $includeParent = false): array |
| 1522 | 1625 | { |
| 1523 | 1626 | $where = $includeParent ? ' OR `id` = '.intval($mailId) : ''; |
| 1524 | 1627 | |
| @@ -1531,8 +1634,11 @@ | ||
| 1531 | 1634 | ) |
| 1532 | 1635 | ); |
| 1533 | 1636 | } |
| 1534 | 1637 | |
| 1638 | + /** | |
| 1639 | + * Get all abtest mails linked to a parent mail, also get the parent mail | |
| 1640 | + */ | |
| 1535 | 1641 | public function getParentAndChildMails(int $parentId): array |
| 1536 | 1642 | { |
| 1537 | 1643 | return $this->decode( |
| 1538 | 1644 | acym_loadObjectList( |
| @@ -1596,7 +1702,7 @@ | ||
| 1596 | 1702 | } |
| 1597 | 1703 | |
| 1598 | 1704 | public function getMailType(int $mailId): string |
| 1599 | 1705 | { |
| 1600 | - return acym_loadResult('SELECT type FROM #__acym_mail WHERE id = '.intval($mailId)); | |
| 1706 | + return (string)acym_loadResult('SELECT type FROM #__acym_mail WHERE id = '.intval($mailId)); | |
| 1601 | 1707 | } |
| 1602 | 1708 | } |