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