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 / Controllers / Stats / GlobalStats.php

GlobalStats.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.0.5, at back/Controllers/Stats/GlobalStats.php

945 lines 37.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\Controllers\Stats;
4
5 use AcyMailing\Classes\CampaignClass;
6 use AcyMailing\Classes\MailClass;
7 use AcyMailing\Classes\MailStatClass;
8 use AcyMailing\Classes\UrlClickClass;
9 use AcyMailing\Classes\UserStatClass;
10 use AcyMailing\Helpers\ExportHelper;
11 use AcyMailing\Helpers\MailerHelper;
12 use AcyMailing\Helpers\PaginationHelper;
13 use AcyMailing\Helpers\WorkflowHelper;
14 use AcyMailing\Libraries\Browser\BrowserDetection;
15 use AcyMailing\Classes\UserClass;
16
17 trait GlobalStats
18 {
19 public function globalStats(): void
20 {
21 acym_setVar('layout', 'global_stats');
22
23 $data = [];
24
25 $this->prepareDefaultPageInfo($data);
26
27 $this->prepareOpenTimeChart($data);
28 $this->preparecharts($data);
29 $this->prepareListReceivers($data);
30 $this->prepareDefaultLineChart($data);
31 $this->prepareDefaultDevicesChart($data);
32 $this->prepareDefaultBrowsersChart($data);
33 if (count($this->selectedMailIds) == 1) {
34 if ($data['isAbTest']) {
35 $this->prepareAbTestMails($data);
36 } elseif (acym_isMultilingual()) {
37 $this->prepareMultilingualMails($data);
38 }
39 }
40 $this->prepareMailFilter($data);
41
42 parent::display($data);
43 }
44
45 private function prepareMailFilter(array &$data): void
46 {
47 $data['input_mail_ids'] = '';
48
49 if (!empty($this->multiLanguageMailAdded) || !empty($this->generatedMailAdded)) {
50 $this->selectedMailIds = array_filter(
51 $this->selectedMailIds,
52 function ($mailId) {
53 return !in_array($mailId, $this->multiLanguageMailAdded) && !in_array($mailId, $this->generatedMailAdded);
54 }
55 );
56 }
57
58 if (count($this->selectedMailIds) > 1) {
59 $data['input_mail_ids'] = '<input type="hidden" value="'.implode(',', $this->selectedMailIds).'" name="mail_ids">';
60 }
61
62 $attributes = [
63 'class' => 'acym__select acym_select2_ajax acym__stats__select__mails',
64 'data-placeholder' => acym_translation('ACYM_ALL_EMAILS'),
65 'data-ctrl' => 'stats',
66 'data-task' => 'searchSentMail',
67 'data-min' => '0',
68 'id' => 'mail_ids',
69 ];
70
71 if (!empty($this->selectedMailIds)) {
72 $attributes['data-selected'] = implode(',', $this->selectedMailIds);
73 }
74
75 $data['mail_filter_options'] = $attributes;
76 $data['emailVersionsFilters'] = [];
77
78 if (!empty($data['emailVersions'])) {
79 $data['emailVersionsFilters'] = [
80 'options' => $data['emailVersions'],
81 'mailId' => $this->selectedMailIds[0],
82 ];
83 }
84 if (!empty($data['emailTranslations'])) {
85 $data['emailVersionsFilters'] = [
86 'options' => $data['emailTranslations'],
87 'mailId' => $this->selectedMailIds[0],
88 ];
89 }
90 }
91
92 private function prepareClickStats(array &$data): void
93 {
94 if (empty($data['selectedMailid'])) {
95 return;
96 }
97
98 $urlClickClass = new UrlClickClass();
99 $allClickInfo = $urlClickClass->getAllLinkFromEmail($this->selectedMailIds[0]);
100
101 $data['url_click'] = [];
102 $data['url_click']['allClick'] = $allClickInfo['allClick'];
103
104 $allPercentage = [];
105 foreach ($allClickInfo['urls_click'] as $url) {
106 $percentage = 0;
107 if (empty($url->click)) {
108 $data['url_click'][$url->name] = ['percentage' => $percentage, 'numberClick' => '0'];
109 } else {
110 $percentage = intval(($url->click * 100) / $allClickInfo['allClick']);
111 $data['url_click'][$url->name] = ['percentage' => $percentage, 'numberClick' => $url->click];
112 }
113 $allPercentage[] = $percentage;
114 }
115
116 $helperMailer = new MailerHelper();
117 if (!empty($data['mailInformation'])) {
118 $helperMailer->body = $data['mailInformation']->body;
119 $helperMailer->statClick(intval($data['mailInformation']->id), 0, true);
120 $data['mailInformation']->body = preg_replace('#&(amp;)?autoSubId=[^"]+"#Uis', '"', $helperMailer->body);
121 }
122
123 if (!empty($allPercentage)) {
124 $maxPercentage = max($allPercentage);
125
126 foreach ($data['url_click'] as $name => $val) {
127 if ($name === 'allClick') continue;
128 $percentageRecalc = intval(($val['percentage'] * 100) / $maxPercentage);
129 if ($percentageRecalc <= 33) {
130 $data['url_click'][$name]['color'] = '0, 164, 255';
131 } elseif ($percentageRecalc <= 66) {
132 $data['url_click'][$name]['color'] = '248, 31, 255';
133 } else {
134 $data['url_click'][$name]['color'] = '255, 82, 89';
135 }
136 }
137 }
138
139 $data['url_click'] = json_encode($data['url_click']);
140
141 $data['url_foundation_email'] = ACYM_CSS.'libraries/foundation_email.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'libraries'.DS.'foundation_email.min.css');
142 $data['url_click_map_email'] = ACYM_CSS.'click_map.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'click_map.min.css');
143 }
144
145 public function prepareDefaultLineChart(array &$data): void
146 {
147 $data['example_line_chart'] = [
148 'month' => [
149 'Jan 18' => ['open' => '150', 'click' => '40'],
150 ],
151 'day' => [
152 '23 Jan' => ['open' => '150', 'click' => '40'],
153 ],
154 'hour' => [
155 '23 Jan' => ['open' => '150', 'click' => '40'],
156 '23 Jan 08:00' => ['open' => '25', 'click' => '10'],
157 '23 Jan 09:00' => ['open' => '50', 'click' => '10'],
158 '23 Jan 10:00' => ['open' => '16', 'click' => '10'],
159 '23 Jan 11:00' => ['open' => '59', 'click' => '10'],
160 ],
161 ];
162 }
163
164 public function prepareDefaultDevicesChart(array &$data): void
165 {
166 $data['example_devices_chart'] = [
167 'ACYM_MOBILE' => 25662,
168 'ACYM_DESKTOP' => 12471,
169 'ACYM_OTHER' => 3548,
170 'ACYM_UNKNOWN' => 6213,
171 ];
172 }
173
174 public function prepareDefaultBrowsersChart(array &$data): void
175 {
176 $data['example_source_chart'] = [
177 'Apple Mail' => 15835,
178 'Google Chrome' => 13375,
179 'Safari' => 2667,
180 'Outlook' => 2458,
181 'Edge' => 1190,
182 'Firefox' => 826,
183 'ACYM_OTHER' => 4775,
184 'ACYM_UNKNOWN' => 6123,
185 ];
186 }
187
188 public function setDataForChartLine(): void
189 {
190 $newStart = acym_date(acym_getVar('string', 'start'), 'Y-m-d H:i:s');
191 $newEnd = acym_date(acym_getVar('string', 'end'), 'Y-m-d H:i:s');
192 $mailIds = acym_getVar('int', 'id');
193
194 if (empty($mailIds)) {
195 $mailIds = [];
196 }
197
198 $mailClass = new MailClass();
199 if (!empty($mailIds)) {
200 $mailIds = $mailClass->getAutomaticMailIds($mailIds);
201 }
202
203 if (!empty($mailIds) && !is_array($mailIds)) {
204 $mailIds = [$mailIds];
205 }
206
207 if ($newStart >= $newEnd) {
208 echo 'error';
209 exit;
210 }
211
212 $statsCampaignSelected = new \stdClass();
213 $this->prepareLineChart($statsCampaignSelected, $mailIds, $newStart, $newEnd);
214
215 acym_lineChart($statsCampaignSelected->month, $statsCampaignSelected->day, $statsCampaignSelected->hour, true);
216 exit;
217 }
218
219 private function getValues(string $modifier, string $intervalCode, array $campaignOpens, array $campaignClicks, string $dateCode, bool $hour = false): array
220 {
221 $opens = [];
222 foreach ($campaignOpens as $one) {
223 if (!empty($one->open_date)) {
224 $opens[acym_date(acym_getTimeFromUTCDate($one->open_date), $dateCode)] = $one->open;
225 }
226 }
227
228 $clicks = [];
229 foreach ($campaignClicks as $one) {
230 if (!empty($one->date_click)) {
231 $clicks[acym_date(acym_getTimeFromUTCDate($one->date_click), $dateCode)] = $one->click;
232 }
233 }
234
235 $startDate = $campaignOpens[0]->open_date ?? null;
236 if (!empty($campaignClicks) && !empty($campaignClicks[0]->date_click)) {
237 $clickDate = $campaignClicks[0]->date_click;
238 if (!empty($startDate)) {
239 $startDate = min([$startDate, $clickDate]);
240 } else {
241 $startDate = $clickDate;
242 }
243 }
244
245 $lastOpen = empty($campaignOpens) ? null : end($campaignOpens);
246 $endDate = $lastOpen->open_date ?? null;
247 if (!empty($campaignClicks)) {
248 $lastClick = end($campaignClicks);
249 $clickDate = $lastClick->date_click ?? null;
250 if (!empty($endDate) && !empty($clickDate)) {
251 $endDate = max([$endDate, $clickDate]);
252 } elseif (!empty($clickDate)) {
253 $endDate = $clickDate;
254 }
255 }
256
257 if (empty($startDate) || empty($endDate)) {
258 return [];
259 }
260
261 // Apply timezone for chart by hour
262 if ($modifier === 'min') {
263 $startDate = acym_date(acym_getTimeFromUTCDate($startDate), 'Y-m-d H:i:s');
264 $endDate = acym_date(acym_getTimeFromUTCDate($endDate), 'Y-m-d H:i:s');
265 }
266
267 $begin = new \DateTime($startDate);
268 $end = new \DateTime($endDate);
269
270 $end->modify('+1 '.$modifier);
271
272 $interval = new \DateInterval($intervalCode);
273 $daterange = new \DatePeriod($begin, $interval, $end);
274
275 $result = [];
276 foreach ($daterange as $date) {
277 $one = $date->format($dateCode);
278
279 $current = [];
280 $current['open'] = empty($opens[$one]) ? 0 : $opens[$one];
281 $current['click'] = empty($clicks[$one]) ? 0 : $clicks[$one];
282
283 $key = $hour ? $one.':00' : $one;
284 $result[$key] = $current;
285 }
286
287 return $result;
288 }
289
290 public function prepareLineChart(object &$statsCampaignSelected, array $mailIdsOfCampaign, string $newStart = '', string $newEnd = ''): void
291 {
292 $statsCampaignSelected->hasStats = true;
293 $anonymousStats = !empty($this->config->get('anonymous_stats', 0));
294
295 //We get the opening by month, day, hour
296 if ($anonymousStats) {
297 $mailStatClass = new MailStatClass();
298 $campaignOpenByMonth = $mailStatClass->getAnonOpensByFormat($mailIdsOfCampaign, '%Y-%m', $newStart, $newEnd);
299 $campaignOpenByDay = $mailStatClass->getAnonOpensByFormat($mailIdsOfCampaign, '%Y-%m-%d', $newStart, $newEnd);
300 $campaignOpenByHour = $mailStatClass->getAnonOpensByFormat($mailIdsOfCampaign, '%Y-%m-%d %H:00:00', $newStart, $newEnd);
301 } else {
302 $campaignClass = new CampaignClass();
303 $campaignOpenByMonth = $campaignClass->getOpenByMonth($mailIdsOfCampaign, $newStart, $newEnd);
304 $campaignOpenByDay = $campaignClass->getOpenByDay($mailIdsOfCampaign, $newStart, $newEnd);
305 $campaignOpenByHour = $campaignClass->getOpenByHour($mailIdsOfCampaign, $newStart, $newEnd);
306 }
307
308 $urlClickClass = new UrlClickClass();
309 $campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd);
310 $campaignClickByDay = $urlClickClass->getAllClickByMailDay($mailIdsOfCampaign, $newStart, $newEnd);
311 $campaignClickByHour = $urlClickClass->getAllClickByMailHour($mailIdsOfCampaign, $newStart, $newEnd);
312
313 // In anonymous mode a click doesn't register an open, and in any mode a link can be clicked without the open pixel firing: gating on opens alone wrongly hid the whole chart.
314 $hasOpens = !empty($campaignOpenByMonth) && !empty($campaignOpenByDay) && !empty($campaignOpenByHour);
315 $hasClicks = !empty($campaignClickByMonth) && !empty($campaignClickByDay) && !empty($campaignClickByHour);
316 if (!$hasOpens && !$hasClicks) {
317 $statsCampaignSelected->hasStats = false;
318
319 return;
320 }
321
322 $statsCampaignSelected->month = $this->getValues('day', 'P1M', $campaignOpenByMonth, $campaignClickByMonth, 'Y-m');
323 $statsCampaignSelected->day = $this->getValues('hour', 'P1D', $campaignOpenByDay, $campaignClickByDay, 'Y-m-d');
324 $statsCampaignSelected->hour = $this->getValues('min', 'PT1H', $campaignOpenByHour, $campaignClickByHour, 'Y-m-d H:i', true);
325
326 $allHour = array_keys($statsCampaignSelected->hour);
327
328 //those are the dates when the first was open and the last was open
329 $statsCampaignSelected->startEndDateHour = [];
330 if (!empty($allHour)) {
331 $statsCampaignSelected->startEndDateHour['start'] = $allHour[0];
332 $statsCampaignSelected->startEndDateHour['end'] = end($allHour);
333 }
334 }
335
336 public function exportGlobal(): void
337 {
338 wp_verify_nonce(acym_getVar('cmd', '_wpnonce'), 'acymnonce') || die('Invalid Token');
339
340 $exportType = acym_getVar('string', 'export_type', 'charts');
341
342 $functionName = 'exportGlobal'.ucfirst($exportType);
343
344 if (!method_exists($this, $functionName)) {
345 acym_enqueueMessage(acym_translation('ACYM_EXPORT_METHOD_NOT_FOUND'), 'error');
346 $this->listing();
347
348 return;
349 }
350
351 $this->$functionName();
352 }
353
354 public function prepareOpenTimeChart(array &$data): void
355 {
356 $userStatClass = new UserStatClass();
357 if (!empty($this->config->get('anonymous_stats', 0))) {
358 $mailStatClass = new MailStatClass();
359 $statsDB = $mailStatClass->getAnonOpenTimeStats($this->selectedMailIds);
360 } else {
361 $statsDB = $userStatClass->getOpenTimeStats($this->selectedMailIds);
362 }
363
364 if (empty($statsDB['total_open'])) {
365 $data['openTime'] = $userStatClass->getDefaultStat();
366 $data['empty_open'] = true;
367
368 return;
369 }
370
371 $data['empty_open'] = false;
372
373 $stats = [];
374
375 for ($day = 0; $day < 7; $day++) {
376 $stats[$day] = [];
377 for ($hour = 0; $hour < 8; $hour++) {
378 if (empty($statsDB['stats'][$day.'_'.$hour])) {
379 $percentage = 0;
380 } else {
381 $percentage = ($statsDB['stats'][$day.'_'.$hour]->open_total * 100) / $statsDB['total_open'];
382 }
383 $stats[$day][$hour] = round($percentage);
384 }
385 }
386
387 $data['openTime'] = $stats;
388 }
389
390 public function prepareStatByList(array &$data): void
391 {
392 $selectedMailIds = array_unique($data['selectedMailid']);
393 $mailid = count($selectedMailIds) !== 1 ? 0 : (int)reset($selectedMailIds);
394
395 if (empty($mailid)) {
396 return;
397 }
398
399 $mailSelected = (int)$this->getVarFiltersListing('int', 'mail_id_language', $mailid);
400
401 $data['lists'] = [];
402 $data['emailsSent'] = [];
403 $data['emailsOpen'] = [];
404 $data['bounces'] = [];
405 $data['unsubscribed'] = [];
406 $data['click'] = [];
407 $data['listsStats'] = [];
408 $data['userPerList'] = [];
409
410 $mailClass = new MailClass();
411 $mail = $mailClass->getOneById($mailSelected);
412
413 if (empty($mail) || !in_array($mailid, [$mail->id, $mail->parent_id])) {
414 $mailSelected = $mailid;
415 $mail = $mailClass->getOneById($mailSelected);
416 }
417
418 if (empty($mail)) {
419 return;
420 }
421
422 $mainMailId = empty($mail->parent_id) ? $mail->id : $mail->parent_id;
423
424 // get lists from the selected campaign
425 $query = 'SELECT l.id, l.name, l.color FROM #__acym_list l';
426 $query .= ' JOIN #__acym_mail_has_list ml on l.id = ml.list_id';
427 $query .= ' WHERE ml.mail_id = '.intval($mainMailId);
428
429 $data['lists'] = acym_loadObjectList($query, 'id');
430 $listIds = implode(',', array_keys($data['lists']));
431
432 if (!empty($listIds)) {
433 //get nbSent, nbOpen, nbBounce, nbUnsubscribe
434 $query = 'SELECT ul.list_id, SUM(us.sent) as nbSent, SUM(IF(us.open >= 1, 1, 0)) as nbOpen, SUM(us.bounce) as nbBounce, SUM(us.unsubscribe) as nbUnsub from #__acym_user_has_list ul';
435 $query .= ' JOIN #__acym_user_stat us on ul.user_id = us.user_id';
436 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
437 $query .= ' GROUP BY ul.list_id;';
438 $data['listsStats'] = acym_loadObjectList($query, 'list_id');
439
440 //get nbClick
441 $query = 'SELECT ul.list_id, COUNT(uc.click) as nbClick';
442 $query .= ' FROM #__acym_user_has_list ul ';
443 $query .= ' JOIN #__acym_url_click uc on ul.user_id = uc.user_id';
444 $query .= ' WHERE ul.list_id in ('.$listIds.') AND uc.mail_id = '.$mailSelected;
445 $query .= ' GROUP BY ul.list_id;';
446
447 $data['nbClick'] = acym_loadObjectList($query, 'list_id');
448
449 //get nbUser per list
450 $query = 'SELECT ul.list_id, COUNT(ul.user_id) as nbUser';
451 $query .= ' FROM #__acym_user_has_list ul';
452 $query .= ' JOIN #__acym_user_stat us ON ul.user_id = us.user_id';
453 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
454 $query .= ' GROUP BY ul.list_id;';
455
456 $data['userPerList'] = acym_loadObjectList($query, 'list_id');
457 }
458
459 //format data for barChart
460 foreach ($data['listsStats'] as $listId => $item) {
461 $nbSent = empty($item->nbSent) ? 1 : $item->nbSent;
462
463 $data['emailsSent'][$listId] = [
464 'label' => $data['lists'][$listId]->name,
465 'value' => $item->nbSent,
466 'color' => $data['lists'][$listId]->color,
467 ];
468 $data['emailsOpen'][$listId] = [
469 'label' => $data['lists'][$listId]->name,
470 'value' => round(($item->nbOpen * 100) / $nbSent),
471 'color' => $data['lists'][$listId]->color,
472 ];
473 $data['bounces'][$listId] = [
474 'label' => $data['lists'][$listId]->name,
475 'value' => round(($item->nbBounce * 100) / $nbSent),
476 'color' => $data['lists'][$listId]->color,
477 ];
478 $data['unsubscribed'][$listId] = [
479 'label' => $data['lists'][$listId]->name,
480 'value' => round(($item->nbUnsub * 100) / $nbSent),
481 'color' => $data['lists'][$listId]->color,
482 ];
483 }
484
485 foreach ($data['userPerList'] as $listId => $item) {
486 $nbUser = $item->nbUser;
487 // we can't provide lists and their colors if there is no click,
488 // so we initialize the values with $data['lists'] to display lists with no click rather than nothing
489 $data['click'][$listId] = ['label' => $data['lists'][$listId]->name, 'value' => 0, 'color' => $data['lists'][$listId]->color];
490
491 if ($nbUser > 0 && !empty($data['nbClick'][$listId]->nbClick)) {
492 $data['click'][$listId]['value'] = round(($data['nbClick'][$listId]->nbClick * 100) / $nbUser);
493 }
494 }
495 }
496
497 private function prepareAbTestMails(array &$data): void
498 {
499 if (empty($this->selectedMailIds)) return;
500
501 $mailClass = new MailClass();
502
503 $mailVersions = [];
504
505 if (empty($data['mailInformation']->parent_id)) {
506 $mailVersions = $mailClass->getParentAndChildMails($data['mailInformation']->id);
507 } elseif (!empty($data['mailInformation']->parent_id)) {
508 $parentEmail = $mailClass->getOneById($data['mailInformation']->parent_id);
509 if (empty($parentEmail)) return;
510 $mailVersions = $mailClass->getParentAndChildMails($parentEmail->id);
511 }
512
513 $data['emailVersions'] = [];
514
515 foreach ($mailVersions as $email) {
516 $data['emailVersions'][$email->id] = $email->name.' - '.$email->subject;
517 }
518 }
519
520 private function prepareMultilingualMails(array &$data): void
521 {
522 if (empty($this->selectedMailIds)) return;
523
524 $mailClass = new MailClass();
525
526 $translatedEmails = [];
527
528 if (empty($data['mailInformation']->parent_id)) {
529 $translatedEmails = $mailClass->getTranslationsById($data['mailInformation']->id, true, true);
530 } elseif (!empty($data['mailInformation']->parent_id)) {
531 $parentEmail = $mailClass->getOneById($data['mailInformation']->parent_id);
532 if (empty($parentEmail)) return;
533 $translatedEmails = $mailClass->getTranslationsById($parentEmail->id, true, true);
534 }
535
536 $data['emailTranslations'] = [];
537 $allLanguages = acym_getLanguages();
538
539 foreach ($translatedEmails as $email) {
540 if (!empty($email->language)) {
541 $data['emailTranslations'][$email->id] = empty($allLanguages[$email->language]) ? $email->language
542 : $allLanguages[$email->language]->name;
543 }
544 }
545 }
546
547 private function prepareDevicesStats(array &$data): void
548 {
549 if (!empty($this->config->get('anonymous_stats', 0))) {
550 $mailStatClass = new MailStatClass();
551 $devicesCampaign = $mailStatClass->getAnonDevices($this->selectedMailIds);
552 } else {
553 $campaignClass = new CampaignClass();
554 $devicesCampaign = $campaignClass->getDevicesWithCountByMailId($this->selectedMailIds);
555 }
556
557 $defaultDataDevices = [
558 'ACYM_MOBILE' => 0,
559 'ACYM_DESKTOP' => 0,
560 'ACYM_OTHER' => 0,
561 'ACYM_UNKNOWN' => 0,
562 ];
563 $data['devices'] = $defaultDataDevices;
564
565 foreach ($devicesCampaign as $oneDevice) {
566 if (empty($oneDevice->number)) {
567 continue;
568 }
569
570 if (empty($oneDevice->device)) {
571 $device = 'ACYM_UNKNOWN';
572 } elseif (in_array($oneDevice->device, array_merge(BrowserDetection::PLATFORMS_MOBILE, BrowserDetection::LEGACY_PLATFORMS_MOBILE))) {
573 $device = 'ACYM_MOBILE';
574 } elseif (in_array($oneDevice->device, array_merge(BrowserDetection::PLATFORMS_DESKTOP, BrowserDetection::LEGACY_PLATFORMS_DESKTOP))) {
575 $device = 'ACYM_DESKTOP';
576 } else {
577 $device = 'ACYM_OTHER';
578 }
579
580 $data['devices'][$device] += $oneDevice->number;
581 }
582
583 if ($data['devices'] === $defaultDataDevices) {
584 $data['devices'] = [];
585 }
586 }
587
588 private function prepareOpenSourcesStats(array &$data): void
589 {
590 if (!empty($this->config->get('anonymous_stats', 0))) {
591 $mailStatClass = new MailStatClass();
592 $openedFromStats = $mailStatClass->getAnonOpenSources($this->selectedMailIds);
593 } else {
594 $userStatClass = new UserStatClass();
595 $openedFromStats = $userStatClass->getOpenSourcesStats($this->selectedMailIds);
596 }
597
598 $data['openedWith'] = [];
599 foreach ($openedFromStats as $oneSource) {
600 if (empty($oneSource->number)) {
601 continue;
602 }
603
604 if (!empty($oneSource->opened_with) && isset(BrowserDetection::LEGACY_OPENED_WITH_MAP[$oneSource->opened_with])) {
605 $oneSource->opened_with = BrowserDetection::LEGACY_OPENED_WITH_MAP[$oneSource->opened_with];
606 }
607
608 if (empty($oneSource->opened_with)) {
609 $oneSource->opened_with = 'ACYM_UNKNOWN';
610 }
611
612 $data['openedWith'][$oneSource->opened_with] = $oneSource->number;
613 }
614 }
615
616 public function preparecharts(array &$data): void
617 {
618 $mailStatClass = new MailStatClass();
619
620 $data['mail'] = $mailStatClass->getSentFailByMailIds($this->selectedMailIds);
621 if (empty($data['mail'])) return;
622
623 $campaignClass = new CampaignClass();
624 $urlClickClass = new UrlClickClass();
625
626 //For the total opening, the doughnut chart
627 $data['mail']->totalMail = $data['mail']->sent + $data['mail']->fail;
628 $data['mail']->percentageSent = empty($data['mail']->totalMail) ? 0 : number_format(($data['mail']->sent * 100) / $data['mail']->totalMail, 2);
629 $data['mail']->allSent = empty($data['mail']->totalMail)
630 ? acym_translationSprintf('ACYM_X_MAIL_SUCCESSFULLY_SENT_OF_X', 0, 0)
631 : acym_translationSprintf(
632 'ACYM_X_MAIL_SUCCESSFULLY_SENT_OF_X',
633 $data['mail']->sent,
634 $data['mail']->totalMail
635 );
636
637 // Anonymized mode has no per-subscriber data: show total opens/clicks instead of unique counts
638 $anonymousStats = !empty($this->config->get('anonymous_stats', 0));
639 $data['anonymousStats'] = $anonymousStats;
640
641 if ($anonymousStats) {
642 $cumulatedTotals = $mailStatClass->getCumulatedStatsByMailIds($this->selectedMailIds);
643 $openCount = (int)($cumulatedTotals->open ?? 0);
644 $clickCount = (int)($cumulatedTotals->click_unique ?? 0);
645 $openSent = (int)$data['mail']->sent;
646 } else {
647 $openRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getOpenRateAllCampaign() : $campaignClass->getOpenRateCampaigns($this->selectedMailIds);
648 $openCount = (int)$openRateCampaign->open_unique;
649 $openSent = (int)$openRateCampaign->sent;
650 $clickCount = $urlClickClass->getNumberUsersClicked($this->selectedMailIds);
651 }
652
653 //open rate
654 // In anonymous mode $openCount is a total number of opens (not a unique-user count), so use a wording that reflects the total number of times mails were opened
655 $openLabelKey = $anonymousStats ? 'ACYM_X_MAIL_OPENED_TIMES_OF_X' : 'ACYM_X_MAIL_OPENED_OF_X';
656 $data['mail']->openCount = $openCount;
657 // In anonymous mode the count is a total (not a unique), so the rate can exceed 100%: cap it to keep a meaningful percentage
658 $openRate = empty($openSent) ? 0 : ($openCount * 100) / $openSent;
659 $data['mail']->percentageOpen = number_format($anonymousStats ? min($openRate, 100) : $openRate, 2);
660 $data['mail']->allOpen = empty($openSent)
661 ? acym_translationSprintf($openLabelKey, 0, 0)
662 : acym_translationSprintf($openLabelKey, $openCount, $openSent);
663
664 //click rate
665 // In anonymous mode $clickCount is a total (aggregated with user_id = 0), not a unique-user count, so use a wording that reflects total clicks
666 $clickLabelKey = $anonymousStats ? 'ACYM_X_CLICKS_REGISTERED_OF_X' : 'ACYM_X_MAIL_CLICKED_OF_X';
667 $data['mail']->clickCount = $clickCount;
668 // In anonymous mode the count is a total (not a unique), so the rate can exceed 100%: cap it to keep a meaningful percentage
669 $clickRate = empty($data['mail']->sent) ? 0 : ($clickCount * 100) / $data['mail']->sent;
670 $data['mail']->percentageClick = number_format($anonymousStats ? min($clickRate, 100) : $clickRate, 2);
671 $data['mail']->allClick = empty($data['mail']->sent)
672 ? acym_translationSprintf($clickLabelKey, 0, 0)
673 : acym_translationSprintf($clickLabelKey, $clickCount, $data['mail']->sent);
674
675 //bounce rate
676 $bounceRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getBounceRateAllCampaign() : $campaignClass->getBounceRateCampaigns($this->selectedMailIds);
677 $data['mail']->percentageBounce = empty($data['mail']->sent) ? 0 : number_format(($bounceRateCampaign->bounce_unique * 100) / $data['mail']->sent, 2);
678 $data['mail']->allBounce = empty($data['mail']->sent)
679 ? acym_translationSprintf('ACYM_X_BOUNCE_OF_X', 0, 0)
680 : acym_translationSprintf(
681 'ACYM_X_BOUNCE_OF_X',
682 $bounceRateCampaign->bounce_unique,
683 $data['mail']->sent
684 );
685
686 if (!empty($this->selectedMailIds)) {
687 //unsubscribe rate
688 $mailStat = $mailStatClass->getByMailIds($this->selectedMailIds);
689 $data['mail']->allUnsub = 0;
690 $data['mail']->percentageUnsub = 0;
691 if (!empty($mailStat)) {
692 $data['mail']->percentageUnsub = empty($data['mail']->sent) ? 0 : number_format(($mailStat->unsubscribe_total * 100) / $data['mail']->sent, 2);
693 $data['mail']->allUnsub = empty($data['mail']->sent)
694 ? acym_translationSprintf('ACYM_X_USERS_UNSUBSCRIBED_OF_X', 0, 0)
695 : acym_translationSprintf(
696 'ACYM_X_USERS_UNSUBSCRIBED_OF_X',
697 $mailStat->unsubscribe_total,
698 $data['mail']->sent
699 );
700 }
701 }
702
703 $this->prepareDevicesStats($data);
704 $this->prepareOpenSourcesStats($data);
705 $this->prepareLineChart($data['mail'], $this->selectedMailIds);
706 $this->prepareComparedStats($data);
707 }
708
709 private function decode(array &$detailedStats, array $columnsToDecode = ['mail_subject', 'mail_name']): void
710 {
711 foreach ($detailedStats as $oneDetailedStat) {
712 foreach ($columnsToDecode as $column) {
713 if (!empty($oneDetailedStat->$column)) {
714 $oneDetailedStat->$column = acym_utf8Decode($oneDetailedStat->$column);
715 }
716 }
717 }
718 }
719
720 private function prepareLinksDetailsListing(array &$data): void
721 {
722 $data['search'] = $this->getVarFiltersListing('string', 'links_details_search', '');
723 $data['ordering'] = $this->getVarFiltersListing('cmd', 'links_details_ordering', 'id');
724 $data['orderingSortOrder'] = $this->getVarFiltersListing('cmd', 'links_details_ordering_sort_order', 'desc');
725
726 if (empty($this->selectedMailIds)) return;
727
728 $pagination = new PaginationHelper();
729 $urlClickClass = new UrlClickClass();
730
731 $detailedStatsPerPage = $pagination->getListLimit();
732 $page = $this->getVarFiltersListing('int', 'links_details_pagination_page', 1);
733
734 $urlClicks = $urlClickClass->getUrlsFromMailsWithDetails(
735 [
736 'ordering' => $data['ordering'],
737 'search' => $data['search'],
738 'detailedStatsPerPage' => $detailedStatsPerPage,
739 'offset' => ($page - 1) * $detailedStatsPerPage,
740 'ordering_sort_order' => $data['orderingSortOrder'],
741 'mail_ids' => $this->selectedMailIds,
742 ]
743 );
744
745 $this->decode($urlClicks['links_details']);
746
747 // Prepare the pagination
748 $pagination->setStatus((int)$urlClicks['total']->total, $page, $detailedStatsPerPage);
749
750 $data['pagination'] = $pagination;
751 $data['links_details'] = $urlClicks['links_details'];
752 $data['query'] = $urlClicks['query'];
753 }
754
755 private function prepareListReceivers(array &$data): void
756 {
757 if (empty($this->selectedMailIds)) return;
758
759 $mailStatClass = new MailStatClass();
760 $mailClass = new MailClass();
761
762 $data['mailStat'] = $mailStatClass->getByMailIds($this->selectedMailIds);
763 $data['lists'] = $mailClass->getAllListsByMailId($this->selectedMailIds[0]);
764 }
765
766 private function exportGlobalFormatted(): void
767 {
768 $exportHelper = new ExportHelper();
769 $data = [];
770 $this->prepareDefaultPageInfo($data);
771 $data['show_date_filters'] = true;
772 $data['page_title'] = false;
773 $timeLinechart = acym_getVar('string', 'time_linechart', 'month');
774
775 $this->prepareMailFilter($data);
776 $this->prepareClickStats($data);
777 $this->preparecharts($data);
778 $this->prepareDefaultLineChart($data);
779 $this->prepareDefaultDevicesChart($data);
780 $this->prepareDefaultBrowsersChart($data);
781
782 $globalDonut = [
783 $data['mail']->percentageSent,
784 $data['mail']->percentageOpen,
785 $data['mail']->percentageClick,
786 $data['mail']->percentageBounce,
787 $data['mail']->percentageUnsub,
788 ];
789 $mailName = empty($this->selectedMailIds) ? acym_translation('ACYM_ALL_MAILS') : $data['mailInformation']->name;
790 $globalLine = $data['mail']->$timeLinechart;
791
792 $exportHelper->exportStatsFormattedCSV($mailName, $globalDonut, $globalLine, $timeLinechart);
793 exit;
794 }
795
796 private function exportGlobalFull(): void
797 {
798 $exportHelper = new ExportHelper();
799 $data = [];
800 $this->prepareDefaultPageInfo($data);
801
802 $where = '';
803 if (!empty($this->selectedMailIds)) {
804 $where = 'WHERE mail_id IN ('.implode(',', $this->selectedMailIds).')';
805 }
806
807 $columnsMailStat = acym_getColumns('mail_stat');
808 $columnsToExport = [];
809
810 $columnsToExport['mail.subject'] = acym_translation('ACYM_EMAIL_SUBJECT');
811 foreach ($columnsMailStat as $column) {
812 if ($column === 'mail_id') {
813 continue;
814 }
815
816 $translation = acym_translation('ACYM_'.strtoupper($column).'_COLUMN_STAT');
817 if ($column === 'send_date') {
818 $translation = acym_translation('ACYM_SEND_DATE');
819 } elseif ($column === 'click_total') {
820 $translation = acym_translation('ACYM_TOTAL_CLICKS');
821 } elseif ($column === 'click_unique') {
822 $translation = acym_translation('ACYM_UNIQUE_CLICKS');
823 }
824
825 $columnsToExport['mailstat.'.$column] = $translation;
826 }
827
828 $query = 'SELECT '.implode(', ', array_keys($columnsToExport)).' FROM #__acym_mail_stat AS mailstat LEFT JOIN #__acym_mail AS mail ON mail.id = mailstat.mail_id '.$where;
829 $exportHelper->exportStatsFullCSV($query, $columnsToExport);
830 exit;
831 }
832
833 private function prepareComparedStats(array &$data): void
834 {
835 if (empty($data['totalSubscribers']) || empty($data['mail']->percentageOpen) || empty($data['mail']->percentageClick) || empty($data['mail']->percentageBounce)) {
836 return;
837 }
838
839 $currentDate = acym_date('now', 'Y-m-d');
840
841 $newStats = [
842 'date' => $currentDate,
843 'value' => [
844 'totalSubscribers' => $data['totalSubscribers'],
845 'openRate' => $data['mail']->percentageOpen,
846 'clickRate' => $data['mail']->percentageClick,
847 'bounceRate' => $data['mail']->percentageBounce,
848 'totalNewSubscribers' => $data['totalSubscribers'],
849 ],
850 ];
851
852 $keys = [
853 'totalSubscribersHistory' => true,
854 'percentageOpenHistory' => false,
855 'percentageClickHistory' => false,
856 'percentageBounceHistory' => false,
857 'totalNewSubscribersHistory' => false,
858 ];
859
860 $oldEvolutionData = json_decode($this->config->get('statsEvolution', '{}'), true);
861
862 foreach ($keys as $key => $isPercentage) {
863 $history = json_decode($this->config->get($key, '[]'), true);
864
865 if (!empty($history) && end($history)['date'] === $currentDate) {
866 continue;
867 }
868
869 $history[] = ['date' => $currentDate, 'value' => $newStats['value'][$this->getStatKey($key)]];
870
871 if (count($history) > 30) {
872 array_shift($history);
873 }
874
875 $this->config->saveConfig([$key => json_encode($history)]);
876
877 $evolutionKey = $this->getEvolutionKey($key);
878 $oldEvolutionData[$evolutionKey] = $this->calculateEvolution($history, $isPercentage);
879 }
880
881 $this->config->saveConfig(['statsEvolution' => json_encode($oldEvolutionData)]);
882
883 if (!empty($data)) {
884 $this->updateDashboardData($data);
885 }
886 }
887
888 private function getStatKey(string $key): ?string
889 {
890 return [
891 'totalSubscribersHistory' => 'totalSubscribers',
892 'percentageOpenHistory' => 'openRate',
893 'percentageClickHistory' => 'clickRate',
894 'percentageBounceHistory' => 'bounceRate',
895 'totalNewSubscribersHistory' => 'totalNewSubscribers',
896 ][$key] ?? null;
897 }
898
899 private function getEvolutionKey(string $key): ?string
900 {
901 return [
902 'totalSubscribersHistory' => 'totalSubscribersEvolution',
903 'percentageOpenHistory' => 'openRateEvolution',
904 'percentageClickHistory' => 'clickRateEvolution',
905 'percentageBounceHistory' => 'bounceRateEvolution',
906 'totalNewSubscribersHistory' => 'totalNewSubscribersEvolution',
907 ][$key] ?? null;
908 }
909
910 private function calculateEvolution(array $history, bool $isPercentage = false): ?float
911 {
912 if (empty($history)) {
913 return null;
914 }
915
916 $oldValue = $history[0]['value'] ?? 0;
917 $newValue = end($history)['value'] ?? 0;
918
919 if ($oldValue === 0) {
920 return null;
921 }
922
923 if ($isPercentage) {
924 // For totalSubscribers: percentage variation
925 return round((($newValue - $oldValue) / $oldValue) * 100, 2);
926 } else {
927 // For other stats: absolute difference
928 return round($newValue - $oldValue, 2);
929 }
930 }
931
932 private function updateDashboardData(array &$data): void
933 {
934 $evolutionData = json_decode($this->config->get('statsEvolution', '{}'), true);
935
936 if (!empty($evolutionData)) {
937 $data['totalSubscribersEvolution'] = $evolutionData['totalSubscribersEvolution'] ?? null;
938 $data['openRateEvolution'] = $evolutionData['openRateEvolution'] ?? null;
939 $data['clickRateEvolution'] = $evolutionData['clickRateEvolution'] ?? null;
940 $data['bounceRateEvolution'] = $evolutionData['bounceRateEvolution'] ?? null;
941 $data['totalNewSubscribersEvolution'] = $evolutionData['totalNewSubscribersEvolution'] ?? null;
942 }
943 }
944 }
945