Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.23% covered (success)
89.23%
58 / 65
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CategoryController
89.23% covered (success)
89.23%
58 / 65
80.00% covered (success)
80.00%
4 / 5
14.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
74.07% covered (warning)
74.07%
20 / 27
0.00% covered (danger)
0.00%
0 / 1
5.44
 permissions
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 translations
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 updateOrder
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * The Admin Category 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 2023-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     2023-10-27
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Administration\Api;
21
22use phpMyFAQ\Category;
23use phpMyFAQ\Category\Image;
24use phpMyFAQ\Category\Order;
25use phpMyFAQ\Category\Permission;
26use phpMyFAQ\Category\Relation;
27use phpMyFAQ\Core\Exception;
28use phpMyFAQ\Enums\AdminLogType;
29use phpMyFAQ\Enums\PermissionType;
30use phpMyFAQ\Filter;
31use phpMyFAQ\Session\Token;
32use phpMyFAQ\Translation;
33use phpMyFAQ\User\CurrentUser;
34use Symfony\Component\HttpFoundation\JsonResponse;
35use Symfony\Component\HttpFoundation\Request;
36use Symfony\Component\HttpFoundation\Response;
37use Symfony\Component\Routing\Attribute\Route;
38
39final class CategoryController extends AbstractAdministrationApiController
40{
41    public function __construct(
42        private readonly Image $categoryImage,
43        private readonly Order $categoryOrder,
44        private readonly Permission $categoryPermission,
45    ) {
46        parent::__construct();
47    }
48
49    /**
50     * @throws Exception
51     * @throws \Exception
52     */
53    #[Route(path: 'category/delete', name: 'admin.api.category.delete', methods: ['DELETE'])]
54    public function delete(Request $request): JsonResponse
55    {
56        $this->userHasPermission(PermissionType::CATEGORY_DELETE);
57
58        $data = $this->getJsonObject($request);
59
60        if (!Token::getInstance($this->session)->verifyToken('category', (string) ($data->csrfToken ?? ''))) {
61            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
62        }
63
64        $categoryId = (int) ($data->categoryId ?? 0);
65        $categoryLang = (string) ($data->language ?? '');
66
67        [$currentAdminUser, $currentAdminGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser);
68
69        $category = new Category($this->configuration, [], false);
70        $category->setUser($currentAdminUser);
71        $category->setGroups($currentAdminGroups);
72
73        $categoryRelation = new Relation($this->configuration, $category);
74
75        $this->categoryImage->setFileName($category->getCategoryData($categoryId)->getImage() ?? '');
76
77        $this->categoryOrder->remove($categoryId);
78
79        if (count($category->getCategoryLanguagesTranslated($categoryId)) === 1) {
80            $this->categoryPermission->delete(Permission::USER, [$categoryId]);
81            $this->categoryPermission->delete(Permission::GROUP, [$categoryId]);
82            $this->categoryImage->delete();
83        }
84
85        if ($category->delete($categoryId, $categoryLang) && $categoryRelation->delete($categoryId, $categoryLang)) {
86            $this->adminLog->log($this->currentUser, AdminLogType::CATEGORY_DELETE->value . ':' . $categoryId);
87
88            return $this->json(['success' => Translation::get(key: 'ad_categ_deleted')], Response::HTTP_OK);
89        }
90
91        $this->configuration->getLogger()->error('Failed to delete category', [
92            'categoryId' => $categoryId,
93            'sqlError' => $this->configuration->getDb()->error(),
94        ]);
95
96        return $this->json([
97            'error' => Translation::get(key: 'ad_adus_dberr'),
98        ], Response::HTTP_INTERNAL_SERVER_ERROR);
99    }
100
101    #[Route(
102        path: 'category/permissions/{categories}',
103        name: 'admin.api.category.permissions',
104        methods: ['GET'],
105        defaults: ['categories' => null],
106    )]
107    public function permissions(Request $request): JsonResponse
108    {
109        $this->userHasPermission(PermissionType::CATEGORY_EDIT);
110
111        $categoryData = $request->attributes->get('categories');
112
113        // Access for all users and groups unless specific categories are requested
114        $categories = [-1];
115        if (!in_array($categoryData, [null, '', false], strict: true)) {
116            $rawCategories = explode(',', (string) $categoryData);
117            $validatedCategories = filter_var_array($rawCategories, FILTER_VALIDATE_INT);
118            if (!is_array($validatedCategories) || in_array(false, $validatedCategories, strict: true)) {
119                return $this->json(['error' => 'Only integer values are valid.'], Response::HTTP_BAD_REQUEST);
120            }
121
122            $categories = array_map(static fn(mixed $categoryId): int => (int) $categoryId, $validatedCategories);
123        }
124
125        return $this->json([
126            'user' => $this->categoryPermission->get(Permission::USER, $categories),
127            'group' => $this->categoryPermission->get(Permission::GROUP, $categories),
128        ], Response::HTTP_OK);
129    }
130
131    #[Route(path: 'category/translations/{categoryId}', name: 'admin.api.category.translations', methods: ['GET'])]
132    public function translations(Request $request): JsonResponse
133    {
134        $this->userHasPermission(PermissionType::CATEGORY_EDIT);
135
136        $category = new Category($this->configuration, [], false);
137
138        $categoryId = (int) Filter::filterVar($request->attributes->get('categoryId'), FILTER_VALIDATE_INT);
139
140        $translations = $category->getCategoryLanguagesTranslated($categoryId);
141
142        return $this->json($translations, Response::HTTP_OK);
143    }
144
145    /**
146     * @throws \Exception
147     */
148    #[Route(path: 'category/update-order', name: 'admin.api.category.update-order', methods: ['POST'])]
149    public function updateOrder(Request $request): JsonResponse
150    {
151        $this->userHasPermission(PermissionType::CATEGORY_EDIT);
152
153        $data = $this->getJsonObject($request);
154
155        if (!Token::getInstance($this->session)->verifyToken('category', (string) ($data->csrfToken ?? ''))) {
156            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
157        }
158
159        $categoryId = (int) ($data->categoryId ?? 0);
160        $categoryTreeRaw = $data->categoryTree ?? [];
161        $categoryTree = array_values(array_filter(
162            is_array($categoryTreeRaw) ? $categoryTreeRaw : [],
163            static fn(mixed $node): bool => $node instanceof \stdClass,
164        ));
165
166        [$currentAdminUser, $currentAdminGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser);
167
168        $this->categoryOrder->setCategoryTree($categoryTree);
169
170        $parentId = $this->categoryOrder->getParentId($categoryTree, $categoryId);
171
172        $category = new Category($this->configuration, [], false);
173        $category->setUser($currentAdminUser);
174        $category->setGroups($currentAdminGroups);
175        $category->updateParentCategory($categoryId, $parentId ?? 0);
176
177        $this->adminLog->log($this->currentUser, AdminLogType::CATEGORY_REORDER->value . ':' . $categoryId);
178
179        return $this->json(['success' => Translation::get(key: 'ad_categ_save_order')], Response::HTTP_OK);
180    }
181}