# Code Coverage for /home/users/tracer/www/spookie.lab.24unix.net/Spookie/src/Controller/SecurityController.php

| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 0.00% covered (danger) | 0.00% | 0 / 150 | 0.00% covered (danger) | 0.00% | 0 / 13 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
SecurityController | 0.00% covered (danger) | 0.00% | 0 / 150 | 0.00% covered (danger) | 0.00% | 0 / 13 | 1190 | 0.00% covered (danger) | 0.00% | 0 / 1
__construct | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
index | 0.00% covered (danger) | 0.00% | 0 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
logout | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
register | 0.00% covered (danger) | 0.00% | 0 / 19 | 0.00% covered (danger) | 0.00% | 0 / 1 | 20 |
verifyUserEmail | 0.00% covered (danger) | 0.00% | 0 / 12 | 0.00% covered (danger) | 0.00% | 0 / 1 | 20 |
request | 0.00% covered (danger) | 0.00% | 0 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
checkEmail | 0.00% covered (danger) | 0.00% | 0 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
reset | 0.00% covered (danger) | 0.00% | 0 / 30 | 0.00% covered (danger) | 0.00% | 0 / 1 | 42 |
processSendingPasswordResetEmail | 0.00% covered (danger) | 0.00% | 0 / 28 | 0.00% covered (danger) | 0.00% | 0 / 1 | 20 |
sendEmailConfirmation | 0.00% covered (danger) | 0.00% | 0 / 14 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
handleEmailConfirmation | 0.00% covered (danger) | 0.00% | 0 / 4 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
generateSignedUrlAndEmailToTheUser | 0.00% covered (danger) | 0.00% | 0 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
resendVerifyEmail | 0.00% covered (danger) | 0.00% | 0 / 9 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |

1 | <?php
2 |
3 | namespace App\Controller;
4 |
5 | use App\Entity\User;
6 | use App\Form\ChangePasswordFormType;
7 | use App\Form\RegistrationFormType;
8 | use App\Form\ResetPasswordRequestFormType;
9 | use App\Repository\UserRepository;
10 | use Doctrine\ORM\EntityManagerInterface;
11 | use Exception;
12 | use Symfony\Bridge\Twig\Mime\TemplatedEmail;
13 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
14 | use Symfony\Component\HttpFoundation\RedirectResponse;
15 | use Symfony\Component\HttpFoundation\Request;
16 | use Symfony\Component\HttpFoundation\Response;
17 | use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
18 | use Symfony\Component\Mailer\MailerInterface;
19 | use Symfony\Component\Mime\Address;
20 | use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
21 | use Symfony\Component\Routing\Annotation\Route;
22 | use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
23 | use Symfony\Contracts\Translation\TranslatorInterface;
24 | use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
25 | use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
26 | use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
27 | use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
28 | use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
29 |
30 | class SecurityController extends AbstractController
31 | {
32 | use ResetPasswordControllerTrait;
33 |
34 | public function __construct(private readonly ResetPasswordHelperInterface $resetPasswordHelper,
35 | private readonly EntityManagerInterface $entityManager,
36 | private readonly VerifyEmailHelperInterface $verifyEmailHelper,
37 | private readonly MailerInterface $mailer,
38 | )
39 | {
40 | // empty body
41 | }
42 |
43 | #[Route(path: '/security/login', name: 'security_login')]
44 | public function index(AuthenticationUtils $authenticationUtils): Response
45 | {
46 | // get the login error if there is one
47 | if ($error = $authenticationUtils->getLastAuthenticationError()) {
48 | $this->addFlash(type: 'error', message: $error->getMessageKey());
49 | }
50 |
51 | // last username entered by the user
52 | $lastUsername = $authenticationUtils->getLastUsername();
53 |
54 | return $this->render(view: '@default/security/login.html.twig', parameters: [
55 | 'last_username' => $lastUsername,
56 | 'error' => '',
57 | ]);
58 | }
59 |
60 | /**
61 | * @throws Exception
62 | */
63 | #[Route(path: '/security/logout', name: 'security_logout')]
64 | public function logout(): never
65 | {
66 | throw new Exception(message: 'Logout should never be reached.');
67 | }
68 |
69 | #[Route(path: '/security/register', name: 'security_register')]
70 | public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response
71 | {
72 | $form = $this->createForm(type: RegistrationFormType::class);
73 | $form->handleRequest(request: $request);
74 |
75 | if ($form->isSubmitted() && $form->isValid()) {
76 | $user = $form->getData();
77 | // hash the plain password
78 | $user->setPassword(
79 | password: $userPasswordHasher->hashPassword(
80 | user: $user,
81 | plainPassword: $form->get(name: 'password')->getData()
82 | )
83 | );
84 |
85 | if ($form->get(name: 'agreeTerms')->getData()) {
86 | $user->agreeTerms();
87 | } // no else, we already confirmed in the form itself
88 | $entityManager->persist(entity: $user);
89 | $entityManager->flush();
90 | $this->generateSignedUrlAndEmailToTheUser($user);
91 |
92 | return $this->render(view: '@default/security/registration_finished.html.twig');
93 | }
94 |
95 | return $this->renderForm(view: '@default/security/register.html.twig', parameters: [
96 | 'registrationForm' => $form,
97 | ]);
98 | }
99 |
100 | #[Route(path: '/security/verify/email', name: 'security_verify_email')]
101 | public function verifyUserEmail(Request $request, TranslatorInterface $translator, UserRepository $userRepository): Response
102 | {
103 | $id = $request->get(key: 'id');
104 |
105 | if ($id === null) {
106 | return $this->redirectToRoute(route: 'app_register');
107 | }
108 |
109 | $user = $userRepository->find($id);
110 |
111 | if ($user === null) {
112 | return $this->redirectToRoute(route: 'app_register');
113 | }
114 |
115 | // validate email confirmation link, sets User::isVerified=true and persists
116 | try {
117 | $this->handleEmailConfirmation(request: $request, user: $user);
118 | } catch (VerifyEmailExceptionInterface $exception) {
119 | $this->addFlash(type: 'error', message: $translator->trans(id: $exception->getReason(), parameters: [], domain: 'VerifyEmailBundle'));
120 |
121 | return $this->redirectToRoute(route: 'app_main');
122 | }
123 |
124 | $this->addFlash(type: 'success', message: 'Your email address has been verified.');
125 |
126 | return $this->redirectToRoute(route: 'app_main');
127 | }
128 |
129 | /**
130 | * Display & process form to request a password reset.
131 | */
132 | #[Route(path: '/security/forgot/password', name: 'security_forgot_password')]
133 | public function request(Request $request, MailerInterface $mailer): Response
134 | {
135 | $form = $this->createForm(type: ResetPasswordRequestFormType::class);
136 | $form->handleRequest(request: $request);
137 |
138 | if ($form->isSubmitted() && $form->isValid()) {
139 | return $this->processSendingPasswordResetEmail(
140 | formData: $form->get(name: 'account')->getData(),
141 | mailer: $mailer
142 | );
143 | }
144 |
145 | return $this->renderForm(view: '@default/security/forgot_password.html.twig', parameters: [
146 | 'requestForm' => $form,
147 | ]);
148 | }
149 |
150 | /**
151 | * Confirmation page after a user has requested a password reset.
152 | */
153 | #[Route(path: '/security/recovery/mail/sent', name: 'security_recovery_mail_sent')]
154 | public function checkEmail(): Response
155 | {
156 | // Generate a fake token if the user does not exist or someone hit this page directly.
157 | // This prevents exposing whether a user was found with the given email address or username or not
158 | if (null === ($resetToken = $this->getTokenObjectFromSession())) {
159 | $resetToken = $this->resetPasswordHelper->generateFakeResetToken();
160 | }
161 |
162 | return $this->render(view: '@default/security/recovery_mail_sent.html.twig', parameters: [
163 | 'resetToken' => $resetToken,
164 | ]);
165 | }
166 |
167 | /**
168 | * Validates and process the reset URL that the user clicked in their email.
169 | */
170 | #[Route(path: '/security/recovery/reset/{token}', name: 'security_recovery_reset')]
171 | public function reset(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, string $token = null): Response
172 | {
173 | if ($token) {
174 | // We store the token in session and remove it from the URL, to avoid the URL being
175 | // loaded in a browser and potentially leaking the token to 3rd party JavaScript.
176 | $this->storeTokenInSession(token: $token);
177 |
178 | return $this->redirectToRoute(route: 'security_recovery_reset');
179 | }
180 |
181 | $token = $this->getTokenFromSession();
182 | if (null === $token) {
183 | throw $this->createNotFoundException(message: 'No reset password token found in the URL or in the session.');
184 | }
185 |
186 | try {
187 | $user = $this->resetPasswordHelper->validateTokenAndFetchUser(fullToken: $token);
188 | } catch (ResetPasswordExceptionInterface $e) {
189 | $this->addFlash(type: 'reset_password_error', message: sprintf(
190 | '%s - %s',
191 | $translator->trans(id: ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, parameters: [], domain: 'ResetPasswordBundle'),
192 | $translator->trans(id: $e->getReason(), parameters: [], domain: 'ResetPasswordBundle')
193 | ));
194 |
195 | return $this->redirectToRoute(route: 'app_forgot_password_request');
196 | }
197 |
198 | // The token is valid; allow the user to change their password.
199 | $form = $this->createForm(type: ChangePasswordFormType::class);
200 | $form->handleRequest(request: $request);
201 |
202 | if ($form->isSubmitted() && $form->isValid()) {
203 | // A password reset token should be used only once, remove it.
204 | $this->resetPasswordHelper->removeResetRequest(fullToken: $token);
205 |
206 | // Encode(hash) the plain password, and set it.
207 | $encodedPassword = $passwordHasher->hashPassword(
208 | user: $user,
209 | plainPassword: $form->get(name: 'plainPassword')->getData()
210 | );
211 |
212 | $user->setPassword($encodedPassword);
213 | $this->entityManager->flush();
214 |
215 | // The session is cleaned up after the password has been changed.
216 | $this->cleanSessionAfterReset();
217 |
218 | $this->addFlash(type: 'success', message: 'Your password has been changed.');
219 |
220 | return $this->redirectToRoute(route: 'app_main');
221 | }
222 |
223 | return $this->renderForm(view: '@default/security/reset_password.html.twig', parameters: [
224 | 'resetForm' => $form,
225 | ]);
226 | }
227 |
228 | private function processSendingPasswordResetEmail(string $formData, MailerInterface $mailer): RedirectResponse
229 | {
230 | $user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [
231 | 'email' => $formData,
232 | ]);
233 |
234 | if (!$user) {
235 | $user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [
236 | 'username' => $formData,
237 | ]);
238 | }
239 |
240 | // Do not reveal whether a user account was found or not.
241 | // if (!$user) {
242 | // return $this->redirectToRoute(route: 'app_check_email');
243 | // }
244 |
245 | try {
246 | $resetToken = $this->resetPasswordHelper->generateResetToken(user: $user);
247 | } catch (ResetPasswordExceptionInterface $e) {
248 | $this->addFlash(type: 'reset_password_error', message: sprintf(
249 | '%s - %s',
250 | ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE,
251 | $e->getReason()
252 | ));
253 |
254 | return $this->redirectToRoute(route: 'security_forgot_password');
255 | }
256 |
257 | $email = (new TemplatedEmail())
258 | ->from(new Address(address: 'tracer@24unix.net', name: '24unix.net'))
259 | ->to($user->getEmail())
260 | ->subject(subject: 'Your password reset request')
261 | ->htmlTemplate(template: '@default/security/mail/recovery.html.twig')
262 | ->context(context: [
263 | 'resetToken' => $resetToken,
264 | ]);
265 |
266 | try {
267 | $mailer->send(message: $email);
268 | } catch (TransportExceptionInterface $e) {
269 | $this->addFlash(type: 'error', message: $e->getMessage());
270 | }
271 |
272 | // Store the token object in session for retrieval in check-email route.
273 | $this->setTokenObjectInSession(token: $resetToken);
274 |
275 | return $this->redirectToRoute(route: 'security_recovery_mail_sent');
276 | }
277 |
278 |
279 | public function sendEmailConfirmation(string $verifyEmailRouteName, User /* UserInterface */ $user, TemplatedEmail $email): void
280 | {
281 | $signatureComponents = $this->verifyEmailHelper->generateSignature(
282 | routeName: $verifyEmailRouteName,
283 | userId: $user->getId(),
284 | userEmail: $user->getEmail(),
285 | extraParams: ['id' => $user->getId()]
286 | );
287 |
288 | $context = $email->getContext();
289 | $context['signedUrl'] = $signatureComponents->getSignedUrl();
290 | $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
291 | $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
292 |
293 | $email->context(context: $context);
294 |
295 | try {
296 | $this->mailer->send(message: $email);
297 | } catch (TransportExceptionInterface $e) {
298 | die($e->getMessage());
299 | }
300 | }
301 |
302 | /**
303 | * @throws VerifyEmailExceptionInterface
304 | */
305 | public function handleEmailConfirmation(Request $request, User /*UserInterface*/ $user): void
306 | {
307 | $this->verifyEmailHelper->validateEmailConfirmation(signedUrl: $request->getUri(), userId: $user->getId(), userEmail: $user->getEmail());
308 | $user->setIsVerified(isVerified: true);
309 | $this->entityManager->persist(entity: $user);
310 | $this->entityManager->flush();
311 | }
312 |
313 | /**
314 | * @param mixed $user
315 | * @return void
316 | */
317 | public function generateSignedUrlAndEmailToTheUser(mixed $user): void
318 | {
319 | $this->sendEmailConfirmation(verifyEmailRouteName: 'security_verify_email', user: $user,
320 | email: (new TemplatedEmail())
321 | ->from(new Address(address: 'info@24unix.net', name: '24unix.net'))
322 | ->to($user->getEmail())
323 | ->subject(subject: 'Please Confirm your Email')
324 | ->htmlTemplate(template: '@default/security/mail/registration.html.twig')
325 | ->context(context: [
326 | 'username' => $user->getUsername()
327 | ])
328 | );
329 | }
330 |
331 |
332 | #[Route('/security/resend/verify_email', name: 'security_resend_verify_email')]
333 | public function resendVerifyEmail(Request $request, UserRepository $userRepository): Response
334 | {
335 |
336 | if ($request->isMethod('POST')) {
337 |
338 | $email = $request->getSession()->get('non_verified_email');
339 | $user = $userRepository->findOneBy(['email' => $email]);
340 | if (!$user) {
341 | throw $this->createNotFoundException('user not found for email');
342 | }
343 |
344 | $this->generateSignedUrlAndEmailToTheUser(user: $user);
345 | $this->addFlash('success', 'eMail has been sent.');
346 |
347 | return $this->redirectToRoute('app_main');
348 | }
349 | return $this->render('@default/security/resend_activation.html.twig');
350 | }
351 | }