PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.1
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.1
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
← All changes | back/Controllers/Stats/GlobalStats.php +0 -944 11.0.5 → 11.1.1 View file →
@@ -1,944 +1,0 @@
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 -}