]> Raphaƫl G. Git Repositories - userbundle/blob - Controller/DefaultController.php
Add preUpdate life cycle callback
[userbundle] / Controller / DefaultController.php
1 <?php
2
3 namespace Rapsys\UserBundle\Controller;
4
5 use Rapsys\UserBundle\Utils\Slugger;
6 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
7 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
8 use Symfony\Component\DependencyInjection\ContainerInterface;
9 use Symfony\Component\Form\FormError;
10 use Symfony\Component\HttpFoundation\Request;
11 use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
12 use Symfony\Component\Mailer\MailerInterface;
13 use Symfony\Component\Mime\Address;
14 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
15 use Symfony\Component\Routing\RouterInterface;
16 use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
17 use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
18 use Symfony\Component\Translation\TranslatorInterface;
19
20 class DefaultController extends AbstractController {
21 //Config array
22 protected $config;
23
24 //Translator instance
25 protected $translator;
26
27 /**
28 * Constructor
29 *
30 * @param ContainerInterface $container The containter instance
31 * @param RouterInterface $router The router instance
32 * @param TranslatorInterface $translator The translator instance
33 */
34 public function __construct(ContainerInterface $container, RouterInterface $router, TranslatorInterface $translator) {
35 //Retrieve config
36 $this->config = $container->getParameter($this->getAlias());
37
38 //Set the translator
39 $this->translator = $translator;
40
41 //Get current action
42 //XXX: we don't use this as it would be too slow, maybe ???
43 #$action = str_replace(self::getAlias().'_', '', $container->get('request_stack')->getCurrentRequest()->get('_route'));
44
45 //Set translate array
46 $translates = [];
47
48 //Look for keys to translate
49 if (!empty($this->config['translate'])) {
50 //Iterate on keys to translate
51 foreach($this->config['translate'] as $translate) {
52 //Set tmp
53 $tmp = null;
54 //Iterate on keys
55 foreach(array_reverse(explode('.', $translate)) as $curkey) {
56 $tmp = array_combine([$curkey], [$tmp]);
57 }
58 //Append tree
59 $translates = array_replace_recursive($translates, $tmp);
60 }
61 }
62
63 //Inject every requested route in view and mail context
64 foreach($this->config as $tag => $current) {
65 //Look for entry with route subkey
66 if (!empty($current['route'])) {
67 //Generate url for both view and mail
68 foreach(['view', 'mail'] as $view) {
69 //Check that context key is usable
70 if (isset($current[$view]['context']) && is_array($current[$view]['context'])) {
71 //Process every routes
72 foreach($current['route'] as $route => $key) {
73 //Skip recover_mail route as it requires some parameters
74 if ($route == 'recover_mail') {
75 continue;
76 }
77
78 //Set value
79 $value = $router->generate(
80 $this->config['route'][$route]['name'],
81 $this->config['route'][$route]['context'],
82 //Generate absolute url for mails
83 $view=='mail'?UrlGeneratorInterface::ABSOLUTE_URL:UrlGeneratorInterface::ABSOLUTE_PATH
84 );
85
86 //Multi level key
87 if (strpos($key, '.') !== false) {
88 //Set tmp
89 $tmp = $value;
90
91 //Iterate on key
92 foreach(array_reverse(explode('.', $key)) as $curkey) {
93 $tmp = array_combine([$curkey], [$tmp]);
94 }
95
96 //Set value
97 $this->config[$tag][$view]['context'] = array_replace_recursive($this->config[$tag][$view]['context'], $tmp);
98 //Single level key
99 } else {
100 //Set value
101 $this->config[$tag][$view]['context'][$key] = $value;
102 }
103 }
104
105 //Look for successful intersections
106 if (!empty(array_intersect_key($translates, $current[$view]['context']))) {
107 //Iterate on keys to translate
108 foreach($this->config['translate'] as $translate) {
109 //Set keys
110 $keys = explode('.', $translate);
111
112 //Set tmp
113 $tmp = $current[$view]['context'];
114
115 //Iterate on keys
116 foreach($keys as $curkey) {
117 //Get child key
118 $tmp = $tmp[$curkey];
119 }
120
121 //Translate tmp value
122 $tmp = $translator->trans($tmp);
123
124 //Iterate on keys
125 foreach(array_reverse($keys) as $curkey) {
126 //Set parent key
127 $tmp = array_combine([$curkey], [$tmp]);
128 }
129
130 //Set value
131 $this->config[$tag][$view]['context'] = array_replace_recursive($this->config[$tag][$view]['context'], $tmp);
132 }
133 }
134
135 //Get current locale
136 $currentLocale = $router->getContext()->getParameters()['_locale'];
137
138 //Iterate on locales excluding current one
139 foreach($this->config['locales'] as $locale) {
140 //Set titles
141 $titles = [];
142
143 //Iterate on other locales
144 foreach(array_diff($this->config['locales'], [$locale]) as $other) {
145 $titles[$other] = $translator->trans($this->config['languages'][$locale], [], null, $other);
146 }
147
148 //Get context path
149 $path = $router->getContext()->getPathInfo();
150
151 //Retrieve route matching path
152 $route = $router->match($path);
153
154 //Get route name
155 $name = $route['_route'];
156
157 //Unset route name
158 unset($route['_route']);
159
160 //With current locale
161 if ($locale == $currentLocale) {
162 //Set locale locales context
163 $this->config[$tag][$view]['context']['canonical'] = $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL);
164 } else {
165 //Set locale locales context
166 $this->config[$tag][$view]['context']['alternates'][] = [
167 'lang' => $locale,
168 'absolute' => $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
169 'relative' => $router->generate($name, ['_locale' => $locale]+$route),
170 'title' => implode('/', $titles),
171 'translated' => $translator->trans($this->config['languages'][$locale], [], null, $locale)
172 ];
173 }
174 }
175 }
176 }
177 }
178 }
179 }
180
181 /**
182 * Login
183 *
184 * @param Request $request The request
185 * @param AuthenticationUtils $authenticationUtils The authentication utils
186 * @return Response The response
187 */
188 public function login(Request $request, AuthenticationUtils $authenticationUtils) {
189 //Create the LoginType form and give the proper parameters
190 $login = $this->createForm($this->config['login']['view']['form'], null, [
191 //Set action to login route name and context
192 'action' => $this->generateUrl($this->config['route']['login']['name'], $this->config['route']['login']['context']),
193 'method' => 'POST'
194 ]);
195
196 //Init context
197 $context = [];
198
199 //Last username entered by the user
200 if ($lastUsername = $authenticationUtils->getLastUsername()) {
201 $login->get('mail')->setData($lastUsername);
202 }
203
204 //Get the login error if there is one
205 if ($error = $authenticationUtils->getLastAuthenticationError()) {
206 //Get translated error
207 $error = $this->translator->trans($error->getMessageKey());
208
209 //Add error message to mail field
210 $login->get('mail')->addError(new FormError($error));
211
212 //Create the RecoverType form and give the proper parameters
213 $recover = $this->createForm($this->config['recover']['view']['form'], null, [
214 //Set action to recover route name and context
215 'action' => $this->generateUrl($this->config['route']['recover']['name'], $this->config['route']['recover']['context']),
216 'method' => 'POST'
217 ]);
218
219 //Get recover mail entity
220 $recover->get('mail')
221 //Set mail from login form
222 ->setData($login->get('mail')->getData())
223 //Add recover error
224 ->addError(new FormError($this->translator->trans('Use this form to recover your account')));
225
226 //Add recover form to context
227 $context['recover'] = $recover->createView();
228 }
229
230 //Render view
231 return $this->render(
232 //Template
233 $this->config['login']['view']['name'],
234 //Context
235 ['login' => $login->createView()]+$context+$this->config['login']['view']['context']
236 );
237 }
238
239 /**
240 * Recover account
241 *
242 * @param Request $request The request
243 * @param Slugger $slugger The slugger
244 * @param MailerInterface $mailer The mailer
245 * @return Response The response
246 */
247 public function recover(Request $request, Slugger $slugger, MailerInterface $mailer) {
248 //Create the RecoverType form and give the proper parameters
249 $form = $this->createForm($this->config['recover']['view']['form'], null, array(
250 //Set action to recover route name and context
251 'action' => $this->generateUrl($this->config['route']['recover']['name'], $this->config['route']['recover']['context']),
252 'method' => 'POST'
253 ));
254
255 if ($request->isMethod('POST')) {
256 //Refill the fields in case the form is not valid.
257 $form->handleRequest($request);
258
259 if ($form->isValid()) {
260 //Get doctrine
261 $doctrine = $this->getDoctrine();
262
263 //Set data
264 $data = $form->getData();
265
266 //Try to find user
267 if ($user = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($data['mail'])) {
268 //Set mail shortcut
269 $mail =& $this->config['recover']['mail'];
270
271 //Generate each route route
272 foreach($this->config['recover']['route'] as $route => $tag) {
273 //Only process defined routes
274 if (empty($mail['context'][$tag]) && !empty($this->config['route'][$route])) {
275 //Process for recover mail url
276 if ($route == 'recover_mail') {
277 //Set the url in context
278 $mail['context'][$tag] = $this->get('router')->generate(
279 $this->config['route'][$route]['name'],
280 //Prepend recover context with tag
281 [
282 'recipient' => $slugger->short($user->getMail()),
283 'hash' => $slugger->hash($user->getPassword())
284 ]+$this->config['route'][$route]['context'],
285 UrlGeneratorInterface::ABSOLUTE_URL
286 );
287 }
288 }
289 }
290
291 //Set recipient_name
292 $mail['context']['recipient_mail'] = $data['mail'];
293
294 //Set recipient_name
295 $mail['context']['recipient_name'] = trim($user->getForename().' '.$user->getSurname().($user->getPseudonym()?' ('.$user->getPseudonym().')':''));
296
297 //Init subject context
298 $subjectContext = $this->flatten(array_replace_recursive($this->config['recover']['view']['context'], $mail['context']), null, '.', '%', '%');
299
300 //Translate subject
301 $mail['subject'] = ucfirst($this->translator->trans($mail['subject'], $subjectContext));
302
303 //Create message
304 $message = (new TemplatedEmail())
305 //Set sender
306 ->from(new Address($this->config['contact']['mail'], $this->config['contact']['name']))
307 //Set recipient
308 //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
309 ->to(new Address($mail['context']['recipient_mail'], $mail['context']['recipient_name']))
310 //Set subject
311 ->subject($mail['subject'])
312
313 //Set path to twig templates
314 ->htmlTemplate($mail['html'])
315 ->textTemplate($mail['text'])
316
317 //Set context
318 //XXX: require recursive merge to avoid loosing subkeys
319 //['subject' => $mail['subject']]+$mail['context']+$this->config['recover']['view']['context']
320 ->context(array_replace_recursive($this->config['recover']['view']['context'], $mail['context'], ['subject' => $mail['subject']]));
321
322 //Try sending message
323 //XXX: mail delivery may silently fail
324 try {
325 //Send message
326 $mailer->send($message);
327
328 //Redirect on the same route with sent=1 to cleanup form
329 #return $this->redirectToRoute('rapsys_user_register', array('sent' => 1));
330 return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
331 //Catch obvious transport exception
332 } catch(TransportExceptionInterface $e) {
333 //Add error message mail unreachable
334 $form->get('mail')->addError(new FormError($this->translator->trans('Account found but unable to contact: %mail%', array('%mail%' => $data['mail']))));
335 }
336 //Accout not found
337 } else {
338 //Add error message to mail field
339 $form->get('mail')->addError(new FormError($this->translator->trans('Unable to find account %mail%', ['%mail%' => $data['mail']])));
340 }
341 }
342 }
343
344 //Render view
345 return $this->render(
346 //Template
347 $this->config['recover']['view']['name'],
348 //Context
349 ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['recover']['view']['context']
350 );
351 }
352
353 /**
354 * Recover account with mail link
355 *
356 * @param Request $request The request
357 * @param UserPasswordEncoderInterface $encoder The password encoder
358 * @param Slugger $slugger The slugger
359 * @param MailerInterface $mailer The mailer
360 * @param string $recipient The shorted recipient mail address
361 * @param string $hash The hashed password
362 * @return Response The response
363 */
364 public function recoverMail(Request $request, UserPasswordEncoderInterface $encoder, Slugger $slugger, MailerInterface $mailer, $recipient, $hash) {
365 //Create the RecoverType form and give the proper parameters
366 $form = $this->createForm($this->config['recover_mail']['view']['form'], null, array(
367 //Set action to recover route name and context
368 'action' => $this->generateUrl($this->config['route']['recover_mail']['name'], ['recipient' => $recipient, 'hash' => $hash]+$this->config['route']['recover_mail']['context']),
369 'method' => 'POST'
370 ));
371
372 //Get doctrine
373 $doctrine = $this->getDoctrine();
374
375 //Init found
376 $found = false;
377
378 //Retrieve user
379 if (($user = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($slugger->unshort($recipient))) && $found = ($hash == $slugger->hash($user->getPassword()))) {
380 if ($request->isMethod('POST')) {
381 //Refill the fields in case the form is not valid.
382 $form->handleRequest($request);
383
384 if ($form->isValid()) {
385 //Set data
386 $data = $form->getData();
387
388 //set encoded password
389 $encoded = $encoder->encodePassword($user, $data['password']);
390
391 //Set user password
392 $user->setPassword($encoded);
393
394 //Get manager
395 $manager = $doctrine->getManager();
396
397 //Persist user
398 $manager->persist($user);
399
400 //Send to database
401 $manager->flush();
402
403 //Set mail shortcut
404 $mail =& $this->config['recover_mail']['mail'];
405
406 //Regen hash
407 $hash = $slugger->hash($encoded);
408
409 //Generate each route route
410 foreach($this->config['recover_mail']['route'] as $route => $tag) {
411 //Only process defined routes
412 if (empty($mail['context'][$tag]) && !empty($this->config['route'][$route])) {
413 //Process for recover mail url
414 if ($route == 'recover_mail') {
415 //Prepend recover context with tag
416 $this->config['route'][$route]['context'] = [
417 'recipient' => $recipient,
418 'hash' => $hash
419 ]+$this->config['route'][$route]['context'];
420 }
421 //Set the url in context
422 $mail['context'][$tag] = $this->get('router')->generate(
423 $this->config['route'][$route]['name'],
424 $this->config['route'][$route]['context'],
425 UrlGeneratorInterface::ABSOLUTE_URL
426 );
427 }
428 }
429
430 //Set recipient_name
431 $mail['context']['recipient_mail'] = $user->getMail();
432
433 //Set recipient_name
434 $mail['context']['recipient_name'] = trim($user->getForename().' '.$user->getSurname().($user->getPseudonym()?' ('.$user->getPseudonym().')':''));
435
436 //Init subject context
437 $subjectContext = $this->flatten(array_replace_recursive($this->config['recover_mail']['view']['context'], $mail['context']), null, '.', '%', '%');
438
439 //Translate subject
440 $mail['subject'] = ucfirst($this->translator->trans($mail['subject'], $subjectContext));
441
442 //Create message
443 $message = (new TemplatedEmail())
444 //Set sender
445 ->from(new Address($this->config['contact']['mail'], $this->config['contact']['name']))
446 //Set recipient
447 //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
448 ->to(new Address($mail['context']['recipient_mail'], $mail['context']['recipient_name']))
449 //Set subject
450 ->subject($mail['subject'])
451
452 //Set path to twig templates
453 ->htmlTemplate($mail['html'])
454 ->textTemplate($mail['text'])
455
456 //Set context
457 //XXX: require recursive merge to avoid loosing subkeys
458 //['subject' => $mail['subject']]+$mail['context']+$this->config['recover_mail']['view']['context']
459 ->context(array_replace_recursive($this->config['recover_mail']['view']['context'], $mail['context'], ['subject' => $mail['subject']]));
460
461 //Try sending message
462 //XXX: mail delivery may silently fail
463 try {
464 //Send message
465 $mailer->send($message);
466
467 //Redirect on the same route with sent=1 to cleanup form
468 return $this->redirectToRoute($request->get('_route'), ['recipient' => $recipient, 'hash' => $hash, 'sent' => 1]+$request->get('_route_params'));
469 //Catch obvious transport exception
470 } catch(TransportExceptionInterface $e) {
471 //Add error message mail unreachable
472 $form->get('password')->get('first')->addError(new FormError($this->translator->trans('Account %mail% updated but unable to contact', array('%mail%' => $mail['context']['recipient_mail']))));
473 }
474 }
475 }
476 //Accout not found
477 } else {
478 //Add error in flash message
479 //XXX: prevent slugger reverse engineering by not displaying decoded recipient
480 #$this->addFlash('error', $this->translator->trans('Unable to find account %mail%', ['%mail%' => $slugger->unshort($recipient)]));
481 }
482
483 //Render view
484 return $this->render(
485 //Template
486 $this->config['recover_mail']['view']['name'],
487 //Context
488 ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0), 'found' => $found]+$this->config['recover_mail']['view']['context']
489 );
490 }
491
492 /**
493 * Register an account
494 *
495 * @todo: activation link
496 *
497 * @param Request $request The request
498 * @param UserPasswordEncoderInterface $encoder The password encoder
499 * @param MailerInterface $mailer The mailer
500 * @return Response The response
501 */
502 public function register(Request $request, UserPasswordEncoderInterface $encoder, MailerInterface $mailer) {
503 //Get doctrine
504 $doctrine = $this->getDoctrine();
505
506 //Create the RegisterType form and give the proper parameters
507 $form = $this->createForm($this->config['register']['view']['form'], null, array(
508 'class_civility' => $this->config['class']['civility'],
509 'civility' => $doctrine->getRepository($this->config['class']['civility'])->findOneByTitle($this->config['default']['civility']),
510 //Set action to register route name and context
511 'action' => $this->generateUrl($this->config['route']['register']['name'], $this->config['route']['register']['context']),
512 'method' => 'POST'
513 ));
514
515 if ($request->isMethod('POST')) {
516 //Refill the fields in case the form is not valid.
517 $form->handleRequest($request);
518
519 if ($form->isValid()) {
520 //Set data
521 $data = $form->getData();
522
523 //Set mail shortcut
524 $mail =& $this->config['register']['mail'];
525
526 //Generate each route route
527 foreach($this->config['register']['route'] as $route => $tag) {
528 if (empty($mail['context'][$tag]) && !empty($this->config['route'][$route])) {
529 $mail['context'][$tag] = $this->get('router')->generate(
530 $this->config['route'][$route]['name'],
531 $this->config['route'][$route]['context'],
532 UrlGeneratorInterface::ABSOLUTE_URL
533 );
534 }
535 }
536
537 //Set recipient_name
538 $mail['context']['recipient_mail'] = $data['mail'];
539
540 //Set recipient_name
541 $mail['context']['recipient_name'] = trim($data['forename'].' '.$data['surname'].($data['pseudonym']?' ('.$data['pseudonym'].')':''));
542
543 //Init subject context
544 $subjectContext = $this->flatten(array_replace_recursive($this->config['register']['view']['context'], $mail['context']), null, '.', '%', '%');
545
546 //Translate subject
547 $mail['subject'] = ucfirst($this->translator->trans($mail['subject'], $subjectContext));
548
549 //Create message
550 $message = (new TemplatedEmail())
551 //Set sender
552 ->from(new Address($this->config['contact']['mail'], $this->config['contact']['name']))
553 //Set recipient
554 //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
555 ->to(new Address($mail['context']['recipient_mail'], $mail['context']['recipient_name']))
556 //Set subject
557 ->subject($mail['subject'])
558
559 //Set path to twig templates
560 ->htmlTemplate($mail['html'])
561 ->textTemplate($mail['text'])
562
563 //Set context
564 //XXX: require recursive merge to avoid loosing subkeys
565 //['subject' => $mail['subject']]+$mail['context']+$this->config['register']['view']['context']
566 ->context(array_replace_recursive($this->config['register']['view']['context'], $mail['context'], ['subject' => $mail['subject']]));
567
568 //Get manager
569 $manager = $doctrine->getManager();
570
571 //Init reflection
572 $reflection = new \ReflectionClass($this->config['class']['user']);
573
574 //Create new user
575 $user = $reflection->newInstance();
576
577 $user->setMail($data['mail']);
578 $user->setPseudonym($data['pseudonym']);
579 $user->setForename($data['forename']);
580 $user->setSurname($data['surname']);
581 $user->setPhone($data['phone']);
582 $user->setPassword($encoder->encodePassword($user, $data['password']));
583 $user->setActive(true);
584 $user->setCivility($data['civility']);
585
586 //Iterate on default group
587 foreach($this->config['default']['group'] as $i => $groupTitle) {
588 //Fetch group
589 if (($group = $doctrine->getRepository($this->config['class']['group'])->findOneByTitle($groupTitle))) {
590 //Set default group
591 //XXX: see vendor/symfony/security-core/Role/Role.php
592 $user->addGroup($group);
593 //Group not found
594 } else {
595 //Throw exception
596 //XXX: consider missing group as fatal
597 throw new \Exception(sprintf('Group from rapsys_user.default.group[%d] not found by title: %s', $i, $groupTitle));
598 }
599 }
600
601 $user->setCreated(new \DateTime('now'));
602 $user->setUpdated(new \DateTime('now'));
603
604 //Persist user
605 $manager->persist($user);
606
607 //Try saving in database
608 try {
609 //Send to database
610 $manager->flush();
611
612 //Try sending message
613 //XXX: mail delivery may silently fail
614 try {
615 //Send message
616 $mailer->send($message);
617
618 //Redirect on the same route with sent=1 to cleanup form
619 #return $this->redirectToRoute('rapsys_user_register', array('sent' => 1));
620 return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
621 //Catch obvious transport exception
622 } catch(TransportExceptionInterface $e) {
623 //Add error message mail unreachable
624 $form->get('mail')->addError(new FormError($this->translator->trans('Account %mail% created but unable to contact', array('%mail%' => $data['mail']))));
625 }
626 //Catch double subscription
627 } catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
628 //Add error message mail already exists
629 $form->get('mail')->addError(new FormError($this->translator->trans('Account %mail% already exists', ['%mail%' => $data['mail']])));
630 }
631 }
632 }
633
634 //Render view
635 return $this->render(
636 //Template
637 $this->config['register']['view']['name'],
638 //Context
639 ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['register']['view']['context']
640 );
641 }
642
643 /**
644 * Recursively flatten an array
645 *
646 * @param array $data The data tree
647 * @param string|null $current The current prefix
648 * @param string $sep The key separator
649 * @param string $prefix The key prefix
650 * @param string $suffix The key suffix
651 * @return array The flattened data
652 */
653 protected function flatten($data, $current = null, $sep = '.', $prefix = '', $suffix = '') {
654 //Init result
655 $ret = [];
656
657 //Look for data array
658 if (is_array($data)) {
659 //Iteare on each pair
660 foreach($data as $k => $v) {
661 //Merge flattened value in return array
662 $ret += $this->flatten($v, empty($current) ? $k : $current.$sep.$k, $sep, $prefix, $suffix);
663 }
664 //Look flat data
665 } else {
666 //Store data in flattened key
667 $ret[$prefix.$current.$suffix] = $data;
668 }
669
670 //Return result
671 return $ret;
672 }
673
674 /**
675 * {@inheritdoc}
676 */
677 public function getAlias() {
678 return 'rapsys_user';
679 }
680 }