PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.0
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Classes / CampaignClass.php

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

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