Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.47% covered (warning)
76.47%
26 / 34
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
QuestionController
76.47% covered (warning)
76.47%
26 / 34
50.00% covered (danger)
50.00%
1 / 2
5.33
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
 create
75.76% covered (warning)
75.76%
25 / 33
0.00% covered (danger)
0.00%
0 / 1
4.23
1<?php
2
3/**
4 * The Question Controller for the REST API
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-02-27
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Api;
21
22use OpenApi\Attributes as OA;
23use phpMyFAQ\Category;
24use phpMyFAQ\Core\Exception;
25use phpMyFAQ\Entity\QuestionEntity;
26use phpMyFAQ\Enums\PermissionType;
27use phpMyFAQ\Filter;
28use phpMyFAQ\Notification;
29use phpMyFAQ\Question;
30use Symfony\Component\HttpFoundation\JsonResponse;
31use Symfony\Component\HttpFoundation\Request;
32use Symfony\Component\HttpFoundation\Response;
33use Symfony\Component\Routing\Attribute\Route;
34
35final class QuestionController extends AbstractApiController
36{
37    public function __construct(
38        private readonly Notification $notification,
39    ) {
40        parent::__construct();
41    }
42
43    /**
44     * @throws Exception
45     * @throws \JsonException
46     * @throws \Exception
47     */
48    #[OA\Post(path: '/api/v4.0/question', operationId: 'createQuestion', tags: ['Endpoints with Authentication'])]
49    #[OA\Header(
50        header: 'Accept-Language',
51        description: 'The language code for the question.',
52        schema: new OA\Schema(type: 'string'),
53    )]
54    #[OA\Header(
55        header: 'x-pmf-token',
56        description: 'phpMyFAQ client API Token, generated in admin backend',
57        schema: new OA\Schema(type: 'string'),
58    )]
59    #[OA\RequestBody(required: true, content: new OA\MediaType(
60        mediaType: 'application/json',
61        schema: new OA\Schema(
62            required: [
63                'category-id',
64                'question',
65                'author',
66                'email',
67            ],
68            properties: [
69                new OA\Property(property: 'category-id', type: 'integer'),
70                new OA\Property(property: 'question', type: 'string'),
71                new OA\Property(property: 'author', type: 'string'),
72                new OA\Property(property: 'email', type: 'string'),
73            ],
74            type: 'object',
75        ),
76        example: '{
77                "category-id": "1",
78                "question": "Is this the world we created?",
79                "author": "Freddie Mercury",
80                "email": "freddie.mercury@example.org"
81            }',
82    ))]
83    #[OA\Response(
84        response: 201,
85        description: 'Used to add a new question in one existing category.',
86        content: new OA\JsonContent(example: ['stored' => true]),
87    )]
88    #[OA\Response(response: 401, description: 'If the user is not authenticated.')]
89    #[Route(path: 'v4.0/question', name: 'api.question.create', methods: ['POST'])]
90    public function create(Request $request): JsonResponse
91    {
92        $this->hasValidToken();
93        $this->userHasPermission(PermissionType::QUESTION_ADD);
94
95        $data = json_decode(json: $request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR);
96        if (!$data instanceof \stdClass) {
97            return $this->json([
98                'stored' => false,
99                'error' => 'The request body must be a JSON object.',
100            ], Response::HTTP_BAD_REQUEST);
101        }
102
103        $categoryId = Filter::filterVar($data->{'category-id'} ?? null, FILTER_VALIDATE_INT);
104        if (!is_int($categoryId)) {
105            return $this->json([
106                'stored' => false,
107                'error' => 'Invalid category id.',
108            ], Response::HTTP_BAD_REQUEST);
109        }
110
111        $question = Filter::filterVar($data->question ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
112        $author = Filter::filterVar($data->author ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
113        $email = Filter::filterVar($data->email ?? '', FILTER_SANITIZE_SPECIAL_CHARS, '');
114
115        $visibility = $this->configuration->get(item: 'records.enableVisibilityQuestions') ? 'Y' : 'N';
116
117        $questionEntity = new QuestionEntity();
118        $questionEntity
119            ->setUsername($author)
120            ->setEmail($email)
121            ->setCategoryId($categoryId)
122            ->setQuestion($question)
123            ->setLanguage($this->configuration->getLanguage()->getLanguage())
124            ->setIsVisible($visibility === 'Y');
125
126        $questionObject = new Question($this->configuration);
127        $questionObject->add($questionEntity);
128
129        $category = new Category($this->configuration);
130        $category->getCategoryData($categoryId);
131
132        $categories = $category->getAllCategories();
133
134        $this->notification->sendQuestionSuccessMail($questionEntity, $categories);
135
136        return $this->json(['stored' => true], Response::HTTP_CREATED);
137    }
138}