Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.00% covered (success)
86.00%
43 / 50
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
TagController
86.00% covered (success)
86.00%
43 / 50
25.00% covered (danger)
25.00%
1 / 4
13.46
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
 update
85.71% covered (success)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
4.05
 search
88.89% covered (success)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
5.03
 delete
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
1<?php
2
3/**
4 * The Admin Tag 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\Controller\AbstractController;
23use phpMyFAQ\Core\Exception;
24use phpMyFAQ\Entity\Tag;
25use phpMyFAQ\Enums\PermissionType;
26use phpMyFAQ\Filter;
27use phpMyFAQ\Session\Token;
28use phpMyFAQ\Tags;
29use phpMyFAQ\Translation;
30use stdClass;
31use Symfony\Component\HttpFoundation\JsonResponse;
32use Symfony\Component\HttpFoundation\Request;
33use Symfony\Component\HttpFoundation\Response;
34use Symfony\Component\Routing\Attribute\Route;
35
36final class TagController extends AbstractController
37{
38    public function __construct(
39        private readonly Tags $tags,
40    ) {
41        parent::__construct();
42    }
43
44    /**
45     * @throws Exception
46     * @throws \Exception
47     */
48    #[Route(path: 'content/tag', name: 'admin.api.content.tag', methods: ['PUT'])]
49    public function update(Request $request): JsonResponse
50    {
51        $this->userHasPermission(PermissionType::FAQ_EDIT);
52
53        $postData = $this->getJsonObject($request);
54
55        if (!Token::getInstance($this->session)->verifyToken('tags', (string) ($postData->csrf ?? ''))) {
56            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
57        }
58
59        $id = Filter::filterVar($postData->id ?? null, FILTER_VALIDATE_INT);
60
61        if (!is_int($id)) {
62            return $this->json(['error' => Translation::get(key: 'msgErrorOccurred')], Response::HTTP_BAD_REQUEST);
63        }
64
65        $newTag = Filter::filterVar($postData->tag ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
66
67        $tag = new Tag();
68        $tag->setId($id);
69        $tag->setName($newTag);
70
71        if ($this->tags->update($tag)) {
72            return $this->json(['updated' => Translation::get(key: 'ad_entryins_suc')], Response::HTTP_OK);
73        }
74
75        return $this->json(['error' => Translation::get(key: 'msgErrorOccurred')], Response::HTTP_BAD_REQUEST);
76    }
77
78    /**
79     * @throws Exception|\Exception
80     */
81    #[Route(path: 'content/tags', name: 'admin.api.content.tags', methods: ['GET'])]
82    public function search(Request $request): JsonResponse
83    {
84        $this->userHasAnyPermission(PermissionType::FAQ_EDIT, PermissionType::FAQ_ADD);
85
86        $this->tags->setBypassPermissionCheck();
87
88        $autoCompleteValue = Filter::filterVar($request->query->get('search'), FILTER_SANITIZE_SPECIAL_CHARS);
89
90        $tags = [];
91        if (!is_null($autoCompleteValue)) {
92            if (strpos((string) $autoCompleteValue, needle: ',')) {
93                $arrayOfValues = explode(separator: ',', string: (string) $autoCompleteValue);
94                $autoCompleteValue = end($arrayOfValues);
95            }
96
97            $tags = $this->tags->getAllTags(
98                strtolower(trim((string) $autoCompleteValue)),
99                PMF_TAGS_CLOUD_RESULT_SET_SIZE,
100            );
101        }
102
103        if (is_null($autoCompleteValue)) {
104            $tags = $this->tags->getAllTags();
105        }
106
107        $tagNames = [];
108        $suggestions = array_slice(
109            $tags,
110            offset: 0,
111            length: PMF_TAGS_AUTOCOMPLETE_RESULT_SET_SIZE,
112            preserve_keys: true,
113        );
114
115        foreach ($suggestions as $tagId => $tag) {
116            $currentTag = new stdClass();
117            $currentTag->id = (string) $tagId;
118            $currentTag->name = $tag;
119            $tagNames[] = $currentTag;
120        }
121
122        return $this->json($tagNames, Response::HTTP_OK);
123    }
124
125    /**
126     * @throws \Exception
127     */
128    #[Route(path: 'content/tags/{tagId}', name: 'admin.api.content.tags.id', methods: ['DELETE'])]
129    public function delete(Request $request): JsonResponse
130    {
131        $this->userHasPermission(PermissionType::FAQ_EDIT);
132
133        $data = $this->getJsonObject($request);
134
135        if (!Token::getInstance($this->session)->verifyToken('tags', (string) ($data->csrfToken ?? ''))) {
136            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
137        }
138
139        $tagId = (int) Filter::filterVar($request->attributes->get('tagId'), FILTER_VALIDATE_INT);
140
141        if ($this->tags->delete($tagId)) {
142            return $this->json(['success' => Translation::get(key: 'ad_tag_delete_success')], Response::HTTP_OK);
143        }
144
145        return $this->json(['error' => Translation::get(key: 'ad_tag_delete_error')], Response::HTTP_BAD_REQUEST);
146    }
147}