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