Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.21% covered (warning)
78.21%
61 / 78
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdminLogController
78.21% covered (warning)
78.21%
61 / 78
66.67% covered (warning)
66.67%
2 / 3
13.49
0.00% covered (danger)
0.00%
0 / 1
 delete
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 export
60.47% covered (warning)
60.47%
26 / 43
0.00% covered (danger)
0.00%
0 / 1
4.99
 verify
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3/**
4 * The Admin Log API Controller
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 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     2026-01-06
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Administration\Api;
21
22use Exception;
23use JsonException;
24use phpMyFAQ\Enums\AdminLogType;
25use phpMyFAQ\Enums\PermissionType;
26use phpMyFAQ\Filter;
27use phpMyFAQ\Session\Token;
28use phpMyFAQ\Translation;
29use phpMyFAQ\User;
30use Symfony\Component\HttpFoundation\JsonResponse;
31use Symfony\Component\HttpFoundation\Request;
32use Symfony\Component\HttpFoundation\Response;
33use Symfony\Component\Routing\Attribute\Route;
34
35final class AdminLogController extends AbstractAdministrationApiController
36{
37    /**
38     * @throws \phpMyFAQ\Core\Exception|JsonException
39     * @throws Exception
40     */
41    #[Route(path: 'statistics/admin-log', name: 'admin.api.statistics.admin-log.delete', methods: ['DELETE'])]
42    public function delete(Request $request): JsonResponse
43    {
44        $this->userHasPermission(PermissionType::STATISTICS_ADMINLOG);
45
46        $data = $this->getJsonObject($request);
47
48        if (!Token::getInstance($this->session)->verifyToken('delete-adminlog', (string) ($data->csrfToken ?? ''))) {
49            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
50        }
51
52        if ($this->adminLog->delete()) {
53            return $this->json(['success' => Translation::get(key: 'ad_adminlog_delete_success')], Response::HTTP_OK);
54        }
55
56        return $this->json([
57            'error' => Translation::get(key: 'ad_adminlog_delete_failure'),
58        ], Response::HTTP_BAD_REQUEST);
59    }
60
61    /**
62     * @throws Exception
63     */
64    #[Route(path: 'statistics/admin-log/export', name: 'admin.api.statistics.admin-log.export', methods: ['POST'])]
65    public function export(Request $request): Response|JsonResponse
66    {
67        $this->userHasPermission(PermissionType::STATISTICS_ADMINLOG);
68
69        $data = $this->getJsonObject($request);
70
71        if (!Token::getInstance($this->session)->verifyToken('export-adminlog', (string) ($data->csrf ?? ''))) {
72            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
73        }
74
75        $loggingData = $this->adminLog->getAll();
76
77        $handle = fopen(filename: 'php://temp', mode: 'r+');
78        fputcsv(
79            $handle,
80            ['ID', 'Date/Time', 'User ID', 'Username', 'IP Address', 'Action'],
81            separator: ',',
82            enclosure: '"',
83            eol: PHP_EOL,
84        );
85
86        foreach ($loggingData as $log) {
87            $user = new User($this->configuration);
88            $user->getUserById($log->getUserId());
89            $username = $user->getLogin();
90
91            fputcsv(
92                $handle,
93                [
94                    $log->getId(),
95                    date('Y-m-d H:i:s', $log->getTime()),
96                    $log->getUserId(),
97                    $username,
98                    $log->getIp(),
99                    $log->getText(),
100                ],
101                separator: ',',
102                enclosure: '"',
103                eol: PHP_EOL,
104            );
105        }
106
107        rewind($handle);
108        $content = stream_get_contents($handle);
109        $content = $content === false ? '' : $content;
110        fclose($handle);
111
112        $this->adminLog->log($this->currentUser, AdminLogType::DATA_EXPORT_LOGS->value);
113
114        $response = new Response($content);
115        $response->headers->set('Content-Type', 'text/csv');
116        $response->headers->set(
117            'Content-Disposition',
118            'attachment; filename="admin-log-export-' . date('Y-m-d-His') . '.csv"',
119        );
120
121        return $response;
122    }
123
124    /**
125     * @throws Exception
126     */
127    #[Route(path: 'statistics/admin-log/verify', name: 'admin.api.statistics.admin-log.verify', methods: ['GET'])]
128    public function verify(Request $request): JsonResponse
129    {
130        $this->userHasPermission(PermissionType::STATISTICS_ADMINLOG);
131
132        $csrfToken = Filter::filterVar($request->query->get('csrf'), FILTER_SANITIZE_SPECIAL_CHARS);
133        if (!Token::getInstance($this->session)->verifyToken('admin-log-verify', $csrfToken)) {
134            return $this->json(['error' => 'Invalid CSRF token'], Response::HTTP_FORBIDDEN);
135        }
136
137        try {
138            $result = $this->adminLog->verifyChainIntegrity();
139
140            return $this->json(
141                [
142                    'success' => true,
143                    'verification' => [
144                        'valid' => $result['valid'],
145                        'total' => $result['total'],
146                        'verified' => $result['verified'],
147                        'failed' => $result['total'] - $result['verified'],
148                        'errors' => $result['errors'],
149                    ],
150                    'message' => $result['valid']
151                        ? 'Admin log integrity verified successfully'
152                        : 'Admin log integrity check failed - tampering detected',
153                ],
154                $result['valid'] ? Response::HTTP_OK : Response::HTTP_CONFLICT,
155            );
156        } catch (Exception $exception) {
157            return $this->json([
158                'success' => false,
159                'error' => $exception->getMessage(),
160            ], Response::HTTP_INTERNAL_SERVER_ERROR);
161        }
162    }
163}