Lines
95.65%
44 / 46
Methods
85.71%
6 / 7
Classes
0.00%
0 / 1
| 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 | ||
| 32 | readonly 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 | } |