Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.66% covered (success)
89.66%
52 / 58
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
MediaBrowserController
89.66% covered (success)
89.66%
52 / 58
0.00% covered (danger)
0.00%
0 / 1
12.16
0.00% covered (danger)
0.00%
0 / 1
 index
89.66% covered (success)
89.66%
52 / 58
0.00% covered (danger)
0.00%
0 / 1
12.16
1<?php
2
3/**
4 * The Administration Media Browser 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 2024-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     2024-12-28
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Administration\Api;
21
22use DateTime;
23use phpMyFAQ\Controller\AbstractController;
24use phpMyFAQ\Core\Exception;
25use phpMyFAQ\Enums\PermissionType;
26use phpMyFAQ\Filter;
27use phpMyFAQ\Session\Token;
28use phpMyFAQ\Translation;
29use phpMyFAQ\Utils;
30use RecursiveDirectoryIterator;
31use RecursiveIteratorIterator;
32use Symfony\Component\HttpFoundation\JsonResponse;
33use Symfony\Component\HttpFoundation\Request;
34use Symfony\Component\HttpFoundation\Response;
35use Symfony\Component\Routing\Attribute\Route;
36use Twig\Error\LoaderError;
37
38final class MediaBrowserController extends AbstractController
39{
40    /**
41     * @throws LoaderError
42     * @throws Exception
43     */
44    #[Route(path: 'media-browser', name: 'admin.api.media.browser', methods: ['GET', 'POST'])]
45    public function index(Request $request): JsonResponse|Response
46    {
47        $this->userHasPermission(PermissionType::FAQ_EDIT);
48
49        $allowedExtensions = ['png', 'gif', 'jpg', 'jpeg', 'mov', 'mpg', 'mp4', 'ogg', 'wmv', 'avi', 'webm'];
50
51        if (!is_dir(PMF_CONTENT_DIR . '/user/images')) {
52            return $this->json(['error' => sprintf(
53                Translation::getString(key: 'ad_dir_missing'),
54                '/images',
55            )], Response::HTTP_BAD_REQUEST);
56        }
57
58        $data = $this->getJsonObject($request);
59        $action = Filter::filterVar($data->action ?? null, FILTER_SANITIZE_SPECIAL_CHARS);
60
61        if ($action === 'fileRemove') {
62            if (!Token::getInstance($this->session)->verifyToken('media-browser', (string) ($data->csrfToken ?? ''))) {
63                return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
64            }
65
66            $file = basename((string) Filter::filterVar($data->name ?? '', FILTER_SANITIZE_SPECIAL_CHARS, ''));
67            $allowedDir = realpath(PMF_CONTENT_DIR . '/user/images');
68            $targetPath = realpath(PMF_CONTENT_DIR . '/user/images/' . $file);
69
70            if (
71                $allowedDir === false
72                || $targetPath === false
73                || !str_starts_with($targetPath, $allowedDir . DIRECTORY_SEPARATOR)
74            ) {
75                return $this->json(['error' => 'Invalid file path'], Response::HTTP_BAD_REQUEST);
76            }
77
78            if (file_exists($targetPath)) {
79                unlink($targetPath);
80            }
81
82            $response = [
83                'success' => true,
84                'data' => [
85                    'code' => 220,
86                ],
87            ];
88
89            return $this->json($response, Response::HTTP_OK);
90        }
91
92        $files = [];
93        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(PMF_CONTENT_DIR . '/user/images'));
94        foreach ($iterator as $file) {
95            if (!$file instanceof \SplFileInfo || $file->isDir()) {
96                continue;
97            }
98
99            if (!in_array(strtolower($file->getExtension()), $allowedExtensions, strict: true)) {
100                continue;
101            }
102
103            $files[] = [
104                'file' => $file->getFilename(),
105                'size' => Utils::formatBytes((int) $file->getSize()),
106                'isImage' => true,
107                'thumb' => $file->getFilename(),
108                'changed' => date(format: 'Y-m-d H:i:s', timestamp: (int) $file->getMTime()),
109            ];
110        }
111
112        $response = [
113            'success' => true,
114            'time' => new DateTime()->format('Y-m-d H:i:s'),
115            'data' => [
116                'sources' => [
117                    [
118                        'baseurl' => $this->configuration->getDefaultUrl(),
119                        'path' => 'content/user/images/',
120                        'files' => $files,
121                        'name' => 'default',
122                    ],
123                ],
124                'code' => 220,
125            ],
126        ];
127
128        return $this->json($response, Response::HTTP_OK);
129    }
130}