Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.59% covered (success)
94.59%
70 / 74
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewsController
94.59% covered (success)
94.59%
70 / 74
66.67% covered (warning)
66.67%
2 / 3
8.01
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
 index
92.16% covered (success)
92.16%
47 / 51
0.00% covered (danger)
0.00%
0 / 1
5.01
 prepareCommentsData
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * News 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 * @author    Matteo Scaramuccia <matteo@scaramuccia.com>
13 * @copyright 2006-2026 phpMyFAQ Team
14 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
15 * @link      https://www.phpmyfaq.de
16 * @since     2006-07-23
17 */
18
19declare(strict_types=1);
20
21namespace phpMyFAQ\Controller\Frontend;
22
23use phpMyFAQ\Captcha\CaptchaInterface;
24use phpMyFAQ\Captcha\Helper\CaptchaHelper;
25use phpMyFAQ\Comments;
26use phpMyFAQ\Core\Exception;
27use phpMyFAQ\Date;
28use phpMyFAQ\Entity\CommentType;
29use phpMyFAQ\Filter;
30use phpMyFAQ\Mail;
31use phpMyFAQ\News\NewsService;
32use phpMyFAQ\Service\Gravatar;
33use phpMyFAQ\Session\Token;
34use phpMyFAQ\Strings;
35use phpMyFAQ\Translation;
36use phpMyFAQ\User\UserSession;
37use phpMyFAQ\Utils;
38use Symfony\Component\HttpFoundation\Request;
39use Symfony\Component\HttpFoundation\Response;
40use Symfony\Component\Routing\Attribute\Route;
41
42final class NewsController extends AbstractFrontController
43{
44    public function __construct(
45        private readonly UserSession $faqSession,
46        private readonly CaptchaInterface $captcha,
47        private readonly Date $date,
48        private readonly Mail $mail,
49        private readonly Gravatar $gravatar,
50    ) {
51        parent::__construct();
52    }
53
54    /**
55     * Displays a news article with comments.
56     *
57     * @throws Exception
58     * @throws \Exception
59     */
60    #[Route(
61        path: '/news/{newsId}/{newsLang}/{slug}.html',
62        name: 'public.news',
63        requirements: [
64            'newsId' => '\d+',
65            'newsLang' => '[a-z\-_]+',
66        ],
67        methods: ['GET'],
68    )]
69    public function index(Request $request): Response
70    {
71        $newsId = Filter::filterVar($request->attributes->get('newsId'), FILTER_VALIDATE_INT);
72
73        if ($newsId === null) {
74            return $this->render('404.twig', [
75                ...$this->getHeader($request),
76            ]);
77        }
78
79        $this->faqSession->setCurrentUser($this->currentUser);
80        $this->faqSession->userTracking('news_view', $newsId);
81
82        $newsService = new NewsService($this->configuration, $this->currentUser);
83        $news = $newsService->getProcessedNews($newsId);
84
85        if ($news === []) {
86            $response = $this->render('404.twig', [
87                ...$this->getHeader($request),
88            ]);
89            $response->setStatusCode(Response::HTTP_NOT_FOUND);
90
91            return $response;
92        }
93
94        $captchaHelper = CaptchaHelper::getInstance($this->configuration);
95
96        $comment = new Comments($this->configuration);
97        $comments = $comment->getCommentsData($newsId, CommentType::NEWS);
98
99        return $this->render('news.twig', [
100            ...$this->getHeader($request),
101            'writeNewsHeader' => $this->configuration->getTitle() . Translation::getString(key: 'msgNews'),
102            'newsHeader' => $news['processedHeader'],
103            'mainPageContent' => $news['processedContent'],
104            'writeDateMsg' => $newsService->formatNewsDate($news),
105            'msgAboutThisNews' => Translation::get(key: 'msgAboutThisNews'),
106            'writeAuthor' => $newsService->getAuthorInfo($news),
107            'editThisEntry' => $newsService->getEditLink($newsId),
108            'writeCommentMsg' => $newsService->getCommentMessage($news),
109            'msgWriteComment' => Translation::get(key: 'newsWriteComment'),
110            'newsId' => $newsId,
111            'newsLang' => $news['lang'],
112            'msgCommentHeader' => Translation::get(key: 'msgCommentHeader'),
113            'msgNewContentName' => Translation::get(key: 'msgNewContentName'),
114            'msgNewContentMail' => Translation::get(key: 'msgNewContentMail'),
115            'defaultContentMail' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getUserData('email') : '',
116            'defaultContentName' => $this->currentUser->getUserId() > 0
117                ? $this->currentUser->getUserData('display_name')
118                : '',
119            'msgYourComment' => Translation::get(key: 'msgYourComment'),
120            'csrfInput' => Token::getInstance($this->session)->getTokenInput('add-comment'),
121            'msgCancel' => Translation::get(key: 'ad_gen_cancel'),
122            'msgNewContentSubmit' => Translation::get(key: 'msgNewContentSubmit'),
123            'captchaFieldset' => $captchaHelper->renderCaptcha(
124                $this->captcha,
125                'writecomment',
126                Translation::getString(key: 'msgCaptcha'),
127                $this->currentUser->isLoggedIn(),
128            ),
129            'comments' => $this->prepareCommentsData($comments),
130            'msgShowMore' => Translation::get(key: 'msgShowMore'),
131        ]);
132    }
133
134    /**
135     * Prepares comment data for the Twig macro
136     *
137     * @param \phpMyFAQ\Entity\Comment[] $comments Array of Comment objects
138     * @throws \Exception
139     * @return array<string, array<array-key, mixed>>
140     */
141    private function prepareCommentsData(array $comments): array
142    {
143        $preparedComments = [];
144        $gravatarImages = [];
145        $safeEmails = [];
146        $formattedDates = [];
147
148        foreach ($comments as $comment) {
149            $commentId = $comment->getId();
150            $preparedComments[] = [
151                'id' => $commentId,
152                'email' => $comment->getEmail(),
153                'username' => Strings::htmlentities($comment->getUsername()),
154                'date' => $comment->getDate(),
155                'comment' => Utils::parseUrl($comment->getComment()),
156            ];
157
158            $gravatarImages[$commentId] = $this->gravatar->getImage($comment->getEmail(), ['class' => 'img-thumbnail']);
159            $safeEmails[$commentId] = $this->mail->safeEmail($comment->getEmail());
160            $formattedDates[$commentId] = $this->date->format($comment->getDate());
161        }
162
163        return [
164            'comments' => $preparedComments,
165            'gravatarImages' => $gravatarImages,
166            'safeEmails' => $safeEmails,
167            'formattedDates' => $formattedDates,
168        ];
169    }
170}