]> Raphaël G. Git Repositories - userbundle/blob - Controller/DefaultController.php
3a6571267e4400c807c471715f1c92c3290c6de4
[userbundle] / Controller / DefaultController.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys UserBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\UserBundle\Controller;
13
14 use Doctrine\Bundle\DoctrineBundle\Registry;
15 use Doctrine\ORM\EntityManagerInterface;
16 use Psr\Log\LoggerInterface;
17 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
18 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
19 use Symfony\Component\DependencyInjection\ContainerInterface;
20 use Symfony\Component\Form\FormError;
21 use Symfony\Component\HttpFoundation\Request;
22 use Symfony\Component\HttpFoundation\Response;
23 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
24 use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
25 use Symfony\Component\Mailer\MailerInterface;
26 use Symfony\Component\Mime\Address;
27 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
28 use Symfony\Component\Routing\RouterInterface;
29 use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
30 use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
31 use Symfony\Component\Translation\TranslatorInterface;
32
33 use Rapsys\PackBundle\Util\SluggerUtil;
34 use Rapsys\UserBundle\RapsysUserBundle;
35
36 class DefaultController extends AbstractController {
37 //Config array
38 protected $config;
39
40 //Translator instance
41 protected $translator;
42
43 /**
44 * Constructor
45 *
46 * @TODO: move all canonical and other view related stuff in an user AbstractController like in RapsysAir render feature !!!!
47 *
48 * @param ContainerInterface $container The containter instance
49 * @param RouterInterface $router The router instance
50 * @param TranslatorInterface $translator The translator instance
51 */
52 public function __construct(ContainerInterface $container, RouterInterface $router, TranslatorInterface $translator) {
53 //Retrieve config
54 $this->config = $container->getParameter(self::getAlias());
55
56 //Set the translator
57 $this->translator = $translator;
58
59 //Get request stack
60 $stack = $container->get('request_stack');
61
62 //Get current request
63 $request = $stack->getCurrentRequest();
64
65 //Get current locale
66 $currentLocale = $request->getLocale();
67
68 //Set locale
69 $this->config['context']['locale'] = str_replace('_', '-', $currentLocale);
70
71 //Set translate array
72 $translates = [];
73
74 //Look for keys to translate
75 if (!empty($this->config['translate'])) {
76 //Iterate on keys to translate
77 foreach($this->config['translate'] as $translate) {
78 //Set tmp
79 $tmp = null;
80 //Iterate on keys
81 foreach(array_reverse(explode('.', $translate)) as $curkey) {
82 $tmp = array_combine([$curkey], [$tmp]);
83 }
84 //Append tree
85 $translates = array_replace_recursive($translates, $tmp);
86 }
87 }
88
89 //Inject every requested route in view and mail context
90 foreach($this->config as $tag => $current) {
91 //Look for entry with title subkey
92 if (!empty($current['title'])) {
93 //Translate title value
94 $this->config[$tag]['title'] = $translator->trans($current['title']);
95 }
96
97 //Look for entry with route subkey
98 if (!empty($current['route'])) {
99 //Generate url for both view and mail
100 foreach(['view', 'mail'] as $view) {
101 //Check that context key is usable
102 if (isset($current[$view]['context']) && is_array($current[$view]['context'])) {
103 //Merge with global context
104 $this->config[$tag][$view]['context'] = array_replace_recursive($this->config['context'], $this->config[$tag][$view]['context']);
105
106 //Process every routes
107 foreach($current['route'] as $route => $key) {
108 //With confirm route
109 if ($route == 'confirm') {
110 //Skip route as it requires some parameters
111 continue;
112 }
113
114 //Set value
115 $value = $router->generate(
116 $this->config['route'][$route]['name'],
117 $this->config['route'][$route]['context'],
118 //Generate absolute url for mails
119 $view=='mail'?UrlGeneratorInterface::ABSOLUTE_URL:UrlGeneratorInterface::ABSOLUTE_PATH
120 );
121
122 //Multi level key
123 if (strpos($key, '.') !== false) {
124 //Set tmp
125 $tmp = $value;
126
127 //Iterate on key
128 foreach(array_reverse(explode('.', $key)) as $curkey) {
129 $tmp = array_combine([$curkey], [$tmp]);
130 }
131
132 //Set value
133 $this->config[$tag][$view]['context'] = array_replace_recursive($this->config[$tag][$view]['context'], $tmp);
134 //Single level key
135 } else {
136 //Set value
137 $this->config[$tag][$view]['context'][$key] = $value;
138 }
139 }
140
141 //Look for successful intersections
142 if (!empty(array_intersect_key($translates, $this->config[$tag][$view]['context']))) {
143 //Iterate on keys to translate
144 foreach($this->config['translate'] as $translate) {
145 //Set keys
146 $keys = explode('.', $translate);
147
148 //Set tmp
149 $tmp = $this->config[$tag][$view]['context'];
150
151 //Iterate on keys
152 foreach($keys as $curkey) {
153 //Without child key
154 if (!isset($tmp[$curkey])) {
155 //Skip to next key
156 continue(2);
157 }
158
159 //Get child key
160 $tmp = $tmp[$curkey];
161 }
162
163 //Translate tmp value
164 $tmp = $translator->trans($tmp);
165
166 //Iterate on keys
167 foreach(array_reverse($keys) as $curkey) {
168 //Set parent key
169 $tmp = array_combine([$curkey], [$tmp]);
170 }
171
172 //Set value
173 $this->config[$tag][$view]['context'] = array_replace_recursive($this->config[$tag][$view]['context'], $tmp);
174 }
175 }
176
177 //With view context
178 if ($view == 'view') {
179 //Get context path
180 $pathInfo = $router->getContext()->getPathInfo();
181
182 //Iterate on locales excluding current one
183 foreach($this->config['locales'] as $locale) {
184 //Set titles
185 $titles = [];
186
187 //Iterate on other locales
188 foreach(array_diff($this->config['locales'], [$locale]) as $other) {
189 $titles[$other] = $translator->trans($this->config['languages'][$locale], [], null, $other);
190 }
191
192 //Retrieve route matching path
193 $route = $router->match($pathInfo);
194
195 //Get route name
196 $name = $route['_route'];
197
198 //Unset route name
199 unset($route['_route']);
200
201 //With current locale
202 if ($locale == $currentLocale) {
203 //Set locale locales context
204 $this->config[$tag][$view]['context']['canonical'] = $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL);
205 } else {
206 //Set locale locales context
207 $this->config[$tag][$view]['context']['alternates'][$locale] = [
208 'absolute' => $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
209 'relative' => $router->generate($name, ['_locale' => $locale]+$route),
210 'title' => implode('/', $titles),
211 'translated' => $translator->trans($this->config['languages'][$locale], [], null, $locale)
212 ];
213 }
214
215 //Add shorter locale
216 if (empty($this->config[$tag][$view]['context']['alternates'][$slocale = substr($locale, 0, 2)])) {
217 //Add shorter locale
218 $this->config[$tag][$view]['context']['alternates'][$slocale] = [
219 'absolute' => $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
220 'relative' => $router->generate($name, ['_locale' => $locale]+$route),
221 'title' => implode('/', $titles),
222 'translated' => $translator->trans($this->config['languages'][$locale], [], null, $locale)
223 ];
224 }
225 }
226 }
227 }
228 }
229 }
230 }
231 }
232
233 /**
234 * Confirm account from mail link
235 *
236 * @param Request $request The request
237 * @param Registry $manager The doctrine registry
238 * @param UserPasswordEncoderInterface $encoder The password encoder
239 * @param EntityManagerInterface $manager The doctrine entity manager
240 * @param SluggerUtil $slugger The slugger
241 * @param MailerInterface $mailer The mailer
242 * @param string $mail The shorted mail address
243 * @param string $hash The hashed password
244 * @return Response The response
245 */
246 public function confirm(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, $mail, $hash): Response {
247 //With invalid hash
248 if ($hash != $slugger->hash($mail)) {
249 //Throw bad request
250 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'hash', '%value%' => $hash]));
251 }
252
253 //Get mail
254 $mail = $slugger->unshort($smail = $mail);
255
256 //Without valid mail
257 if (filter_var($mail, FILTER_VALIDATE_EMAIL) === false) {
258 //Throw bad request
259 //XXX: prevent slugger reverse engineering by not displaying decoded mail
260 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'mail', '%value%' => $smail]));
261 }
262
263 //Without existing registrant
264 if (!($user = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail))) {
265 //Add error message mail already exists
266 //XXX: prevent slugger reverse engineering by not displaying decoded mail
267 $this->addFlash('error', $this->translator->trans('Account %mail% do not exists', ['%mail%' => $smail]));
268
269 //Redirect to register view
270 return $this->redirectToRoute($this->config['route']['register']['name'], ['mail' => $smail, 'field' => $sfield = $slugger->serialize([]), 'hash' => $slugger->hash($smail.$sfield)]+$this->config['route']['register']['context']);
271 }
272
273 //Set active
274 $user->setActive(true);
275
276 //Persist user
277 $manager->persist($user);
278
279 //Send to database
280 $manager->flush();
281
282 //Add error message mail already exists
283 $this->addFlash('notice', $this->translator->trans('Your account has been activated'));
284
285 //Redirect to user view
286 return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']);
287 }
288
289 /**
290 * Edit account by shorted mail
291 *
292 * @param Request $request The request
293 * @param Registry $manager The doctrine registry
294 * @param UserPasswordEncoderInterface $encoder The password encoder
295 * @param EntityManagerInterface $manager The doctrine entity manager
296 * @param SluggerUtil $slugger The slugger
297 * @param string $mail The shorted mail address
298 * @param string $hash The hashed password
299 * @return Response The response
300 */
301 public function edit(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, $mail, $hash): Response {
302 //With invalid hash
303 if ($hash != $slugger->hash($mail)) {
304 //Throw bad request
305 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'hash', '%value%' => $hash]));
306 }
307
308 //Get mail
309 $mail = $slugger->unshort($smail = $mail);
310
311 //With existing subscriber
312 if (empty($user = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail))) {
313 //Throw not found
314 //XXX: prevent slugger reverse engineering by not displaying decoded mail
315 throw $this->createNotFoundException($this->translator->trans('Unable to find account %mail%', ['%mail%' => $smail]));
316 }
317
318 //Prevent access when not admin, user is not guest and not currently logged user
319 if (!$this->isGranted('ROLE_ADMIN') && $user != $this->getUser() || !$this->isGranted('IS_AUTHENTICATED_FULLY')) {
320 //Throw access denied
321 //XXX: prevent slugger reverse engineering by not displaying decoded mail
322 throw $this->createAccessDeniedException($this->translator->trans('Unable to access user: %mail%', ['%mail%' => $smail]));
323 }
324
325 //Create the RegisterType form and give the proper parameters
326 $edit = $this->createForm($this->config['edit']['view']['edit'], $user, [
327 //Set action to register route name and context
328 'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']),
329 //Set civility class
330 'civility_class' => $this->config['class']['civility'],
331 //Set civility default
332 'civility_default' => $doctrine->getRepository($this->config['class']['civility'])->findOneByTitle($this->config['default']['civility']),
333 //Disable mail
334 'mail' => $this->isGranted('ROLE_ADMIN'),
335 //Disable password
336 'password' => false,
337 //Set method
338 'method' => 'POST'
339 ]);
340
341 //With admin role
342 if ($this->isGranted('ROLE_ADMIN')) {
343 //Create the LoginType form and give the proper parameters
344 $reset = $this->createForm($this->config['edit']['view']['reset'], $user, [
345 //Set action to register route name and context
346 'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']),
347 //Disable mail
348 'mail' => false,
349 //Set method
350 'method' => 'POST'
351 ]);
352
353 //With post method
354 if ($request->isMethod('POST')) {
355 //Refill the fields in case the form is not valid.
356 $reset->handleRequest($request);
357
358 //With reset submitted and valid
359 if ($reset->isSubmitted() && $reset->isValid()) {
360 //Set data
361 $data = $reset->getData();
362
363 //Set password
364 $data->setPassword($encoder->encodePassword($data, $data->getPassword()));
365
366 //Queue snippet save
367 $manager->persist($data);
368
369 //Flush to get the ids
370 $manager->flush();
371
372 //Add notice
373 $this->addFlash('notice', $this->translator->trans('Account %mail% password updated', ['%mail%' => $mail = $data->getMail()]));
374
375 //Redirect to cleanup the form
376 return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']);
377 }
378 }
379
380 //Add reset view
381 $this->config['edit']['view']['context']['reset'] = $reset->createView();
382 //Without admin role
383 //XXX: prefer a reset on login to force user unspam action
384 } else {
385 //Add notice
386 $this->addFlash('notice', $this->translator->trans('To change your password login with your mail and any password then follow the procedure'));
387 }
388
389 //With post method
390 if ($request->isMethod('POST')) {
391 //Refill the fields in case the form is not valid.
392 $edit->handleRequest($request);
393
394 //With edit submitted and valid
395 if ($edit->isSubmitted() && $edit->isValid()) {
396 //Set data
397 $data = $edit->getData();
398
399 //Queue snippet save
400 $manager->persist($data);
401
402 //Flush to get the ids
403 $manager->flush();
404
405 //Add notice
406 $this->addFlash('notice', $this->translator->trans('Account %mail% updated', ['%mail%' => $mail = $data->getMail()]));
407
408 //Redirect to cleanup the form
409 return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']);
410 }
411 }
412
413 //Render view
414 return $this->render(
415 //Template
416 $this->config['edit']['view']['name'],
417 //Context
418 ['edit' => $edit->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['edit']['view']['context']
419 );
420 }
421
422 /**
423 * Login
424 *
425 * @param Request $request The request
426 * @param AuthenticationUtils $authenticationUtils The authentication utils
427 * @param RouterInterface $router The router instance
428 * @param SluggerUtil $slugger The slugger
429 * @param string $mail The shorted mail address
430 * @param string $hash The hashed password
431 * @return Response The response
432 */
433 public function login(Request $request, AuthenticationUtils $authenticationUtils, RouterInterface $router, SluggerUtil $slugger, $mail, $hash): Response {
434 //Create the LoginType form and give the proper parameters
435 $login = $this->createForm($this->config['login']['view']['form'], null, [
436 //Set action to login route name and context
437 'action' => $this->generateUrl($this->config['route']['login']['name'], $this->config['route']['login']['context']),
438 //Disable repeated password
439 'password_repeated' => false,
440 //Set method
441 'method' => 'POST'
442 ]);
443
444 //Init context
445 $context = [];
446
447 //With mail
448 if (!empty($mail) && !empty($hash)) {
449 //With invalid hash
450 if ($hash != $slugger->hash($mail)) {
451 //Throw bad request
452 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'hash', '%value%' => $hash]));
453 }
454
455 //Get mail
456 $mail = $slugger->unshort($smail = $mail);
457
458 //Without valid mail
459 if (filter_var($mail, FILTER_VALIDATE_EMAIL) === false) {
460 //Throw bad request
461 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'mail', '%value%' => $smail]));
462 }
463
464 //Prefilled mail
465 $login->get('mail')->setData($mail);
466 //Last username entered by the user
467 } elseif ($lastUsername = $authenticationUtils->getLastUsername()) {
468 $login->get('mail')->setData($lastUsername);
469 }
470
471 //Get the login error if there is one
472 if ($error = $authenticationUtils->getLastAuthenticationError()) {
473 //Get translated error
474 $error = $this->translator->trans($error->getMessageKey());
475
476 //Add error message to mail field
477 $login->get('mail')->addError(new FormError($error));
478
479 //Create the LoginType form and give the proper parameters
480 $recover = $this->createForm($this->config['recover']['view']['form'], null, [
481 //Set action to recover route name and context
482 'action' => $this->generateUrl($this->config['route']['recover']['name'], $this->config['route']['recover']['context']),
483 //Without password
484 'password' => false,
485 //Set method
486 'method' => 'POST'
487 ]);
488
489 //Get recover mail entity
490 $recover->get('mail')
491 //Set mail from login form
492 ->setData($login->get('mail')->getData())
493 //Add recover error
494 ->addError(new FormError($this->translator->trans('Use this form to recover your account')));
495
496 //Add recover form to context
497 $context['recover'] = $recover->createView();
498 } else {
499 //Add notice
500 $this->addFlash('notice', $this->translator->trans('To change your password login with your mail and any password then follow the procedure'));
501 }
502
503 //Render view
504 return $this->render(
505 //Template
506 $this->config['login']['view']['name'],
507 //Context
508 ['login' => $login->createView()]+$context+$this->config['login']['view']['context']
509 );
510 }
511
512 /**
513 * Recover account
514 *
515 * @param Request $request The request
516 * @param Registry $manager The doctrine registry
517 * @param UserPasswordEncoderInterface $encoder The password encoder
518 * @param EntityManagerInterface $manager The doctrine entity manager
519 * @param SluggerUtil $slugger The slugger
520 * @param MailerInterface $mailer The mailer
521 * @param string $mail The shorted mail address
522 * @param string $pass The shorted password
523 * @param string $hash The hashed password
524 * @return Response The response
525 */
526 public function recover(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, $mail, $pass, $hash): Response {
527 //Without mail, pass and hash
528 if (empty($mail) && empty($pass) && empty($hash)) {
529 //Create the LoginType form and give the proper parameters
530 $form = $this->createForm($this->config['recover']['view']['form'], null, [
531 //Set action to recover route name and context
532 'action' => $this->generateUrl($this->config['route']['recover']['name'], $this->config['route']['recover']['context']),
533 //Without password
534 'password' => false,
535 //Set method
536 'method' => 'POST'
537 ]);
538
539 if ($request->isMethod('POST')) {
540 //Refill the fields in case the form is not valid.
541 $form->handleRequest($request);
542
543 if ($form->isValid()) {
544 //Set data
545 $data = $form->getData();
546
547 //Find user by data mail
548 if ($user = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($data['mail'])) {
549 //Set mail shortcut
550 $recoverMail =& $this->config['recover']['mail'];
551
552 //Set mail
553 $mail = $slugger->short($user->getMail());
554
555 //Set pass
556 $pass = $slugger->hash($user->getPassword());
557
558 //Generate each route route
559 foreach($this->config['recover']['route'] as $route => $tag) {
560 //Only process defined routes
561 if (!empty($this->config['route'][$route])) {
562 //Process for recover mail url
563 if ($route == 'recover') {
564 //Set the url in context
565 $recoverMail['context'][$tag] = $this->get('router')->generate(
566 $this->config['route'][$route]['name'],
567 //Prepend recover context with tag
568 [
569 'mail' => $mail,
570 'pass' => $pass,
571 'hash' => $slugger->hash($mail.$pass)
572 ]+$this->config['route'][$route]['context'],
573 UrlGeneratorInterface::ABSOLUTE_URL
574 );
575 }
576 }
577 }
578
579 //Set recipient_name
580 $recoverMail['context']['recipient_mail'] = $user->getMail();
581
582 //Set recipient_name
583 $recoverMail['context']['recipient_name'] = trim($user->getForename().' '.$user->getSurname().($user->getPseudonym()?' ('.$user->getPseudonym().')':''));
584
585 //Init subject context
586 $subjectContext = $slugger->flatten(array_replace_recursive($this->config['recover']['view']['context'], $recoverMail['context']), null, '.', '%', '%');
587
588 //Translate subject
589 $recoverMail['subject'] = ucfirst($this->translator->trans($recoverMail['subject'], $subjectContext));
590
591 //Create message
592 $message = (new TemplatedEmail())
593 //Set sender
594 ->from(new Address($this->config['contact']['mail'], $this->config['contact']['title']))
595 //Set recipient
596 //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
597 ->to(new Address($recoverMail['context']['recipient_mail'], $recoverMail['context']['recipient_name']))
598 //Set subject
599 ->subject($recoverMail['subject'])
600
601 //Set path to twig templates
602 ->htmlTemplate($recoverMail['html'])
603 ->textTemplate($recoverMail['text'])
604
605 //Set context
606 //XXX: require recursive merge to avoid loosing subkeys
607 //['subject' => $recoverMail['subject']]+$recoverMail['context']+$this->config['recover']['view']['context']
608 ->context(array_replace_recursive($this->config['recover']['view']['context'], $recoverMail['context'], ['subject' => $recoverMail['subject']]));
609
610 //Try sending message
611 //XXX: mail delivery may silently fail
612 try {
613 //Send message
614 $mailer->send($message);
615
616 //Redirect on the same route with sent=1 to cleanup form
617 return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
618 //Catch obvious transport exception
619 } catch(TransportExceptionInterface $e) {
620 //Add error message mail unreachable
621 $form->get('mail')->addError(new FormError($this->translator->trans('Account found but unable to contact: %mail%', array('%mail%' => $data['mail']))));
622 }
623 //Accout not found
624 } else {
625 //Add error message to mail field
626 $form->get('mail')->addError(new FormError($this->translator->trans('Unable to find account %mail%', ['%mail%' => $data['mail']])));
627 }
628 }
629 }
630
631 //Render view
632 return $this->render(
633 //Template
634 $this->config['recover']['view']['name'],
635 //Context
636 ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['recover']['view']['context']
637 );
638 }
639
640 //With invalid hash
641 if ($hash != $slugger->hash($mail.$pass)) {
642 //Throw bad request
643 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'hash', '%value%' => $hash]));
644 }
645
646 //Get mail
647 $mail = $slugger->unshort($smail = $mail);
648
649 //Without valid mail
650 if (filter_var($mail, FILTER_VALIDATE_EMAIL) === false) {
651 //Throw bad request
652 //XXX: prevent slugger reverse engineering by not displaying decoded mail
653 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'mail', '%value%' => $smail]));
654 }
655
656 //With existing subscriber
657 if (empty($user = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail))) {
658 //Throw not found
659 //XXX: prevent slugger reverse engineering by not displaying decoded mail
660 throw $this->createNotFoundException($this->translator->trans('Unable to find account %mail%', ['%mail%' => $smail]));
661 }
662
663 //With unmatched pass
664 if ($pass != $slugger->hash($user->getPassword())) {
665 //Throw not found
666 //XXX: prevent use of outdated recover link
667 throw $this->createNotFoundException($this->translator->trans('Outdated recover link'));
668 }
669
670 //Create the LoginType form and give the proper parameters
671 $form = $this->createForm($this->config['recover']['view']['form'], $user, [
672 //Set action to recover route name and context
673 'action' => $this->generateUrl($this->config['route']['recover']['name'], ['mail' => $smail, 'pass' => $pass, 'hash' => $hash]+$this->config['route']['recover']['context']),
674 //Without mail
675 'mail' => false,
676 //Set method
677 'method' => 'POST'
678 ]);
679
680 if ($request->isMethod('POST')) {
681 //Refill the fields in case the form is not valid.
682 $form->handleRequest($request);
683
684 if ($form->isValid()) {
685 //Set data
686 $data = $form->getData();
687
688 //Set encoded password
689 $encoded = $encoder->encodePassword($user, $user->getPassword());
690
691 //Update pass
692 $pass = $slugger->hash($encoded);
693
694 //Set user password
695 $user->setPassword($encoded);
696
697 //Persist user
698 $manager->persist($user);
699
700 //Send to database
701 $manager->flush();
702
703 //Add notice
704 $this->addFlash('notice', $this->translator->trans('Account %mail% password updated', ['%mail%' => $mail]));
705
706 //Redirect to user login
707 return $this->redirectToRoute($this->config['route']['login']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['login']['context']);
708 }
709 }
710
711 //Render view
712 return $this->render(
713 //Template
714 $this->config['recover']['view']['name'],
715 //Context
716 ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['recover']['view']['context']
717 );
718 }
719
720 /**
721 * Register an account
722 *
723 * @param Request $request The request
724 * @param Registry $manager The doctrine registry
725 * @param UserPasswordEncoderInterface $encoder The password encoder
726 * @param EntityManagerInterface $manager The doctrine entity manager
727 * @param SluggerUtil $slugger The slugger
728 * @param MailerInterface $mailer The mailer
729 * @param LoggerInterface $logger The logger
730 * @param string $mail The shorted mail address
731 * @param string $field The serialized then shorted form field array
732 * @param string $hash The hashed serialized field array
733 * @return Response The response
734 */
735 public function register(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, LoggerInterface $logger, $mail, $field, $hash): Response {
736 //With mail
737 if (!empty($_POST['register']['mail'])) {
738 //Log new user infos
739 $logger->emergency(
740 $this->translator->trans(
741 'register: mail=%mail% locale=%locale% confirm=%confirm%',
742 [
743 '%mail%' => $postMail = $_POST['register']['mail'],
744 '%locale%' => $request->getLocale(),
745 '%confirm%' => $this->get('router')->generate(
746 $this->config['route']['confirm']['name'],
747 //Prepend subscribe context with tag
748 [
749 'mail' => $postSmail = $slugger->short($postMail),
750 'hash' => $slugger->hash($postSmail)
751 ]+$this->config['route']['confirm']['context'],
752 UrlGeneratorInterface::ABSOLUTE_URL
753 )
754 ]
755 )
756 );
757 }
758
759 //With mail and field
760 if (!empty($field) && !empty($hash)) {
761 //With invalid hash
762 if ($hash != $slugger->hash($mail.$field)) {
763 //Throw bad request
764 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'hash', '%value%' => $hash]));
765 }
766
767 //With mail
768 if (!empty($mail)) {
769 //Get mail
770 $mail = $slugger->unshort($smail = $mail);
771
772 //Without valid mail
773 if (filter_var($mail, FILTER_VALIDATE_EMAIL) === false) {
774 //Throw bad request
775 //XXX: prevent slugger reverse engineering by not displaying decoded mail
776 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'mail', '%value%' => $smail]));
777 }
778
779 //With existing registrant
780 if ($existing = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail)) {
781 //With disabled existing
782 if ($existing->isDisabled()) {
783 //Render view
784 return $this->render(
785 //Template
786 $this->config['register']['view']['name'],
787 //Context
788 ['title' => $this->translator->trans('Access denied'), 'disabled' => 1]+$this->config['register']['view']['context'],
789 //Set 403
790 new Response('', 403)
791 );
792 //With unactivated existing
793 } elseif (!$existing->isActivated()) {
794 //Set mail shortcut
795 //TODO: change for activate ???
796 $activateMail =& $this->config['register']['mail'];
797
798 //Generate each route route
799 foreach($this->config['register']['route'] as $route => $tag) {
800 //Only process defined routes
801 if (!empty($this->config['route'][$route])) {
802 //Process for confirm url
803 if ($route == 'confirm') {
804 //Set the url in context
805 $activateMail['context'][$tag] = $this->get('router')->generate(
806 $this->config['route'][$route]['name'],
807 //Prepend subscribe context with tag
808 [
809 'mail' => $smail = $slugger->short($existing->getMail()),
810 'hash' => $slugger->hash($smail)
811 ]+$this->config['route'][$route]['context'],
812 UrlGeneratorInterface::ABSOLUTE_URL
813 );
814 }
815 }
816 }
817
818 //Set recipient_name
819 $activateMail['context']['recipient_mail'] = $existing->getMail();
820
821 //Set recipient name
822 $activateMail['context']['recipient_name'] = implode(' ', [$existing->getForename(), $existing->getSurname(), $existing->getPseudonym()?'('.$existing->getPseudonym().')':'']);
823
824 //Init subject context
825 $subjectContext = $slugger->flatten(array_replace_recursive($this->config['register']['view']['context'], $activateMail['context']), null, '.', '%', '%');
826
827 //Translate subject
828 $activateMail['subject'] = ucfirst($this->translator->trans($activateMail['subject'], $subjectContext));
829
830 //Create message
831 $message = (new TemplatedEmail())
832 //Set sender
833 ->from(new Address($this->config['contact']['mail'], $this->config['contact']['title']))
834 //Set recipient
835 //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
836 ->to(new Address($activateMail['context']['recipient_mail'], $activateMail['context']['recipient_name']))
837 //Set subject
838 ->subject($activateMail['subject'])
839
840 //Set path to twig templates
841 ->htmlTemplate($activateMail['html'])
842 ->textTemplate($activateMail['text'])
843
844 //Set context
845 ->context(['subject' => $activateMail['subject']]+$activateMail['context']);
846
847 //Try sending message
848 //XXX: mail delivery may silently fail
849 try {
850 //Send message
851 $mailer->send($message);
852 //Catch obvious transport exception
853 } catch(TransportExceptionInterface $e) {
854 //Add error message mail unreachable
855 $this->addFlash('error', $this->translator->trans('Account %mail% tried activate but unable to contact', ['%mail%' => $existing->getMail()]));
856 }
857
858 //Get route params
859 $routeParams = $request->get('_route_params');
860
861 //Remove mail, field and hash from route params
862 unset($routeParams['mail'], $routeParams['field'], $routeParams['hash']);
863
864 //Redirect on the same route with sent=1 to cleanup form
865 return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$routeParams);
866 }
867
868 //Add error message mail already exists
869 $this->addFlash('warning', $this->translator->trans('Account %mail% already exists', ['%mail%' => $existing->getMail()]));
870
871 //Redirect to user view
872 return $this->redirectToRoute(
873 $this->config['route']['edit']['name'],
874 [
875 'mail' => $smail = $slugger->short($existing->getMail()),
876 'hash' => $slugger->hash($smail)
877 ]+$this->config['route']['edit']['context']
878 );
879 }
880 //Without mail
881 } else {
882 //Set smail
883 $smail = $mail;
884 }
885
886 //Try
887 try {
888 //Unshort then unserialize field
889 $field = $slugger->unserialize($sfield = $field);
890 //Catch type error
891 } catch (\Error|\Exception $e) {
892 //Throw bad request
893 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'field', '%value%' => $field]), $e);
894 }
895
896 //With non array field
897 if (!is_array($field)) {
898 //Throw bad request
899 throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'field', '%value%' => $field]));
900 }
901 //Without field and hash
902 } else {
903 //Set smail
904 $smail = $mail;
905
906 //Set smail
907 $sfield = $field;
908
909 //Reset field
910 $field = [];
911 }
912
913 //Init reflection
914 $reflection = new \ReflectionClass($this->config['class']['user']);
915
916 //Create new user
917 $user = $reflection->newInstance(strval($mail));
918
919 //Create the RegisterType form and give the proper parameters
920 $form = $this->createForm($this->config['register']['view']['form'], $user, $field+[
921 //Set action to register route name and context
922 'action' => $this->generateUrl($this->config['route']['register']['name'], ['mail' => $smail, 'field' => $sfield, 'hash' => $hash]+$this->config['route']['register']['context']),
923 //Set civility class
924 'civility_class' => $this->config['class']['civility'],
925 //Set civility default
926 'civility_default' => $doctrine->getRepository($this->config['class']['civility'])->findOneByTitle($this->config['default']['civility']),
927 //With mail
928 'mail' => true,
929 //Set method
930 'method' => 'POST'
931 ]);
932
933 if ($request->isMethod('POST')) {
934 //Refill the fields in case the form is not valid.
935 $form->handleRequest($request);
936
937 if ($form->isValid()) {
938 //Set data
939 $data = $form->getData();
940
941 //With existing registrant
942 if ($doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail = $data->getMail())) {
943 //Add error message mail already exists
944 $this->addFlash('warning', $this->translator->trans('Account %mail% already exists', ['%mail%' => $mail]));
945
946 //Redirect to user view
947 return $this->redirectToRoute(
948 $this->config['route']['edit']['name'],
949 [
950 'mail' => $smail = $slugger->short($mail),
951 'hash' => $slugger->hash($smail)
952 ]+$this->config['route']['edit']['context']
953 );
954 }
955
956 //Set mail shortcut
957 $registerMail =& $this->config['register']['mail'];
958
959 //Extract names and pseudonym from mail
960 $names = explode(' ', $pseudonym = ucwords(trim(preg_replace('/[^a-zA-Z]+/', ' ', current(explode('@', $data->getMail()))))));
961
962 //Set pseudonym
963 $user->setPseudonym($user->getPseudonym()??$pseudonym);
964
965 //Set forename
966 $user->setForename($user->getForename()??$names[0]);
967
968 //Set surname
969 $user->setSurname($user->getSurname()??$names[1]??$names[0]);
970
971 //Set password
972 $user->setPassword($encoder->encodePassword($user, $user->getPassword()??$data->getMail()));
973
974 //Set created
975 $user->setCreated(new \DateTime('now'));
976
977 //Set updated
978 $user->setUpdated(new \DateTime('now'));
979
980 //Persist user
981 $manager->persist($user);
982
983 //Iterate on default group
984 foreach($this->config['default']['group'] as $i => $groupTitle) {
985 //Fetch group
986 if (($group = $doctrine->getRepository($this->config['class']['group'])->findOneByTitle($groupTitle))) {
987 //Set default group
988 //XXX: see vendor/symfony/security-core/Role/Role.php
989 $user->addGroup($group);
990 //Group not found
991 } else {
992 //Throw exception
993 //XXX: consider missing group as fatal
994 throw new \Exception(sprintf('Group from rapsys_user.default.group[%d] not found by title: %s', $i, $groupTitle));
995 }
996 }
997
998 //Generate each route route
999 foreach($this->config['register']['route'] as $route => $tag) {
1000 //Only process defined routes
1001 if (!empty($this->config['route'][$route])) {
1002 //Process for confirm url
1003 if ($route == 'confirm') {
1004 //Set the url in context
1005 $registerMail['context'][$tag] = $this->get('router')->generate(
1006 $this->config['route'][$route]['name'],
1007 //Prepend subscribe context with tag
1008 [
1009 'mail' => $smail = $slugger->short($data->getMail()),
1010 'hash' => $slugger->hash($smail)
1011 ]+$this->config['route'][$route]['context'],
1012 UrlGeneratorInterface::ABSOLUTE_URL
1013 );
1014 }
1015 }
1016 }
1017
1018 //XXX: DEBUG: remove me
1019 //die($registerMail['context']['confirm_url']);
1020
1021 //Set recipient_name
1022 $registerMail['context']['recipient_mail'] = $data->getMail();
1023
1024 //Set recipient name
1025 $registerMail['context']['recipient_name'] = '';
1026
1027 //Set recipient name
1028 $registerMail['context']['recipient_name'] = implode(' ', [$data->getForename(), $data->getSurname(), $data->getPseudonym()?'('.$data->getPseudonym().')':'']);
1029
1030 //Init subject context
1031 $subjectContext = $slugger->flatten(array_replace_recursive($this->config['register']['view']['context'], $registerMail['context']), null, '.', '%', '%');
1032
1033 //Translate subject
1034 $registerMail['subject'] = ucfirst($this->translator->trans($registerMail['subject'], $subjectContext));
1035
1036 //Create message
1037 $message = (new TemplatedEmail())
1038 //Set sender
1039 ->from(new Address($this->config['contact']['mail'], $this->config['contact']['title']))
1040 //Set recipient
1041 //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
1042 ->to(new Address($registerMail['context']['recipient_mail'], $registerMail['context']['recipient_name']))
1043 //Set subject
1044 ->subject($registerMail['subject'])
1045
1046 //Set path to twig templates
1047 ->htmlTemplate($registerMail['html'])
1048 ->textTemplate($registerMail['text'])
1049
1050 //Set context
1051 ->context(['subject' => $registerMail['subject']]+$registerMail['context']);
1052
1053 //Try saving in database
1054 try {
1055 //Send to database
1056 $manager->flush();
1057
1058 //Add error message mail already exists
1059 $this->addFlash('notice', $this->translator->trans('Your account has been created'));
1060
1061 //Try sending message
1062 //XXX: mail delivery may silently fail
1063 try {
1064 //Send message
1065 $mailer->send($message);
1066
1067 //Redirect on the same route with sent=1 to cleanup form
1068 return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
1069 //Catch obvious transport exception
1070 } catch(TransportExceptionInterface $e) {
1071 //Add error message mail unreachable
1072 $form->get('mail')->addError(new FormError($this->translator->trans('Account %mail% tried subscribe but unable to contact', ['%mail%' => $data->getMail()])));
1073 }
1074 //Catch double subscription
1075 } catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
1076 //Add error message mail already exists
1077 $this->addFlash('error', $this->translator->trans('Account %mail% already exists', ['%mail%' => $mail]));
1078 }
1079 }
1080 }
1081
1082 //Render view
1083 return $this->render(
1084 //Template
1085 $this->config['register']['view']['name'],
1086 //Context
1087 ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['register']['view']['context']
1088 );
1089 }
1090
1091 /**
1092 * {@inheritdoc}
1093 */
1094 public function getAlias(): string {
1095 return RapsysUserBundle::getAlias();
1096 }
1097 }