| @@ -241,9 +241,9 @@ | ||
| 241 | 241 | $startDate = $clickDate; |
| 242 | 242 | } |
| 243 | 243 | } |
| 244 | 244 | |
| 245 | - $lastOpen = end($campaignOpens); | |
| 245 | + $lastOpen = empty($campaignOpens) ? null : end($campaignOpens); | |
| 246 | 246 | $endDate = $lastOpen->open_date ?? null; |
| 247 | 247 | if (!empty($campaignClicks)) { |
| 248 | 248 | $lastClick = end($campaignClicks); |
| 249 | 249 | $clickDate = $lastClick->date_click ?? null; |
| @@ -288,20 +288,22 @@ | ||
| 288 | 288 | } |
| 289 | 289 | |
| 290 | 290 | public function prepareLineChart(object &$statsCampaignSelected, array $mailIdsOfCampaign, string $newStart = '', string $newEnd = ''): void |
| 291 | 291 | { |
| 292 | - $campaignClass = new CampaignClass(); | |
| 293 | 292 | $statsCampaignSelected->hasStats = true; |
| 293 | + $anonymousStats = !empty($this->config->get('anonymous_stats', 0)); | |
| 294 | 294 | |
| 295 | 295 | //We get the opening by month, day, hour |
| 296 | - $campaignOpenByMonth = $campaignClass->getOpenByMonth($mailIdsOfCampaign, $newStart, $newEnd); | |
| 297 | - $campaignOpenByDay = $campaignClass->getOpenByDay($mailIdsOfCampaign, $newStart, $newEnd); | |
| 298 | - $campaignOpenByHour = $campaignClass->getOpenByHour($mailIdsOfCampaign, $newStart, $newEnd); | |
| 299 | - | |
| 300 | - if (empty($campaignOpenByMonth) || empty($campaignOpenByDay) || empty($campaignOpenByHour)) { | |
| 301 | - $statsCampaignSelected->hasStats = false; | |
| 302 | - | |
| 303 | - return; | |
| 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); | |
| 304 | 306 | } |
| 305 | 307 | |
| 306 | 308 | $urlClickClass = new UrlClickClass(); |
| 307 | 309 | $campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd); |
| @@ -307,8 +309,17 @@ | ||
| 307 | 309 | $campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd); |
| 308 | 310 | $campaignClickByDay = $urlClickClass->getAllClickByMailDay($mailIdsOfCampaign, $newStart, $newEnd); |
| 309 | 311 | $campaignClickByHour = $urlClickClass->getAllClickByMailHour($mailIdsOfCampaign, $newStart, $newEnd); |
| 310 | 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 | + | |
| 311 | 322 | $statsCampaignSelected->month = $this->getValues('day', 'P1M', $campaignOpenByMonth, $campaignClickByMonth, 'Y-m'); |
| 312 | 323 | $statsCampaignSelected->day = $this->getValues('hour', 'P1D', $campaignOpenByDay, $campaignClickByDay, 'Y-m-d'); |
| 313 | 324 | $statsCampaignSelected->hour = $this->getValues('min', 'PT1H', $campaignOpenByHour, $campaignClickByHour, 'Y-m-d H:i', true); |
| 314 | 325 | |
| @@ -342,9 +353,14 @@ | ||
| 342 | 353 | |
| 343 | 354 | public function prepareOpenTimeChart(array &$data): void |
| 344 | 355 | { |
| 345 | 356 | $userStatClass = new UserStatClass(); |
| 346 | - $statsDB = $userStatClass->getOpenTimeStats($this->selectedMailIds); | |
| 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 | + } | |
| 347 | 363 | |
| 348 | 364 | if (empty($statsDB['total_open'])) { |
| 349 | 365 | $data['openTime'] = $userStatClass->getDefaultStat(); |
| 350 | 366 | $data['empty_open'] = true; |
| @@ -372,20 +388,17 @@ | ||
| 372 | 388 | } |
| 373 | 389 | |
| 374 | 390 | public function prepareStatByList(array &$data): void |
| 375 | 391 | { |
| 376 | - $mailid = (count(array_unique($data['selectedMailid'])) != 1) ? 0 : $data['selectedMailid'][0]; | |
| 392 | + $selectedMailIds = array_unique($data['selectedMailid']); | |
| 393 | + $mailid = count($selectedMailIds) !== 1 ? 0 : (int)reset($selectedMailIds); | |
| 377 | 394 | |
| 378 | 395 | if (empty($mailid)) { |
| 379 | 396 | return; |
| 380 | 397 | } |
| 381 | 398 | |
| 382 | - $mailSelected = $this->getVarFiltersListing('int', 'mail_id_language', $mailid); | |
| 399 | + $mailSelected = (int)$this->getVarFiltersListing('int', 'mail_id_language', $mailid); | |
| 383 | 400 | |
| 384 | - $mailClass = new MailClass(); | |
| 385 | - $mail = $mailClass->getOneById($mailSelected); | |
| 386 | - $mainMailId = empty($mail->parent_id) ? $mail->id : $mail->parent_id; | |
| 387 | - | |
| 388 | 401 | $data['lists'] = []; |
| 389 | 402 | $data['emailsSent'] = []; |
| 390 | 403 | $data['emailsOpen'] = []; |
| 391 | 404 | $data['bounces'] = []; |
| @@ -393,12 +406,26 @@ | ||
| 393 | 406 | $data['click'] = []; |
| 394 | 407 | $data['listsStats'] = []; |
| 395 | 408 | $data['userPerList'] = []; |
| 396 | 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 | + | |
| 397 | 424 | // get lists from the selected campaign |
| 398 | 425 | $query = 'SELECT l.id, l.name, l.color FROM #__acym_list l'; |
| 399 | 426 | $query .= ' JOIN #__acym_mail_has_list ml on l.id = ml.list_id'; |
| 400 | - $query .= ' WHERE ml.mail_id = '.$mainMailId; | |
| 427 | + $query .= ' WHERE ml.mail_id = '.intval($mainMailId); | |
| 401 | 428 | |
| 402 | 429 | $data['lists'] = acym_loadObjectList($query, 'id'); |
| 403 | 430 | $listIds = implode(',', array_keys($data['lists'])); |
| 404 | 431 | |
| @@ -518,10 +545,15 @@ | ||
| 518 | 545 | } |
| 519 | 546 | |
| 520 | 547 | private function prepareDevicesStats(array &$data): void |
| 521 | 548 | { |
| 522 | - $campaignClass = new CampaignClass(); | |
| 523 | - $devicesCampaign = $campaignClass->getDevicesWithCountByMailId($this->selectedMailIds); | |
| 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 | + } | |
| 524 | 556 | |
| 525 | 557 | $defaultDataDevices = [ |
| 526 | 558 | 'ACYM_MOBILE' => 0, |
| 527 | 559 | 'ACYM_DESKTOP' => 0, |
| @@ -554,10 +586,15 @@ | ||
| 554 | 586 | } |
| 555 | 587 | |
| 556 | 588 | private function prepareOpenSourcesStats(array &$data): void |
| 557 | 589 | { |
| 558 | - $userStatClass = new UserStatClass(); | |
| 559 | - $openedFromStats = $userStatClass->getOpenSourcesStats($this->selectedMailIds); | |
| 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 | + } | |
| 560 | 597 | |
| 561 | 598 | $data['openedWith'] = []; |
| 562 | 599 | foreach ($openedFromStats as $oneSource) { |
| 563 | 600 | if (empty($oneSource->number)) { |
| @@ -596,29 +633,45 @@ | ||
| 596 | 633 | $data['mail']->sent, |
| 597 | 634 | $data['mail']->totalMail |
| 598 | 635 | ); |
| 599 | 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 | + | |
| 600 | 653 | //open rate |
| 601 | - $openRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getOpenRateAllCampaign() : $campaignClass->getOpenRateCampaigns($this->selectedMailIds); | |
| 602 | - $data['mail']->percentageOpen = empty($openRateCampaign->sent) ? 0 : number_format(($openRateCampaign->open_unique * 100) / $openRateCampaign->sent, 2); | |
| 603 | - $data['mail']->allOpen = empty($openRateCampaign->sent) | |
| 604 | - ? acym_translationSprintf('ACYM_X_MAIL_OPENED_OF_X', 0, 0) | |
| 605 | - : acym_translationSprintf( | |
| 606 | - 'ACYM_X_MAIL_OPENED_OF_X', | |
| 607 | - $openRateCampaign->open_unique, | |
| 608 | - $openRateCampaign->sent | |
| 609 | - ); | |
| 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); | |
| 610 | 663 | |
| 611 | 664 | //click rate |
| 612 | - $clickRateCampaign = $urlClickClass->getNumberUsersClicked($this->selectedMailIds); | |
| 613 | - $data['mail']->percentageClick = empty($data['mail']->sent) ? 0 : number_format(($clickRateCampaign * 100) / $data['mail']->sent, 2); | |
| 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); | |
| 614 | 671 | $data['mail']->allClick = empty($data['mail']->sent) |
| 615 | - ? acym_translationSprintf('ACYM_X_MAIL_CLICKED_OF_X', 0, 0) | |
| 616 | - : acym_translationSprintf( | |
| 617 | - 'ACYM_X_MAIL_CLICKED_OF_X', | |
| 618 | - $clickRateCampaign, | |
| 619 | - $data['mail']->sent | |
| 620 | - ); | |
| 672 | + ? acym_translationSprintf($clickLabelKey, 0, 0) | |
| 673 | + : acym_translationSprintf($clickLabelKey, $clickCount, $data['mail']->sent); | |
| 621 | 674 | |
| 622 | 675 | //bounce rate |
| 623 | 676 | $bounceRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getBounceRateAllCampaign() : $campaignClass->getBounceRateCampaigns($this->selectedMailIds); |
| 624 | 677 | $data['mail']->percentageBounce = empty($data['mail']->sent) ? 0 : number_format(($bounceRateCampaign->bounce_unique * 100) / $data['mail']->sent, 2); |
| @@ -666,10 +719,10 @@ | ||
| 666 | 719 | |
| 667 | 720 | private function prepareLinksDetailsListing(array &$data): void |
| 668 | 721 | { |
| 669 | 722 | $data['search'] = $this->getVarFiltersListing('string', 'links_details_search', ''); |
| 670 | - $data['ordering'] = $this->getVarFiltersListing('string', 'links_details_ordering', 'id'); | |
| 671 | - $data['orderingSortOrder'] = $this->getVarFiltersListing('string', 'links_details_ordering_sort_order', 'desc'); | |
| 723 | + $data['ordering'] = $this->getVarFiltersListing('cmd', 'links_details_ordering', 'id'); | |
| 724 | + $data['orderingSortOrder'] = $this->getVarFiltersListing('cmd', 'links_details_ordering_sort_order', 'desc'); | |
| 672 | 725 | |
| 673 | 726 | if (empty($this->selectedMailIds)) return; |
| 674 | 727 | |
| 675 | 728 | $pagination = new PaginationHelper(); |