Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
80 / 85
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewsController
94.12% covered (success)
94.12%
80 / 85
20.00% covered (danger)
20.00%
1 / 5
15.05
0.00% covered (danger)
0.00%
0 / 1
 create
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 delete
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 update
87.50% covered (success)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
4.03
 activate
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 newsMessageFromPayload
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * The Admin News Controller
5 * This Source Code Form is subject to the terms of the Mozilla Public License,
6 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
7 * obtain one at https://mozilla.org/MPL/2.0/.
8 *
9 * @package   phpMyFAQ
10 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
11 * @author    Jan Harms <modelrailroader@gmx-topmail.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-04-20
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Administration\Api;
21
22use DateTime;
23use phpMyFAQ\Entity\NewsMessage;
24use phpMyFAQ\Enums\AdminLogType;
25use phpMyFAQ\Enums\PermissionType;
26use phpMyFAQ\Filter;
27use phpMyFAQ\News;
28use phpMyFAQ\Session\Token;
29use phpMyFAQ\Translation;
30use Symfony\Component\HttpFoundation\JsonResponse;
31use Symfony\Component\HttpFoundation\Request;
32use Symfony\Component\HttpFoundation\Response;
33use Symfony\Component\Routing\Attribute\Route;
34
35final class NewsController extends AbstractAdministrationApiController
36{
37    /**
38     * @throws \Exception
39     */
40    #[Route(path: 'news/create', name: 'admin.api.news.create', methods: ['POST'])]
41    public function create(Request $request): JsonResponse
42    {
43        $this->userHasPermission(PermissionType::NEWS_ADD);
44
45        $data = $this->getJsonObject($request);
46
47        $news = new News($this->configuration);
48
49        if (!Token::getInstance($this->session)->verifyToken(
50            page: 'save-news',
51            requestToken: (string) ($data->csrfToken ?? ''),
52        )) {
53            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
54        }
55
56        $newsMessage = $this->newsMessageFromPayload($data);
57
58        if ($news->create($newsMessage)) {
59            $this->adminLog->log($this->currentUser, AdminLogType::NEWS_ADD->value);
60
61            return $this->json(['success' => Translation::get(key: 'ad_news_updatesuc')], Response::HTTP_OK);
62        }
63
64        return $this->json(['error' => Translation::get(key: 'ad_news_insertfail')], Response::HTTP_BAD_GATEWAY);
65    }
66
67    /**
68     * @throws \Exception
69     */
70    #[Route(path: 'news/delete', name: 'admin.api.news.delete', methods: ['DELETE'])]
71    public function delete(Request $request): JsonResponse
72    {
73        $this->userHasPermission(PermissionType::NEWS_DELETE);
74
75        $data = $this->getJsonObject($request);
76
77        $news = new News($this->configuration);
78
79        if (!Token::getInstance($this->session)->verifyToken(
80            page: 'delete-news',
81            requestToken: (string) ($data->csrfToken ?? ''),
82        )) {
83            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
84        }
85
86        $deleteId = (int) Filter::filterVar($data->id ?? null, FILTER_VALIDATE_INT);
87
88        if ($news->delete($deleteId)) {
89            $this->adminLog->log($this->currentUser, AdminLogType::NEWS_DELETE->value . ':' . $deleteId);
90
91            return $this->json(['success' => Translation::get(key: 'ad_news_delsuc')], Response::HTTP_OK);
92        }
93
94        return $this->json(['error' => Translation::get(key: 'ad_news_updatefail')], Response::HTTP_BAD_GATEWAY);
95    }
96
97    /**
98     * @throws \Exception
99     */
100    #[Route(path: 'news/update', name: 'admin.api.news.update', methods: ['PUT'])]
101    public function update(Request $request): JsonResponse
102    {
103        $this->userHasPermission(PermissionType::NEWS_EDIT);
104
105        $data = $this->getJsonObject($request);
106
107        $news = new News($this->configuration);
108
109        if (!Token::getInstance($this->session)->verifyToken(
110            page: 'update-news',
111            requestToken: (string) ($data->csrfToken ?? ''),
112        )) {
113            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
114        }
115
116        $newsId = Filter::filterVar($data->id ?? null, FILTER_VALIDATE_INT);
117
118        if (!is_int($newsId)) {
119            return $this->json(['error' => Translation::get(key: 'ad_news_updatefail')], Response::HTTP_BAD_REQUEST);
120        }
121
122        $newsMessage = $this->newsMessageFromPayload($data)->setId($newsId);
123
124        if ($news->update($newsMessage)) {
125            $this->adminLog->log($this->currentUser, AdminLogType::NEWS_EDIT->value . ':' . $newsId);
126
127            return $this->json(['success' => Translation::get(key: 'ad_news_updatesuc')], Response::HTTP_OK);
128        }
129
130        return $this->json(['error' => Translation::get(key: 'ad_news_updatefail')], Response::HTTP_BAD_GATEWAY);
131    }
132
133    /**
134     * @throws \Exception
135     */
136    #[Route(path: 'news/activate', name: 'admin.api.news.activate', methods: ['PUT'])]
137    public function activate(Request $request): JsonResponse
138    {
139        $this->userHasPermission(PermissionType::NEWS_EDIT);
140        $data = $this->getJsonObject($request);
141
142        $news = new News($this->configuration);
143
144        if (!Token::getInstance($this->session)->verifyToken(
145            page: 'activate-news',
146            requestToken: (string) ($data->csrfToken ?? ''),
147        )) {
148            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
149        }
150
151        $newsId = Filter::filterVar($data->id ?? null, FILTER_VALIDATE_INT);
152
153        if (!is_int($newsId)) {
154            return $this->json(['error' => Translation::get(key: 'ad_news_updatefail')], Response::HTTP_BAD_REQUEST);
155        }
156
157        $status = (bool) Filter::filterVar($data->status ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
158
159        if ($status) {
160            $news->activate($newsId);
161            $this->adminLog->log($this->currentUser, AdminLogType::NEWS_EDIT->value . ':' . $newsId);
162
163            return $this->json(['success' => Translation::get(key: 'ad_news_updatesuc')], Response::HTTP_OK);
164        }
165
166        $news->deactivate($newsId);
167        $this->adminLog->log($this->currentUser, AdminLogType::NEWS_EDIT->value . ':' . $newsId);
168
169        return $this->json(['success' => Translation::get(key: 'ad_news_updatesuc')], Response::HTTP_OK);
170    }
171
172    /**
173     * Builds a news message entity from the validated request payload.
174     */
175    private function newsMessageFromPayload(\stdClass $data): NewsMessage
176    {
177        $header = Filter::filterVar($data->newsHeader ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
178        $content = Filter::filterVar($data->news ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
179        $author = Filter::filterVar($data->authorName ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
180        $email = (string) Filter::filterEmail($data->authorEmail ?? '', default: '');
181        $active = Filter::filterVar($data->active ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
182        $comment = Filter::filterVar($data->comment ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
183        $link = Filter::filterVar($data->link ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
184        $linkTitle = Filter::filterVar($data->linkTitle ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
185        $newsLang = Filter::filterVar($data->langTo ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
186        $target = Filter::filterVar($data->target ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
187
188        $newsMessage = new NewsMessage();
189        $newsMessage
190            ->setLanguage($newsLang)
191            ->setHeader($header)
192            ->setMessage(html_entity_decode($content))
193            ->setAuthor($author)
194            ->setEmail($email)
195            ->setActive((bool) $active)
196            ->setComment((bool) $comment)
197            ->setLink($link)
198            ->setLinkTitle($linkTitle)
199            ->setLinkTarget($target)
200            ->setCreated(new DateTime());
201
202        return $newsMessage;
203    }
204}