Lines 95.65% 44 / 46
Methods 85.71% 6 / 7
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 getNumberOfEntries 100.00% 1 / 1 100.00% 1 / 1 1
 getAll 100.00% 1 / 1 100.00% 1 / 1 1
 log 100.00% 5 / 5 100.00% 1 / 1 2
 delete 100.00% 2 / 2 100.00% 1 / 1 1
 verifyChainIntegrity 94.28% 33 / 35 0.00% 0 / 1 8.01
 calculateHash 100.00% 1 / 1 100.00% 1 / 1 1
32readonly class AdminLog
33{
34    private AdminLogRepository $adminLogRepository;
35
36    /**
37     * Constructor.
38     */
39    public function __construct(
40        private Configuration $configuration,
41    ) {
42        $this->adminLogRepository = new AdminLogRepository($this->configuration);
43    }
44
45    /**
46     * Returns the number of entries.
47     */
48    public function getNumberOfEntries(): int
49    {
50        return $this->adminLogRepository->getNumberOfEntries();
51    }
52
53    /**
54     * Returns all data from the admin log.
55     * @return AdminLogEntity[]
56     */
57    public function getAll(): array
58    {
59        return $this->adminLogRepository->getAll();
60    }
61
62    /**
63     * Adds a new admin log entry.
64     *
65     * @param User   $user    User object
66     * @param string $logText Logged string
67     */
68    public function log(User $user, string $logText = ''): bool
69    {
70        if (!$this->configuration->get(item: 'main.enableAdminLog')) {
71            return false;
72        }
73
74        $request = Request::createFromGlobals();
75
76        // Get the hash of the last log entry for chaining
77        $previousHash = $this->adminLogRepository->getLastHash();
78
79        return $this->adminLogRepository->add($user, $logText, $request, $previousHash);
80    }
81
82    /**
83     * Deletes logging data older than 30 days.
84     */
85    public function delete(): bool
86    {
87        $timestamp = (int) Request::createFromGlobals()->server->get(key: 'REQUEST_TIME') - (30 * 86_400);
88        return $this->adminLogRepository->deleteOlderThan($timestamp);
89    }
90
91    /**
92     * Verifies the integrity of the entire admin log chain.
93     * @return array{valid: bool, errors: array<int, string>, total: int, verified: int}
94     */
95    public function verifyChainIntegrity(): array
96    {
97        $logs = $this->getAll();
98        $errors = [];
99        $verified = 0;
100        $total = count($logs);
101
102        if ($total === 0) {
103            return [
104                'valid' => true,
105                'errors' => [],
106                'total' => 0,
107                'verified' => 0,
108            ];
109        }
110
111        $previousHash = null;
112
113        foreach ($logs as $log) {
114            // Verify the hash matches the stored hash
115            if (!$log->verifyIntegrity()) {
116                $errors[] = sprintf('Log ID %d: Hash verification failed - data has been tampered', $log->getId());
117                continue;
118            }
119
120            // Verify the chain (previous hash matches)
121            if ($previousHash !== null && $log->getPreviousHash() !== $previousHash) {
122                $errors[] = sprintf(
123                    'Log ID %d: Chain broken - previous hash mismatch (expected: %s, got: %s)',
124                    $log->getId(),
125                    substr(string: $previousHash, offset: 0, length: 8) . '...',
126                    substr(string: $log->getPreviousHash() ?? 'NULL', offset: 0, length: 8) . '...',
127                );
128                continue;
129            }
130
131            // The first entry should have null previous hash
132            if ($previousHash === null && $log->getPreviousHash() !== null) {
133                $errors[] = sprintf('Log ID %d: First entry should have null previous hash', $log->getId());
134                continue;
135            }
136
137            $verified++;
138            $previousHash = $log->getHash();
139        }
140
141        return [
142            'valid' => $errors === [],
143            'errors' => $errors,
144            'total' => $total,
145            'verified' => $verified,
146        ];
147    }
148
149    /**
150     * Calculates hash for a single log entry (for migration or manual verification).
151     */
152    public function calculateHash(AdminLogEntity $log): string
153    {
154        return $log->calculateHash();
155    }
156}