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 / Classes / CampaignClass.php

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

1,361 lines 53.8 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\Controllers\CampaignsController;
6 use AcyMailing\Controllers\SegmentsController;
7 use AcyMailing\Helpers\AutomationHelper;
8 use AcyMailing\Helpers\MailerHelper;
9 use AcyMailing\Helpers\PaginationHelper;
10 use AcyMailing\Core\AcymClass;
11
12 class CampaignClass extends AcymClass
13 {
14 const SENDING_TYPE_NOW = 'now';
15 const SENDING_TYPE_SCHEDULED = 'scheduled';
16 const SENDING_TYPE_AUTO = 'auto';
17 const SENDING_TYPES = [
18 self::SENDING_TYPE_NOW,
19 self::SENDING_TYPE_SCHEDULED,
20 self::SENDING_TYPE_AUTO,
21 ];
22 const AB_TEST_VERSIONS = ['B'];
23
24 public function __construct()
25 {
26 parent::__construct();
27
28 $this->table = 'campaign';
29 $this->pkey = 'id';
30 $this->jsonColumns = ['sending_params'];
31 $this->intColumns = [
32 'id',
33 'mail_id',
34 'parent_id',
35 'draft',
36 'active',
37 'sent',
38 'visible',
39 'subscribers',
40 'open_unique',
41 'tracking_sale',
42 'tracking',
43 ];
44 }
45
46 public function decode($campaign, bool $decodeMail = true)
47 {
48 if (empty($campaign)) return $campaign;
49
50 if (is_array($campaign)) {
51 foreach ($campaign as $i => $oneCampaign) {
52 $campaign[$i] = $this->decode($oneCampaign, false);
53 }
54 } else {
55 $this->fixTypes($campaign);
56 }
57
58 if ($decodeMail) {
59 $mailClass = new MailClass();
60 $campaign = $mailClass->decode($campaign);
61 }
62
63 return $campaign;
64 }
65
66 public function getAll(?string $key = null): array
67 {
68 $allCampaigns = parent::getAll($key);
69
70 return $this->decode($allCampaigns);
71 }
72
73 public function getMatchingElements(array $settings = []): array
74 {
75 $tagClass = new TagClass();
76 $mailClass = new MailClass();
77
78 $query = 'SELECT campaign.*, mail.name, SUM(mail_stat.sent) AS subscribers, mail_stat.open_unique, mail_stat.tracking_sale, mail_stat.currency FROM #__acym_campaign AS campaign';
79 $queryCount = 'FROM #__acym_campaign AS campaign';
80
81 $filters = [];
82 $task = acym_getVar('string', 'task');
83 if ($task === 'campaigns') {
84 $filters[] = 'mail.type = '.acym_escapeDB(MailClass::TYPE_STANDARD);
85 } elseif ($task === 'mailbox_action') {
86 $filters[] = 'mail.type = '.acym_escapeDB(MailClass::TYPE_MAILBOX_ACTION);
87 }
88 $mailIds = [];
89
90 $query .= ' JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id';
91 $queryCount .= ' JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id';
92 $query .= ' LEFT JOIN #__acym_mail_stat AS mail_stat ON campaign.mail_id = mail_stat.mail_id';
93
94 if (!acym_isAdmin()) {
95 $condition = 'mail.creator_id = '.intval(acym_currentUserId());
96 if ($this->config->get('front_campaigns_filter', 'own') === 'allowed') {
97 $listClass = new ListClass();
98 $allowedLists = $listClass->getManageableLists();
99 if (!empty($allowedLists)) {
100 acym_arrayToInteger($allowedLists);
101 $query .= ' LEFT JOIN #__acym_mail_has_list AS mailList ON campaign.mail_id = mailList.mail_id AND mailList.list_id IN ('.implode(',', $allowedLists).')';
102 $queryCount .= ' LEFT JOIN #__acym_mail_has_list AS mailList ON campaign.mail_id = mailList.mail_id AND mailList.list_id IN ('.implode(',', $allowedLists).')';
103 $condition .= ' OR mailList.list_id IS NOT NULL';
104 }
105 }
106 $filters[] = $condition;
107 $filters[] = 'campaign.sending_params NOT LIKE "%abtest%"';
108 }
109
110 if (!empty($settings['tag'])) {
111 $tagJoin = ' JOIN #__acym_tag AS tag ON campaign.mail_id = tag.id_element';
112 $query .= $tagJoin;
113 $queryCount .= $tagJoin;
114 $filters[] = 'tag.name = '.acym_escapeDB($settings['tag']);
115 $filters[] = 'tag.type = '.acym_escapeDB(TagClass::TYPE_MAIL);
116 }
117
118 if (!empty($settings['search'])) {
119 $filters[] = 'mail.name LIKE '.acym_escapeDB('%'.acym_utf8Encode($settings['search']).'%');
120 }
121
122 if ($settings['status'] !== 'generated') {
123 $operator = $settings['element_tab'] === 'campaigns_auto' ? '=' : '!=';
124 if ($settings['element_tab'] === 'campaigns_auto') {
125 $filters[] = 'campaign.sending_type '.$operator.' '.acym_escapeDB(self::SENDING_TYPE_AUTO);
126 } elseif ($settings['element_tab'] === 'campaigns') {
127 $filters[] = 'campaign.sending_type IN ('.acym_escapeDB(self::SENDING_TYPE_NOW).', '.acym_escapeDB(self::SENDING_TYPE_SCHEDULED).')';
128 } elseif (!empty($settings['element_tab'])) {
129 acym_trigger('onAcymCampaignAddFiltersSpecificListing', [&$filters, $settings['element_tab']]);
130 }
131 }
132
133 if (!empty($filters)) {
134 $query .= ' WHERE ('.implode(') AND (', $filters).')';
135 $queryCount .= ' WHERE ('.implode(') AND (', $filters).')';
136 }
137
138 $statusRequests = [
139 'all' => '(campaign.parent_id IS NULL OR campaign.parent_id = 0)',
140 'scheduled' => 'campaign.sending_type = '.acym_escapeDB(self::SENDING_TYPE_SCHEDULED).' AND (campaign.parent_id IS NULL OR campaign.parent_id = 0)',
141 'sent' => 'campaign.sent = 1 AND (campaign.parent_id IS NULL OR campaign.parent_id = 0)',
142 'draft' => 'campaign.draft = 1 AND (campaign.parent_id IS NULL OR campaign.parent_id = 0)',
143 ];
144
145 if ($settings['element_tab'] == 'campaigns_auto') {
146 $statusRequests['generated'] = 'campaign.sending_type = '.acym_escapeDB(self::SENDING_TYPE_NOW).' AND campaign.parent_id > 0';
147 }
148
149 if (empty($settings['status'])) $settings['status'] = 'all';
150 if (!empty($statusRequests[$settings['status']])) {
151 $query .= empty($filters) ? ' WHERE ' : ' AND ';
152 $query .= $statusRequests[$settings['status']];
153 }
154
155 $query .= ' GROUP BY campaign.id';
156
157 if (!empty($settings['ordering'])) {
158 $table = in_array($settings['ordering'], ['name', 'creation_date']) ? 'mail' : 'campaign';
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);
161 }
162
163 if (empty($settings['offset']) || $settings['offset'] < 0) {
164 $settings['offset'] = 0;
165 }
166
167 if (empty($settings['elementsPerPage']) || $settings['elementsPerPage'] < 1) {
168 $pagination = new PaginationHelper();
169 $settings['elementsPerPage'] = $pagination->getListLimit();
170 }
171
172 $results['elements'] = $this->decode(acym_loadObjectList($query, '', $settings['offset'], $settings['elementsPerPage']));
173
174 foreach ($results['elements'] as $oneCampaign) {
175 $mailIds[] = $oneCampaign->mail_id;
176 $oneCampaign->tags = '';
177 }
178
179 $tags = $tagClass->getAllTagsByTypeAndElementIds(TagClass::TYPE_MAIL, $mailIds);
180 $lists = $mailClass->getAllListsByMailIds($mailIds);
181
182 $isMultilingual = acym_isMultilingual();
183
184 $urlClickClass = new UrlClickClass();
185 foreach ($results['elements'] as $i => $oneCampaign) {
186 $results['elements'][$i]->tags = [];
187 $results['elements'][$i]->lists = [];
188 $results['elements'][$i]->automation_id = null;
189
190 foreach ($tags as $tag) {
191 if ($oneCampaign->id == $tag->id_element) {
192 $results['elements'][$i]->tags[] = $tag;
193 }
194 }
195
196 foreach ($lists as $list) {
197 if ($oneCampaign->mail_id == $list->mail_id) {
198 array_push($results['elements'][$i]->lists, $list);
199 }
200 }
201
202 if ($settings['element_tab'] === 'campaigns_auto' && $settings['status'] !== 'generated') {
203 $this->getStatsCampaignAuto($results['elements'][$i], $urlClickClass);
204 } else {
205 $isAbTest = isset($results['elements'][$i]->sending_params['abtest']);
206 if ($isMultilingual || $isAbTest) {
207 $this->prepareStatsCampaign($results['elements'][$i]);
208 }
209 $this->getStatsCampaign($results['elements'][$i], $urlClickClass, $isAbTest);
210 }
211 }
212
213 if (!empty($settings['advanced_total'])) {
214 if ($settings['campaign_type'] === 'campaigns') {
215 $queryCount .= (empty($filters) ? ' WHERE ' : ' AND ').'campaign.parent_id IS NULL';
216
217 $results['total'] = acym_loadObject('SELECT COUNT(campaign.id) AS total, SUM(campaign.sent) AS sent, SUM(campaign.draft) AS draft '.$queryCount);
218 $results['total']->all = $results['total']->total;
219 $results['total']->scheduled = acym_loadResult(
220 'SELECT COUNT(campaign.id) '.$queryCount.' AND campaign.`sending_type` = '.acym_escapeDB(self::SENDING_TYPE_SCHEDULED)
221 );
222 } else {
223 $results['total'] = acym_loadObject('SELECT COUNT(campaign.id) AS total '.$queryCount);
224 $results['total']->all = 0;
225 if (!empty($results['total']->total)) {
226 $results['total']->all = $results['total']->total;
227 }
228
229 $results['total']->generated = $this->getNbGeneratedCampaigns();
230 }
231
232 if (CampaignsController::TASK_TYPE_CAMPAIGN_AUTO === $settings['campaign_type'] && 'generated' === $settings['status']) {
233 $results['total']->all = $this->getCountCampaignType(self::SENDING_TYPE_AUTO);
234 }
235 } else {
236 $results['total'] = acym_loadObject('SELECT COUNT(campaign.id) AS total '.$queryCount);
237 }
238
239 return $results;
240 }
241
242 private function prepareStatsCampaign(object $element): void
243 {
244 $query = 'SELECT SUM(mailstat.sent) AS subscribers, SUM(mailstat.open_unique) AS open_unique, SUM(mailstat.tracking_sale) AS tracking_sale
245 FROM #__acym_mail_stat AS mailstat
246 LEFT JOIN #__acym_mail AS mail ON mail.id = mailstat.mail_id WHERE mail.parent_id = '.intval($element->mail_id);
247 $stats = acym_loadObject($query);
248 $element->subscribers += $stats->subscribers;
249 $element->open_unique += $stats->open_unique;
250 $element->tracking_sale += $stats->tracking_sale;
251 }
252
253 public function getStatsCampaign(object $element, UrlClickClass $urlClickClass, bool $isAbTest = false): void
254 {
255 $element->open = 0;
256 $element->click = 0;
257
258 if (!empty($element->subscribers)) {
259 $element->open = number_format($element->open_unique / $element->subscribers * 100, 2);
260
261 $clicksNb = $urlClickClass->getNumberUsersClicked([$element->mail_id], $isAbTest);
262 $element->click = number_format($clicksNb / $element->subscribers * 100, 2);
263 }
264 }
265
266 public function getTrackingSales(object $element): void
267 {
268 $element->sale = 0;
269 $element->currency = '';
270
271 if (acym_isMultilingual()) {
272 $trackingSales = acym_loadObject(
273 'SELECT SUM(tracking_sale) as sale, currency FROM #__acym_user_stat WHERE mail_id IN (SELECT id FROM #__acym_mail WHERE id = '.intval(
274 $element->mail_id
275 ).' OR parent_id = '.intval($element->mail_id).') AND currency IS NOT NULL'
276 );
277 } else {
278 $trackingSales = acym_loadObject(
279 'SELECT SUM(tracking_sale) as sale, currency FROM #__acym_user_stat WHERE mail_id = '.intval($element->mail_id).' AND currency IS NOT NULL'
280 );
281 }
282 $this->formatSaleTracking($element, $trackingSales);
283 }
284
285 private function getStatsCampaignAuto(object $element, UrlClickClass $urlClickClass): void
286 {
287 $generatedMailsStats = acym_loadObjectList(
288 'SELECT mail_stat.*
289 FROM #__acym_mail AS mail
290 JOIN #__acym_mail_stat AS mail_stat
291 ON mail.id = mail_stat.mail_id
292 WHERE mail.id IN (SELECT mail_id FROM #__acym_campaign WHERE parent_id = '.intval($element->id).')'
293 );
294
295 $element->open = 0;
296 $element->click = 0;
297 $element->subscribers = 0;
298 if (empty($generatedMailsStats)) {
299 return;
300 }
301
302 foreach ($generatedMailsStats as $key => $mailsStat) {
303 $element->open += $mailsStat->open_unique;
304 $element->click += $urlClickClass->getNumberUsersClicked([$element->id]);
305 $element->subscribers += $mailsStat->sent;
306 }
307
308 if (!empty($element->subscribers)) {
309 $element->open = number_format($element->open / $element->subscribers * 100, 2);
310 $element->click = number_format($element->click / $element->subscribers * 100, 2);
311 }
312
313 //Tracking sales
314 if (!acym_isTrackingSalesActive()) {
315 return;
316 }
317
318 $trackingSales = acym_loadObject(
319 'SELECT SUM(mail_stat.tracking_sale) AS sale, mail_stat.currency
320 FROM #__acym_mail_stat AS mail_stat
321 WHERE mail_id IN (SELECT mail_id FROM #__acym_campaign WHERE parent_id = '.intval($element->id).') AND currency IS NOT NULL'
322 );
323 $this->formatSaleTracking($element, $trackingSales);
324 }
325
326 private function formatSaleTracking(object $element, object $trackingSales): void
327 {
328 $element->sale = $trackingSales->sale;
329 if (empty($element->currency)) $element->currency = '';
330 acym_trigger('getCurrency', [&$element->currency]);
331 }
332
333 public function getOneById(int $id): ?object
334 {
335 $campaign = parent::getOneById($id);
336
337 return empty($campaign) ? null : $this->decode($campaign);
338 }
339
340 public function getOneByMailId(int $mailId): ?object
341 {
342 $campaign = acym_loadObject(
343 'SELECT *
344 FROM #__acym_campaign
345 WHERE `mail_id` = '.intval($mailId)
346 );
347
348 return empty($campaign) ? null : $campaign;
349 }
350
351 public function getOneByIdWithMail(int $id): ?object
352 {
353 $campaign = acym_loadObject(
354 '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
355 FROM #__acym_campaign AS campaign
356 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id
357 WHERE campaign.id = '.intval($id)
358 );
359
360 return empty($campaign) ? null : $this->decode($campaign);
361 }
362
363 public function getOneCampaignByMailId(int $mailId)
364 {
365 return $this->decode(acym_loadObject('SELECT * FROM #__acym_campaign WHERE mail_id = '.intval($mailId)));
366 }
367
368 public function getAutoCampaignFromGeneratedMailId(int $mailId)
369 {
370 $queryCampaign = 'SELECT * FROM #__acym_campaign WHERE id = (SELECT parent_id FROM #__acym_campaign WHERE mail_id = '.intval($mailId).')';
371
372 return $this->decode(acym_loadObject($queryCampaign));
373 }
374
375 public function manageListsToCampaign($listsIds, int $mailId, array $unselectedListIds = []): bool
376 {
377 if (!empty($unselectedListIds)) {
378 acym_arrayToInteger($unselectedListIds);
379 acym_query('DELETE FROM #__acym_mail_has_list WHERE mail_id = '.intval($mailId).' AND list_id IN ('.implode(', ', $unselectedListIds).')');
380 }
381
382 acym_arrayToInteger($listsIds);
383 if (empty($listsIds)) return false;
384
385 $values = [];
386 $listsIds = array_unique($listsIds);
387 foreach ($listsIds as $listId) {
388 $values[] = '('.intval($mailId).', '.intval($listId).')';
389 }
390
391 if (!empty($values)) {
392 acym_query('INSERT IGNORE INTO #__acym_mail_has_list (`mail_id`, `list_id`) VALUES '.implode(',', $values));
393 }
394
395 return true;
396 }
397
398 public function save(object $element): ?int
399 {
400 $campaign = clone $element;
401 if (isset($campaign->tags)) {
402 $tags = $campaign->tags;
403 unset($campaign->tags);
404 }
405
406 foreach ($campaign as $oneAttribute => $value) {
407 if (in_array($oneAttribute, $this->jsonColumns)) {
408 $campaign->$oneAttribute = json_encode(empty($value) ? [] : $value);
409 } else {
410 if (empty($value)) continue;
411 $campaign->$oneAttribute = acym_stripTags($value);
412 }
413 }
414
415 $campaignID = parent::save($campaign);
416
417 if (!empty($campaignID) && isset($tags)) {
418 $tagClass = new TagClass();
419 $tagClass->setTags(TagClass::TYPE_MAIL, intval($campaign->mail_id), $tags);
420 }
421
422 return $campaignID;
423 }
424
425 public function onlyManageableCampaigns(array &$elements): void
426 {
427 if (acym_isAdmin()) return;
428
429 $idCurrentUser = acym_currentUserId();
430 if (empty($idCurrentUser)) return;
431
432 $manageable = acym_loadResultArray(
433 'SELECT campaign.id
434 FROM #__acym_campaign AS campaign
435 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id
436 WHERE mail.creator_id = '.intval($idCurrentUser)
437 );
438 $elements = array_intersect($elements, $manageable);
439 }
440
441 public function hasUserAccess(int $campaignId): bool
442 {
443 $userId = acym_currentUserId();
444 if (empty($userId)) {
445 return false;
446 }
447
448 if (acym_isAdmin() && acym_isAllowed('campaigns')) {
449 return true;
450 }
451
452 $query = 'SELECT COUNT(*) FROM #__acym_campaign AS campaign
453 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
454
455 $condition = 'mail.creator_id = '.intval($userId);
456 if ($this->config->get('front_campaigns_filter', 'own') === 'allowed') {
457 $listClass = new ListClass();
458 $allowedLists = $listClass->getManageableLists();
459 if (!empty($allowedLists)) {
460 $query .= ' LEFT JOIN #__acym_mail_has_list AS mailList ON campaign.mail_id = mailList.mail_id AND mailList.list_id IN ('.implode(',', $allowedLists).')';
461 $condition .= ' OR mailList.list_id IS NOT NULL';
462 }
463 }
464
465 $query .= 'WHERE campaign.id = '.intval($campaignId).' AND ('.$condition.')';
466
467 return acym_loadResult($query) > 0;
468 }
469
470 /**
471 * Delete a campaign. Needs to delete the tag associated with the campaign and the mail attached to the campaign.
472 * Deleting a mail of a campaign needs to clean the association table mail_has_list and its tags
473 */
474 public function delete(array $elements): int
475 {
476 acym_arrayToInteger($elements);
477 $this->onlyManageableCampaigns($elements);
478
479 if (empty($elements)) return 0;
480
481 $mailsToDelete = [];
482 foreach ($elements as $id) {
483 $mailsToDelete[] = acym_loadResult('SELECT mail_id FROM #__acym_campaign WHERE id = '.intval($id));
484 acym_query('UPDATE #__acym_campaign SET mail_id = NULL WHERE id = '.intval($id));
485 }
486
487 $mailClass = new MailClass();
488 $mailClass->delete($mailsToDelete);
489
490 return parent::delete($elements);
491 }
492
493 public function countUsersCampaign(int $campaignID, bool $onlyActive = false): int
494 {
495 $campaign = $this->getOneById($campaignID);
496 if (empty($campaign)) {
497 return 0;
498 }
499
500 $mailClass = new MailClass();
501 $lists = $mailClass->getAllListsByMailId($campaign->mail_id);
502 $listsIds = array_column($lists, 'id');
503
504 if (empty($listsIds)) {
505 return 0;
506 }
507
508 $automationHelperBase = new AutomationHelper();
509 $automationHelperBase->removeFlag(SegmentsController::FLAG_COUNT);
510
511 $exclude = !empty($campaign->sending_params['segment']['invert']) && $campaign->sending_params['segment']['invert'] === 'exclude';
512
513 $automationHelpers = [];
514 $filters = $this->getFilterCampaign($campaign->sending_params);
515 foreach ($filters as $or => $orValues) {
516 if (empty($orValues)) continue;
517
518 $automationHelpers[$or] = new AutomationHelper();
519 foreach ($orValues as $and => $andValues) {
520 $and = intval($and);
521 foreach ($andValues as $filterName => $options) {
522 acym_trigger('onAcymProcessFilter_'.$filterName, [&$automationHelpers[$or], &$options, &$and]);
523 }
524 }
525
526 $automationHelpers[$or]->excludeSelected = $exclude;
527 $automationHelpers[$or]->addFlag(SegmentsController::FLAG_COUNT);
528 }
529
530 $join = ' #__acym_user_has_list AS user_list ON user_list.user_id = user.id ';
531 $join .= 'AND user_list.list_id IN ('.implode(',', $listsIds).') ';
532 $join .= 'AND user_list.status = 1 ';
533 if ($this->config->get('require_confirmation', 1) == 1) {
534 $join .= 'AND user.confirmed = 1 ';
535 }
536 if ($onlyActive) {
537 $join .= 'AND user.active = 1 ';
538 }
539
540 if (empty($automationHelpers)) {
541 $automationHelperBase->join['user_list'] = $join;
542
543 return acym_loadResult($automationHelperBase->getQuery(['COUNT(DISTINCT user.id)']));
544 } else {
545 $automationHelperBase = array_pop($automationHelpers);
546 $automationHelperBase->join['user_list'] = $join;
547 $numberOfRecipients = acym_loadResult($automationHelperBase->getQuery(['COUNT(DISTINCT user.id)']));
548 $automationHelperBase->removeFlag(SegmentsController::FLAG_COUNT);
549
550 return $numberOfRecipients;
551 }
552 }
553
554 public function getFilterCampaign($sendingParams): array
555 {
556 if (empty($sendingParams['segment'])) {
557 return [];
558 }
559
560 if (!empty($sendingParams['segment']['filters'])) {
561 return $sendingParams['segment']['filters'];
562 }
563
564 if (!empty($sendingParams['segment']['segment_id'])) {
565 $segmentClass = new SegmentClass();
566 $segment = $segmentClass->getOneById($sendingParams['segment']['segment_id']);
567 if (!empty($segment)) {
568 return $segment->filters;
569 }
570 }
571
572 return [];
573 }
574
575 public function send(int $campaignID, int $result = 0, bool $abTestFinal = false)
576 {
577 // Make sure the email we're trying to send exists
578 $campaign = $this->getOneById($campaignID);
579
580 if (empty($campaign->mail_id)) {
581 $this->errors[] = acym_translation('ACYM_EMAIL_NOT_FOUND');
582
583 return false;
584 }
585
586 $filters = $this->getFilterCampaign($campaign->sending_params);
587 $pluginIsExisting = true;
588
589 if (empty($filters)) {
590 $filters = [0 => []];
591 acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[0], &$pluginIsExisting]);
592 } else {
593 // Adds the special campaigns conditions to the "OR" blocs of the segment
594 foreach ($filters as $key => $filter) {
595 acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[$key], &$pluginIsExisting]);
596 }
597 }
598
599 // This is a special campaign type, but the required plugin is not installed
600 if (!$pluginIsExisting) {
601 return false;
602 }
603
604 // Make sure some receivers have been selected
605 $lists = acym_loadResultArray('SELECT list_id FROM #__acym_mail_has_list WHERE mail_id = '.intval($campaign->mail_id));
606 if (empty($lists)) {
607 $this->errors[] = acym_translation('ACYM_NO_LIST_SELECTED');
608
609 return false;
610 }
611 acym_arrayToInteger($lists);
612
613 $date = acym_date('now', 'Y-m-d H:i:s', false);
614 $numberUsersInsertedByMailId = [];
615
616 if (empty($result)) {
617 $conditions = [
618 '`user`.`active` = 1',
619 '`ul`.`status` = 1',
620 '`ul`.`list_id` IN ('.implode(',', $lists).')',
621 ];
622 if ($this->config->get('require_confirmation', 1) == 1) {
623 $conditions[] = '`user`.`confirmed` = 1';
624 }
625
626 $automationHelper = new AutomationHelper();
627 $automationHelper->removeFlag(SegmentsController::FLAG_COUNT);
628 $automationHelper->join['ul'] = ' #__acym_user_has_list AS ul ON ul.user_id = user.id ';
629
630 if (!empty($filters)) {
631 $automationHelpers = [];
632 foreach ($filters as $or => $orValues) {
633 if (empty($orValues)) continue;
634
635 $automationHelpers[$or] = new AutomationHelper();
636 foreach ($orValues as $and => $andValues) {
637 $and = intval($and);
638 foreach ($andValues as $filterName => $options) {
639 acym_trigger('onAcymProcessFilter_'.$filterName, [&$automationHelpers[$or], &$options, &$and]);
640 }
641 }
642
643 $automationHelpers[$or]->addFlag(SegmentsController::FLAG_COUNT);
644 }
645
646 if (!empty($automationHelpers)) {
647 $excludingSegment = !empty($campaign->sending_params['segment']['invert']) && $campaign->sending_params['segment']['invert'] === 'exclude';
648 if ($excludingSegment) {
649 $conditions[] = 'user.automation NOT LIKE "%a'.SegmentsController::FLAG_COUNT.'a%"';
650 } else {
651 $conditions[] = 'user.automation LIKE "%a'.SegmentsController::FLAG_COUNT.'a%"';
652 }
653 }
654 }
655
656 if (acym_isMultilingual()) {
657 $select = ['IF(mail.id IS NULL, '.intval($campaign->mail_id).', `mail`.`id`)', 'ul.`user_id`', acym_escapeDB($date)];
658 $automationHelper->leftjoin['mail'] = '`#__acym_mail` AS mail ON `mail`.`language` = `user`.language AND `mail`.`parent_id` = '.intval($campaign->mail_id);
659 } else {
660 $select = [intval($campaign->mail_id), 'ul.`user_id`', acym_escapeDB($date)];
661 }
662
663 // Resending a campaign only to users who didn't receive it
664 if (!empty($campaign->sending_params['resendTarget']) && 'new' === $campaign->sending_params['resendTarget']) {
665 if (acym_isMultilingual()) {
666 $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(
667 $campaign->mail_id
668 ).' OR id = '.intval($campaign->mail_id).')';
669 } else {
670 $automationHelper->leftjoin['us'] = '`#__acym_user_stat` AS `us` ON `us`.`user_id` = `user`.`id` AND `us`.`mail_id` = '.intval($campaign->mail_id);
671 }
672 $conditions[] = '`us`.`user_id` IS NULL';
673 }
674
675 $automationHelper->where = $conditions;
676 if (!empty($campaign->sending_params['abtest']['repartition']) && !$abTestFinal) {
677 $totalNumberOfUsers = acym_loadResult($automationHelper->getQuery(['count(user.id)']));
678 $numberOfUsersToSend = round($campaign->sending_params['abtest']['repartition'] * $totalNumberOfUsers / 100);
679
680 $numberOfUsersToSend1 = round($numberOfUsersToSend / 2);
681 $automationHelper->limit = '0, '.$numberOfUsersToSend1;
682 $select1 = [intval($campaign->mail_id), 'ul.`user_id`', acym_escapeDB($date)];
683 $numberUsersInsertedByMailId[intval($campaign->mail_id)] = acym_query(
684 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select1)
685 );
686
687 // If we have an odd number of users to send, we send one more to the first mail
688 $numberOfUsersToSend2 = $numberOfUsersToSend1 * 2 > $numberOfUsersToSend ? $numberOfUsersToSend1 - 1 : $numberOfUsersToSend1;
689 $automationHelper->limit = $numberOfUsersToSend1.', '.$numberOfUsersToSend2;
690 $select2 = [intval($campaign->sending_params['abtest']['B']), 'ul.`user_id`', acym_escapeDB($date)];
691 $numberUsersInsertedByMailId[intval($campaign->sending_params['abtest']['B'])] = acym_query(
692 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select2)
693 );
694 } elseif ($abTestFinal && !empty($campaign->sending_params['abtest']['final'])) {
695 $mailIds = [$campaign->mail_id, $campaign->sending_params['abtest']['B']];
696 acym_arrayToInteger($mailIds);
697 $automationHelper->where[] = 'user.id NOT IN (SELECT user_id FROM #__acym_user_stat WHERE mail_id IN ('.implode(',', $mailIds).'))';
698
699 $select = [intval($campaign->sending_params['abtest']['final']), 'ul.`user_id`', acym_escapeDB($date)];
700 $numberUsersInsertedByMailId[intval($campaign->sending_params['abtest']['final'])] = acym_query(
701 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select)
702 );
703 } else {
704 $insertQuery = 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select);
705 $numberUsersInsertedByMailId[intval($campaign->mail_id)] = acym_query($insertQuery);
706 }
707
708 $automationHelper->removeFlag(SegmentsController::FLAG_COUNT);
709 } else {
710 $numberUsersInsertedByMailId[intval($campaign->mail_id)] = $result;
711 }
712
713
714 $result += array_sum($numberUsersInsertedByMailId);
715
716 if ($campaign->sending_type == self::SENDING_TYPE_NOW) {
717 $campaign->sending_date = $date;
718 $campaign->draft = 0;
719 $this->save($campaign);
720 }
721
722 $mailStatClass = new MailStatClass();
723 foreach ($numberUsersInsertedByMailId as $mailId => $numberOfUsersInserted) {
724 $newMailStat = new \stdClass();
725 $newMailStat->mail_id = intval($mailId);
726 $newMailStat->total_subscribers = intval($numberOfUsersInserted);
727 $newMailStat->send_date = $date;
728
729 $mailStatClass->save($newMailStat);
730 }
731
732 if ($result === 0) {
733 $this->errors[] = acym_translation('ACYM_NO_SUBSCRIBERS_FOUND');
734
735 return false;
736 }
737
738 acym_query('UPDATE `#__acym_campaign` SET `sent` = 1, `active` = 1 WHERE `mail_id` = '.intval($campaign->mail_id));
739
740 return $result;
741 }
742
743 public function getCampaignForDashboard(): array
744 {
745 $campaigns = acym_loadObjectList(
746 'SELECT campaign.*, mail.name AS name
747 FROM #__acym_campaign AS campaign
748 LEFT JOIN #__acym_mail AS mail
749 ON campaign.mail_id = mail.id
750 WHERE `active` = 1
751 AND `sent` = 0
752 AND `sending_type` = '.acym_escapeDB(self::SENDING_TYPE_SCHEDULED).'
753 LIMIT 3'
754 );
755
756 return $this->decode($campaigns);
757 }
758
759 public function getOpenRateCampaigns(array $mailIds)
760 {
761 acym_arrayToInteger($mailIds);
762
763 //TODO move these methods in mail stat class
764 $query = 'SELECT SUM(sent) AS sent, SUM(open_unique) AS open_unique FROM #__acym_mail_stat
765 WHERE mail_id IN ('.implode(',', $mailIds).')';
766
767 return acym_loadObject($query);
768 }
769
770 public function getOpenRateAllCampaign()
771 {
772 $query = 'SELECT SUM(sent) as sent, SUM(open_unique) as open_unique FROM #__acym_mail_stat';
773
774 return acym_loadObject($query);
775 }
776
777 public function getBounceRateAllCampaign()
778 {
779 $query = 'SELECT SUM(sent) as sent, SUM(bounce_unique) as bounce_unique FROM #__acym_mail_stat';
780
781 return acym_loadObject($query);
782 }
783
784 public function getBounceRateCampaigns(array $mailIds)
785 {
786 acym_arrayToInteger($mailIds);
787
788 $query = 'SELECT SUM(sent) AS sent, SUM(bounce_unique) AS bounce_unique FROM #__acym_mail_stat
789 WHERE mail_id IN ('.implode(',', $mailIds).')';
790
791 return acym_loadObject($query);
792 }
793
794 public function getOpenByMonth(array $mailIds = [], string $start = '', string $end = ''): array
795 {
796 if (!is_array($mailIds)) $mailIds = [$mailIds];
797 acym_arrayToInteger($mailIds);
798
799 $query = 'SELECT COUNT(user_id) as open, DATE_FORMAT(open_date, \'%Y-%m\') as open_date FROM #__acym_user_stat WHERE open > 0';
800 $query .= empty($mailIds) ? '' : ' AND `mail_id` IN ('.implode(',', $mailIds).')';
801 $query .= ' AND `open_date` > "0000-00-00"';
802 $query .= empty($start) ? '' : ' AND `open_date` >= '.acym_escapeDB($start);
803 $query .= empty($end) ? '' : ' AND `open_date` <= '.acym_escapeDB($end);
804 $query .= ' GROUP BY MONTH(open_date), YEAR(open_date) ORDER BY open_date';
805
806 return acym_loadObjectList($query);
807 }
808
809 public function getOpenByDay(array $mailIds = [], string $start = '', string $end = ''): array
810 {
811 if (!is_array($mailIds)) $mailIds = [$mailIds];
812 acym_arrayToInteger($mailIds);
813
814 $query = 'SELECT COUNT(user_id) as open, DATE_FORMAT(open_date, \'%Y-%m-%d\') as open_date FROM #__acym_user_stat WHERE open > 0';
815 $query .= empty($mailIds) ? '' : ' AND `mail_id` IN ('.implode(',', $mailIds).')';
816 $query .= ' AND `open_date` > "0000-00-00"';
817 $query .= empty($start) ? '' : ' AND `open_date` >= '.acym_escapeDB($start);
818 $query .= empty($end) ? '' : ' AND `open_date` <= '.acym_escapeDB($end);
819 $query .= ' GROUP BY DAYOFYEAR(open_date), YEAR(open_date) ORDER BY open_date';
820
821 return acym_loadObjectList($query);
822 }
823
824 public function getOpenByHour(array $mailIds = [], string $start = '', string $end = ''): array
825 {
826 if (!is_array($mailIds)) $mailIds = [$mailIds];
827 acym_arrayToInteger($mailIds);
828
829 $query = 'SELECT COUNT(user_id) as open, DATE_FORMAT(open_date, \'%Y-%m-%d %H:00:00\') as open_date FROM #__acym_user_stat WHERE open > 0';
830 $query .= empty($mailIds) ? '' : ' AND `mail_id` IN ('.implode(',', $mailIds).')';
831 $query .= ' AND `open_date` > "0000-00-00 00:00:00"';
832 $query .= empty($start) ? '' : ' AND `open_date` >= '.acym_escapeDB($start);
833 $query .= empty($end) ? '' : ' AND `open_date` <= '.acym_escapeDB($end);
834 $query .= ' GROUP BY HOUR(open_date), DAYOFYEAR(open_date), YEAR(open_date) ORDER BY open_date';
835
836 return acym_loadObjectList($query);
837 }
838
839 public function getDevicesWithCountByMailId(array $mailIds = []): array
840 {
841 acym_arrayToInteger($mailIds);
842
843 $query = 'SELECT device, COUNT(*) as number FROM #__acym_user_stat WHERE `open` > 0';
844 if (!empty($mailIds)) $query .= ' AND mail_id IN ('.implode(',', $mailIds).')';
845 $query .= ' GROUP BY device';
846
847 return acym_loadObjectList($query);
848 }
849
850 public function getLastNewsletters(array &$params): array
851 {
852 // Init select elements
853 $querySelect = 'SELECT mail.*, campaign.sending_date ';
854 $queryCountSelect = 'SELECT COUNT(*) FROM (SELECT DISTINCT mail.id ';
855
856 // Form the query
857 $query = 'FROM #__acym_campaign AS campaign
858 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
859
860 // We may need some joins depending on the selected options
861 if (isset($params['userId']) || isset($params['lists'])) {
862 $query .= 'JOIN #__acym_mail_has_list AS maillist ON mail.id = maillist.mail_id ';
863 if (isset($params['userId'])) $query .= 'JOIN #__acym_user_has_list AS userlist ON maillist.list_id = userlist.list_id ';
864 }
865
866 // Make sure we display only active campaigns
867 $where = 'WHERE campaign.active = 1 AND campaign.sent = 1 AND mail.type = '.acym_escapeDB(MailClass::TYPE_STANDARD).' AND campaign.visible = 1 ';
868
869 // If we want an archive of some specific lists
870 if (isset($params['lists'])) {
871 acym_arrayToInteger($params['lists']);
872 $where .= 'AND maillist.list_id IN ('.implode(', ', $params['lists']).') ';
873 }
874
875 // If we want an archive for a specific user
876 if (isset($params['userId']) && !empty($params['displayUserListOnly'])) {
877 $where .= 'AND userlist.user_id = '.intval($params['userId']).' ';
878 }
879
880 // If the user search for a newsletter
881 if (isset($params['search'])) {
882 $search = acym_escapeDB('%'.acym_utf8Encode($params['search']).'%');
883 $where .= 'AND (mail.subject LIKE '.$search.' OR mail.body LIKE '.$search.')';
884 }
885
886 $query .= $where;
887 $return = [];
888 $return['count'] = (int)acym_loadResult($queryCountSelect.$query.') AS r ');
889
890 // Make sure we display campaigns only once
891 $endQuerySelect = 'GROUP BY mail.id ';
892 $endQuerySelect .= 'ORDER BY campaign.sending_date DESC';
893
894 // Init the pagination
895 $page = $params['page'] ?? 0;
896 $numberPerPage = $params['numberPerPage'] ?? 0;
897 $lastNewsletters = $params['limit'] ?? 0;
898
899 if (!empty($page) && !empty($numberPerPage)) {
900 if (!empty($lastNewsletters)) {
901 $limit = $page * $numberPerPage > $lastNewsletters ? fmod($lastNewsletters, $numberPerPage) : $numberPerPage;
902 } else {
903 $limit = $numberPerPage;
904 $nbTotalPage = ceil($return['count'] / $numberPerPage);
905 if ($params['page'] > $nbTotalPage) {
906 $params['page'] = 1;
907 }
908 }
909
910 $offset = ($params['page'] - 1) * $numberPerPage;
911 $endQuerySelect .= ' LIMIT '.intval($offset).', '.intval($limit);
912 } elseif (!empty($lastNewsletters)) {
913 $limit = $lastNewsletters;
914
915 $endQuerySelect .= ' LIMIT '.intval($limit);
916 }
917
918
919 $return['matchingNewsletters'] = $this->decode(acym_loadObjectList($querySelect.$query.$endQuerySelect));
920
921 if (isset($params['language']) && !empty($return['matchingNewsletters'])) {
922 $mailIds = array_column($return['matchingNewsletters'], 'id');
923
924 $translatedEmails = $this->decode(
925 acym_loadObjectList(
926 'SELECT mail.*
927 FROM #__acym_mail AS mail
928 WHERE mail.parent_id IN ('.implode(',', $mailIds).') AND mail.language = '.acym_escapeDB($params['language']),
929 'parent_id'
930 )
931 );
932 }
933
934 $userClass = new UserClass();
935 $userEmail = acym_currentUserEmail();
936 $user = $userClass->getOneByEmail($userEmail);
937
938 foreach ($return['matchingNewsletters'] as $i => $oneNewsletter) {
939 if (isset($translatedEmails[$oneNewsletter->id])) {
940 $translatedEmails[$oneNewsletter->id]->sending_date = $oneNewsletter->sending_date;
941 $oneNewsletter = $translatedEmails[$oneNewsletter->id];
942 }
943 acym_trigger('replaceContent', [&$oneNewsletter, false]);
944 acym_trigger('replaceUserInformation', [&$oneNewsletter, &$user, false]);
945
946 $return['matchingNewsletters'][$i] = $oneNewsletter;
947 }
948
949 return $return;
950 }
951
952 public function getListsByMailId(int $mailId): array
953 {
954 $listIds = acym_loadResultArray('SELECT list_id FROM #__acym_mail_has_list WHERE mail_id = '.intval($mailId));
955 acym_arrayToInteger($listIds);
956
957 return $listIds;
958 }
959
960 public function triggerAutoCampaign(): void
961 {
962 $activeAutoCampaigns = acym_loadObjectList(
963 'SELECT campaign.*, mail.name
964 FROM #__acym_campaign AS campaign
965 JOIN #__acym_mail AS mail ON campaign.`mail_id` = mail.`id`
966 WHERE `active` = 1 AND `sending_type` = '.acym_escapeDB(self::SENDING_TYPE_AUTO)
967 );
968 $activeAutoCampaigns = $this->decode($activeAutoCampaigns);
969
970 if (empty($activeAutoCampaigns)) {
971 return;
972 }
973
974 $mailClass = new MailClass();
975 $mailerHelper = new MailerHelper();
976 $mailerHelper->autoAddUser = true;
977 $adminNotificationEmail = $mailClass->getOneByName('acy_notification_auto_campaign_admin');
978 $time = time();
979
980 foreach ($activeAutoCampaigns as $campaign) {
981 // Check the start date
982 $nextTrigger = $campaign->next_trigger;
983 if (empty($nextTrigger) && !empty($campaign->sending_params['start_date'])) {
984 $nextTrigger = $campaign->sending_params['start_date'];
985 }
986
987 if (!empty($nextTrigger) && acym_getTime(acym_date($nextTrigger, 'Y-m-d H:i')) > $time) {
988 continue;
989 }
990
991 //check if we trigger the campaign
992 $step = new \stdClass();
993 $step->triggers = $campaign->sending_params;
994 $step->last_execution = $campaign->last_generated;
995 $step->next_execution = $campaign->next_trigger;
996
997 $data = ['time' => $time];
998 $execute = !empty($step->next_execution) && $step->next_execution <= $data['time'];
999 acym_trigger('onAcymExecuteTrigger', [&$step, &$execute, &$data], 'plgAcymTime');
1000 $campaign->next_trigger = $step->next_execution;
1001 if (!$execute) {
1002 $this->save($campaign);
1003 continue;
1004 }
1005
1006 //update the campaign
1007 $campaignMail = $mailClass->getOneById($campaign->mail_id);
1008
1009 $lastGenerated = $campaign->last_generated;
1010 $shouldGenerate = $this->updateAutoCampaign($campaign, $campaignMail, $time);
1011 $this->save($campaign);
1012
1013 if (!$shouldGenerate) {
1014 continue;
1015 }
1016
1017 //We generate the new campaign
1018 $generatedCampaign = $this->generateCampaign($campaign, $campaignMail, $lastGenerated, $mailClass);
1019 if (empty($generatedCampaign)) {
1020 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_FAILED_GENERATING', $campaign->name);
1021 continue;
1022 }
1023
1024 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_GENERATED', $campaign->name, $campaign->sending_params['number_generated']);
1025
1026 // We send it directly if no confirmation is needed
1027 if (empty($campaign->sending_params['need_confirm_to_send'])) {
1028 $this->send($generatedCampaign->id);
1029 } elseif (!empty($campaign->sending_params['admin_notification_emails']) && !empty($adminNotificationEmail)) {
1030 $mailerHelper->addParam('campaign_name', $campaign->name);
1031 $campaignLink = '<a href="'.acym_backendLink('campaigns&task=summaryGenerated&campaignId='.$generatedCampaign->id).'">'.$campaign->name.'</a>';
1032 $mailerHelper->addParam('campaign_link', $campaignLink);
1033
1034 foreach ($campaign->sending_params['admin_notification_emails'] as $email) {
1035 $mailerHelper->sendOne($adminNotificationEmail->id, $email);
1036 }
1037 }
1038 }
1039 }
1040
1041 private function shouldGenerateCampaign(object $campaign, object $campaignMail): bool
1042 {
1043 // The generateByCategory function is the only one that can stop a campaign generation, with min number of items
1044 $results = acym_trigger('generateByCategory', [&$campaignMail], null, function ($plugin) {
1045 $plugin->generateCampaignResult->status = true;
1046 });
1047
1048 // If one of the return statuses is "false", we won't generate the campaign
1049 foreach ($results as $oneResult) {
1050 if (isset($oneResult->status) && !$oneResult->status) {
1051 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_NOT_GENERATED', $campaign->name, $oneResult->message);
1052
1053 return false;
1054 }
1055 }
1056
1057 return true;
1058 }
1059
1060 private function updateAutoCampaign(object $campaign, object $campaignMail, int $time): bool
1061 {
1062 if (!$this->shouldGenerateCampaign($campaign, $campaignMail)) {
1063 return false;
1064 }
1065
1066 if (empty($campaign->sending_params['number_generated'])) {
1067 $campaign->sending_params['number_generated'] = 1;
1068 } else {
1069 $campaign->sending_params['number_generated']++;
1070 }
1071 $campaign->last_generated = $time;
1072
1073 return true;
1074 }
1075
1076 private function generateCampaign(object $campaign, object $campaignMail, ?int $lastGenerated, MailClass $mailClass): ?object
1077 {
1078 $newMail = $this->generateMailAutoCampaign($campaignMail, $campaign->sending_params['number_generated'], $mailClass);
1079 if (empty($newMail)) {
1080 return null;
1081 }
1082
1083 $newCampaign = new \stdClass();
1084 $newCampaign->mail_id = $newMail->id;
1085 $newCampaign->parent_id = $campaign->id;
1086 $newCampaign->active = 1;
1087 $newCampaign->visible = $campaign->visible;
1088 $newCampaign->draft = 1;
1089 $newCampaign->sending_type = self::SENDING_TYPE_NOW;
1090 $newCampaign->sent = 0;
1091 $newCampaign->last_generated = $lastGenerated;
1092 $newCampaign->sending_params = empty($campaign->sending_params['segment']) ? '' : ['segment' => $campaign->sending_params['segment']];
1093
1094 $newCampaign->id = $this->save($newCampaign);
1095
1096 // Replace content in the generated mail. MUST be done after campaign has been saved
1097 acym_trigger('replaceContent', [&$newMail, false]);
1098 $mailClass->save($newMail);
1099
1100 return $newCampaign;
1101 }
1102
1103 private function generateMailAutoCampaign(object $newMail, int $generatedMailNumber, MailClass $mailClass): ?object
1104 {
1105 $mailId = $newMail->id;
1106 unset($newMail->id);
1107 $newMail->creation_date = acym_date('now', 'Y-m-d H:i:s', false);
1108 $newMail->name .= ' #'.$generatedMailNumber;
1109
1110 $newMail->id = $mailClass->save($newMail);
1111 if (empty($newMail->id)) {
1112 return null;
1113 }
1114
1115 $this->setListToGeneratedCampaign($mailId, $newMail->id, $mailClass);
1116
1117 if (acym_isMultilingual()) {
1118 $this->generateMailAutoCampaignMultilingual($mailId, $generatedMailNumber, $newMail->id);
1119 }
1120
1121 return $newMail;
1122 }
1123
1124 private function generateMailAutoCampaignMultilingual(int $mailId, int $generatedMailNumber, int $newParentId): void
1125 {
1126 $mailClass = new MailClass();
1127 $mails = $mailClass->getTranslationsById($mailId, true);
1128
1129 foreach ($mails as $mail) {
1130 unset($mail->id);
1131 $mail->name .= ' #'.$generatedMailNumber;
1132 $mail->parent_id = $newParentId;
1133
1134 $mailClass->save($mail);
1135 }
1136 }
1137
1138 private function setListToGeneratedCampaign(int $parentMailId, int $newMailId, MailClass $mailClass): void
1139 {
1140 $lists = $mailClass->getAllListsByMailId($parentMailId);
1141 $listIds = array_keys($lists);
1142
1143 $this->manageListsToCampaign($listIds, $newMailId);
1144 }
1145
1146 public function getLastGenerated(int $mailId): int
1147 {
1148 $lastGenerated = acym_loadResult(
1149 'SELECT `last_generated`
1150 FROM #__acym_campaign
1151 WHERE `mail_id` = '.intval($mailId)
1152 );
1153
1154 return empty($lastGenerated) ? 0 : $lastGenerated;
1155 }
1156
1157 private function getNbGeneratedCampaigns(): int
1158 {
1159 return (int)acym_loadResult(
1160 'SELECT COUNT(*)
1161 FROM #__acym_campaign
1162 WHERE `parent_id` IS NOT NULL
1163 AND `sending_type` = '.acym_escapeDB(self::SENDING_TYPE_NOW)
1164 );
1165 }
1166
1167 public function getAllCampaignsGeneratedWaiting(): array
1168 {
1169 $campaignIds = acym_loadObjectList(
1170 'SELECT id
1171 FROM #__acym_campaign
1172 WHERE parent_id IS NOT NULL
1173 AND sending_type = '.acym_escapeDB(self::SENDING_TYPE_NOW).'
1174 AND draft = 1
1175 AND active = 1
1176 AND sent = 0'
1177 );
1178 acym_arrayToInteger($campaignIds);
1179
1180 return $campaignIds;
1181 }
1182
1183 public function getCampaignsByTypes(array $campaignTypes, bool $onlyActives = false): array
1184 {
1185 if (empty($campaignTypes)) return [];
1186 $campaignTypes = array_map('acym_escapeDB', $campaignTypes);
1187 $query = 'SELECT campaign.* FROM #__acym_campaign AS campaign WHERE campaign.sending_type IN ('.implode(',', $campaignTypes).')';
1188 if ($onlyActives) $query .= ' AND campaign.active = 1';
1189
1190 return $this->decode(acym_loadObjectList($query), false);
1191 }
1192
1193 public function getCountCampaignType(string $type): int
1194 {
1195 if (empty($type)) return 0;
1196
1197 return (int)acym_loadResult('SELECT COUNT(*) FROM #__acym_campaign WHERE sending_type = '.acym_escapeDB($type));
1198 }
1199
1200 public function getAllAbTestCampaignsToFinishSending(): array
1201 {
1202 $query = 'SELECT * FROM #__acym_campaign WHERE sending_params LIKE \'%"abtest":%\' AND draft = 0 AND active = 1 AND sent = 1';
1203
1204 return $this->decode(acym_loadObjectList($query));
1205 }
1206
1207 public function finishAbTestCampaign(object $campaign): bool
1208 {
1209 $abTestParams = empty($campaign->sending_params['abtest']) ? [] : $campaign->sending_params['abtest'];
1210
1211 if (empty($abTestParams['B']) || empty($abTestParams['repartition']) || !empty($abTestParams['abtest_finished']) || empty($abTestParams['after']['action'])) {
1212 return false;
1213 }
1214
1215 if (strtotime($campaign->sending_date.' + '.$abTestParams['after']['days'].' days') > time()) {
1216 return false;
1217 }
1218
1219 $mailStatsClass = new MailStatClass();
1220 $mailIds = [$campaign->mail_id, $abTestParams['B']];
1221 $bestMails = $mailStatsClass->getBestEmailByStats($mailIds, $abTestParams['after']['action']);
1222
1223 if (empty($bestMails)) {
1224 return false;
1225 }
1226
1227 $mailClass = new MailClass();
1228
1229 switch ($abTestParams['after']['action']) {
1230 case 'open_rate':
1231 case 'click_rate':
1232 $mail = $mailClass->getOneById($bestMails[$abTestParams['after']['action']]);
1233
1234 if (empty($mail)) {
1235 return false;
1236 }
1237 break;
1238 case 'click_open_rate':
1239 $bestMailClickRate = $mailClass->getOneById($bestMails['click_rate']);
1240 $bestMailOpenRate = $mailClass->getOneById($bestMails['open_rate']);
1241
1242 if (empty($bestMailClickRate) || empty($bestMailOpenRate)) {
1243 return false;
1244 }
1245
1246 $mail = $bestMailClickRate;
1247 $mail->preheader = $bestMailOpenRate->preheader;
1248 $mail->subject = $bestMailOpenRate->subject;
1249 $mail->from_name = $bestMailOpenRate->from_name;
1250 $mail->from_email = $bestMailOpenRate->from_email;
1251 $mail->reply_to_name = $bestMailOpenRate->reply_to_name;
1252 $mail->reply_to_email = $bestMailOpenRate->reply_to_email;
1253 break;
1254 }
1255
1256 $mail->id = 0;
1257 $mail->parent_id = $campaign->mail_id;
1258 $mail->name .= ' - '.acym_translation('ACYM_ABTEST_WINNER');
1259 $newMailId = $mailClass->save($mail);
1260 if (empty($newMailId)) {
1261 return false;
1262 }
1263
1264 $campaign->sending_params['abtest']['abtest_finished'] = 1;
1265 $campaign->sending_params['abtest']['final'] = $newMailId;
1266
1267 $this->save($campaign);
1268 $this->send($campaign->id, 0, true);
1269
1270 return true;
1271 }
1272
1273 public function isAbTestMail(int $mailId): bool
1274 {
1275 $mailClass = new MailClass();
1276 $mainMailId = $mailClass->getMainMailId($mailId);
1277
1278 $campaign = acym_loadObject(
1279 'SELECT *
1280 FROM #__acym_campaign
1281 WHERE mail_id = '.intval($mainMailId).'
1282 AND sending_params LIKE \'%"abtest":%\''
1283 );
1284
1285 return !empty($campaign);
1286 }
1287
1288 public function resetAbTestVersion(int $campaignId): void
1289 {
1290 if (empty($campaignId)) {
1291 return;
1292 }
1293
1294 $campaign = $this->getOneById($campaignId);
1295
1296 if (empty($campaign)) {
1297 return;
1298 }
1299
1300 if (empty($campaign->sending_params['abtest'])) {
1301 return;
1302 }
1303
1304 foreach (self::AB_TEST_VERSIONS as $version) {
1305 if (!empty($campaign->sending_params['abtest'][$version])) {
1306 $mailClass = new MailClass();
1307 $mailClass->delete([$campaign->sending_params['abtest'][$version]]);
1308 unset($campaign->sending_params['abtest'][$version]);
1309 }
1310 }
1311
1312 $this->save($campaign);
1313 }
1314
1315 public function getXCampaigns(array $options): array
1316 {
1317 $limit = $options['limit'] ?? 10;
1318 $offset = $options['offset'] ?? 0;
1319 $filters = $options['filters'] ?? [];
1320
1321 $conditions = [];
1322 foreach ($filters as $column => $filter) {
1323 switch ($column) {
1324 case 'id':
1325 case 'draf':
1326 case 'active':
1327 case 'mail_id':
1328 case 'sent':
1329 case 'parent_id':
1330 case 'next_trigger':
1331 case 'visible':
1332 case 'last_trigger':
1333 $conditions[] = 'campaign.'.acym_secureDBColumn($column).' = '.intval($filter);
1334 break;
1335 case 'name':
1336 case 'subject':
1337 $conditions[] = 'mail.'.acym_secureDBColumn($column).' LIKE '.acym_escapeDB('%'.$filter.'%');
1338 break;
1339 default:
1340 $conditions[] = 'campaign.'.acym_secureDBColumn($column).' = '.acym_escapeDB('%'.$filter.'%');
1341 }
1342 }
1343
1344 $query = 'SELECT campaign.*, mail.subject, mail.name FROM #__acym_campaign AS campaign
1345 LEFT JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id';
1346
1347 if (!empty($conditions)) {
1348 $query .= ' WHERE '.implode(' AND ', $conditions);
1349 }
1350
1351 return $this->decode(acym_loadObjectList($query, $this->pkey, $offset, $limit));
1352 }
1353
1354 public function getOneCampaign(): bool
1355 {
1356 $firstCampaign = acym_loadObject('SELECT * FROM #__acym_campaign');
1357
1358 return !empty($firstCampaign);
1359 }
1360 }
1361