Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.21% covered (success)
84.21%
112 / 133
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
StatisticsHelper
84.21% covered (success)
84.21%
112 / 133
22.22% covered (danger)
22.22%
2 / 9
64.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTrackingFilesStatistics
81.48% covered (success)
81.48%
22 / 27
0.00% covered (danger)
0.00%
0 / 1
9.51
 getFirstTrackingDate
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
6.01
 getLastTrackingDate
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
8.01
 getAllTrackingDates
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
7.04
 deleteTrackingFiles
60.00% covered (warning)
60.00%
12 / 20
0.00% covered (danger)
0.00%
0 / 1
18.74
 clearAllVisits
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
5.40
 renderMonthSelector
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 renderDaySelector
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * The statistics helper class.
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-01-13
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Helper;
21
22use phpMyFAQ\Administration\Session;
23use phpMyFAQ\Date;
24use phpMyFAQ\Translation;
25use phpMyFAQ\Visits;
26use stdClass;
27use Symfony\Component\HttpFoundation\Request;
28
29readonly class StatisticsHelper
30{
31    public function __construct(
32        private Session $session,
33        private Visits $visits,
34        private Date $date,
35    ) {
36    }
37
38    public function getTrackingFilesStatistics(): stdClass
39    {
40        $numberOfDays = 0;
41        $first = PHP_INT_MAX;
42        $last = 0;
43        $dir = opendir((string) PMF_ROOT_DIR . '/content/core/data');
44        if ($dir === false) {
45            $result = new stdClass();
46            $result->numberOfDays = 0;
47            $result->firstDate = $first;
48            $result->lastDate = $last;
49            return $result;
50        }
51
52        while (true) {
53            $dat = readdir($dir);
54            if ($dat === false) {
55                break;
56            }
57
58            if ($dat !== '.' && $dat !== '..') {
59                ++$numberOfDays;
60            }
61
62            if ($this->date->getTrackingFileDateStart($dat) > $last) {
63                $last = $this->date->getTrackingFileDateStart($dat);
64            }
65
66            if (
67                $this->date->getTrackingFileDateStart($dat) < $first
68                && $this->date->getTrackingFileDateStart($dat) > 0
69            ) {
70                $first = $this->date->getTrackingFileDateStart($dat);
71            }
72        }
73
74        closedir($dir);
75
76        $result = new stdClass();
77        $result->numberOfDays = $numberOfDays;
78        $result->firstDate = $first;
79        $result->lastDate = $last;
80
81        return $result;
82    }
83
84    public function getFirstTrackingDate(int $firstDate): string
85    {
86        $request = Request::createFromGlobals();
87        $requestTime = $request->server->get('REQUEST_TIME');
88        $date = 0;
89
90        $trackingFile =
91            (string) PMF_ROOT_DIR . '/content/core/data/tracking' . date(format: 'dmY', timestamp: $firstDate);
92        if (is_file($trackingFile)) {
93            $fp = fopen(filename: $trackingFile, mode: 'r');
94            if ($fp === false) {
95                return Translation::getString('ad_sess_noentry');
96            }
97
98            while (($data = fgetcsv($fp, length: 1024, separator: ';', enclosure: '"', escape: '\\')) !== false) {
99                $field = $data[7] ?? null;
100                $date = is_string($field) && 10 === strlen($field) ? $field : $requestTime;
101            }
102
103            fclose($fp);
104            return $this->date->format(date(format: 'Y-m-d H:i', timestamp: (int) $date));
105        }
106
107        return Translation::getString('ad_sess_noentry');
108    }
109
110    public function getLastTrackingDate(int $lastDate): string
111    {
112        $request = Request::createFromGlobals();
113        $requestTime = $request->server->get('REQUEST_TIME');
114
115        $trackingFile =
116            (string) PMF_ROOT_DIR . '/content/core/data/tracking' . date(format: 'dmY', timestamp: $lastDate);
117        if (is_file($trackingFile)) {
118            $fp = fopen(filename: $trackingFile, mode: 'r');
119            if ($fp === false) {
120                return Translation::getString('ad_sess_noentry');
121            }
122
123            $date = null;
124            while (($data = fgetcsv($fp, length: 1024, separator: ';', enclosure: '"', escape: '\\')) !== false) {
125                $field = $data[7] ?? null;
126                $date = is_string($field) && 10 === strlen($field) ? $field : $requestTime;
127            }
128
129            fclose($fp);
130
131            if ($date === null || $date === 0) {
132                $date = $request->server->get('REQUEST_TIME');
133            }
134
135            return $this->date->format(date(format: 'Y-m-d H:i', timestamp: (int) $date));
136        }
137
138        return Translation::getString('ad_sess_noentry');
139    }
140
141    /**
142     * Returns all tracking dates.
143     *
144     * @return int[]
145     */
146    public function getAllTrackingDates(): array
147    {
148        $dir = opendir((string) PMF_ROOT_DIR . '/content/core/data');
149        $trackingDates = [];
150        if ($dir === false) {
151            return $trackingDates;
152        }
153
154        while (false !== ($dat = readdir($dir))) {
155            if (!($dat !== '.' && $dat !== '..' && strlen($dat) === 16 && !is_dir($dat))) {
156                continue;
157            }
158
159            $trackingDates[] = $this->date->getTrackingFileDateStart($dat);
160        }
161
162        closedir($dir);
163        sort($trackingDates);
164
165        return $trackingDates;
166    }
167
168    public function deleteTrackingFiles(string $month): bool
169    {
170        $dir = opendir((string) PMF_ROOT_DIR . '/content/core/data');
171        $first = PHP_INT_MAX;
172        $last = 0;
173        if ($dir === false) {
174            return false;
175        }
176
177        while (true) {
178            $trackingFile = readdir($dir);
179            if ($trackingFile === false) {
180                break;
181            }
182
183            // The filename format is: trackingDDMMYYYY
184            // e.g.: tracking02042006
185            if (!($trackingFile !== '.' && $trackingFile !== '..' && 10 === strpos($trackingFile, $month))) {
186                continue;
187            }
188
189            $candidateFirst = $this->date->getTrackingFileDateStart($trackingFile);
190            $candidateLast = $this->date->getTrackingFileDateEnd($trackingFile);
191            if ($candidateLast > 0 && $candidateLast > $last) {
192                $last = $candidateLast;
193            }
194
195            if ($candidateFirst > 0 && $candidateFirst < $first) {
196                $first = $candidateFirst;
197            }
198
199            unlink(PMF_CONTENT_DIR . '/core/data/' . $trackingFile);
200        }
201
202        closedir($dir);
203
204        return $this->session->deleteSessions($first, $last);
205    }
206
207    public function clearAllVisits(): bool
208    {
209        $this->visits->resetAll();
210
211        $files = glob(PMF_CONTENT_DIR . '/core/data/*');
212        if ($files === false) {
213            $files = [];
214        }
215
216        foreach ($files as $file) {
217            if (!is_file($file)) {
218                continue;
219            }
220
221            unlink($file);
222        }
223
224        return $this->session->deleteAllSessions();
225    }
226
227    public function renderMonthSelector(): string
228    {
229        $oldValue = (int) mktime(hour: 0, minute: 0, second: 0, month: 1, day: 1, year: 1970);
230        $renderedHtml = sprintf('<option value="" selected>%s</option>', Translation::getString('ad_stat_choose'));
231
232        $trackingDates = $this->getAllTrackingDates();
233        foreach ($trackingDates as $trackingDate) {
234            if (date(format: 'Y-m', timestamp: $oldValue) === date(format: 'Y-m', timestamp: (int) $trackingDate)) {
235                continue;
236            }
237
238            $renderedHtml .= sprintf(
239                '<option value="%s">%s</option>',
240                date(format: 'mY', timestamp: (int) $trackingDate),
241                date(format: 'Y-m', timestamp: (int) $trackingDate),
242            );
243            $oldValue = $trackingDate;
244        }
245
246        return $renderedHtml;
247    }
248
249    public function renderDaySelector(): string
250    {
251        $request = Request::createFromGlobals();
252        $trackingDates = $this->getAllTrackingDates();
253        $renderedHtml = '';
254
255        if ($trackingDates === []) {
256            return sprintf(
257                '%s<option value="" selected>%s</option>',
258                $renderedHtml,
259                Translation::getString('ad_stat_choose'),
260            );
261        }
262
263        foreach ($trackingDates as $trackingDate) {
264            $renderedHtml .= sprintf('<option value="%d"', $trackingDate);
265            if (
266                date(format: 'Y-m-d', timestamp: (int) $trackingDate) === date(
267                    format: 'Y-m-d',
268                    timestamp: (int) $request->server->get('REQUEST_TIME'),
269                )
270            ) {
271                $renderedHtml .= ' selected';
272            }
273
274            $renderedHtml .= '>';
275            $renderedHtml .= $this->date->format(date(format: 'Y-m-d H:i', timestamp: (int) $trackingDate));
276            $renderedHtml .= "</option>\n";
277        }
278
279        return $renderedHtml;
280    }
281}