Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
StatisticsSessionsController
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
4 / 4
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 index
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
2
 viewDay
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 viewSession
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * The Session statistics Administration Controller
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2024-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2024-11-25
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Administration;
21
22use phpMyFAQ\Administration\Session as AdminSession;
23use phpMyFAQ\Core\Exception;
24use phpMyFAQ\Date;
25use phpMyFAQ\Enums\PermissionType;
26use phpMyFAQ\Filter;
27use phpMyFAQ\Helper\StatisticsHelper;
28use phpMyFAQ\Session\Token;
29use phpMyFAQ\Translation;
30use phpMyFAQ\Visits;
31use Symfony\Component\HttpFoundation\Request;
32use Symfony\Component\HttpFoundation\Response;
33use Symfony\Component\Routing\Attribute\Route;
34use Twig\Error\LoaderError;
35
36final class StatisticsSessionsController extends AbstractAdministrationController
37{
38    public function __construct(
39        private readonly AdminSession $adminSession,
40        private readonly Date $date,
41        private readonly Visits $visits,
42    ) {
43        parent::__construct();
44    }
45
46    /**
47     * @throws LoaderError
48     * @throws Exception
49     * @throws \Exception
50     */
51    #[Route(path: '/statistics/sessions', name: 'admin.statistics.sessions', methods: ['GET'])]
52    public function index(Request $request): Response
53    {
54        $this->userHasPermission(PermissionType::STATISTICS_VIEWLOGS);
55
56        $adminSession = $this->adminSession;
57        $date = $this->date;
58        $visits = $this->visits;
59        $statisticsHelper = new StatisticsHelper($adminSession, $visits, $date);
60
61        $stats = $statisticsHelper->getTrackingFilesStatistics();
62        $visitsPerDay = $adminSession->getNumberOfSessions();
63
64        return $this->render(file: '@admin/statistics/sessions.twig', context: [
65            ...$this->getHeader($request),
66            ...$this->getFooter(),
67            'adminHeaderSessions' => Translation::get(key: 'ad_stat_sess'),
68            'csrfTokenClearVisits' => Token::getInstance($this->session)->getTokenString(page: 'clear-visits'),
69            'msgClearVisits' => Translation::get(key: 'ad_clear_all_visits'),
70            'msgDays' => Translation::get(key: 'ad_stat_days'),
71            'numberOfDays' => $stats->numberOfDays,
72            'msgVisits' => Translation::get(key: 'ad_stat_vis'),
73            'numberOfVisits' => $visitsPerDay,
74            'msgVisitsPerDay' => Translation::get(key: 'ad_stat_vpd'),
75            'visitsPerDay' => (int) $stats->numberOfDays !== 0
76                ? round($visitsPerDay / (int) $stats->numberOfDays, precision: 2)
77                : 0,
78            'msgFirstDate' => Translation::get(key: 'ad_stat_fien'),
79            'firstDate' => $statisticsHelper->getFirstTrackingDate((int) $stats->firstDate),
80            'msgLastDate' => Translation::get(key: 'ad_stat_laen'),
81            'lastDate' => $statisticsHelper->getLastTrackingDate((int) $stats->lastDate),
82            'msgSessionBrowse' => Translation::get(key: 'ad_stat_browse'),
83            'renderedDaySelector' => $statisticsHelper->renderDaySelector(),
84            'buttonOkay' => Translation::get(key: 'ad_stat_ok'),
85            'msgSessionManagement' => Translation::get(key: 'ad_stat_management'),
86            'csrfTokenSessions' => Token::getInstance($this->session)->getTokenInput(page: 'sessions'),
87            'msgChooseMonth' => Translation::get(key: 'ad_stat_choose'),
88            'renderedMonthSelector' => $statisticsHelper->renderMonthSelector(),
89            'buttonDeleteMonth' => Translation::get(key: 'ad_stat_delete'),
90            'csrfTokenExport' => Token::getInstance($this->session)->getTokenString(page: 'export-sessions'),
91            'dateToday' => date(format: 'Y-m-d'),
92            'datePickerMinDate' => date(format: 'Y-m-d', timestamp: (int) $stats->firstDate),
93        ]);
94    }
95
96    /**
97     * @throws Exception
98     * @throws LoaderError
99     * @throws \Exception
100     */
101    #[Route(path: '/statistics/sessions/{date}', name: 'admin.statistics.sessions.day', methods: ['POST', 'GET'])]
102    public function viewDay(Request $request): Response
103    {
104        $this->userHasPermission(PermissionType::STATISTICS_VIEWLOGS);
105
106        $day = (int) Filter::filterVar($request->getPayload()->get(key: 'day'), FILTER_VALIDATE_INT);
107
108        if ($day === 0) {
109            $day = (int) strtotime((string) $request->attributes->get(key: 'date'));
110        }
111
112        $firstHour = (int) strtotime(datetime: 'midnight', baseTimestamp: $day);
113        $lastHour = (int) strtotime(datetime: 'tomorrow', baseTimestamp: $firstHour) - 1;
114
115        $sessionData = $this->adminSession->getSessionsByDate($firstHour, $lastHour);
116
117        return $this->render(file: '@admin/statistics/sessions.day.twig', context: [
118            ...$this->getHeader($request),
119            ...$this->getFooter(),
120            'adminHeaderSessionsPerDay' => Translation::get(key: 'ad_sess_session'),
121            'currentDay' => date(format: 'Y-m-d', timestamp: $day),
122            'msgIpAddress' => Translation::get(key: 'ad_sess_ip'),
123            'msgSessionDate' => Translation::get(key: 'ad_sess_s_date'),
124            'msgSession' => Translation::get(key: 'ad_sess_session'),
125            'sessionData' => $sessionData,
126        ]);
127    }
128
129    /**
130     * @throws Exception
131     * @throws LoaderError
132     * @throws \Exception
133     */
134    #[Route(path: '/statistics/session/{sessionId}', name: 'admin.statistics.session.id', methods: ['GET'])]
135    public function viewSession(Request $request): Response
136    {
137        $this->userHasPermission(PermissionType::STATISTICS_VIEWLOGS);
138
139        $sessionId = (int) Filter::filterVar($request->attributes->get(key: 'sessionId'), FILTER_VALIDATE_INT);
140
141        $time = $this->adminSession->getTimeFromSessionId($sessionId);
142        $trackingFile = (string) PMF_CONTENT_DIR . '/core/data/tracking' . date(format: 'dmY', timestamp: $time);
143        $trackingFileContent = file_get_contents($trackingFile);
144        $trackingData = explode(separator: "\n", string: $trackingFileContent === false ? '' : $trackingFileContent);
145
146        return $this->render(file: '@admin/statistics/sessions.session.twig', context: [
147            ...$this->getHeader($request),
148            ...$this->getFooter(),
149            'ad_sess_session' => Translation::get(key: 'ad_sess_session'),
150            'sessionId' => $sessionId,
151            'ad_sess_back' => Translation::get(key: 'ad_sess_back'),
152            'ad_sess_referer' => Translation::get(key: 'ad_sess_referer'),
153            'ad_sess_browser' => Translation::get(key: 'ad_sess_browser'),
154            'ad_sess_ip' => Translation::get(key: 'ad_sess_ip'),
155            'trackingData' => $trackingData,
156            'thisDay' => date(format: 'Y-m-d', timestamp: $time),
157        ]);
158    }
159}