From c9e609cd2dcfd79a9a714d2a1f5acfe699bb1c81 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 08:22:02 +0200 Subject: [PATCH 01/16] Add strict mode --- Handler/LogoutSuccessHandler.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Handler/LogoutSuccessHandler.php b/Handler/LogoutSuccessHandler.php index de862d5..825cd5b 100644 --- a/Handler/LogoutSuccessHandler.php +++ b/Handler/LogoutSuccessHandler.php @@ -1,10 +1,20 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace Rapsys\UserBundle\Handler; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouterInterface; @@ -51,7 +61,7 @@ class LogoutSuccessHandler extends DefaultLogoutSuccessHandler { /** * {@inheritdoc} */ - public function onLogoutSuccess(Request $request) { + public function onLogoutSuccess(Request $request): Response { //Retrieve logout route $logout = $request->get('_route'); -- 2.41.3 From 2b55a565f43b0581c625cbc00948749d983056cc Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 08:22:20 +0200 Subject: [PATCH 02/16] Add auth success handler --- Handler/AuthenticationSuccessHandler.php | 290 +++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 Handler/AuthenticationSuccessHandler.php diff --git a/Handler/AuthenticationSuccessHandler.php b/Handler/AuthenticationSuccessHandler.php new file mode 100644 index 0000000..961c1b1 --- /dev/null +++ b/Handler/AuthenticationSuccessHandler.php @@ -0,0 +1,290 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\UserBundle\Handler; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; +use Symfony\Component\Security\Http\ParameterBagUtils; +use Symfony\Component\Security\Http\Util\TargetPathTrait; + +/** + * {@inheritdoc} + */ +class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler { + /** + * Allows to use getTargetPath and removeTargetPath private functions + */ + use TargetPathTrait; + + /** + * Default options + */ + protected $defaultOptions = [ + 'always_use_default_target_path' => false, + 'default_target_path' => '/', + 'login_path' => '/login', + 'target_path_parameter' => '_target_path', + 'use_referer' => false, + ]; + + /** + * Options + */ + protected $options; + + /** + * Router instance + */ + protected $router; + + /** + * {@inheritdoc} + */ + public function __construct(RouterInterface $router, array $options = []) { + //Set options + $this->setOptions($options); + + //Set router + $this->router = $router; + } + + /** + * This is called when an interactive authentication attempt succeeds + * + * In use_referer case it will handle correctly when login_path is a route name or path + * + * {@inheritdoc} + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response { + //Set login route + $login = $request->get('_route'); + + //With login path option + if (!empty($loginPath = $this->options['login_path'])) { + //With path + if ($loginPath[0] == '/') { + //Create login path request instance + $req = Request::create($loginPath); + + //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); + + //Set login route + if (!empty($route['_route'])) { + //Set login route + $login = $route['_route']; + } + //No route matched + } catch (ResourceNotFoundException $e) { + throw new \UnexpectedValueException(sprintf('The "login_path" path "%s" must match a route', $this->options['login_path']), $e->getCode(), $e); + } + //With route + } else { + //Try with login path route + try { + //Retrieve route matching path + $path = $this->router->generate($loginPath); + + //Set login route + $login = $loginPath; + //No route found + } catch (RouteNotFoundException $e) { + throw new \UnexpectedValueException(sprintf('The "login_path" route "%s" must match a route name', $this->options['login_path']), $e->getCode(), $e); + //Ignore missing or invalid parameter + //XXX: useless or would not work ? + } catch (MissingMandatoryParametersException|InvalidParameterException $e) { + //Set login route + $login = $loginPath; + } + } + } + + //Without always_use_default_target_path + if (empty($this->options['always_use_default_target_path'])) { + //With _target_path + if ($targetUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['target_path_parameter'])) { + //Set target url + $url = $targetUrl; + + //Return redirect to url response + return new RedirectResponse($url, 302); + //With session and target path in session + } elseif ( + !empty($this->providerKey) && + ($session = $request->getSession()) && + ($targetUrl = $this->getTargetPath($session, $this->providerKey)) + ) { + //Remove session target path + $this->removeTargetPath($session, $this->providerKey); + + //Set target url + $url = $targetUrl; + + //Return redirect to url response + return new RedirectResponse($url, 302); + //Extract and process referer + } elseif ($this->options['use_referer'] && ($targetUrl = $request->headers->get('referer'))) { + //Create referer request instance + $req = Request::create($targetUrl); + + //Get referer path + $path = $req->getPathInfo(); + + //Get referer query string + $query = $req->getQueryString(); + + //Remove script name + $path = str_replace($request->getScriptName(), '', $path); + + //Try with referer 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 differing route from login one + if (($name = $route['_route']) != $login) { + //Remove route and controller from route defaults + unset($route['_route'], $route['_controller'], $route['_canonical_route']); + + //Set url to generated one from referer route + $url = $this->router->generate($name, $route); + + //Return redirect to url response + return new RedirectResponse($url, 302); + } + //No route matched + } catch (ResourceNotFoundException $e) { + //Unset target url, route and name + unset($targetUrl, $route, $name); + } + } + } + + //With default target path option + if (!empty($defaultPath = $this->options['default_target_path'])) { + //With path + if ($defaultPath[0] == '/') { + //Create login path request instance + $req = Request::create($defaultPath); + + //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); + + //Without login route name + if (($name = $route['_route']) != $login) { + //Remove route and controller from route defaults + unset($route['_route'], $route['_controller'], $route['_canonical_route']); + + //Generate url + $url = $this->router->generate($name, $route); + + //Return redirect to url response + return new RedirectResponse($url, 302); + //With logout route name + } else { + //Unset default path, name and route + unset($defaultPath, $name, $route); + } + //No route matched + } catch (ResourceNotFoundException $e) { + throw \Exception('', $e->getCode(), $e); + //Unset default path, name and route + unset($defaultPath, $name, $route); + } + //Without login route name + } elseif ($defaultPath != $login) { + //Try with login path route + try { + //Retrieve route matching path + $url = $this->router->generate($defaultPath); + + //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($defaultPath, $url); + } + } + } + + //Throw exception + throw new \UnexpectedValueException('You must provide a valid login target url or route name'); + } +} -- 2.41.3 From 167e6d4a1d7db9331ed16f27d2558ef1992bff39 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 09:17:49 +0200 Subject: [PATCH 03/16] Add strict Fix edit permission check Restrict reset to admin Remove edit form Add edit editForm Cleanup --- Controller/DefaultController.php | 121 +++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 24 deletions(-) diff --git a/Controller/DefaultController.php b/Controller/DefaultController.php index f364144..272f7bb 100644 --- a/Controller/DefaultController.php +++ b/Controller/DefaultController.php @@ -1,4 +1,13 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace Rapsys\UserBundle\Controller; @@ -10,6 +19,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; @@ -38,7 +48,6 @@ class DefaultController extends AbstractController { * Constructor * * @TODO: move all canonical and other view related stuff in an user AbstractController like in RapsysAir render feature !!!! - * @TODO: add resetpassword ? with $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); https://symfony.com/doc/current/security/remember_me.html * * @param ContainerInterface $container The containter instance * @param RouterInterface $router The router instance @@ -238,7 +247,7 @@ class DefaultController extends AbstractController { * @param string $hash The hashed password * @return Response The response */ - public function confirm(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, $mail, $hash) { + public function confirm(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, $mail, $hash): Response { //With invalid hash if ($hash != $slugger->hash($mail)) { //Throw bad request @@ -289,13 +298,14 @@ class DefaultController extends AbstractController { * * @param Request $request The request * @param Registry $manager The doctrine registry + * @param UserPasswordEncoderInterface $encoder The password encoder * @param EntityManagerInterface $manager The doctrine entity manager * @param SluggerUtil $slugger The slugger * @param string $mail The shorted mail address * @param string $hash The hashed password * @return Response The response */ - public function edit(Request $request, Registry $doctrine, EntityManagerInterface $manager, SluggerUtil $slugger, $mail, $hash) { + public function edit(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, $mail, $hash): Response { //With invalid hash if ($hash != $slugger->hash($mail)) { //Throw bad request @@ -313,14 +323,30 @@ class DefaultController extends AbstractController { } //Prevent access when not admin, user is not guest and not currently logged user - if (!$this->isGranted('ROLE_ADMIN') && $user != $this->getUser()) { + if (!$this->isGranted('ROLE_ADMIN') && $user != $this->getUser() || !$this->isGranted('IS_AUTHENTICATED_FULLY')) { //Throw access denied //XXX: prevent slugger reverse engineering by not displaying decoded mail throw $this->createAccessDeniedException($this->translator->trans('Unable to access user: %mail%', ['%mail%' => $smail])); } //Create the RegisterType form and give the proper parameters - $form = $this->createForm($this->config['register']['view']['form'], $user, [ + $editForm = $this->createForm($this->config['register']['view']['form'], $user, [ + //Set action to register route name and context + 'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']), + //Set civility class + 'civility_class' => $this->config['class']['civility'], + //Set civility default + 'civility_default' => $doctrine->getRepository($this->config['class']['civility'])->findOneByTitle($this->config['default']['civility']), + //Disable mail + 'mail' => $this->isGranted('ROLE_ADMIN'), + //Disable password + 'password' => false, + //Set method + 'method' => 'POST' + ]); + + //Create the RegisterType form and give the proper parameters + $edit = $this->createForm($this->config['edit']['view']['edit'], $user, [ //Set action to register route name and context 'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']), //Set civility class @@ -330,19 +356,75 @@ class DefaultController extends AbstractController { //Disable mail 'mail' => $this->isGranted('ROLE_ADMIN'), //Disable password - //XXX: prefer a reset on login to force user unspam action 'password' => false, //Set method 'method' => 'POST' ]); + //With admin role + if ($this->isGranted('ROLE_ADMIN')) { + //Create the LoginType form and give the proper parameters + $reset = $this->createForm($this->config['edit']['view']['reset'], $user, [ + //Set action to register route name and context + 'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']), + //Disable mail + 'mail' => false, + //Set method + 'method' => 'POST' + ]); + + //With post method + if ($request->isMethod('POST')) { + //Refill the fields in case the form is not valid. + $reset->handleRequest($request); + + //With reset submitted and valid + if ($reset->isSubmitted() && $reset->isValid()) { + //Set data + $data = $reset->getData(); + + //Set password + $data->setPassword($encoder->encodePassword($data, $data->getPassword())); + + //Set updated + $data->setUpdated(new \DateTime('now')); + + //Queue snippet save + $manager->persist($data); + + //Flush to get the ids + $manager->flush(); + + //Add notice + $this->addFlash('notice', $this->translator->trans('Account %mail% password updated', ['%mail%' => $mail = $data->getMail()])); + + //Redirect to cleanup the form + //TODO: extract referer ??? or useless ??? + return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']); + } + } + + //Add reset view + $this->config['edit']['view']['context']['reset'] = $reset->createView(); + //Without admin role + //XXX: prefer a reset on login to force user unspam action + } else { + //Add notice + $this->addFlash('notice', $this->translator->trans('To change your password login with your mail and any password then follow the procedure')); + } + + //With post method if ($request->isMethod('POST')) { //Refill the fields in case the form is not valid. - $form->handleRequest($request); + $edit->handleRequest($request); - if ($form->isValid()) { + //With edit submitted and valid + if ($edit->isSubmitted() && $edit->isValid()) { //Set data - $data = $form->getData(); + $data = $edit->getData(); + + //Set updated + $data->setUpdated(new \DateTime('now')); //Queue snippet save $manager->persist($data); @@ -353,16 +435,10 @@ class DefaultController extends AbstractController { //Add notice $this->addFlash('notice', $this->translator->trans('Account %mail% updated', ['%mail%' => $mail = $data->getMail()])); - //Redirect to user view + //Redirect to cleanup the form //TODO: extract referer ??? or useless ??? return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']); - - //Redirect to cleanup the form - return $this->redirectToRoute('rapsys_air', ['user' => $data->getId()]); } - } else { - //Add notice - $this->addFlash('notice', $this->translator->trans('To change your password relogin with your mail %mail% and any password then follow the procedure', ['%mail%' => $mail])); } //Render view @@ -370,16 +446,13 @@ class DefaultController extends AbstractController { //Template $this->config['edit']['view']['name'], //Context - ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['edit']['view']['context'] + ['edit' => $edit->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['edit']['view']['context'] ); } /** * Login * - * @todo When account is not activated, refuse login and send verification mail ? - * @todo Redirect to referer if route is not connect ? - * * @param Request $request The request * @param AuthenticationUtils $authenticationUtils The authentication utils * @param RouterInterface $router The router instance @@ -388,7 +461,7 @@ class DefaultController extends AbstractController { * @param string $hash The hashed password * @return Response The response */ - public function login(Request $request, AuthenticationUtils $authenticationUtils, RouterInterface $router, SluggerUtil $slugger, $mail, $hash) { + public function login(Request $request, AuthenticationUtils $authenticationUtils, RouterInterface $router, SluggerUtil $slugger, $mail, $hash): Response { //Create the LoginType form and give the proper parameters $login = $this->createForm($this->config['login']['view']['form'], null, [ //Set action to login route name and context @@ -482,7 +555,7 @@ class DefaultController extends AbstractController { * @param string $hash The hashed password * @return Response The response */ - public function recover(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, $mail, $pass, $hash) { + public function recover(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, $mail, $pass, $hash): Response { //Without mail, pass and hash if (empty($mail) && empty($pass) && empty($hash)) { //Create the LoginType form and give the proper parameters @@ -694,7 +767,7 @@ class DefaultController extends AbstractController { * @param string $hash The hashed serialized field array * @return Response The response */ - public function register(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, LoggerInterface $logger, $mail, $field, $hash) { + public function register(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, MailerInterface $mailer, LoggerInterface $logger, $mail, $field, $hash): Response { //Init reflection $reflection = new \ReflectionClass($this->config['class']['user']); -- 2.41.3 From efbfa35585b0cf29d0f6f8b0d47c5ac9218f0ab9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 09:28:01 +0200 Subject: [PATCH 04/16] Cleanup --- Controller/DefaultController.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Controller/DefaultController.php b/Controller/DefaultController.php index 272f7bb..c6ac4be 100644 --- a/Controller/DefaultController.php +++ b/Controller/DefaultController.php @@ -399,7 +399,6 @@ class DefaultController extends AbstractController { $this->addFlash('notice', $this->translator->trans('Account %mail% password updated', ['%mail%' => $mail = $data->getMail()])); //Redirect to cleanup the form - //TODO: extract referer ??? or useless ??? return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']); } } @@ -436,7 +435,6 @@ class DefaultController extends AbstractController { $this->addFlash('notice', $this->translator->trans('Account %mail% updated', ['%mail%' => $mail = $data->getMail()])); //Redirect to cleanup the form - //TODO: extract referer ??? or useless ??? return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']); } } @@ -528,7 +526,6 @@ class DefaultController extends AbstractController { $context['recover'] = $recover->createView(); } else { //Add notice - //TODO: drop it if referer route is recover ? $this->addFlash('notice', $this->translator->trans('To change your password login with your mail and any password then follow the procedure')); } -- 2.41.3 From c1f01e64c377c59b3825e80661f60d250363250b Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:27:24 +0200 Subject: [PATCH 05/16] Add strict Add isActivated and isDisabled Add disabled member --- Entity/User.php | 99 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/Entity/User.php b/Entity/User.php index 8fb4105..5df315a 100644 --- a/Entity/User.php +++ b/Entity/User.php @@ -44,6 +44,11 @@ class User implements UserInterface, \Serializable { */ protected $active; + /** + * @var bool + */ + protected $disabled; + /** * @var \DateTime */ @@ -60,7 +65,7 @@ class User implements UserInterface, \Serializable { protected $civility; /** - * @var \Doctrine\Common\Collections\Collection + * @var \Doctrine\Common\Collections\ArrayCollection */ protected $groups; @@ -69,6 +74,7 @@ class User implements UserInterface, \Serializable { */ public function __construct() { $this->active = false; + $this->disabled = false; $this->groups = new ArrayCollection(); } @@ -77,7 +83,7 @@ class User implements UserInterface, \Serializable { * * @return integer */ - public function getId() { + public function getId(): int { return $this->id; } @@ -88,7 +94,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setMail($mail) { + public function setMail(string $mail) { $this->mail = $mail; return $this; @@ -99,7 +105,7 @@ class User implements UserInterface, \Serializable { * * @return string */ - public function getMail() { + public function getMail(): ?string { return $this->mail; } @@ -110,7 +116,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setPseudonym($pseudonym) { + public function setPseudonym(string $pseudonym) { $this->pseudonym = $pseudonym; return $this; @@ -121,7 +127,7 @@ class User implements UserInterface, \Serializable { * * @return string */ - public function getPseudonym() { + public function getPseudonym(): ?string { return $this->pseudonym; } @@ -132,7 +138,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setForename($forename) { + public function setForename(string $forename) { $this->forename = $forename; return $this; @@ -143,7 +149,7 @@ class User implements UserInterface, \Serializable { * * @return string */ - public function getForename() { + public function getForename(): ?string { return $this->forename; } @@ -154,7 +160,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setSurname($surname) { + public function setSurname(string $surname) { $this->surname = $surname; return $this; @@ -165,7 +171,7 @@ class User implements UserInterface, \Serializable { * * @return string */ - public function getSurname() { + public function getSurname(): ?string { return $this->surname; } @@ -176,7 +182,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setPassword($password) { + public function setPassword(string $password) { $this->password = $password; return $this; @@ -189,7 +195,7 @@ class User implements UserInterface, \Serializable { * * @return string */ - public function getPassword() { + public function getPassword(): ?string { return $this->password; } @@ -200,7 +206,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setActive($active) { + public function setActive(bool $active) { $this->active = $active; return $this; @@ -211,10 +217,32 @@ class User implements UserInterface, \Serializable { * * @return bool */ - public function getActive() { + public function getActive(): bool { return $this->active; } + /** + * Set disabled + * + * @param bool $disabled + * + * @return User + */ + public function setDisabled(bool $disabled) { + $this->disabled = $disabled; + + return $this; + } + + /** + * Get disabled + * + * @return bool + */ + public function getDisabled(): bool { + return $this->disabled; + } + /** * Set created * @@ -222,7 +250,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setCreated($created) { + public function setCreated(\DateTime $created) { $this->created = $created; return $this; @@ -233,7 +261,7 @@ class User implements UserInterface, \Serializable { * * @return \DateTime */ - public function getCreated() { + public function getCreated(): \DateTime { return $this->created; } @@ -244,7 +272,7 @@ class User implements UserInterface, \Serializable { * * @return User */ - public function setUpdated($updated) { + public function setUpdated(\DateTime $updated) { $this->updated = $updated; return $this; @@ -255,7 +283,7 @@ class User implements UserInterface, \Serializable { * * @return \DateTime */ - public function getUpdated() { + public function getUpdated(): \DateTime { return $this->updated; } @@ -300,16 +328,16 @@ class User implements UserInterface, \Serializable { /** * Get groups * - * @return \Doctrine\Common\Collections\Collection + * @return \Doctrine\Common\Collections\ArrayCollection */ - public function getGroups() { + public function getGroups(): ArrayCollection { return $this->groups; } /** * {@inheritdoc} */ - public function getRoles() { + public function getRoles(): array { //Get the unique roles list by id return array_unique(array_reduce( //Cast groups as array @@ -329,7 +357,7 @@ class User implements UserInterface, \Serializable { /** * {@inheritdoc} */ - public function getRole() { + public function getRole(): ?string { //Retrieve roles $roles = $this->getRoles(); @@ -356,7 +384,7 @@ class User implements UserInterface, \Serializable { /** * {@inheritdoc} */ - public function getSalt() { + public function getSalt(): ?string { //No salt required with bcrypt return null; } @@ -364,14 +392,14 @@ class User implements UserInterface, \Serializable { /** * {@inheritdoc} */ - public function getUsername() { + public function getUsername(): string { return $this->mail; } /** * {@inheritdoc} */ - public function eraseCredentials() {} + public function eraseCredentials(): void {} public function serialize(): string { return serialize([ @@ -379,6 +407,7 @@ class User implements UserInterface, \Serializable { $this->mail, $this->password, $this->active, + $this->disabled, $this->created, $this->updated ]); @@ -390,16 +419,32 @@ class User implements UserInterface, \Serializable { $this->mail, $this->password, $this->active, + $this->disabled, $this->created, $this->updated ) = unserialize($serialized); } - //XXX: was from vendor/symfony/security-core/User/AdvancedUserInterface.php, see if it's used anymore - public function isEnabled() { + /** + * Check if account is activated + * + * @xxx was from deprecated AdvancedUserInterface, see if it's used anymore + * @see vendor/symfony/security-core/User/AdvancedUserInterface.php + */ + public function isActivated(): bool { return $this->active; } + /** + * Check if account is disabled + * + * @xxx was from deprecated AdvancedUserInterface, see if it's used anymore + * @see vendor/symfony/security-core/User/AdvancedUserInterface.php + */ + public function isDisabled(): bool { + return $this->disabled; + } + /** * {@inheritdoc} */ -- 2.41.3 From 680d677e18cf452174ae266f81a2856c5bd5b322 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:28:01 +0200 Subject: [PATCH 06/16] Add disabled member --- Resources/config/doctrine/User.orm.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Resources/config/doctrine/User.orm.yml b/Resources/config/doctrine/User.orm.yml index 29755a5..7d51e1c 100644 --- a/Resources/config/doctrine/User.orm.yml +++ b/Resources/config/doctrine/User.orm.yml @@ -28,6 +28,10 @@ Rapsys\UserBundle\Entity\User: type: boolean options: default: true + disabled: + type: boolean + options: + default: false created: type: datetime updated: -- 2.41.3 From 5dfed0e66ca51c75d77d9e0a10ca1fc85c4b178f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:28:56 +0200 Subject: [PATCH 07/16] Replace security.authentication.failure_handler and security.user_checker --- Resources/config/packages/rapsys_user.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/config/packages/rapsys_user.yaml b/Resources/config/packages/rapsys_user.yaml index 14e686f..f047b0c 100644 --- a/Resources/config/packages/rapsys_user.yaml +++ b/Resources/config/packages/rapsys_user.yaml @@ -14,7 +14,14 @@ services: security.authentication.success_handler: class: 'Rapsys\UserBundle\Handler\AuthenticationSuccessHandler' arguments: [ '@router', {} ] + #Register Authentication failure handler + security.authentication.failure_handler: + class: 'Rapsys\UserBundle\Handler\AuthenticationFailureHandler' + arguments: [ '@http_kernel', '@security.http_utils', {}, '@logger', '@service_container', '@router', '@rapsys_pack.slugger_util'] #Register logout success handler security.logout.success_handler: class: 'Rapsys\UserBundle\Handler\LogoutSuccessHandler' arguments: [ '@service_container', '/', '@router' ] + #Register security user checker + security.user_checker: + class: 'Rapsys\UserBundle\Checker\UserChecker' -- 2.41.3 From a858bf15e799045a63e30d74dda1c0b7548daf48 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:29:28 +0200 Subject: [PATCH 08/16] Set as static --- RapsysUserBundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RapsysUserBundle.php b/RapsysUserBundle.php index b347c51..627ad61 100644 --- a/RapsysUserBundle.php +++ b/RapsysUserBundle.php @@ -11,7 +11,7 @@ class RapsysUserBundle extends Bundle { * * @return string The bundle alias */ - public function getAlias(): string { + public static function getAlias(): string { //With namespace if ($npos = strrpos(static::class, '\\')) { //Set name pos -- 2.41.3 From ca1fa03aacc37a791a99bc80cefbb2e208126c1b Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:36:39 +0200 Subject: [PATCH 09/16] Remove @xxx as it trigger an annotation error --- Entity/User.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Entity/User.php b/Entity/User.php index 5df315a..3c1c768 100644 --- a/Entity/User.php +++ b/Entity/User.php @@ -428,7 +428,8 @@ class User implements UserInterface, \Serializable { /** * Check if account is activated * - * @xxx was from deprecated AdvancedUserInterface, see if it's used anymore + * It was from deprecated AdvancedUserInterface, see if it's used anymore + * * @see vendor/symfony/security-core/User/AdvancedUserInterface.php */ public function isActivated(): bool { @@ -438,7 +439,8 @@ class User implements UserInterface, \Serializable { /** * Check if account is disabled * - * @xxx was from deprecated AdvancedUserInterface, see if it's used anymore + * It was from deprecated AdvancedUserInterface, see if it's used anymore + * * @see vendor/symfony/security-core/User/AdvancedUserInterface.php */ public function isDisabled(): bool { -- 2.41.3 From a410610ad184292cb2df65cc0dfb052470d2a69d Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:37:35 +0200 Subject: [PATCH 10/16] Cleanup --- Handler/AuthenticationSuccessHandler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Handler/AuthenticationSuccessHandler.php b/Handler/AuthenticationSuccessHandler.php index 961c1b1..90d53d7 100644 --- a/Handler/AuthenticationSuccessHandler.php +++ b/Handler/AuthenticationSuccessHandler.php @@ -59,11 +59,11 @@ class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler { * {@inheritdoc} */ public function __construct(RouterInterface $router, array $options = []) { - //Set options - $this->setOptions($options); - //Set router $this->router = $router; + + //Set options + $this->setOptions($options); } /** -- 2.41.3 From 436b8a2b29c4df2d84bacfe2acf0545fefd55346 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:51:27 +0200 Subject: [PATCH 11/16] Fix variable name Remove useless setUpdated we have doctrine preUpdate lifecycleCallbacks Handle existing, activated and disabled user Cleanup --- Controller/DefaultController.php | 120 ++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 17 deletions(-) diff --git a/Controller/DefaultController.php b/Controller/DefaultController.php index c6ac4be..5c3c51c 100644 --- a/Controller/DefaultController.php +++ b/Controller/DefaultController.php @@ -24,12 +24,8 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; -use Symfony\Component\Routing\Exception\MethodNotAllowedException; -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\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Translation\TranslatorInterface; @@ -277,9 +273,6 @@ class DefaultController extends AbstractController { //Set active $user->setActive(true); - //Set updated - $user->setUpdated(new \DateTime('now')); - //Persist user $manager->persist($user); @@ -386,9 +379,6 @@ class DefaultController extends AbstractController { //Set password $data->setPassword($encoder->encodePassword($data, $data->getPassword())); - //Set updated - $data->setUpdated(new \DateTime('now')); - //Queue snippet save $manager->persist($data); @@ -422,9 +412,6 @@ class DefaultController extends AbstractController { //Set data $data = $edit->getData(); - //Set updated - $data->setUpdated(new \DateTime('now')); - //Queue snippet save $manager->persist($data); @@ -723,9 +710,6 @@ class DefaultController extends AbstractController { //Set user password $user->setPassword($encoded); - //Set updated - $user->setUpdated(new \DateTime('now')); - //Persist user $manager->persist($user); @@ -793,6 +777,108 @@ class DefaultController extends AbstractController { //Set mail $user->setMail($mail); + + //With existing registrant + if ($existing = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail)) { + //With disabled existing + if ($existing->isDisabled()) { + //Render view + return $this->render( + //Template + $this->config['register']['view']['name'], + //Context + ['title' => $this->translator->trans('Access denied'), 'disabled' => 1]+$this->config['register']['view']['context'], + //Set 403 + new Response('', 403) + ); + //With unactivated existing + } elseif (!$existing->isActivated()) { + //Set mail shortcut + //TODO: change for activate ??? + $activateMail =& $this->config['register']['mail']; + + //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 + $activateMail['context'][$tag] = $this->get('router')->generate( + $this->config['route'][$route]['name'], + //Prepend subscribe context with tag + [ + 'mail' => $smail = $slugger->short($existing->getMail()), + 'hash' => $slugger->hash($smail) + ]+$this->config['route'][$route]['context'], + UrlGeneratorInterface::ABSOLUTE_URL + ); + } + } + } + + //Set recipient_name + $activateMail['context']['recipient_mail'] = $existing->getMail(); + + //Set recipient name + $activateMail['context']['recipient_name'] = implode(' ', [$existing->getForename(), $existing->getSurname(), $existing->getPseudonym()?'('.$existing->getPseudonym().')':'']); + + //Init subject context + $subjectContext = $slugger->flatten(array_replace_recursive($this->config['register']['view']['context'], $activateMail['context']), null, '.', '%', '%'); + + //Translate subject + $activateMail['subject'] = ucfirst($this->translator->trans($activateMail['subject'], $subjectContext)); + + //Create message + $message = (new TemplatedEmail()) + //Set sender + ->from(new Address($this->config['contact']['mail'], $this->config['contact']['title'])) + //Set recipient + //XXX: remove the debug set in vendor/symfony/mime/Address.php +46 + ->to(new Address($activateMail['context']['recipient_mail'], $activateMail['context']['recipient_name'])) + //Set subject + ->subject($activateMail['subject']) + + //Set path to twig templates + ->htmlTemplate($activateMail['html']) + ->textTemplate($activateMail['text']) + + //Set context + ->context(['subject' => $activateMail['subject']]+$activateMail['context']); + + //Try sending message + //XXX: mail delivery may silently fail + try { + //Send message + $mailer->send($message); + //Catch obvious transport exception + } catch(TransportExceptionInterface $e) { + //Add error message mail unreachable + $this->addFlash('error', $this->translator->trans('Account %mail% tried activate but unable to contact', ['%mail%' => $existing->getMail()])); + } + + //Get route params + $routeParams = $request->get('_route_params'); + + //Remove mail, field and hash from route params + unset($routeParams['mail'], $routeParams['field'], $routeParams['hash']); + + //Redirect on the same route with sent=1 to cleanup form + return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$routeParams); + } + + //Add error message mail already exists + $this->addFlash('warning', $this->translator->trans('Account %mail% already exists', ['%mail%' => $existing->getMail()])); + + //Redirect to user view + return $this->redirectToRoute( + $this->config['route']['edit']['name'], + [ + 'mail' => $smail = $slugger->short($existing->getMail()), + 'hash' => $slugger->hash($smail) + ]+$this->config['route']['edit']['context'] + ); + } //Without mail } else { //Set smail @@ -820,7 +906,7 @@ class DefaultController extends AbstractController { $smail = $mail; //Set smail - $sfield = $sfield; + $sfield = $field; //Reset field $field = []; -- 2.41.3 From 17869e03521ae16d21e3e7d7998ec23fd5da1686 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 13:56:41 +0200 Subject: [PATCH 12/16] Add user checker Add authentication failure handler Add unactivated exception --- Checker/UserChecker.php | 48 +++++ Exception/UnactivatedException.php | 28 +++ Handler/AuthenticationFailureHandler.php | 261 +++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 Checker/UserChecker.php create mode 100644 Exception/UnactivatedException.php create mode 100644 Handler/AuthenticationFailureHandler.php diff --git a/Checker/UserChecker.php b/Checker/UserChecker.php new file mode 100644 index 0000000..1240c8a --- /dev/null +++ b/Checker/UserChecker.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\UserBundle\Checker; + +use Symfony\Component\Security\Core\User\UserChecker as BaseUserChecker; +use Symfony\Component\Security\Core\Exception\DisabledException; +use Symfony\Component\Security\Core\User\UserInterface; + +use Rapsys\UserBundle\Entity\User; +use Rapsys\UserBundle\Exception\UnactivatedException; + +/** + * {@inheritdoc} + */ +class UserChecker extends BaseUserChecker { + /** + * {@inheritdoc} + */ + public function checkPostAuth(UserInterface $user): void { + //Without User instance + if (!$user instanceof User) { + return; + } + + //With not activated user + if (!$user->isActivated()) { + $ex = new UnactivatedException('Account is not activated'); + $ex->setUser($user); + throw $ex; + } + + //With disabled user + if ($user->isDisabled()) { + $ex = new DisabledException('Account is disabled'); + $ex->setUser($user); + throw $ex; + } + } +} diff --git a/Exception/UnactivatedException.php b/Exception/UnactivatedException.php new file mode 100644 index 0000000..cbf367d --- /dev/null +++ b/Exception/UnactivatedException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\UserBundle\Exception; + +use Symfony\Component\Security\Core\Exception\AccountStatusException; + +/** + * UnactivatedException is thrown when the user account is unactivated. + * + * {@inheritdoc} + */ +class UnactivatedException extends AccountStatusException { + /** + * {@inheritdoc} + */ + public function getMessageKey(): string { + return 'Account is not activated'; + } +} diff --git a/Handler/AuthenticationFailureHandler.php b/Handler/AuthenticationFailureHandler.php new file mode 100644 index 0000000..5945644 --- /dev/null +++ b/Handler/AuthenticationFailureHandler.php @@ -0,0 +1,261 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\UserBundle\Handler; + +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +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\Http\Authentication\DefaultAuthenticationFailureHandler; +use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\Security\Http\ParameterBagUtils; + +use Rapsys\PackBundle\Util\SluggerUtil; +use Rapsys\UserBundle\Exception\UnactivatedException; +use Rapsys\UserBundle\RapsysUserBundle; + +/** + * {@inheritdoc} + */ +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', + ]; + + /** + * Router instance + */ + protected $router; + + /** + * Slugger instance + */ + protected $slugger; + + /** + * {@inheritdoc} + */ + public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options = [], LoggerInterface $logger, ContainerInterface $container, RouterInterface $router, SluggerUtil $slugger) { + //Set config + $this->config = $container->getParameter(self::getAlias()); + + //Set router + $this->router = $router; + + //Set slugger + $this->slugger = $slugger; + + //Call parent constructor + parent::__construct($httpKernel, $httpUtils, $options, $logger); + } + + /** + * This is called when an interactive authentication attempt fails + * + * User may retrieve mail + field + hash for each unactivated/locked accounts + * + * {@inheritdoc} + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { + //With bad credential exception + if ($exception instanceof BadCredentialsException) { + //With parent exception + if ($parent = $exception->getPrevious()) { + //Retrieve login + //TODO: check form _token validity ??? + if ( + $request->request->has('login') && + !empty($login = $request->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)]; + + //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 index route from config + if (!empty($name = $this->config['route']['register']['name']) && is_array($context = $this->config['route']['register']['context'])) { + //Try index route + try { + //Generate url + $url = $this->router->generate($name, $extra+$context); + + //Return generated route + return new RedirectResponse($url, 302); + //No route matched + } catch (ResourceNotFoundException $e) { + //Unset name and context + unset($name, $context); + } + } + + //With login target path option + if (!empty($loginPath = $this->options['login_path'])) { + //With path + if ($loginPath[0] == '/') { + //Create login path request instance + $req = Request::create($loginPath); + + //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($loginPath, $name, $route); + } + //With route name + } else { + //Try with login path route + try { + //Retrieve route matching path + $url = $this->router->generate($loginPath, $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($loginPath, $url); + } + } + } + } + } + } + } + + //Call parent function + return parent::onAuthenticationFailure($request, $exception); + } + + /** + * {@inheritdoc} + */ + public function getAlias(): string { + return RapsysUserBundle::getAlias(); + } +} -- 2.41.3 From fb368fd997affe677b6dfb8d9672974726c29716 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 14:25:07 +0200 Subject: [PATCH 13/16] Update doc --- README.md | 205 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 188 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index dc14527..7867c59 100644 --- a/README.md +++ b/README.md @@ -10,23 +10,32 @@ Add bundle custom repository to your project's `composer.json` file: { ..., "repositories": [ - { - "type": "package", - "package": { - "name": "rapsys/userbundle", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://git.rapsys.eu/userbundle", - "reference": "master" - }, - "autoload": { - "psr-4": { - "Rapsys\\UserBundle\\": "" - } - } - } - } + { + "type": "package", + "package": { + "name": "rapsys/userbundle", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://git.rapsys.eu/userbundle", + "reference": "master" + }, + "autoload": { + "psr-4": { + "Rapsys\\UserBundle\\": "" + } + }, + "require": { + "doctrine/doctrine-bundle": "^1.12", + "rapsys/packbundle": "dev-master", + "symfony/flex": "^1.5", + "symfony/form": "^4.4", + "symfony/framework-bundle": "^4.4", + "symfony/security-bundle": "^4.4", + "symfony/validator": "^4.4" + } + } + } ], ... } @@ -79,3 +88,165 @@ class AppKernel extends Kernel // ... } ``` + +### Step 3: Configure the Bundle + +Setup configuration file `config/packages/rapsys_user.yaml` with the following +content available in `Rapsys/UserBundle/Resources/config/packages/rapsys_user.yaml`: + +```yaml +#Doctrine configuration +doctrine: + #Orm configuration + orm: + #Force resolution of UserBundle entities to CustomBundle one + #XXX: without these lines, relations are lookup in parent namespace ignoring CustomBundle extension + resolve_target_entities: + Rapsys\UserBundle\Entity\Group: 'CustomBundle\Entity\Group' + Rapsys\UserBundle\Entity\Civility: 'CustomBundle\Entity\Civility' + Rapsys\UserBundle\Entity\User: 'CustomBundle\Entity\User' + +#RapsysUser configuration +rapsys_user: + #Class replacement + class: + group: 'CustomBundle\Entity\Group' + civility: 'CustomBundle\Entity\Civility' + user: 'CustomBundle\Entity\User' + #Default replacement + default: + group: [ 'User' ] + civility: 'Mister' + #Route replacement + route: + index: + name: 'custom_index' + +#Service configuration +services: + #Register security context service + rapsys_user.access_decision_manager: + class: 'Symfony\Component\Security\Core\Authorization\AccessDecisionManager' + public: true + arguments: [ [ '@security.access.role_hierarchy_voter' ] ] + #Register default controller + Rapsys\UserBundle\Controller\DefaultController: + arguments: [ '@service_container', '@router', '@translator' ] + autowire: true + tags: [ 'controller.service_arguments' ] + #Register Authentication success handler + security.authentication.success_handler: + class: 'Rapsys\UserBundle\Handler\AuthenticationSuccessHandler' + arguments: [ '@router', {} ] + #Register Authentication failure handler + security.authentication.failure_handler: + class: 'Rapsys\UserBundle\Handler\AuthenticationFailureHandler' + arguments: [ '@http_kernel', '@security.http_utils', {}, '@logger', '@service_container', '@router', '@rapsys_pack.slugger_util'] + #Register logout success handler + security.logout.success_handler: + class: 'Rapsys\UserBundle\Handler\LogoutSuccessHandler' + arguments: [ '@service_container', '/', '@router' ] + #Register security user checker + security.user_checker: + class: 'Rapsys\UserBundle\Checker\UserChecker' +``` + +Open a command console, enter your project directory and execute the following +command to see default bundle configuration: + +```console +$ php bin/console config:dump-reference RapsysUserBundle +``` + +Open a command console, enter your project directory and execute the following +command to see current bundle configuration: + +```console +$ php bin/console debug:config RapsysUserBundle +``` + +### Step 4: Setup custom bundle entities + +Setup configuration file `CustomBundle/Resources/config/doctrine/User.orm.yml` with the +following content: + +```yaml +CustomBundle\Entity\User: + type: entity + #repositoryClass: CustomBundle\Repository\UserRepository + table: users + associationOverride: + groups: + joinTable: + name: users_groups + joinColumns: + id: + name: user_id + inverseJoinColumns: + id: + name: group_id +``` + +Setup configuration file `Resources/config/doctrine/Group.orm.yml` with the +following content: + +```yaml +CustomBundle\Entity\Group: + type: entity + #repositoryClass: CustomBundle\Repository\GroupRepository + table: groups + manyToMany: + users: + targetEntity: Rapsys\AirBundle\Entity\User + mappedBy: groups +``` + +Setup configuration file `Resources/config/doctrine/Civility.orm.yml` with the +following content: + +```yaml +CustomBundle\Entity\Civility: + type: entity + #repositoryClass: CustomBundle\Repository\CivilityRepository + table: civilities + oneToMany: + users: + targetEntity: User + mappedBy: civility +``` + +Setup entity file `CustomBundle/Entity/User.php` with the following content: + +```php + Date: Thu, 12 Aug 2021 14:39:50 +0200 Subject: [PATCH 14/16] Add strict type Cleanup --- Entity/Civility.php | 45 +++++++++++++++++++++++++-------------- Entity/Group.php | 51 +++++++++++++++++++++++++++++---------------- Entity/User.php | 30 ++++++++++++++++++-------- 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/Entity/Civility.php b/Entity/Civility.php index 83c7f47..8e1719c 100644 --- a/Entity/Civility.php +++ b/Entity/Civility.php @@ -1,7 +1,20 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace Rapsys\UserBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; + +use Rapsys\UserBundle\Entity\User; + /** * Civility */ @@ -27,7 +40,7 @@ class Civility { protected $updated; /** - * @var \Doctrine\Common\Collections\Collection + * @var ArrayCollection */ protected $users; @@ -35,7 +48,7 @@ class Civility { * Constructor */ public function __construct() { - $this->users = new \Doctrine\Common\Collections\ArrayCollection(); + $this->users = new ArrayCollection(); } /** @@ -43,7 +56,7 @@ class Civility { * * @return integer */ - public function getId() { + public function getId(): int { return $this->id; } @@ -54,7 +67,7 @@ class Civility { * * @return Civility */ - public function setTitle($title) { + public function setTitle(string $title) { $this->title = $title; return $this; @@ -65,7 +78,7 @@ class Civility { * * @return string */ - public function getTitle() { + public function getTitle(): ?string { return $this->title; } @@ -76,7 +89,7 @@ class Civility { * * @return Civility */ - public function setCreated($created) { + public function setCreated(\DateTime $created) { $this->created = $created; return $this; @@ -87,7 +100,7 @@ class Civility { * * @return \DateTime */ - public function getCreated() { + public function getCreated(): \DateTime { return $this->created; } @@ -98,7 +111,7 @@ class Civility { * * @return Civility */ - public function setUpdated($updated) { + public function setUpdated(\DateTime $updated) { $this->updated = $updated; return $this; @@ -109,18 +122,18 @@ class Civility { * * @return \DateTime */ - public function getUpdated() { + public function getUpdated(): \DateTime { return $this->updated; } /** * Add user * - * @param \Rapsys\UserBundle\Entity\User $user + * @param User $user * * @return Civility */ - public function addUser(\Rapsys\UserBundle\Entity\User $user) { + public function addUser(User $user): Civility { $this->users[] = $user; return $this; @@ -129,18 +142,18 @@ class Civility { /** * Remove user * - * @param \Rapsys\UserBundle\Entity\User $user + * @param User $user */ - public function removeUser(\Rapsys\UserBundle\Entity\User $user) { + public function removeUser(User $user) { $this->users->removeElement($user); } /** * Get users * - * @return \Doctrine\Common\Collections\Collection + * @return ArrayCollection */ - public function getUsers() { + public function getUsers(): ArrayCollection { return $this->users; } diff --git a/Entity/Group.php b/Entity/Group.php index c503b2f..1141f91 100644 --- a/Entity/Group.php +++ b/Entity/Group.php @@ -1,8 +1,23 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ -// src/Rapsys/UserBundle/Entity/Group.php namespace Rapsys\UserBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; + +use Rapsys\UserBundle\Entity\User; + +/** + * Group + */ class Group { /** * @var integer @@ -25,7 +40,7 @@ class Group { protected $updated; /** - * @var \Doctrine\Common\Collections\Collection + * @var ArrayCollection */ protected $users; @@ -36,7 +51,7 @@ class Group { */ public function __construct(string $title) { $this->title = (string) $title; - $this->users = new \Doctrine\Common\Collections\ArrayCollection(); + $this->users = new ArrayCollection(); } /** @@ -44,7 +59,7 @@ class Group { * * @return integer */ - public function getId() { + public function getId(): int { return $this->id; } @@ -55,7 +70,7 @@ class Group { * * @return User */ - public function setTitle($title) { + public function setTitle(string $title) { $this->title = $title; return $this; @@ -66,7 +81,7 @@ class Group { * * @return string */ - public function getTitle() { + public function getTitle(): ?string { return $this->title; } @@ -77,7 +92,7 @@ class Group { * * @return User */ - public function setCreated($created) { + public function setCreated(\DateTime $created) { $this->created = $created; return $this; @@ -88,7 +103,7 @@ class Group { * * @return \DateTime */ - public function getCreated() { + public function getCreated(): \DateTime { return $this->created; } @@ -99,7 +114,7 @@ class Group { * * @return User */ - public function setUpdated($updated) { + public function setUpdated(\DateTime $updated) { $this->updated = $updated; return $this; @@ -110,18 +125,18 @@ class Group { * * @return \DateTime */ - public function getUpdated() { + public function getUpdated(): \DateTime { return $this->updated; } /** * Add user * - * @param \Rapsys\UserBundle\Entity\User $user + * @param User $user * * @return Group */ - public function addUser(\Rapsys\UserBundle\Entity\User $user) { + public function addUser(User $user) { $this->users[] = $user; return $this; @@ -130,18 +145,18 @@ class Group { /** * Remove user * - * @param \Rapsys\UserBundle\Entity\User $user + * @param User $user */ - public function removeUser(\Rapsys\UserBundle\Entity\User $user) { + public function removeUser(User $user) { $this->users->removeElement($user); } /** * Get users * - * @return \Doctrine\Common\Collections\Collection + * @return ArrayCollection */ - public function getUsers() { + public function getUsers(): ArrayCollection { return $this->users; } @@ -159,7 +174,7 @@ class Group { * * @return string */ - public function getRole() { + public function getRole(): string { return 'ROLE_'.strtoupper($this->title); } } diff --git a/Entity/User.php b/Entity/User.php index 3c1c768..8e6a671 100644 --- a/Entity/User.php +++ b/Entity/User.php @@ -1,13 +1,25 @@ - + * + * for the full copyright and license information, please view the license + * file that was distributed with this source code. + */ -// src/Rapsys/UserBundle/Entity/User.php namespace Rapsys\UserBundle\Entity; -use Rapsys\UserBundle\Entity\Group; -use Symfony\Component\Security\Core\User\UserInterface; use Doctrine\Common\Collections\ArrayCollection; +use Symfony\Component\Security\Core\User\UserInterface; + use Rapsys\UserBundle\Entity\Civility; +use Rapsys\UserBundle\Entity\Group; +/** + * User + */ class User implements UserInterface, \Serializable { /** * @var integer @@ -60,12 +72,12 @@ class User implements UserInterface, \Serializable { protected $updated; /** - * @var \Rapsys\UserBundle\Entity\Civility + * @var Civility */ protected $civility; /** - * @var \Doctrine\Common\Collections\ArrayCollection + * @var ArrayCollection */ protected $groups; @@ -306,7 +318,7 @@ class User implements UserInterface, \Serializable { /** * Add group * - * @param \Rapsys\UserBundle\Entity\Group $group + * @param Group $group * * @return User */ @@ -319,7 +331,7 @@ class User implements UserInterface, \Serializable { /** * Remove group * - * @param \Rapsys\UserBundle\Entity\Group $group + * @param Group $group */ public function removeGroup(Group $group) { $this->groups->removeElement($group); @@ -328,7 +340,7 @@ class User implements UserInterface, \Serializable { /** * Get groups * - * @return \Doctrine\Common\Collections\ArrayCollection + * @return ArrayCollection */ public function getGroups(): ArrayCollection { return $this->groups; -- 2.41.3 From ee0b2896d4f625a791c940e34aa7e5e5e56cbebb Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 14:40:27 +0200 Subject: [PATCH 15/16] Remove unused form types --- Form/RecoverMailType.php | 36 ------------------------ Form/RecoverType.php | 59 ---------------------------------------- 2 files changed, 95 deletions(-) delete mode 100644 Form/RecoverMailType.php delete mode 100644 Form/RecoverType.php diff --git a/Form/RecoverMailType.php b/Form/RecoverMailType.php deleted file mode 100644 index e0ae50c..0000000 --- a/Form/RecoverMailType.php +++ /dev/null @@ -1,36 +0,0 @@ -add('password', RepeatedType::class, ['type' => PasswordType::class, 'invalid_message' => 'The password and confirmation must match', 'first_options' => ['attr' => ['placeholder' => 'Your password'], 'label' => 'Password'], 'second_options' => ['attr' => ['placeholder' => 'Your password confirmation'], 'label' => 'Confirm password'], 'options' => ['constraints' => [new NotBlank(['message' => 'Please provide your password'])]]]) - ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]); - } - - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) { - $resolver->setDefaults(['error_bubbling' => true]); - } - - /** - * {@inheritdoc} - */ - public function getName() { - return 'rapsys_user_recover_mail'; - } -} diff --git a/Form/RecoverType.php b/Form/RecoverType.php deleted file mode 100644 index b1785dd..0000000 --- a/Form/RecoverType.php +++ /dev/null @@ -1,59 +0,0 @@ -add('mail', EmailType::class, ['attr' => ['placeholder' => 'Your mail'], 'constraints' => [new NotBlank(['message' => 'Please provide your mail']), new Email(['message' => 'Your mail doesn\'t seems to be valid'])]]); - } - - //Add extra password field - if (!empty($options['password'])) { - $form->add('password', RepeatedType::class, ['type' => PasswordType::class, 'invalid_message' => 'The password and confirmation must match', 'first_options' => ['attr' => ['placeholder' => 'Your password'], 'label' => 'Password'], 'second_options' => ['attr' => ['placeholder' => 'Your password confirmation'], 'label' => 'Confirm password'], 'options' => ['constraints' => [new NotBlank(['message' => 'Please provide your password'])]]]); - } - - //Add submit - $form->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]); - - //Return form - return $form; - } - - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) { - //Set defaults - $resolver->setDefaults(['error_bubbling' => true, 'mail' => true, 'password' => true]); - - //Add extra mail option - $resolver->setAllowedTypes('mail', 'boolean'); - - //Add extra password option - $resolver->setAllowedTypes('password', 'boolean'); - } - - /** - * {@inheritdoc} - */ - public function getName() { - return 'rapsys_user_recover'; - } -} -- 2.41.3 From 8e4e85c8806751174baf531b04ad78c51e3083dc Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 12 Aug 2021 14:54:10 +0200 Subject: [PATCH 16/16] Add strict Remove flatten function Cleanup --- DependencyInjection/RapsysUserExtension.php | 43 ++++++--------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/DependencyInjection/RapsysUserExtension.php b/DependencyInjection/RapsysUserExtension.php index 1c1d1cf..a0f7315 100644 --- a/DependencyInjection/RapsysUserExtension.php +++ b/DependencyInjection/RapsysUserExtension.php @@ -1,4 +1,13 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace Rapsys\UserBundle\DependencyInjection; @@ -16,7 +25,7 @@ class RapsysUserExtension extends Extension { /** * {@inheritdoc} */ - public function load(array $configs, ContainerBuilder $container) { + public function load(array $configs, ContainerBuilder $container): void { //Load configuration $configuration = $this->getConfiguration($configs, $container); @@ -39,34 +48,4 @@ class RapsysUserExtension extends Extension { public function getAlias(): string { return RapsysUserBundle::getAlias(); } - - /** - * The function that parses the array to flatten it into a one level depth array - * - * @param $array The config values array - * @param $path The current key path - * @param $depth The maxmium depth - * @param $sep The separator string - */ - /*protected function flatten($array, $path = '', $depth = 10, $sep = '.') { - //Init res - $res = array(); - - //Pass through non hashed or empty array - if ($depth && is_array($array) && ($array === [] || array_keys($array) === range(0, count($array) - 1))) { - $res[$path] = $array; - //Flatten hashed array - } elseif ($depth && is_array($array)) { - foreach($array as $k => $v) { - $sub = $path ? $path.$sep.$k:$k; - $res += $this->flatten($v, $sub, $depth - 1, $sep); - } - //Pass scalar value directly - } else { - $res[$path] = $array; - } - - //Return result - return $res; - }*/ } -- 2.41.3