Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.24% covered (success)
90.24%
37 / 41
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
RegistrationController
90.24% covered (success)
90.24%
37 / 41
66.67% covered (warning)
66.67%
2 / 3
14.18
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setRegistrationHelperFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
89.19% covered (success)
89.19%
33 / 37
0.00% covered (danger)
0.00%
0 / 1
11.15
1<?php
2
3/**
4 * The Registration 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\Controller\AbstractController;
24use phpMyFAQ\Core\Exception;
25use phpMyFAQ\Filter;
26use phpMyFAQ\Helper\RegistrationHelper;
27use phpMyFAQ\Translation;
28use Symfony\Component\HttpFoundation\JsonResponse;
29use Symfony\Component\HttpFoundation\Request;
30use Symfony\Component\HttpFoundation\Response;
31use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
32use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
33use Symfony\Component\Routing\Attribute\Route;
34
35final class RegistrationController extends AbstractController
36{
37    /** @var null|callable(\phpMyFAQ\Configuration): RegistrationHelper */
38    private $registrationHelperFactory = null;
39
40    public function __construct()
41    {
42        parent::__construct();
43
44        if (!$this->isApiEnabled()) {
45            throw new UnauthorizedHttpException(challenge: 'API is not enabled');
46        }
47    }
48
49    /**
50     * @param callable(\phpMyFAQ\Configuration): RegistrationHelper $registrationHelperFactory
51     */
52    public function setRegistrationHelperFactory(callable $registrationHelperFactory): void
53    {
54        $this->registrationHelperFactory = $registrationHelperFactory;
55    }
56
57    /**
58     * @throws TransportExceptionInterface
59     * @throws Exception
60     * @throws \JsonException
61     */
62    #[OA\Post(path: '/api/v4.0/register', operationId: 'createUser', tags: ['Endpoints with Authentication'])]
63    #[OA\Header(
64        header: 'Accept-Language',
65        description: 'The language code for the question.',
66        schema: new OA\Schema(type: 'string'),
67    )]
68    #[OA\Header(
69        header: 'x-pmf-token',
70        description: 'phpMyFAQ client API Token, generated in admin backend',
71        schema: new OA\Schema(type: 'string'),
72    )]
73    #[OA\RequestBody(required: true, content: new OA\MediaType(
74        mediaType: 'application/json',
75        schema: new OA\Schema(
76            required: [
77                'username',
78                'fullname',
79                'email',
80                'is-visible',
81            ],
82            properties: [
83                new OA\Property(property: 'username', type: 'string'),
84                new OA\Property(property: 'fullname', type: 'string'),
85                new OA\Property(property: 'email', type: 'string'),
86                new OA\Property(property: 'is-visible', type: 'boolean'),
87            ],
88            type: 'object',
89        ),
90        example: '{
91                "username": "ada",
92                "fullname": "Ada Lovelace",
93                "email": "ada.lovelace@example.org",
94                "is-visible": false
95            }',
96    ))]
97    #[OA\Response(
98        response: 201,
99        description: 'If "username", "fullname", "email", and "is-visible" combination is correct.',
100        content: new OA\JsonContent(example: ['registered' => true, 'success' => 'User created.']),
101    )]
102    #[OA\Response(
103        response: 400,
104        description: 'If "username", "fullname", "email", and "is-visible" combination is not correct.',
105        content: new OA\JsonContent(example: ['registered' => false, 'error' => 'Error message']),
106    )]
107    #[OA\Response(
108        response: 409,
109        description: 'If the domain of the email address is not allowed.',
110        content: new OA\JsonContent(example: ['registered' => false, 'error' => 'The domain is not allowed.']),
111    )]
112    #[OA\Response(
113        response: 401,
114        description: 'If the user is not authenticated.',
115        content: new OA\JsonContent(example: ['registered' => false]),
116    )]
117    #[Route(path: 'v4.0/register', name: 'api.registration.create', methods: ['POST'])]
118    public function create(Request $request): JsonResponse
119    {
120        $this->hasValidToken();
121
122        if (!$this->configuration->get(item: 'security.enableRegistration')) {
123            return $this->json([
124                'registered' => false,
125                'error' => 'User registration is disabled.',
126            ], Response::HTTP_FORBIDDEN);
127        }
128
129        $registrationHelper = is_callable($this->registrationHelperFactory)
130            ? ($this->registrationHelperFactory)($this->configuration)
131            : new RegistrationHelper($this->configuration);
132
133        $data = json_decode(json: $request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR);
134        if (!$data instanceof \stdClass) {
135            return $this->json([
136                'registered' => false,
137                'error' => 'The request body must be a JSON object.',
138            ], Response::HTTP_BAD_REQUEST);
139        }
140
141        $userName = trim((string) Filter::filterVar($data->username ?? '', FILTER_SANITIZE_SPECIAL_CHARS));
142        $fullName = trim((string) Filter::filterVar($data->fullname ?? '', FILTER_SANITIZE_SPECIAL_CHARS));
143        $email = trim((string) Filter::filterVar($data->email ?? '', FILTER_SANITIZE_EMAIL));
144        $isVisible = Filter::filterVar($data->{'is-visible'} ?? '', FILTER_SANITIZE_SPECIAL_CHARS);
145        $isVisible = $isVisible === 'true';
146
147        if (!$registrationHelper->isDomainAllowed($email)) {
148            $result = [
149                'registered' => false,
150                'error' => 'The domain is not allowed.',
151            ];
152            return $this->json($result, Response::HTTP_CONFLICT);
153        }
154
155        if (
156            $userName !== ''
157            && $userName !== '0'
158            && $fullName !== ''
159            && $fullName !== '0'
160            && ($email !== '' && $email !== '0')
161        ) {
162            $result = $registrationHelper->createUser($userName, $fullName, $email, $isVisible);
163
164            return $this->json($result, Response::HTTP_CREATED);
165        }
166
167        return $this->json([
168            'registered' => false,
169            'error' => Translation::get(key: 'err_sendMail'),
170        ], Response::HTTP_BAD_REQUEST);
171    }
172}