Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
AdminLogRepository
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
6 / 6
13
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
 getNumberOfEntries
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAll
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 add
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 deleteOlderThan
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getLastHash
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * AdminLog Repository.
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 2025-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     2025-10-16
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Administration;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Database;
24use phpMyFAQ\Entity\AdminLog as AdminLogEntity;
25use phpMyFAQ\User;
26use Symfony\Component\HttpFoundation\Request;
27
28readonly class AdminLogRepository
29{
30    public function __construct(
31        private Configuration $configuration,
32    ) {
33    }
34
35    public function getNumberOfEntries(): int
36    {
37        $query = sprintf('SELECT id FROM %sfaqadminlog', Database::getTablePrefix());
38
39        return $this->configuration->getDb()->numRows($this->configuration->getDb()->query($query));
40    }
41
42    /**
43     * @return array<int, AdminLogEntity>
44     */
45    public function getAll(): array
46    {
47        $data = [];
48
49        $query = sprintf(
50            'SELECT id, time, usr AS user, text, ip, hash, previous_hash FROM %sfaqadminlog ORDER BY id ASC',
51            Database::getTablePrefix(),
52        );
53
54        $result = $this->configuration->getDb()->query($query);
55
56        while (true) {
57            $row = $this->configuration->getDb()->fetchObject($result);
58            if (!is_object($row)) {
59                break;
60            }
61
62            $hash = $row->hash ?? null;
63            $previousHash = $row->previous_hash ?? null;
64
65            $adminLog = new AdminLogEntity();
66            $adminLog
67                ->setId((int) $row->id)
68                ->setTime((int) $row->time)
69                ->setUserId((int) $row->user)
70                ->setText((string) $row->text)
71                ->setIp((string) $row->ip)
72                ->setHash($hash === null ? null : (string) $hash)
73                ->setPreviousHash($previousHash === null ? null : (string) $previousHash);
74            $data[(int) $row->id] = $adminLog;
75        }
76
77        return $data;
78    }
79
80    /**
81     * Adds a new logging entry with hash chain integrity.
82     *
83     * @param User    $user         User object
84     * @param string  $logText      Logged string
85     * @param Request $request      Request object
86     * @param string|null $previousHash Hash of the previous entry
87     */
88    public function add(User $user, string $logText, Request $request, ?string $previousHash = null): bool
89    {
90        $time = (int) $request->server->get('REQUEST_TIME', time());
91        $userId = $user->getUserId();
92        $ip = $request->getClientIp() ?? '';
93
94        // Create a temporary entity to calculate hash
95        $entity = new AdminLogEntity();
96        $entity->setTime($time);
97        $entity->setUserId($userId);
98        $entity->setIp($ip);
99        $entity->setText($logText);
100        $entity->setPreviousHash($previousHash);
101
102        // Calculate hash for this entry
103        $hash = $entity->calculateHash();
104
105        $insert = sprintf(
106            'INSERT INTO %sfaqadminlog (id, time, usr, ip, text, hash, previous_hash) '
107            . "VALUES (%d, %d, %d, '%s', '%s', '%s', %s)",
108            Database::getTablePrefix(),
109            $this->configuration->getDb()->nextId(Database::getTablePrefix() . 'faqadminlog', 'id'),
110            $time,
111            $userId,
112            $this->configuration->getDb()->escape($ip),
113            $this->configuration->getDb()->escape($logText),
114            $hash,
115            $previousHash !== null ? "'" . $this->configuration->getDb()->escape($previousHash) . "'" : 'NULL',
116        );
117
118        return (bool) $this->configuration->getDb()->query($insert);
119    }
120
121    public function deleteOlderThan(int $timestamp): bool
122    {
123        $table = Database::getTablePrefix() . 'faqadminlog';
124        $query = strtr('DELETE FROM table: WHERE time < ts:', [
125            'table:' => $table,
126            'ts:' => (string) $timestamp,
127        ]);
128
129        return (bool) $this->configuration->getDb()->query($query);
130    }
131
132    /**
133     * Returns the hash of the most recent log entry for chain linking.
134     *
135     * @return string|null Hash of the last entry or null if no entries exist
136     */
137    public function getLastHash(): ?string
138    {
139        $query = sprintf('SELECT hash FROM %sfaqadminlog ORDER BY id DESC LIMIT 1', Database::getTablePrefix());
140
141        $result = $this->configuration->getDb()->query($query);
142
143        if ($result !== false) {
144            $row = $this->configuration->getDb()->fetchObject($result);
145            if ($row instanceof \stdClass) {
146                return (string) $row->hash;
147            }
148        }
149
150        return null;
151    }
152}