]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/History.php
Improve coding standards
[galette.git] / galette / lib / Galette / Core / History.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 namespace Galette\Core;
23
24 use Throwable;
25 use Analog\Analog;
26 use Galette\Filters\HistoryList;
27 use Laminas\Db\Sql\Expression;
28 use Laminas\Db\Adapter\Adapter;
29 use Laminas\Db\Sql\Select;
30
31 /**
32 * History management
33 *
34 * @author Johan Cwiklinski <johan@x-tnd.be>
35 *
36 * @property HistoryList $filters
37 */
38
39 class History
40 {
41 public const TABLE = 'logs';
42 public const PK = 'id_log';
43
44 protected int $count;
45 protected Db $zdb;
46 protected Login $login;
47 protected Preferences $preferences;
48 protected HistoryList $filters;
49
50 /** @var array<int, string> */
51 protected array $users;
52 /** @var array<int, string> */
53 protected array $actions;
54
55 protected bool $with_lists = true;
56
57 /**
58 * Default constructor
59 *
60 * @param Db $zdb Database
61 * @param Login $login Login
62 * @param Preferences $preferences Preferences
63 * @param ?HistoryList $filters Filtering
64 */
65 public function __construct(Db $zdb, Login $login, Preferences $preferences, HistoryList $filters = null)
66 {
67 $this->zdb = $zdb;
68 $this->login = $login;
69 $this->preferences = $preferences;
70
71 if ($filters === null) {
72 $this->filters = new HistoryList();
73 } else {
74 $this->filters = $filters;
75 }
76 }
77
78 /**
79 * Helper function to find the user IP address
80 *
81 * This function uses the client address or the appropriate part of
82 * X-Forwarded-For, if present and the configuration specifies it.
83 * (blindly trusting X-Forwarded-For would make the IP address logging
84 * very easy to deveive.
85 *
86 * @return string
87 */
88 public static function findUserIPAddress(): string
89 {
90 if (
91 defined('GALETTE_X_FORWARDED_FOR_INDEX')
92 && isset($_SERVER['HTTP_X_FORWARDED_FOR'])
93 ) {
94 $split_xff = preg_split('/,\s*/', $_SERVER['HTTP_X_FORWARDED_FOR']);
95 return $split_xff[count($split_xff) - GALETTE_X_FORWARDED_FOR_INDEX];
96 }
97 return $_SERVER['REMOTE_ADDR'];
98 }
99
100 /**
101 * Add a new entry
102 *
103 * @param string $action the action to log
104 * @param string $argument the argument
105 * @param string $query the query (if relevant)
106 *
107 * @return bool true if entry was successfully added, false otherwise
108 */
109 public function add(string $action, string $argument = '', string $query = ''): bool
110 {
111 if ($this->preferences->pref_log == Preferences::LOG_DISABLED) {
112 //logs are disabled
113 return true;
114 }
115
116 $ip = null;
117 if (PHP_SAPI === 'cli') {
118 $ip = '127.0.0.1';
119 } else {
120 $ip = self::findUserIpAddress();
121 }
122
123 try {
124 $values = array(
125 'date_log' => date('Y-m-d H:i:s'),
126 'ip_log' => $ip,
127 'adh_log' => $this->login->login ?? '',
128 'action_log' => $action,
129 'text_log' => $argument,
130 'sql_log' => $query
131 );
132
133 $insert = $this->zdb->insert($this->getTableName());
134 $insert->values($values);
135 $this->zdb->execute($insert);
136 } catch (Throwable $e) {
137 Analog::log(
138 "An error occurred trying to add log entry. " . $e->getMessage(),
139 Analog::ERROR
140 );
141 throw $e;
142 }
143
144 return true;
145 }
146
147 /**
148 * Delete all entries
149 *
150 * @return boolean
151 */
152 public function clean(): bool
153 {
154 try {
155 $this->zdb->db->query(
156 'TRUNCATE TABLE ' . $this->getTableName(true),
157 Adapter::QUERY_MODE_EXECUTE
158 );
159 $this->add('Logs flushed');
160 $this->filters = new HistoryList();
161 return true;
162 } catch (Throwable $e) {
163 $this->add('Error flushing logs');
164 Analog::log(
165 'Unable to flush logs. | ' . $e->getMessage(),
166 Analog::WARNING
167 );
168 throw $e;
169 }
170 }
171
172 /**
173 * Get the entire history list
174 *
175 * @return array<int, object>
176 */
177 public function getHistory(): array
178 {
179 try {
180 $select = $this->zdb->select($this->getTableName());
181 $this->buildWhereClause($select);
182 $select->order($this->buildOrderClause());
183 if ($this->with_lists === true) {
184 $this->buildLists($select);
185 }
186 $this->proceedCount($select);
187 //add limits to retrieve only relavant rows
188 $this->filters->setLimits($select);
189 $results = $this->zdb->execute($select);
190
191 $entries = [];
192 foreach ($results as $result) {
193 $entries[] = $result;
194 }
195
196 return $entries;
197 } catch (Throwable $e) {
198 Analog::log(
199 'Unable to get history. | ' . $e->getMessage(),
200 Analog::WARNING
201 );
202 throw $e;
203 }
204 }
205
206 /**
207 * Builds users and actions lists
208 *
209 * @param Select $select Original select
210 *
211 * @return void
212 */
213 private function buildLists(Select $select): void
214 {
215 try {
216 $usersSelect = clone $select;
217 $usersSelect->reset($usersSelect::COLUMNS);
218 $usersSelect->reset($usersSelect::ORDER);
219 $usersSelect->quantifier('DISTINCT')->columns(['adh_log']);
220 $usersSelect->order(['adh_log ASC']);
221
222 $results = $this->zdb->execute($usersSelect);
223
224 $this->users = [];
225 foreach ($results as $result) {
226 $this->users[] = $result->adh_log;
227 }
228 } catch (Throwable $e) {
229 Analog::log(
230 'Cannot list members from history! | ' . $e->getMessage(),
231 Analog::WARNING
232 );
233 }
234
235 try {
236 $actionsSelect = clone $select;
237 $actionsSelect->reset($actionsSelect::COLUMNS);
238 $actionsSelect->reset($actionsSelect::ORDER);
239 $actionsSelect->quantifier('DISTINCT')->columns(['action_log']);
240 $actionsSelect->order(['action_log ASC']);
241
242 $results = $this->zdb->execute($actionsSelect);
243
244 $this->actions = [];
245 foreach ($results as $result) {
246 $this->actions[] = $result->action_log;
247 }
248 } catch (Throwable $e) {
249 Analog::log(
250 'Cannot list actions from history! | ' . $e->getMessage(),
251 Analog::WARNING
252 );
253 throw $e;
254 }
255 }
256
257 /**
258 * Builds the order clause
259 *
260 * @return array<int, string> SQL ORDER clauses
261 */
262 protected function buildOrderClause(): array
263 {
264 $order = array();
265
266 switch ($this->filters->orderby) {
267 case HistoryList::ORDERBY_DATE:
268 $order[] = 'date_log ' . $this->filters->ordered;
269 break;
270 case HistoryList::ORDERBY_IP:
271 $order[] = 'ip_log ' . $this->filters->ordered;
272 break;
273 case HistoryList::ORDERBY_USER:
274 $order[] = 'adh_log ' . $this->filters->ordered;
275 break;
276 case HistoryList::ORDERBY_ACTION:
277 $order[] = 'action_log ' . $this->filters->ordered;
278 break;
279 }
280
281 return $order;
282 }
283
284 /**
285 * Builds where clause, for filtering on simple list mode
286 *
287 * @param Select $select Original select
288 *
289 * @return void
290 */
291 private function buildWhereClause(Select $select): void
292 {
293 try {
294 if ($this->filters->start_date_filter != null) {
295 $d = new \DateTime($this->filters->raw_start_date_filter);
296 $d->setTime(0, 0, 0);
297 $select->where->greaterThanOrEqualTo(
298 'date_log',
299 $d->format('Y-m-d H:i:s')
300 );
301 }
302
303 if ($this->filters->end_date_filter != null) {
304 $d = new \DateTime($this->filters->raw_end_date_filter);
305 $d->setTime(23, 59, 59);
306 $select->where->lessThanOrEqualTo(
307 'date_log',
308 $d->format('Y-m-d H:i:s')
309 );
310 }
311
312 //@phpstan-ignore-next-line
313 if ($this->filters->user_filter != null && $this->filters->user_filter != '0') {
314 $select->where->equalTo(
315 'adh_log',
316 $this->filters->user_filter
317 );
318 }
319
320 //@phpstan-ignore-next-line
321 if ($this->filters->action_filter != null && $this->filters->action_filter != '0') {
322 $select->where->equalTo(
323 'action_log',
324 $this->filters->action_filter
325 );
326 }
327 } catch (Throwable $e) {
328 Analog::log(
329 __METHOD__ . ' | ' . $e->getMessage(),
330 Analog::WARNING
331 );
332 throw $e;
333 }
334 }
335
336 /**
337 * Count history entries from the query
338 *
339 * @param Select $select Original select
340 *
341 * @return void
342 */
343 private function proceedCount(Select $select): void
344 {
345 try {
346 $countSelect = clone $select;
347 $countSelect->reset($countSelect::COLUMNS);
348 $countSelect->reset($countSelect::JOINS);
349 $countSelect->reset($countSelect::ORDER);
350 $countSelect->columns(
351 array(
352 $this->getPk() => new Expression('COUNT(' . $this->getPk() . ')')
353 )
354 );
355
356 $results = $this->zdb->execute($countSelect);
357 $result = $results->current();
358
359 $k = $this->getPk();
360 $this->count = (int)$result->$k;
361 $this->filters->setCounter($this->count);
362 } catch (Throwable $e) {
363 Analog::log(
364 'Cannot count history | ' . $e->getMessage(),
365 Analog::WARNING
366 );
367 throw $e;
368 }
369 }
370
371 /**
372 * Global getter method
373 *
374 * @param string $name name of the property we want to retrieve
375 *
376 * @return mixed the called property
377 */
378 public function __get(string $name): mixed
379 {
380 $forbidden = array();
381 if (!in_array($name, $forbidden)) {
382 return $this->$name;
383 }
384
385 throw new \RuntimeException(
386 sprintf(
387 'Unable to get property "%s::%s"!',
388 __CLASS__,
389 $name
390 )
391 );
392 }
393
394 /**
395 * Global isset method
396 * Required for twig to access properties via __get
397 *
398 * @param string $name name of the property we want to retrieve
399 *
400 * @return bool
401 */
402 public function __isset(string $name): bool
403 {
404 if (isset($this->$name)) {
405 return true;
406 }
407 return false;
408 }
409
410 /**
411 * Global setter method
412 *
413 * @param string $name name of the property we want to assign a value to
414 * @param mixed $value a relevant value for the property
415 *
416 * @return void
417 */
418 public function __set(string $name, mixed $value): void
419 {
420 Analog::log(
421 '[History] Setting property `' . $name . '`',
422 Analog::DEBUG
423 );
424
425 $forbidden = array();
426 if (!in_array($name, $forbidden)) {
427 switch ($name) {
428 default:
429 $this->$name = $value;
430 break;
431 }
432 } else {
433 Analog::log(
434 '[History] Unable to set property `' . $name . '`',
435 Analog::WARNING
436 );
437 }
438 }
439
440 /**
441 * Get table's name
442 *
443 * @param boolean $prefixed Whether table name should be prefixed
444 *
445 * @return string
446 */
447 protected function getTableName(bool $prefixed = false): string
448 {
449 if ($prefixed === true) {
450 return PREFIX_DB . self::TABLE;
451 } else {
452 return self::TABLE;
453 }
454 }
455
456 /**
457 * Get table's PK
458 *
459 * @return string
460 */
461 protected function getPk(): string
462 {
463 return self::PK;
464 }
465
466 /**
467 * Set filters
468 *
469 * @param HistoryList $filters Filters
470 *
471 * @return self
472 */
473 public function setFilters(HistoryList $filters): self
474 {
475 $this->filters = $filters;
476 return $this;
477 }
478
479 /**
480 * Get count for current query
481 *
482 * @return int
483 */
484 public function getCount(): int
485 {
486 return $this->count;
487 }
488
489 /**
490 * Get users list
491 *
492 * @return array<int, string>
493 */
494 public function getUsersList(): array
495 {
496 return $this->users;
497 }
498
499 /**
500 * Get actions list
501 *
502 * @return array<int, string>
503 */
504 public function getActionsList(): array
505 {
506 return $this->actions;
507 }
508 }