PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.4
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.4
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.4, at back/Classes/CampaignClass.php

1,372 lines 54.2 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 $listsIds = array_unique($listsIds);
386
387 $existingListsIds = acym_loadResultArray('SELECT `id` FROM #__acym_list WHERE `id` IN ('.implode(',', $listsIds).')');
388 acym_arrayToInteger($existingListsIds);
389
390 $unknownListsIds = array_diff($listsIds, $existingListsIds);
391 if (!empty($unknownListsIds)) {
392 $this->errors[] = acym_translationSprintf('ACYM_LIST_NOT_FOUND', implode(', ', $unknownListsIds));
393 }
394
395 if (empty($existingListsIds)) return false;
396
397 $values = [];
398 foreach ($existingListsIds as $listId) {
399 $values[] = '('.intval($mailId).', '.intval($listId).')';
400 }
401
402 if (!empty($values)) {
403 acym_query('INSERT IGNORE INTO #__acym_mail_has_list (`mail_id`, `list_id`) VALUES '.implode(',', $values));
404 }
405
406 return true;
407 }
408
409 public function save(object $element): ?int
410 {
411 $campaign = clone $element;
412 if (isset($campaign->tags)) {
413 $tags = $campaign->tags;
414 unset($campaign->tags);
415 }
416
417 foreach ($campaign as $oneAttribute => $value) {
418 if (in_array($oneAttribute, $this->jsonColumns)) {
419 $campaign->$oneAttribute = json_encode(empty($value) ? [] : $value);
420 } else {
421 if (empty($value)) continue;
422 $campaign->$oneAttribute = acym_stripTags($value);
423 }
424 }
425
426 $campaignID = parent::save($campaign);
427
428 if (!empty($campaignID) && isset($tags)) {
429 $tagClass = new TagClass();
430 $tagClass->setTags(TagClass::TYPE_MAIL, intval($campaign->mail_id), $tags);
431 }
432
433 return $campaignID;
434 }
435
436 public function onlyManageableCampaigns(array &$elements): void
437 {
438 if (acym_isAdmin()) return;
439
440 $idCurrentUser = acym_currentUserId();
441 if (empty($idCurrentUser)) return;
442
443 $manageable = acym_loadResultArray(
444 'SELECT campaign.id
445 FROM #__acym_campaign AS campaign
446 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id
447 WHERE mail.creator_id = '.intval($idCurrentUser)
448 );
449 $elements = array_intersect($elements, $manageable);
450 }
451
452 public function hasUserAccess(int $campaignId): bool
453 {
454 $userId = acym_currentUserId();
455 if (empty($userId)) {
456 return false;
457 }
458
459 if (acym_isAdmin() && acym_isAllowed('campaigns')) {
460 return true;
461 }
462
463 $query = 'SELECT COUNT(*) FROM #__acym_campaign AS campaign
464 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
465
466 $condition = 'mail.creator_id = '.intval($userId);
467 if ($this->config->get('front_campaigns_filter', 'own') === 'allowed') {
468 $listClass = new ListClass();
469 $allowedLists = $listClass->getManageableLists();
470 if (!empty($allowedLists)) {
471 $query .= ' LEFT JOIN #__acym_mail_has_list AS mailList ON campaign.mail_id = mailList.mail_id AND mailList.list_id IN ('.implode(',', $allowedLists).')';
472 $condition .= ' OR mailList.list_id IS NOT NULL';
473 }
474 }
475
476 $query .= 'WHERE campaign.id = '.intval($campaignId).' AND ('.$condition.')';
477
478 return acym_loadResult($query) > 0;
479 }
480
481 /**
482 * Delete a campaign. Needs to delete the tag associated with the campaign and the mail attached to the campaign.
483 * Deleting a mail of a campaign needs to clean the association table mail_has_list and its tags
484 */
485 public function delete(array $elements): int
486 {
487 acym_arrayToInteger($elements);
488 $this->onlyManageableCampaigns($elements);
489
490 if (empty($elements)) return 0;
491
492 $mailsToDelete = [];
493 foreach ($elements as $id) {
494 $mailsToDelete[] = acym_loadResult('SELECT mail_id FROM #__acym_campaign WHERE id = '.intval($id));
495 acym_query('UPDATE #__acym_campaign SET mail_id = NULL WHERE id = '.intval($id));
496 }
497
498 $mailClass = new MailClass();
499 $mailClass->delete($mailsToDelete);
500
501 return parent::delete($elements);
502 }
503
504 public function countUsersCampaign(int $campaignID, bool $onlyActive = false): int
505 {
506 $campaign = $this->getOneById($campaignID);
507 if (empty($campaign)) {
508 return 0;
509 }
510
511 $mailClass = new MailClass();
512 $lists = $mailClass->getAllListsByMailId($campaign->mail_id);
513 $listsIds = array_column($lists, 'id');
514
515 if (empty($listsIds)) {
516 return 0;
517 }
518
519 $automationHelperBase = new AutomationHelper();
520 $automationHelperBase->removeFlag(SegmentsController::FLAG_COUNT);
521
522 $exclude = !empty($campaign->sending_params['segment']['invert']) && $campaign->sending_params['segment']['invert'] === 'exclude';
523
524 $automationHelpers = [];
525 $filters = $this->getFilterCampaign($campaign->sending_params);
526 foreach ($filters as $or => $orValues) {
527 if (empty($orValues)) continue;
528
529 $automationHelpers[$or] = new AutomationHelper();
530 foreach ($orValues as $and => $andValues) {
531 $and = intval($and);
532 foreach ($andValues as $filterName => $options) {
533 acym_trigger('onAcymProcessFilter_'.$filterName, [&$automationHelpers[$or], &$options, &$and]);
534 }
535 }
536
537 $automationHelpers[$or]->excludeSelected = $exclude;
538 $automationHelpers[$or]->addFlag(SegmentsController::FLAG_COUNT);
539 }
540
541 $join = ' #__acym_user_has_list AS user_list ON user_list.user_id = user.id ';
542 $join .= 'AND user_list.list_id IN ('.implode(',', $listsIds).') ';
543 $join .= 'AND user_list.status = 1 ';
544 if ($this->config->get('require_confirmation', 1) == 1) {
545 $join .= 'AND user.confirmed = 1 ';
546 }
547 if ($onlyActive) {
548 $join .= 'AND user.active = 1 ';
549 }
550
551 if (empty($automationHelpers)) {
552 $automationHelperBase->join['user_list'] = $join;
553
554 return acym_loadResult($automationHelperBase->getQuery(['COUNT(DISTINCT user.id)']));
555 } else {
556 $automationHelperBase = array_pop($automationHelpers);
557 $automationHelperBase->join['user_list'] = $join;
558 $numberOfRecipients = acym_loadResult($automationHelperBase->getQuery(['COUNT(DISTINCT user.id)']));
559 $automationHelperBase->removeFlag(SegmentsController::FLAG_COUNT);
560
561 return $numberOfRecipients;
562 }
563 }
564
565 public function getFilterCampaign($sendingParams): array
566 {
567 if (empty($sendingParams['segment'])) {
568 return [];
569 }
570
571 if (!empty($sendingParams['segment']['filters'])) {
572 return $sendingParams['segment']['filters'];
573 }
574
575 if (!empty($sendingParams['segment']['segment_id'])) {
576 $segmentClass = new SegmentClass();
577 $segment = $segmentClass->getOneById($sendingParams['segment']['segment_id']);
578 if (!empty($segment)) {
579 return $segment->filters;
580 }
581 }
582
583 return [];
584 }
585
586 public function send(int $campaignID, int $result = 0, bool $abTestFinal = false)
587 {
588 // Make sure the email we're trying to send exists
589 $campaign = $this->getOneById($campaignID);
590
591 if (empty($campaign->mail_id)) {
592 $this->errors[] = acym_translation('ACYM_EMAIL_NOT_FOUND');
593
594 return false;
595 }
596
597 $filters = $this->getFilterCampaign($campaign->sending_params);
598 $pluginIsExisting = true;
599
600 if (empty($filters)) {
601 $filters = [0 => []];
602 acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[0], &$pluginIsExisting]);
603 } else {
604 // Adds the special campaigns conditions to the "OR" blocs of the segment
605 foreach ($filters as $key => $filter) {
606 acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[$key], &$pluginIsExisting]);
607 }
608 }
609
610 // This is a special campaign type, but the required plugin is not installed
611 if (!$pluginIsExisting) {
612 return false;
613 }
614
615 // Make sure some receivers have been selected
616 $lists = acym_loadResultArray('SELECT list_id FROM #__acym_mail_has_list WHERE mail_id = '.intval($campaign->mail_id));
617 if (empty($lists)) {
618 $this->errors[] = acym_translation('ACYM_NO_LIST_SELECTED');
619
620 return false;
621 }
622 acym_arrayToInteger($lists);
623
624 $date = acym_date('now', 'Y-m-d H:i:s', false);
625 $numberUsersInsertedByMailId = [];
626
627 if (empty($result)) {
628 $conditions = [
629 '`user`.`active` = 1',
630 '`ul`.`status` = 1',
631 '`ul`.`list_id` IN ('.implode(',', $lists).')',
632 ];
633 if ($this->config->get('require_confirmation', 1) == 1) {
634 $conditions[] = '`user`.`confirmed` = 1';
635 }
636
637 $automationHelper = new AutomationHelper();
638 $automationHelper->removeFlag(SegmentsController::FLAG_COUNT);
639 $automationHelper->join['ul'] = ' #__acym_user_has_list AS ul ON ul.user_id = user.id ';
640
641 if (!empty($filters)) {
642 $automationHelpers = [];
643 foreach ($filters as $or => $orValues) {
644 if (empty($orValues)) continue;
645
646 $automationHelpers[$or] = new AutomationHelper();
647 foreach ($orValues as $and => $andValues) {
648 $and = intval($and);
649 foreach ($andValues as $filterName => $options) {
650 acym_trigger('onAcymProcessFilter_'.$filterName, [&$automationHelpers[$or], &$options, &$and]);
651 }
652 }
653
654 $automationHelpers[$or]->addFlag(SegmentsController::FLAG_COUNT);
655 }
656
657 if (!empty($automationHelpers)) {
658 $excludingSegment = !empty($campaign->sending_params['segment']['invert']) && $campaign->sending_params['segment']['invert'] === 'exclude';
659 if ($excludingSegment) {
660 $conditions[] = 'user.automation NOT LIKE "%a'.SegmentsController::FLAG_COUNT.'a%"';
661 } else {
662 $conditions[] = 'user.automation LIKE "%a'.SegmentsController::FLAG_COUNT.'a%"';
663 }
664 }
665 }
666
667 if (acym_isMultilingual()) {
668 $select = ['IF(mail.id IS NULL, '.intval($campaign->mail_id).', `mail`.`id`)', 'ul.`user_id`', acym_escapeDB($date)];
669 $automationHelper->leftjoin['mail'] = '`#__acym_mail` AS mail ON `mail`.`language` = `user`.language AND `mail`.`parent_id` = '.intval($campaign->mail_id);
670 } else {
671 $select = [intval($campaign->mail_id), 'ul.`user_id`', acym_escapeDB($date)];
672 }
673
674 // Resending a campaign only to users who didn't receive it
675 if (!empty($campaign->sending_params['resendTarget']) && 'new' === $campaign->sending_params['resendTarget']) {
676 if (acym_isMultilingual()) {
677 $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(
678 $campaign->mail_id
679 ).' OR id = '.intval($campaign->mail_id).')';
680 } else {
681 $automationHelper->leftjoin['us'] = '`#__acym_user_stat` AS `us` ON `us`.`user_id` = `user`.`id` AND `us`.`mail_id` = '.intval($campaign->mail_id);
682 }
683 $conditions[] = '`us`.`user_id` IS NULL';
684 }
685
686 $automationHelper->where = $conditions;
687 if (!empty($campaign->sending_params['abtest']['repartition']) && !$abTestFinal) {
688 $totalNumberOfUsers = acym_loadResult($automationHelper->getQuery(['count(user.id)']));
689 $numberOfUsersToSend = round($campaign->sending_params['abtest']['repartition'] * $totalNumberOfUsers / 100);
690
691 $numberOfUsersToSend1 = round($numberOfUsersToSend / 2);
692 $automationHelper->limit = '0, '.$numberOfUsersToSend1;
693 $select1 = [intval($campaign->mail_id), 'ul.`user_id`', acym_escapeDB($date)];
694 $numberUsersInsertedByMailId[intval($campaign->mail_id)] = acym_query(
695 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select1)
696 );
697
698 // If we have an odd number of users to send, we send one more to the first mail
699 $numberOfUsersToSend2 = $numberOfUsersToSend1 * 2 > $numberOfUsersToSend ? $numberOfUsersToSend1 - 1 : $numberOfUsersToSend1;
700 $automationHelper->limit = $numberOfUsersToSend1.', '.$numberOfUsersToSend2;
701 $select2 = [intval($campaign->sending_params['abtest']['B']), 'ul.`user_id`', acym_escapeDB($date)];
702 $numberUsersInsertedByMailId[intval($campaign->sending_params['abtest']['B'])] = acym_query(
703 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select2)
704 );
705 } elseif ($abTestFinal && !empty($campaign->sending_params['abtest']['final'])) {
706 $mailIds = [$campaign->mail_id, $campaign->sending_params['abtest']['B']];
707 acym_arrayToInteger($mailIds);
708 $automationHelper->where[] = 'user.id NOT IN (SELECT user_id FROM #__acym_user_stat WHERE mail_id IN ('.implode(',', $mailIds).'))';
709
710 $select = [intval($campaign->sending_params['abtest']['final']), 'ul.`user_id`', acym_escapeDB($date)];
711 $numberUsersInsertedByMailId[intval($campaign->sending_params['abtest']['final'])] = acym_query(
712 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select)
713 );
714 } else {
715 $insertQuery = 'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select);
716 $numberUsersInsertedByMailId[intval($campaign->mail_id)] = acym_query($insertQuery);
717 }
718
719 $automationHelper->removeFlag(SegmentsController::FLAG_COUNT);
720 } else {
721 $numberUsersInsertedByMailId[intval($campaign->mail_id)] = $result;
722 }
723
724
725 $result += array_sum($numberUsersInsertedByMailId);
726
727 if ($campaign->sending_type == self::SENDING_TYPE_NOW) {
728 $campaign->sending_date = $date;
729 $campaign->draft = 0;
730 $this->save($campaign);
731 }
732
733 $mailStatClass = new MailStatClass();
734 foreach ($numberUsersInsertedByMailId as $mailId => $numberOfUsersInserted) {
735 $newMailStat = new \stdClass();
736 $newMailStat->mail_id = intval($mailId);
737 $newMailStat->total_subscribers = intval($numberOfUsersInserted);
738 $newMailStat->send_date = $date;
739
740 $mailStatClass->save($newMailStat);
741 }
742
743 if ($result === 0) {
744 $this->errors[] = acym_translation('ACYM_NO_SUBSCRIBERS_FOUND');
745
746 return false;
747 }
748
749 acym_query('UPDATE `#__acym_campaign` SET `sent` = 1, `active` = 1 WHERE `mail_id` = '.intval($campaign->mail_id));
750
751 return $result;
752 }
753
754 public function getCampaignForDashboard(): array
755 {
756 $campaigns = acym_loadObjectList(
757 'SELECT campaign.*, mail.name AS name
758 FROM #__acym_campaign AS campaign
759 LEFT JOIN #__acym_mail AS mail
760 ON campaign.mail_id = mail.id
761 WHERE `active` = 1
762 AND `sent` = 0
763 AND `sending_type` = '.acym_escapeDB(self::SENDING_TYPE_SCHEDULED).'
764 LIMIT 3'
765 );
766
767 return $this->decode($campaigns);
768 }
769
770 public function getOpenRateCampaigns(array $mailIds)
771 {
772 acym_arrayToInteger($mailIds);
773
774 //TODO move these methods in mail stat class
775 $query = 'SELECT SUM(sent) AS sent, SUM(open_unique) AS open_unique FROM #__acym_mail_stat
776 WHERE mail_id IN ('.implode(',', $mailIds).')';
777
778 return acym_loadObject($query);
779 }
780
781 public function getOpenRateAllCampaign()
782 {
783 $query = 'SELECT SUM(sent) as sent, SUM(open_unique) as open_unique FROM #__acym_mail_stat';
784
785 return acym_loadObject($query);
786 }
787
788 public function getBounceRateAllCampaign()
789 {
790 $query = 'SELECT SUM(sent) as sent, SUM(bounce_unique) as bounce_unique FROM #__acym_mail_stat';
791
792 return acym_loadObject($query);
793 }
794
795 public function getBounceRateCampaigns(array $mailIds)
796 {
797 acym_arrayToInteger($mailIds);
798
799 $query = 'SELECT SUM(sent) AS sent, SUM(bounce_unique) AS bounce_unique FROM #__acym_mail_stat
800 WHERE mail_id IN ('.implode(',', $mailIds).')';
801
802 return acym_loadObject($query);
803 }
804
805 public function getOpenByMonth(array $mailIds = [], string $start = '', string $end = ''): array
806 {
807 if (!is_array($mailIds)) $mailIds = [$mailIds];
808 acym_arrayToInteger($mailIds);
809
810 $query = 'SELECT COUNT(user_id) as open, DATE_FORMAT(open_date, \'%Y-%m\') as open_date FROM #__acym_user_stat WHERE open > 0';
811 $query .= empty($mailIds) ? '' : ' AND `mail_id` IN ('.implode(',', $mailIds).')';
812 $query .= ' AND `open_date` > "0000-00-00"';
813 $query .= empty($start) ? '' : ' AND `open_date` >= '.acym_escapeDB($start);
814 $query .= empty($end) ? '' : ' AND `open_date` <= '.acym_escapeDB($end);
815 $query .= ' GROUP BY MONTH(open_date), YEAR(open_date) ORDER BY open_date';
816
817 return acym_loadObjectList($query);
818 }
819
820 public function getOpenByDay(array $mailIds = [], string $start = '', string $end = ''): array
821 {
822 if (!is_array($mailIds)) $mailIds = [$mailIds];
823 acym_arrayToInteger($mailIds);
824
825 $query = 'SELECT COUNT(user_id) as open, DATE_FORMAT(open_date, \'%Y-%m-%d\') as open_date FROM #__acym_user_stat WHERE open > 0';
826 $query .= empty($mailIds) ? '' : ' AND `mail_id` IN ('.implode(',', $mailIds).')';
827 $query .= ' AND `open_date` > "0000-00-00"';
828 $query .= empty($start) ? '' : ' AND `open_date` >= '.acym_escapeDB($start);
829 $query .= empty($end) ? '' : ' AND `open_date` <= '.acym_escapeDB($end);
830 $query .= ' GROUP BY DAYOFYEAR(open_date), YEAR(open_date) ORDER BY open_date';
831
832 return acym_loadObjectList($query);
833 }
834
835 public function getOpenByHour(array $mailIds = [], string $start = '', string $end = ''): array
836 {
837 if (!is_array($mailIds)) $mailIds = [$mailIds];
838 acym_arrayToInteger($mailIds);
839
840 $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';
841 $query .= empty($mailIds) ? '' : ' AND `mail_id` IN ('.implode(',', $mailIds).')';
842 $query .= ' AND `open_date` > "0000-00-00 00:00:00"';
843 $query .= empty($start) ? '' : ' AND `open_date` >= '.acym_escapeDB($start);
844 $query .= empty($end) ? '' : ' AND `open_date` <= '.acym_escapeDB($end);
845 $query .= ' GROUP BY HOUR(open_date), DAYOFYEAR(open_date), YEAR(open_date) ORDER BY open_date';
846
847 return acym_loadObjectList($query);
848 }
849
850 public function getDevicesWithCountByMailId(array $mailIds = []): array
851 {
852 acym_arrayToInteger($mailIds);
853
854 $query = 'SELECT device, COUNT(*) as number FROM #__acym_user_stat WHERE `open` > 0';
855 if (!empty($mailIds)) $query .= ' AND mail_id IN ('.implode(',', $mailIds).')';
856 $query .= ' GROUP BY device';
857
858 return acym_loadObjectList($query);
859 }
860
861 public function getLastNewsletters(array &$params): array
862 {
863 // Init select elements
864 $querySelect = 'SELECT mail.*, campaign.sending_date ';
865 $queryCountSelect = 'SELECT COUNT(*) FROM (SELECT DISTINCT mail.id ';
866
867 // Form the query
868 $query = 'FROM #__acym_campaign AS campaign
869 JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
870
871 // We may need some joins depending on the selected options
872 if (isset($params['userId']) || isset($params['lists'])) {
873 $query .= 'JOIN #__acym_mail_has_list AS maillist ON mail.id = maillist.mail_id ';
874 if (isset($params['userId'])) $query .= 'JOIN #__acym_user_has_list AS userlist ON maillist.list_id = userlist.list_id ';
875 }
876
877 // Make sure we display only active campaigns
878 $where = 'WHERE campaign.active = 1 AND campaign.sent = 1 AND mail.type = '.acym_escapeDB(MailClass::TYPE_STANDARD).' AND campaign.visible = 1 ';
879
880 // If we want an archive of some specific lists
881 if (isset($params['lists'])) {
882 acym_arrayToInteger($params['lists']);
883 $where .= 'AND maillist.list_id IN ('.implode(', ', $params['lists']).') ';
884 }
885
886 // If we want an archive for a specific user
887 if (isset($params['userId']) && !empty($params['displayUserListOnly'])) {
888 $where .= 'AND userlist.user_id = '.intval($params['userId']).' ';
889 }
890
891 // If the user search for a newsletter
892 if (isset($params['search'])) {
893 $search = acym_escapeDB('%'.acym_utf8Encode($params['search']).'%');
894 $where .= 'AND (mail.subject LIKE '.$search.' OR mail.body LIKE '.$search.')';
895 }
896
897 $query .= $where;
898 $return = [];
899 $return['count'] = (int)acym_loadResult($queryCountSelect.$query.') AS r ');
900
901 // Make sure we display campaigns only once
902 $endQuerySelect = 'GROUP BY mail.id ';
903 $endQuerySelect .= 'ORDER BY campaign.sending_date DESC';
904
905 // Init the pagination
906 $page = $params['page'] ?? 0;
907 $numberPerPage = $params['numberPerPage'] ?? 0;
908 $lastNewsletters = $params['limit'] ?? 0;
909
910 if (!empty($page) && !empty($numberPerPage)) {
911 if (!empty($lastNewsletters)) {
912 $limit = $page * $numberPerPage > $lastNewsletters ? fmod($lastNewsletters, $numberPerPage) : $numberPerPage;
913 } else {
914 $limit = $numberPerPage;
915 $nbTotalPage = ceil($return['count'] / $numberPerPage);
916 if ($params['page'] > $nbTotalPage) {
917 $params['page'] = 1;
918 }
919 }
920
921 $offset = ($params['page'] - 1) * $numberPerPage;
922 $endQuerySelect .= ' LIMIT '.intval($offset).', '.intval($limit);
923 } elseif (!empty($lastNewsletters)) {
924 $limit = $lastNewsletters;
925
926 $endQuerySelect .= ' LIMIT '.intval($limit);
927 }
928
929
930 $return['matchingNewsletters'] = $this->decode(acym_loadObjectList($querySelect.$query.$endQuerySelect));
931
932 if (isset($params['language']) && !empty($return['matchingNewsletters'])) {
933 $mailIds = array_column($return['matchingNewsletters'], 'id');
934
935 $translatedEmails = $this->decode(
936 acym_loadObjectList(
937 'SELECT mail.*
938 FROM #__acym_mail AS mail
939 WHERE mail.parent_id IN ('.implode(',', $mailIds).') AND mail.language = '.acym_escapeDB($params['language']),
940 'parent_id'
941 )
942 );
943 }
944
945 $userClass = new UserClass();
946 $userEmail = acym_currentUserEmail();
947 $user = $userClass->getOneByEmail($userEmail);
948
949 foreach ($return['matchingNewsletters'] as $i => $oneNewsletter) {
950 if (isset($translatedEmails[$oneNewsletter->id])) {
951 $translatedEmails[$oneNewsletter->id]->sending_date = $oneNewsletter->sending_date;
952 $oneNewsletter = $translatedEmails[$oneNewsletter->id];
953 }
954 acym_trigger('replaceContent', [&$oneNewsletter, false]);
955 acym_trigger('replaceUserInformation', [&$oneNewsletter, &$user, false]);
956
957 $return['matchingNewsletters'][$i] = $oneNewsletter;
958 }
959
960 return $return;
961 }
962
963 public function getListsByMailId(int $mailId): array
964 {
965 $listIds = acym_loadResultArray('SELECT list_id FROM #__acym_mail_has_list WHERE mail_id = '.intval($mailId));
966 acym_arrayToInteger($listIds);
967
968 return $listIds;
969 }
970
971 public function triggerAutoCampaign(): void
972 {
973 $activeAutoCampaigns = acym_loadObjectList(
974 'SELECT campaign.*, mail.name
975 FROM #__acym_campaign AS campaign
976 JOIN #__acym_mail AS mail ON campaign.`mail_id` = mail.`id`
977 WHERE `active` = 1 AND `sending_type` = '.acym_escapeDB(self::SENDING_TYPE_AUTO)
978 );
979 $activeAutoCampaigns = $this->decode($activeAutoCampaigns);
980
981 if (empty($activeAutoCampaigns)) {
982 return;
983 }
984
985 $mailClass = new MailClass();
986 $mailerHelper = new MailerHelper();
987 $mailerHelper->autoAddUser = true;
988 $adminNotificationEmail = $mailClass->getOneByName('acy_notification_auto_campaign_admin');
989 $time = time();
990
991 foreach ($activeAutoCampaigns as $campaign) {
992 // Check the start date
993 $nextTrigger = $campaign->next_trigger;
994 if (empty($nextTrigger) && !empty($campaign->sending_params['start_date'])) {
995 $nextTrigger = $campaign->sending_params['start_date'];
996 }
997
998 if (!empty($nextTrigger) && acym_getTime(acym_date($nextTrigger, 'Y-m-d H:i')) > $time) {
999 continue;
1000 }
1001
1002 //check if we trigger the campaign
1003 $step = new \stdClass();
1004 $step->triggers = $campaign->sending_params;
1005 $step->last_execution = $campaign->last_generated;
1006 $step->next_execution = $campaign->next_trigger;
1007
1008 $data = ['time' => $time];
1009 $execute = !empty($step->next_execution) && $step->next_execution <= $data['time'];
1010 acym_trigger('onAcymExecuteTrigger', [&$step, &$execute, &$data], 'plgAcymTime');
1011 $campaign->next_trigger = $step->next_execution;
1012 if (!$execute) {
1013 $this->save($campaign);
1014 continue;
1015 }
1016
1017 //update the campaign
1018 $campaignMail = $mailClass->getOneById($campaign->mail_id);
1019
1020 $lastGenerated = $campaign->last_generated;
1021 $shouldGenerate = $this->updateAutoCampaign($campaign, $campaignMail, $time);
1022 $this->save($campaign);
1023
1024 if (!$shouldGenerate) {
1025 continue;
1026 }
1027
1028 //We generate the new campaign
1029 $generatedCampaign = $this->generateCampaign($campaign, $campaignMail, $lastGenerated, $mailClass);
1030 if (empty($generatedCampaign)) {
1031 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_FAILED_GENERATING', $campaign->name);
1032 continue;
1033 }
1034
1035 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_GENERATED', $campaign->name, $campaign->sending_params['number_generated']);
1036
1037 // We send it directly if no confirmation is needed
1038 if (empty($campaign->sending_params['need_confirm_to_send'])) {
1039 $this->send($generatedCampaign->id);
1040 } elseif (!empty($campaign->sending_params['admin_notification_emails']) && !empty($adminNotificationEmail)) {
1041 $mailerHelper->addParam('campaign_name', $campaign->name);
1042 $campaignLink = '<a href="'.acym_backendLink('campaigns&task=summaryGenerated&campaignId='.$generatedCampaign->id).'">'.$campaign->name.'</a>';
1043 $mailerHelper->addParam('campaign_link', $campaignLink);
1044
1045 foreach ($campaign->sending_params['admin_notification_emails'] as $email) {
1046 $mailerHelper->sendOne($adminNotificationEmail->id, $email);
1047 }
1048 }
1049 }
1050 }
1051
1052 private function shouldGenerateCampaign(object $campaign, object $campaignMail): bool
1053 {
1054 // The generateByCategory function is the only one that can stop a campaign generation, with min number of items
1055 $results = acym_trigger('generateByCategory', [&$campaignMail], null, function ($plugin) {
1056 $plugin->generateCampaignResult->status = true;
1057 });
1058
1059 // If one of the return statuses is "false", we won't generate the campaign
1060 foreach ($results as $oneResult) {
1061 if (isset($oneResult->status) && !$oneResult->status) {
1062 $this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_NOT_GENERATED', $campaign->name, $oneResult->message);
1063
1064 return false;
1065 }
1066 }
1067
1068 return true;
1069 }
1070
1071 private function updateAutoCampaign(object $campaign, object $campaignMail, int $time): bool
1072 {
1073 if (!$this->shouldGenerateCampaign($campaign, $campaignMail)) {
1074 return false;
1075 }
1076
1077 if (empty($campaign->sending_params['number_generated'])) {
1078 $campaign->sending_params['number_generated'] = 1;
1079 } else {
1080 $campaign->sending_params['number_generated']++;
1081 }
1082 $campaign->last_generated = $time;
1083
1084 return true;
1085 }
1086
1087 private function generateCampaign(object $campaign, object $campaignMail, ?int $lastGenerated, MailClass $mailClass): ?object
1088 {
1089 $newMail = $this->generateMailAutoCampaign($campaignMail, $campaign->sending_params['number_generated'], $mailClass);
1090 if (empty($newMail)) {
1091 return null;
1092 }
1093
1094 $newCampaign = new \stdClass();
1095 $newCampaign->mail_id = $newMail->id;
1096 $newCampaign->parent_id = $campaign->id;
1097 $newCampaign->active = 1;
1098 $newCampaign->visible = $campaign->visible;
1099 $newCampaign->draft = 1;
1100 $newCampaign->sending_type = self::SENDING_TYPE_NOW;
1101 $newCampaign->sent = 0;
1102 $newCampaign->last_generated = $lastGenerated;
1103 $newCampaign->sending_params = empty($campaign->sending_params['segment']) ? '' : ['segment' => $campaign->sending_params['segment']];
1104
1105 $newCampaign->id = $this->save($newCampaign);
1106
1107 // Replace content in the generated mail. MUST be done after campaign has been saved
1108 acym_trigger('replaceContent', [&$newMail, false]);
1109 $mailClass->save($newMail);
1110
1111 return $newCampaign;
1112 }
1113
1114 private function generateMailAutoCampaign(object $newMail, int $generatedMailNumber, MailClass $mailClass): ?object
1115 {
1116 $mailId = $newMail->id;
1117 unset($newMail->id);
1118 $newMail->creation_date = acym_date('now', 'Y-m-d H:i:s', false);
1119 $newMail->name .= ' #'.$generatedMailNumber;
1120
1121 $newMail->id = $mailClass->save($newMail);
1122 if (empty($newMail->id)) {
1123 return null;
1124 }
1125
1126 $this->setListToGeneratedCampaign($mailId, $newMail->id, $mailClass);
1127
1128 if (acym_isMultilingual()) {
1129 $this->generateMailAutoCampaignMultilingual($mailId, $generatedMailNumber, $newMail->id);
1130 }
1131
1132 return $newMail;
1133 }
1134
1135 private function generateMailAutoCampaignMultilingual(int $mailId, int $generatedMailNumber, int $newParentId): void
1136 {
1137 $mailClass = new MailClass();
1138 $mails = $mailClass->getTranslationsById($mailId, true);
1139
1140 foreach ($mails as $mail) {
1141 unset($mail->id);
1142 $mail->name .= ' #'.$generatedMailNumber;
1143 $mail->parent_id = $newParentId;
1144
1145 $mailClass->save($mail);
1146 }
1147 }
1148
1149 private function setListToGeneratedCampaign(int $parentMailId, int $newMailId, MailClass $mailClass): void
1150 {
1151 $lists = $mailClass->getAllListsByMailId($parentMailId);
1152 $listIds = array_keys($lists);
1153
1154 $this->manageListsToCampaign($listIds, $newMailId);
1155 }
1156
1157 public function getLastGenerated(int $mailId): int
1158 {
1159 $lastGenerated = acym_loadResult(
1160 'SELECT `last_generated`
1161 FROM #__acym_campaign
1162 WHERE `mail_id` = '.intval($mailId)
1163 );
1164
1165 return empty($lastGenerated) ? 0 : $lastGenerated;
1166 }
1167
1168 private function getNbGeneratedCampaigns(): int
1169 {
1170 return (int)acym_loadResult(
1171 'SELECT COUNT(*)
1172 FROM #__acym_campaign
1173 WHERE `parent_id` IS NOT NULL
1174 AND `sending_type` = '.acym_escapeDB(self::SENDING_TYPE_NOW)
1175 );
1176 }
1177
1178 public function getAllCampaignsGeneratedWaiting(): array
1179 {
1180 $campaignIds = acym_loadObjectList(
1181 'SELECT id
1182 FROM #__acym_campaign
1183 WHERE parent_id IS NOT NULL
1184 AND sending_type = '.acym_escapeDB(self::SENDING_TYPE_NOW).'
1185 AND draft = 1
1186 AND active = 1
1187 AND sent = 0'
1188 );
1189 acym_arrayToInteger($campaignIds);
1190
1191 return $campaignIds;
1192 }
1193
1194 public function getCampaignsByTypes(array $campaignTypes, bool $onlyActives = false): array
1195 {
1196 if (empty($campaignTypes)) return [];
1197 $campaignTypes = array_map('acym_escapeDB', $campaignTypes);
1198 $query = 'SELECT campaign.* FROM #__acym_campaign AS campaign WHERE campaign.sending_type IN ('.implode(',', $campaignTypes).')';
1199 if ($onlyActives) $query .= ' AND campaign.active = 1';
1200
1201 return $this->decode(acym_loadObjectList($query), false);
1202 }
1203
1204 public function getCountCampaignType(string $type): int
1205 {
1206 if (empty($type)) return 0;
1207
1208 return (int)acym_loadResult('SELECT COUNT(*) FROM #__acym_campaign WHERE sending_type = '.acym_escapeDB($type));
1209 }
1210
1211 public function getAllAbTestCampaignsToFinishSending(): array
1212 {
1213 $query = 'SELECT * FROM #__acym_campaign WHERE sending_params LIKE \'%"abtest":%\' AND draft = 0 AND active = 1 AND sent = 1';
1214
1215 return $this->decode(acym_loadObjectList($query));
1216 }
1217
1218 public function finishAbTestCampaign(object $campaign): bool
1219 {
1220 $abTestParams = empty($campaign->sending_params['abtest']) ? [] : $campaign->sending_params['abtest'];
1221
1222 if (empty($abTestParams['B']) || empty($abTestParams['repartition']) || !empty($abTestParams['abtest_finished']) || empty($abTestParams['after']['action'])) {
1223 return false;
1224 }
1225
1226 if (strtotime($campaign->sending_date.' + '.$abTestParams['after']['days'].' days') > time()) {
1227 return false;
1228 }
1229
1230 $mailStatsClass = new MailStatClass();
1231 $mailIds = [$campaign->mail_id, $abTestParams['B']];
1232 $bestMails = $mailStatsClass->getBestEmailByStats($mailIds, $abTestParams['after']['action']);
1233
1234 if (empty($bestMails)) {
1235 return false;
1236 }
1237
1238 $mailClass = new MailClass();
1239
1240 switch ($abTestParams['after']['action']) {
1241 case 'open_rate':
1242 case 'click_rate':
1243 $mail = $mailClass->getOneById($bestMails[$abTestParams['after']['action']]);
1244
1245 if (empty($mail)) {
1246 return false;
1247 }
1248 break;
1249 case 'click_open_rate':
1250 $bestMailClickRate = $mailClass->getOneById($bestMails['click_rate']);
1251 $bestMailOpenRate = $mailClass->getOneById($bestMails['open_rate']);
1252
1253 if (empty($bestMailClickRate) || empty($bestMailOpenRate)) {
1254 return false;
1255 }
1256
1257 $mail = $bestMailClickRate;
1258 $mail->preheader = $bestMailOpenRate->preheader;
1259 $mail->subject = $bestMailOpenRate->subject;
1260 $mail->from_name = $bestMailOpenRate->from_name;
1261 $mail->from_email = $bestMailOpenRate->from_email;
1262 $mail->reply_to_name = $bestMailOpenRate->reply_to_name;
1263 $mail->reply_to_email = $bestMailOpenRate->reply_to_email;
1264 break;
1265 }
1266
1267 $mail->id = 0;
1268 $mail->parent_id = $campaign->mail_id;
1269 $mail->name .= ' - '.acym_translation('ACYM_ABTEST_WINNER');
1270 $newMailId = $mailClass->save($mail);
1271 if (empty($newMailId)) {
1272 return false;
1273 }
1274
1275 $campaign->sending_params['abtest']['abtest_finished'] = 1;
1276 $campaign->sending_params['abtest']['final'] = $newMailId;
1277
1278 $this->save($campaign);
1279 $this->send($campaign->id, 0, true);
1280
1281 return true;
1282 }
1283
1284 public function isAbTestMail(int $mailId): bool
1285 {
1286 $mailClass = new MailClass();
1287 $mainMailId = $mailClass->getMainMailId($mailId);
1288
1289 $campaign = acym_loadObject(
1290 'SELECT *
1291 FROM #__acym_campaign
1292 WHERE mail_id = '.intval($mainMailId).'
1293 AND sending_params LIKE \'%"abtest":%\''
1294 );
1295
1296 return !empty($campaign);
1297 }
1298
1299 public function resetAbTestVersion(int $campaignId): void
1300 {
1301 if (empty($campaignId)) {
1302 return;
1303 }
1304
1305 $campaign = $this->getOneById($campaignId);
1306
1307 if (empty($campaign)) {
1308 return;
1309 }
1310
1311 if (empty($campaign->sending_params['abtest'])) {
1312 return;
1313 }
1314
1315 foreach (self::AB_TEST_VERSIONS as $version) {
1316 if (!empty($campaign->sending_params['abtest'][$version])) {
1317 $mailClass = new MailClass();
1318 $mailClass->delete([$campaign->sending_params['abtest'][$version]]);
1319 unset($campaign->sending_params['abtest'][$version]);
1320 }
1321 }
1322
1323 $this->save($campaign);
1324 }
1325
1326 public function getXCampaigns(array $options): array
1327 {
1328 $limit = $options['limit'] ?? 10;
1329 $offset = $options['offset'] ?? 0;
1330 $filters = $options['filters'] ?? [];
1331
1332 $conditions = [];
1333 foreach ($filters as $column => $filter) {
1334 switch ($column) {
1335 case 'id':
1336 case 'draf':
1337 case 'active':
1338 case 'mail_id':
1339 case 'sent':
1340 case 'parent_id':
1341 case 'next_trigger':
1342 case 'visible':
1343 case 'last_trigger':
1344 $conditions[] = 'campaign.'.acym_secureDBColumn($column).' = '.intval($filter);
1345 break;
1346 case 'name':
1347 case 'subject':
1348 $conditions[] = 'mail.'.acym_secureDBColumn($column).' LIKE '.acym_escapeDB('%'.$filter.'%');
1349 break;
1350 default:
1351 $conditions[] = 'campaign.'.acym_secureDBColumn($column).' = '.acym_escapeDB('%'.$filter.'%');
1352 }
1353 }
1354
1355 $query = 'SELECT campaign.*, mail.subject, mail.name FROM #__acym_campaign AS campaign
1356 LEFT JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id';
1357
1358 if (!empty($conditions)) {
1359 $query .= ' WHERE '.implode(' AND ', $conditions);
1360 }
1361
1362 return $this->decode(acym_loadObjectList($query, $this->pkey, $offset, $limit));
1363 }
1364
1365 public function getOneCampaign(): bool
1366 {
1367 $firstCampaign = acym_loadObject('SELECT * FROM #__acym_campaign');
1368
1369 return !empty($firstCampaign);
1370 }
1371 }
1372