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
← All changes | back/Classes/CampaignClass.php +80 -13 10.11.1 → 11.1.0 View file →
@@ -153,11 +153,12 @@
153 153 }
154 154
155 155 $query .= ' GROUP BY campaign.id';
156 156
157 - if (!empty($settings['ordering']) && !empty($settings['ordering_sort_order'])) {
157 + if (!empty($settings['ordering'])) {
158 158 $table = in_array($settings['ordering'], ['name', 'creation_date']) ? 'mail' : 'campaign';
159 - $query .= ' ORDER BY '.$table.'.'.acym_secureDBColumn($settings['ordering']).' '.acym_secureDBColumn(strtoupper($settings['ordering_sort_order']));
159 + $order = isset($settings['ordering_sort_order']) && strtolower($settings['ordering_sort_order']) === 'asc' ? 'ASC' : 'DESC';
160 + $query .= ' ORDER BY '.$table.'.'.acym_secureDBColumn($settings['ordering']).' '.acym_secureDBColumn($order);
160 161 }
161 162
162 163 if (empty($settings['offset']) || $settings['offset'] < 0) {
163 164 $settings['offset'] = 0;
@@ -254,12 +255,19 @@
254 255 $element->open = 0;
255 256 $element->click = 0;
256 257
257 258 if (!empty($element->subscribers)) {
258 - $element->open = number_format($element->open_unique / $element->subscribers * 100, 2);
259 + // In anonymous mode opens/clicks are counted globally (totals, not unique subscribers), so a percentage of recipients is meaningless: expose the average number of opens/clicks per email sent instead (e.g. 3.25), rendered as "3.25×" in the view
260 + $anonymousStats = !empty($this->config->get('anonymous_stats', 0));
261 + $clicksNb = $urlClickClass->getNumberUsersClicked([$element->mail_id], $isAbTest);
259 262
260 - $clicksNb = $urlClickClass->getNumberUsersClicked([$element->mail_id], $isAbTest);
261 - $element->click = number_format($clicksNb / $element->subscribers * 100, 2);
263 + if ($anonymousStats) {
264 + $element->open = number_format($element->open_unique / $element->subscribers, 2);
265 + $element->click = number_format($clicksNb / $element->subscribers, 2);
266 + } else {
267 + $element->open = number_format($element->open_unique / $element->subscribers * 100, 2);
268 + $element->click = number_format($clicksNb / $element->subscribers * 100, 2);
269 + }
262 270 }
263 271 }
264 272
265 273 public function getTrackingSales(object $element): void
@@ -304,12 +312,21 @@
304 312 $element->subscribers += $mailsStat->sent;
305 313 }
306 314
307 315 if (!empty($element->subscribers)) {
308 - $element->open = number_format($element->open / $element->subscribers * 100, 2);
309 - $element->click = number_format($element->click / $element->subscribers * 100, 2);
316 + // In anonymous mode opens/clicks are counted globally (totals, not unique subscribers), so expose the average number of opens/clicks per email sent (e.g. 3.25) instead of a percentage, rendered as "3.25×" in the view
317 + $anonymousStats = !empty($this->config->get('anonymous_stats', 0));
318 +
319 + if ($anonymousStats) {
320 + $element->open = number_format($element->open / $element->subscribers, 2);
321 + $element->click = number_format($element->click / $element->subscribers, 2);
322 + } else {
323 + $element->open = number_format($element->open / $element->subscribers * 100, 2);
324 + $element->click = number_format($element->click / $element->subscribers * 100, 2);
325 + }
310 326 }
311 327
328 + //Tracking sales
312 329 if (!acym_isTrackingSalesActive()) {
313 330 return;
314 331 }
315 332
@@ -334,8 +351,19 @@
334 351
335 352 return empty($campaign) ? null : $this->decode($campaign);
336 353 }
337 354
355 + public function getOneByMailId(int $mailId): ?object
356 + {
357 + $campaign = acym_loadObject(
358 + 'SELECT *
359 + FROM #__acym_campaign
360 + WHERE `mail_id` = '.intval($mailId)
361 + );
362 +
363 + return empty($campaign) ? null : $campaign;
364 + }
365 +
338 366 public function getOneByIdWithMail(int $id): ?object
339 367 {
340 368 $campaign = acym_loadObject(
341 369 'SELECT campaign.*, mail.name, mail.subject, mail.body, mail.from_name, mail.from_email, mail.reply_to_name, mail.reply_to_email, mail.bcc, mail.links_language, mail.tracking, mail.translation, mail.bounce_email
@@ -368,11 +396,22 @@
368 396
369 397 acym_arrayToInteger($listsIds);
370 398 if (empty($listsIds)) return false;
371 399
400 + $listsIds = array_unique($listsIds);
401 +
402 + $existingListsIds = acym_loadResultArray('SELECT `id` FROM #__acym_list WHERE `id` IN ('.implode(',', $listsIds).')');
403 + acym_arrayToInteger($existingListsIds);
404 +
405 + $unknownListsIds = array_diff($listsIds, $existingListsIds);
406 + if (!empty($unknownListsIds)) {
407 + $this->errors[] = acym_translationSprintf('ACYM_LIST_NOT_FOUND', implode(', ', $unknownListsIds));
408 + }
409 +
410 + if (empty($existingListsIds)) return false;
411 +
372 412 $values = [];
373 - $listsIds = array_unique($listsIds);
374 - foreach ($listsIds as $listId) {
413 + foreach ($existingListsIds as $listId) {
375 414 $values[] = '('.intval($mailId).', '.intval($listId).')';
376 415 }
377 416
378 417 if (!empty($values)) {
@@ -394,9 +433,9 @@
394 433 if (in_array($oneAttribute, $this->jsonColumns)) {
395 434 $campaign->$oneAttribute = json_encode(empty($value) ? [] : $value);
396 435 } else {
397 436 if (empty($value)) continue;
398 - $campaign->$oneAttribute = strip_tags($value);
437 + $campaign->$oneAttribute = acym_stripTags($value);
399 438 }
400 439 }
401 440
402 441 $campaignID = parent::save($campaign);
@@ -431,13 +470,13 @@
431 470 if (empty($userId)) {
432 471 return false;
433 472 }
434 473
435 - if (acym_isAdmin()) {
474 + if (acym_isAdmin() && acym_isAllowed('campaigns')) {
436 475 return true;
437 476 }
438 477
439 - $query = 'SELECT COUNT(*) FROM #__acym_campaign AS campaign
478 + $query = 'SELECT COUNT(*) FROM #__acym_campaign AS campaign
440 479 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
441 480
442 481 $condition = 'mail.creator_id = '.intval($userId);
443 482 if ($this->config->get('front_campaigns_filter', 'own') === 'allowed') {
@@ -453,8 +492,12 @@
453 492
454 493 return acym_loadResult($query) > 0;
455 494 }
456 495
496 + /**
497 + * Delete a campaign. Needs to delete the tag associated with the campaign and the mail attached to the campaign.
498 + * Deleting a mail of a campaign needs to clean the association table mail_has_list and its tags
499 + */
457 500 public function delete(array $elements): int
458 501 {
459 502 acym_arrayToInteger($elements);
460 503 $this->onlyManageableCampaigns($elements);
@@ -556,8 +599,9 @@
556 599 }
557 600
558 601 public function send(int $campaignID, int $result = 0, bool $abTestFinal = false)
559 602 {
603 + // Make sure the email we're trying to send exists
560 604 $campaign = $this->getOneById($campaignID);
561 605
562 606 if (empty($campaign->mail_id)) {
563 607 $this->errors[] = acym_translation('ACYM_EMAIL_NOT_FOUND');
@@ -571,17 +615,20 @@
571 615 if (empty($filters)) {
572 616 $filters = [0 => []];
573 617 acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[0], &$pluginIsExisting]);
574 618 } else {
619 + // Adds the special campaigns conditions to the "OR" blocs of the segment
575 620 foreach ($filters as $key => $filter) {
576 621 acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[$key], &$pluginIsExisting]);
577 622 }
578 623 }
579 624
625 + // This is a special campaign type, but the required plugin is not installed
580 626 if (!$pluginIsExisting) {
581 627 return false;
582 628 }
583 629
630 + // Make sure some receivers have been selected
584 631 $lists = acym_loadResultArray('SELECT list_id FROM #__acym_mail_has_list WHERE mail_id = '.intval($campaign->mail_id));
585 632 if (empty($lists)) {
586 633 $this->errors[] = acym_translation('ACYM_NO_LIST_SELECTED');
587 634
@@ -638,8 +685,9 @@
638 685 } else {
639 686 $select = [intval($campaign->mail_id), 'ul.`user_id`', acym_escapeDB($date)];
640 687 }
641 688
689 + // Resending a campaign only to users who didn't receive it
642 690 if (!empty($campaign->sending_params['resendTarget']) && 'new' === $campaign->sending_params['resendTarget']) {
643 691 if (acym_isMultilingual()) {
644 692 $automationHelper->leftjoin['us'] = '`#__acym_user_stat` AS `us` ON `us`.`user_id` = `user`.`id` AND `us`.`mail_id` IN (SELECT id FROM #__acym_mail WHERE parent_id = '.intval(
645 693 $campaign->mail_id
@@ -661,10 +709,11 @@
661 709 $numberUsersInsertedByMailId[intval($campaign->mail_id)] = acym_query(
662 710 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select1)
663 711 );
664 712
713 + // If we have an odd number of users to send, we send one more to the first mail
665 714 $numberOfUsersToSend2 = $numberOfUsersToSend1 * 2 > $numberOfUsersToSend ? $numberOfUsersToSend1 - 1 : $numberOfUsersToSend1;
666 - $automationHelper->limit = $numberOfUsersToSend.', '.$numberOfUsersToSend2;
715 + $automationHelper->limit = $numberOfUsersToSend1.', '.$numberOfUsersToSend2;
667 716 $select2 = [intval($campaign->sending_params['abtest']['B']), 'ul.`user_id`', acym_escapeDB($date)];
668 717 $numberUsersInsertedByMailId[intval($campaign->sending_params['abtest']['B'])] = acym_query(
669 718 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select2)
670 719 );
@@ -736,8 +785,9 @@
736 785 public function getOpenRateCampaigns(array $mailIds)
737 786 {
738 787 acym_arrayToInteger($mailIds);
739 788
789 + //TODO move these methods in mail stat class
740 790 $query = 'SELECT SUM(sent) AS sent, SUM(open_unique) AS open_unique FROM #__acym_mail_stat
741 791 WHERE mail_id IN ('.implode(',', $mailIds).')';
742 792
743 793 return acym_loadObject($query);
@@ -824,30 +874,37 @@
824 874 }
825 875
826 876 public function getLastNewsletters(array &$params): array
827 877 {
878 + // Init select elements
828 879 $querySelect = 'SELECT mail.*, campaign.sending_date ';
829 880 $queryCountSelect = 'SELECT COUNT(*) FROM (SELECT DISTINCT mail.id ';
830 881
882 + // Form the query
831 883 $query = 'FROM #__acym_campaign AS campaign
832 884 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
833 885
886 + // We may need some joins depending on the selected options
834 887 if (isset($params['userId']) || isset($params['lists'])) {
835 888 $query .= 'JOIN #__acym_mail_has_list AS maillist ON mail.id = maillist.mail_id ';
836 889 if (isset($params['userId'])) $query .= 'JOIN #__acym_user_has_list AS userlist ON maillist.list_id = userlist.list_id ';
837 890 }
838 891
892 + // Make sure we display only active campaigns
839 893 $where = 'WHERE campaign.active = 1 AND campaign.sent = 1 AND mail.type = '.acym_escapeDB(MailClass::TYPE_STANDARD).' AND campaign.visible = 1 ';
840 894
895 + // If we want an archive of some specific lists
841 896 if (isset($params['lists'])) {
842 897 acym_arrayToInteger($params['lists']);
843 898 $where .= 'AND maillist.list_id IN ('.implode(', ', $params['lists']).') ';
844 899 }
845 900
901 + // If we want an archive for a specific user
846 902 if (isset($params['userId']) && !empty($params['displayUserListOnly'])) {
847 903 $where .= 'AND userlist.user_id = '.intval($params['userId']).' ';
848 904 }
849 905
906 + // If the user search for a newsletter
850 907 if (isset($params['search'])) {
851 908 $search = acym_escapeDB('%'.acym_utf8Encode($params['search']).'%');
852 909 $where .= 'AND (mail.subject LIKE '.$search.' OR mail.body LIKE '.$search.')';
853 910 }
@@ -855,11 +912,13 @@
855 912 $query .= $where;
856 913 $return = [];
857 914 $return['count'] = (int)acym_loadResult($queryCountSelect.$query.') AS r ');
858 915
916 + // Make sure we display campaigns only once
859 917 $endQuerySelect = 'GROUP BY mail.id ';
860 918 $endQuerySelect .= 'ORDER BY campaign.sending_date DESC';
861 919
920 + // Init the pagination
862 921 $page = $params['page'] ?? 0;
863 922 $numberPerPage = $params['numberPerPage'] ?? 0;
864 923 $lastNewsletters = $params['limit'] ?? 0;
865 924
@@ -944,8 +1003,9 @@
944 1003 $adminNotificationEmail = $mailClass->getOneByName('acy_notification_auto_campaign_admin');
945 1004 $time = time();
946 1005
947 1006 foreach ($activeAutoCampaigns as $campaign) {
1007 + // Check the start date
948 1008 $nextTrigger = $campaign->next_trigger;
949 1009 if (empty($nextTrigger) && !empty($campaign->sending_params['start_date'])) {
950 1010 $nextTrigger = $campaign->sending_params['start_date'];
951 1011 }
@@ -953,8 +1013,9 @@
953 1013 if (!empty($nextTrigger) && acym_getTime(acym_date($nextTrigger, 'Y-m-d H:i')) > $time) {
954 1014 continue;
955 1015 }
956 1016
1017 + //check if we trigger the campaign
957 1018 $step = new \stdClass();
958 1019 $step->triggers = $campaign->sending_params;
959 1020 $step->last_execution = $campaign->last_generated;
960 1021 $step->next_execution = $campaign->next_trigger;
@@ -967,8 +1028,9 @@
967 1028 $this->save($campaign);
968 1029 continue;
969 1030 }
970 1031
1032 + //update the campaign
971 1033 $campaignMail = $mailClass->getOneById($campaign->mail_id);
972 1034
973 1035 $lastGenerated = $campaign->last_generated;
974 1036 $shouldGenerate = $this->updateAutoCampaign($campaign, $campaignMail, $time);
@@ -977,8 +1039,9 @@
977 1039 if (!$shouldGenerate) {
978 1040 continue;
979 1041 }
980 1042
1043 + //We generate the new campaign
981 1044 $generatedCampaign = $this->generateCampaign($campaign, $campaignMail, $lastGenerated, $mailClass);
982 1045 if (empty($generatedCampaign)) {
983 1046 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_FAILED_GENERATING', $campaign->name);
984 1047 continue;
@@ -985,8 +1048,9 @@
985 1048 }
986 1049
987 1050 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_GENERATED', $campaign->name, $campaign->sending_params['number_generated']);
988 1051
1052 + // We send it directly if no confirmation is needed
989 1053 if (empty($campaign->sending_params['need_confirm_to_send'])) {
990 1054 $this->send($generatedCampaign->id);
991 1055 } elseif (!empty($campaign->sending_params['admin_notification_emails']) && !empty($adminNotificationEmail)) {
992 1056 $mailerHelper->addParam('campaign_name', $campaign->name);
@@ -1001,12 +1065,14 @@
1001 1065 }
1002 1066
1003 1067 private function shouldGenerateCampaign(object $campaign, object $campaignMail): bool
1004 1068 {
1069 + // The generateByCategory function is the only one that can stop a campaign generation, with min number of items
1005 1070 $results = acym_trigger('generateByCategory', [&$campaignMail], null, function ($plugin) {
1006 1071 $plugin->generateCampaignResult->status = true;
1007 1072 });
1008 1073
1074 + // If one of the return statuses is "false", we won't generate the campaign
1009 1075 foreach ($results as $oneResult) {
1010 1076 if (isset($oneResult->status) && !$oneResult->status) {
1011 1077 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_NOT_GENERATED', $campaign->name, $oneResult->message);
1012 1078
@@ -1052,8 +1118,9 @@
1052 1118 $newCampaign->sending_params = empty($campaign->sending_params['segment']) ? '' : ['segment' => $campaign->sending_params['segment']];
1053 1119
1054 1120 $newCampaign->id = $this->save($newCampaign);
1055 1121
1122 + // Replace content in the generated mail. MUST be done after campaign has been saved
1056 1123 acym_trigger('replaceContent', [&$newMail, false]);
1057 1124 $mailClass->save($newMail);
1058 1125
1059 1126 return $newCampaign;