X-Git-Url: https://git.rapsys.eu/userbundle/blobdiff_plain/17869e03521ae16d21e3e7d7998ec23fd5da1686..15c15d969c9161e0a10515ff91857e687ab6f60a:/Handler/AuthenticationFailureHandler.php

diff --git a/Handler/AuthenticationFailureHandler.php b/Handler/AuthenticationFailureHandler.php
index 5945644..a8ac5a4 100644
--- a/Handler/AuthenticationFailureHandler.php
+++ b/Handler/AuthenticationFailureHandler.php
@@ -11,24 +11,30 @@
 
 namespace Rapsys\UserBundle\Handler;
 
+use Doctrine\Persistence\ManagerRegistry;
 use Psr\Log\LoggerInterface;
+use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mime\Address;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Routing\RequestContext;
 use Symfony\Component\Routing\RouterInterface;
 use Symfony\Component\Security\Core\Exception\AuthenticationException;
 use Symfony\Component\Security\Core\Exception\BadCredentialsException;
-use Symfony\Component\Security\Core\Exception\DisabledException;
+use Symfony\Component\Security\Core\Exception\UserNotFoundException;
 use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
 use Symfony\Component\Security\Http\HttpUtils;
-use Symfony\Component\Security\Http\ParameterBagUtils;
+use Symfony\Contracts\Translation\TranslatorInterface;
 
 use Rapsys\PackBundle\Util\SluggerUtil;
-use Rapsys\UserBundle\Exception\UnactivatedException;
 use Rapsys\UserBundle\RapsysUserBundle;
 
 /**
@@ -38,43 +44,109 @@ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
 	/**
 	 * Config array
 	 */
-	protected $config;
-    protected $options;
-    protected $defaultOptions = [
-        'failure_path' => null,
-        'failure_forward' => false,
-        'login_path' => '/login',
-        'failure_path_parameter' => '_failure_path',
-    ];
+	protected array $config;
+	protected array $options;
+	protected array $defaultOptions = [
+		'failure_path' => null,
+		'failure_forward' => false,
+		'login_path' => '/login',
+		'failure_path_parameter' => '_failure_path',
+	];
+
+	/**
+	 * Doctrine instance
+	 */
+	protected ManagerRegistry $doctrine;
+
+	/**
+	 * MailerInterface
+	 */
+	protected MailerInterface $mailer;
 
 	/**
 	 * Router instance
 	 */
-	protected $router;
+	protected RouterInterface $router;
 
 	/**
 	 * Slugger instance
 	 */
-	protected $slugger;
+	protected SluggerUtil $slugger;
+
+	/**
+	 * RequestStack instance
+	 */
+	protected RequestStack $stack;
+
+	/**
+	 * Translator instance
+	 */
+	protected TranslatorInterface $translator;
 
 	/**
+	 * @xxx Second argument will be replaced by security.firewalls.main.logout.target
+	 * @see vendor/symfony/security-bundle/DependencyInjection/SecurityExtension.php +360
+	 *
+	 * @param HttpKernelInterface $httpKernel The http kernel
+	 * @param HttpUtils $httpUtils The http utils
+	 * @param array $options The options
+	 * @param LoggerInterface $logger The logger instance
+	 * @param ContainerInterface $container The container instance
+	 * @param ManagerRegistry $doctrine The doctrine instance
+	 * @param MailerInterface $mailer The mailer instance
+	 * @param RouterInterface $router The router instance
+	 * @param SluggerUtil $slugger The slugger instance
+	 * @param RequestStack $stack The stack instance
+	 * @param TranslatorInterface $translator The translator instance
+	 *
 	 * {@inheritdoc}
 	 */
-    public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options = [], LoggerInterface $logger, ContainerInterface $container, RouterInterface $router, SluggerUtil $slugger) {
+	public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options, LoggerInterface $logger, ContainerInterface $container, ManagerRegistry $doctrine, MailerInterface $mailer, RouterInterface $router, SluggerUtil $slugger, RequestStack $stack, TranslatorInterface $translator) {
 		//Set config
 		$this->config = $container->getParameter(self::getAlias());
 
+		//Set doctrine
+		$this->doctrine = $doctrine;
+
+		//Set mailer
+		$this->mailer = $mailer;
+
 		//Set router
 		$this->router = $router;
 
 		//Set slugger
 		$this->slugger = $slugger;
 
+		//Set stack
+		$this->stack = $stack;
+
+		//Set translator
+		$this->translator = $translator;
+
 		//Call parent constructor
 		parent::__construct($httpKernel, $httpUtils, $options, $logger);
 	}
 
-    /**
+	/**
+	 * Adds a flash message to the current session for type.
+	 *
+	 * @throws \LogicException
+	 */
+	protected function addFlash(string $type, mixed $message): void {
+		try {
+			$session = $this->stack->getSession();
+		} catch (SessionNotFoundException $e) {
+			throw new \LogicException('You cannot use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".', 0, $e);
+		}
+
+		if (!$session instanceof FlashBagAwareSessionInterface) {
+			throw new \LogicException(sprintf('You cannot use the addFlash method because class "%s" doesn\'t implement "%s".', get_debug_type($session), FlashBagAwareSessionInterface::class));
+		}
+
+		$session->getFlashBag()->add($type, $message);
+	}
+
+	/**
 	 * This is called when an interactive authentication attempt fails
 	 *
 	 * User may retrieve mail + field + hash for each unactivated/locked accounts
@@ -85,84 +157,83 @@ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
 		//With bad credential exception
 		if ($exception instanceof BadCredentialsException) {
 			//With parent exception
-			if ($parent = $exception->getPrevious()) {
+			if (($parent = $exception->getPrevious()) instanceof UserNotFoundException) {
 				//Retrieve login
 				//TODO: check form _token validity ???
 				if (
-					$request->request->has('login') &&
-					!empty($login = $request->request->get('login')) &&
+					!empty($login = $request->get('login')) &&
 					!empty($mail = $login['mail'])
 				) {
-					//Redirect on register
-					if ($parent instanceof UnactivatedException || $parent instanceof DisabledException) {
-						//Set extra parameters
-						$extra = ['mail' => $smail = $this->slugger->short($mail), 'field' => $sfield = $this->slugger->serialize([]), 'hash' => $this->slugger->hash($smail.$sfield)];
+					//Set extra parameters
+					$extra = ['mail' => $smail = $this->slugger->short($mail), 'field' => $sfield = $this->slugger->serialize([]), 'hash' => $this->slugger->hash($smail.$sfield)];
 
-						//With failure target path option
-						if (!empty($failurePath = $this->options['failure_path'])) {
-							//With path
-							if ($failurePath[0] == '/') {
-								//Create login path request instance
-								$req = Request::create($failurePath);
+					//With failure target path option
+					if (!empty($failurePath = $this->options['failure_path'])) {
+						//With path
+						if ($failurePath[0] == '/') {
+							//Create login path request instance
+							$req = Request::create($failurePath);
 
-								//Get login path pathinfo
-								$path = $req->getPathInfo();
+							//Get login path pathinfo
+							$path = $req->getPathInfo();
 
-								//Remove script name
-								$path = str_replace($request->getScriptName(), '', $path);
+							//Remove script name
+							$path = str_replace($request->getScriptName(), '', $path);
 
-								//Try with login path path
-								try {
-									//Save old context
-									$oldContext = $this->router->getContext();
-
-									//Force clean context
-									//XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST
-									//XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42
-									$this->router->setContext(new RequestContext());
+							//Try with login path path
+							try {
+								//Save old context
+								$oldContext = $this->router->getContext();
 
-									//Retrieve route matching path
-									$route = $this->router->match($path);
+								//Force clean context
+								//XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST
+								//XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42
+								$this->router->setContext(new RequestContext());
 
-									//Reset context
-									$this->router->setContext($oldContext);
+								//Retrieve route matching path
+								$route = $this->router->match($path);
 
-									//Clear old context
-									unset($oldContext);
+								//Reset context
+								$this->router->setContext($oldContext);
 
-									//With route name
-									if ($name = $route['_route']) {
-										//Remove route and controller from route defaults
-										unset($route['_route'], $route['_controller'], $route['_canonical_route']);
+								//Clear old context
+								unset($oldContext);
 
-										//Generate url
-										$url = $this->router->generate($name, $extra+$route);
+								//With route name
+								if ($name = $route['_route']) {
+									//Remove route and controller from route defaults
+									unset($route['_route'], $route['_controller'], $route['_canonical_route']);
 
-										//Return redirect to url response
-										return new RedirectResponse($url, 302);
-									}
-								//No route matched
-								} catch (ResourceNotFoundException $e) {
-									//Unset default path, name and route
-									unset($failurePath, $name, $route);
-								}
-							//With route name
-							} else {
-								//Try with login path route
-								try {
-									//Retrieve route matching path
-									$url = $this->router->generate($failurePath, $extra);
+									//Generate url
+									$url = $this->router->generate($name, $extra+$route);
 
 									//Return redirect to url response
 									return new RedirectResponse($url, 302);
-								//Route not found, missing parameter or invalid parameter
-								} catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) {
-									//Unset default path and url
-									unset($failurePath, $url);
 								}
+							//No route matched
+							} catch (ResourceNotFoundException $e) {
+								//Unset default path, name and route
+								unset($failurePath, $name, $route);
+							}
+						//With route name
+						} else {
+							//Try with login path route
+							try {
+								//Retrieve route matching path
+								$url = $this->router->generate($failurePath, $extra);
+
+								//Return redirect to url response
+								return new RedirectResponse($url, 302);
+							//Route not found, missing parameter or invalid parameter
+							} catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) {
+								//Unset default path and url
+								unset($failurePath, $url);
 							}
 						}
+					}
 
+					//Without user
+					if (($user = $this->doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail)) === null) {
 						//With index route from config
 						if (!empty($name = $this->config['route']['register']['name']) && is_array($context = $this->config['route']['register']['context'])) {
 							//Try index route
@@ -243,6 +314,115 @@ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
 								}
 							}
 						}
+					//With not enabled user
+					} elseif (empty($user->isEnabled())) {
+						//Add error message account is not enabled
+						$this->addFlash('error', $this->translator->trans('Your account is not enable'));
+
+						//Redirect on the same route with sent=1 to cleanup form
+						return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302);
+					//With unactivated user
+					} elseif (empty($user->isActivated())) {
+						//Set context
+						$context = [
+							'recipient_mail' => $user->getMail(),
+							'recipient_name' => $user->getRecipientName()
+						] + array_replace_recursive(
+							$this->config['context'],
+							$this->config['register']['view']['context'],
+							$this->config['register']['mail']['context']
+						);
+
+						//Generate each route route
+						foreach($this->config['register']['route'] as $route => $tag) {
+							//Only process defined routes
+							if (!empty($this->config['route'][$route])) {
+								//Process for confirm url
+								if ($route == 'confirm') {
+									//Set the url in context
+									$context[$tag] = $this->router->generate(
+										$this->config['route'][$route]['name'],
+										//Prepend subscribe context with tag
+										[
+											'mail' => $smail = $this->slugger->short($context['recipient_mail']),
+											'hash' => $this->slugger->hash($smail)
+										]+$this->config['route'][$route]['context'],
+										UrlGeneratorInterface::ABSOLUTE_URL
+									);
+								}
+							}
+						}
+
+						//Iterate on keys to translate
+						foreach($this->config['translate'] as $translate) {
+							//Extract keys
+							$keys = explode('.', $translate);
+
+							//Set current
+							$current =& $context;
+
+							//Iterate on each subkey
+							do {
+								//Skip unset translation keys
+								if (!isset($current[current($keys)])) {
+									continue(2);
+								}
+
+								//Set current to subkey
+								$current =& $current[current($keys)];
+							} while(next($keys));
+
+							//Set translation
+							$current = $this->translator->trans($current);
+
+							//Remove reference
+							unset($current);
+						}
+
+						//Translate subject
+						$context['subject'] = $subject = ucfirst(
+							$this->translator->trans(
+								$this->config['register']['mail']['subject'],
+								$this->slugger->flatten($context, null, '.', '%', '%')
+							)
+						);
+
+						//Create message
+						$message = (new TemplatedEmail())
+							//Set sender
+							->from(new Address($this->config['contact']['address'], $this->config['contact']['name']))
+							//Set recipient
+							//XXX: remove the debug set in vendor/symfony/mime/Address.php +46
+							->to(new Address($context['recipient_mail'], $context['recipient_name']))
+							//Set subject
+							->subject($context['subject'])
+
+							//Set path to twig templates
+							->htmlTemplate($this->config['register']['mail']['html'])
+							->textTemplate($this->config['register']['mail']['text'])
+
+							//Set context
+							->context($context);
+
+						//Try sending message
+						//XXX: mail delivery may silently fail
+						try {
+							//Send message
+							$this->mailer->send($message);
+						//Catch obvious transport exception
+						} catch(TransportExceptionInterface $e) {
+							//Add error message mail unreachable
+							$this->addFlash('error', $this->translator->trans('Unable to reach account'));
+						}
+
+						//Add notice
+						$this->addFlash('notice', $this->translator->trans('Your verification mail has been sent, to activate your account you must follow the confirmation link inside'));
+
+						//Add junk warning
+						$this->addFlash('warning', $this->translator->trans('If you did not receive a verification mail, check your Spam or Junk mail folders'));
+
+						//Redirect on the same route with sent=1 to cleanup form
+						return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302);
 					}
 				}
 			}