* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Rapsys\UserBundle\Handler; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; use Rapsys\PackBundle\Util\SluggerUtil; use Rapsys\UserBundle\Exception\UnactivatedException; use Rapsys\UserBundle\RapsysUserBundle; 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\Contracts\Translation\TranslatorInterface; /** * {@inheritdoc} */ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler { /** * Alias string */ protected string $alias; /** * Config array */ protected array $config; protected array $defaultOptions = [ 'failure_path' => null, 'failure_forward' => false, 'login_path' => '/login', 'failure_path_parameter' => '_failure_path', ]; /** * {@inheritdoc} * * @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 */ public function __construct(protected HttpKernelInterface $httpKernel, protected HttpUtils $httpUtils, protected array $options, protected ?LoggerInterface $logger, protected ContainerInterface $container, protected ManagerRegistry $doctrine, protected MailerInterface $mailer, protected RouterInterface $router, protected SluggerUtil $slugger, protected RequestStack $stack, protected TranslatorInterface $translator) { //Call parent constructor parent::__construct($httpKernel, $httpUtils, $options, $logger); //Set config $this->config = $container->getParameter($this->alias = RapsysUserBundle::getAlias()); } /** * 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); } /** * {@inheritdoc} * * This is called when an interactive authentication attempt fails */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { //With bad credential exception if ($exception instanceof BadCredentialsException) { //With parent exception if (($parent = $exception->getPrevious()) instanceof UserNotFoundException) { /** Disabled to prevent user mail + hash retrieval for each unactivated/locked accounts //Get user identifier $mail = $parent->getUserIdentifier(); //Set extra parameters $extra = ['mail' => $smail = $this->slugger->short($mail), 'hash' => $this->slugger->hash($smail)];*/ //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(); //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()); //Retrieve route matching path $route = $this->router->match($path); //Reset context $this->router->setContext($oldContext); //Clear old context unset($oldContext); //With route name if ($name = $route['_route']) { //Remove route and controller from route defaults unset($route['_route'], $route['_controller'], $route['_canonical_route']); //Generate url $url = $this->router->generate($name, /*$extra+*/$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*/); //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); } } } //With not enabled user } elseif ($parent instanceof DisabledException) { //Add error message account is not enabled $this->addFlash('error', $this->translator->trans('Account not enabled', [], $this->alias)); //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 not activated user } elseif ($parent instanceof UnactivatedException) { //Set user $user = $parent->getUser(); //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 confirm 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, [], $this->alias); //Remove reference unset($current); } //Translate subject $context['subject'] = $subject = ucfirst( $this->translator->trans( $this->config['register']['mail']['subject'], $this->slugger->flatten($context, null, '.', '%', '%'), $this->alias ) ); //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', [], $this->alias)); } //Add notice $this->addFlash('notice', $this->translator->trans('Your verification mail has been sent, to activate your account follow the confirmation link inside', [], $this->alias)); //Add junk warning $this->addFlash('warning', $this->translator->trans('If you did not receive a verification mail, check your Spam or Junk mail folder', [], $this->alias)); //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); } } //Call parent function return parent::onAuthenticationFailure($request, $exception); } }