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

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