Symfony2 fosuserbundle deal with/catch unique constraint violation - symfony

I'm using symfony 2.8 and FOSUserBundle. I want to allow admins to edit users' usernames and emails. If the new username or email is already taken then the database gives an error
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
which is good, but I don't know how to communicate that back to the admin who tried to change it to tell them what went wrong (the production version of the app will just give an error 500). What I want to do is show an error message of some kind (preferable like the one FOSUserBundle has in its forms) to say the username (or email) is taken.
The relevant portions of the form is built here:
$userManager = $this->get('fos_user.user_manager');
$user = $userManager->findUserBy(array('id' => $id));
$form = $this->createFormBuilder()
->add('username', TextType::class, array(
'label' => 'Username',
'data' => $user->getUsername(),
))
->add('email', EmailType::class, array(
'label' => 'Email',
'data' => $user->getEmail(),
))
->getForm();
and the database is handled here:
if ($form->isSubmitted() and $form->isValid()) {
// set new username if different
$newUsername = $form['username']->getData();
if ($user->getUsername() !== $newUsername) {
$user->setUsername($newUsername);
}
// set new email if different
$newEmail = $form['email']->getData();
if ($user->getEmail() !== $newEmail) {
$user->setEmail($newEmail);
}
$userManager->updateUser($user);
}
I have tried a number of things, like also setting username_canonical and email_canonical, or adding #UniqueEntity in my User.php class, but they haven't helped (which makes sense since the error is correct - I just can't translate it into a useful message).

If you doesn't want override anymore for make some validation, you need to implement an EventListener that catch the exceptions of your need by listening on the onKernelResponse event.
DBALExceptionResponseListener.php
// src/AcmeBundle/EventListner/DBALExceptionResponseListener.php
<?php
namespace AcmeBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Doctrine\DBAL\DBALException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Router\RouterInterface;
class DBALExceptionResponseListener
{
public function __construct(SessionInterface $session, RouterInterface $router)
{
$this->session = $session;
$this->router = $router;
}
/**
* #param GetResponseForExceptionEvent $event
*/
public function onKernelResponse(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
$exception = $event->getException();
$message = $exception->getMessage();
// Maybe some checks on the route
if ($request->get('_route') !== 'your_route' || $request->headers->get('referer') !== 'your_referer') {
return;
}
// Listen only on the expected exception
if (!$exception instanceof DBALException) {
return;
}
// You can make some checks on the message to return a different response depending on the MySQL error given.
if (strpos($message, 'Integrity constraint violation')) {
// Add your user-friendly error message
$this->session->getFlashBag()->add('error', 'SQL Error: '.$message);
}
// Create your custom response to avoid the error page.
$response = new RedirectResponse($this->router->generate('your_route'));
// Update the Event Response with yours
$event->setResponse($response);
}
}
services.yml
# app/config/services.yml
services:
acme.kernel.listener.dbal_exception_response_listener:
class: AcmeBundle\EventListener\DBALExceptionResponseListener
tags:
- {name: kernel.event_listener, event: kernel.exception, method: onKernelResponse}
arguments:
session: "#session"
router: "#router"
By looking more at the Exception::$message, you can easily find which property causes the problem.
The most common message contains something like :
... column 'propertyname' cannot be null ...

Related

How to modify user Information after requiring his password with Symfony 4

I would like to create a page that allows the user to modify his personal infos.
I want him to enter his current password to modify any information.
I created a form based on the connected user when the form is submitted and valid, I want to check if the password is valid using the function isPasswordValid() of my passwordEncoder..
My problem is that when this function is called with $user as parameter, it always returns false. I found where this problem comes from, it's because the $user used as parameter as been modified when the form has been submitted. I've tried declaring another variable to stock my Initial User ($dbUser for example) and using another to instance the form but when I dump the
$dbUser it has been modified and I don't know why...
The variable shouldn't be changed after the submit because it's never used... I can't find what I doing wrong...
/**
* #Route("/mes-infos", name="account_infos")
*/
public function showMyInfos(Request $request, UserRepository $userRepo)
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->getUser();
// $dbUser = $userRepo->findOneBy(['id' => 13]);
$form = $this->createForm(UserModificationType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$enteredPassword = $request->request->get('user_modification')['plainPassword'];
$passwordEncoder = $this->passwordEncoder;
$manager = $this->getDoctrine()->getManager();
if ($passwordEncoder->isPasswordValid($user, $enteredPassword)) {
dd('it works!!!!!');
// $manager->persist($user);
// $manager->flush();
} else {
dd('It\'s not!!!!');
}
}
return $this->render('account/myaccount-infos.html.twig', [
'form' => $form->createView(),
]);
}
The better solution is to use a constraint.
Symfony already implements a UserPasswordConstraint.
You can add it in your entity directly. Be careful to declare a group for this constraint. If you don't do it, your use case "user update" will works fine, but the use case "user creation" will now failed.
namespace App\Entity;
use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
class User
{
//...
/**
* #SecurityAssert\UserPassword(
* message = "Wrong value for your current password",
* groups = {"update"}
* )
*/
protected $password;
//...
}
In your form, specified the validation groups by updating (or adding) the configureOptions method:
//App\Form\UserModificationType
//...
class UserModificationType {
//...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// ...
'validation_groups' => ['update'],
]);
}
}
Then your $form->isValid() will automatically test the password. So, you can removed all lines in the "if condition".
/**
* #Route("/mes-infos", name="account_infos")
*/
public function showMyInfos(Request $request, UserRepository $userRepo)
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->getUser();
$form = $this->createForm(UserModificationType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dd('Password is good! it works!!!!!');
}
return $this->render('account/myaccount-infos.html.twig', [
'form' => $form->createView()
]);
}
But I think it is a better practice to use a Model like in the documentation when you update your user entity.

Unable to guess how to get a Doctrine instance from the request information for parameter "class"

I'm trying to send an email with a token in order to resend an automated password. But in my view i'm retrieving the following error:
/**
* #Route("/forget-email/", name="forget", methods="GET|POST")
*/
public function emailrestore(Request $request, User $user, \Swift_Mailer $mailer)
{
$url = "test";
$form = $this->createForm(ForgetPasswordType::class, $user);
$form->handleRequest($request);
$email = $form['email']->getData();
$user = $this->getDoctrine()
->getRepository(User::class)
->find($email);
if ( $email === $user ) {
$mail = (new \Swift_Message('Hello Email'))
->setFrom('email#email.email')
->setTo($email)
->setBody(
$this->renderView(
// templates/emails/registration.html.twig
'emails/registration.html.twig',array('url' => $url,)
),
'text/html'
);
$mailer->send($mail);
} else{
var_dump("$email");
}
return $this->render('forget/email.html.twig', [
'form' => $form->createView(),
'error' => null,
]);
}
Inside my entity i have email as unique
* #UniqueEntity(fields="email", message="Email already taken")
I'm retrieving the following error:
"Unable to guess how to get a Doctrine instance from the request information for parameter "user"." Why?
Thanks for your explanation for advance
You have "User $user" in your action parameter but you don't have a parameter 'user' in your request, my guess is your ParamConverter can't convert this one into a User object because of that.
make your route a GET route formatted like:
/**
* #Route("/forget-email/{user}", name="forget", methods="GET")
*/
Where user is whatever you want (mail, username, uid, etc ...) has long as it allows you tyo retrieve your user object and have your paramconverter converting it into a Doctrine object ...
You need to specify an initial value for your $user variable
Try this :
public function emailrestore(Request $request, User $user, \Swift_Mailer $mailer)

FosUserBundle controller override

[SETTINGS]
Symfony 3.4
FosUserBundle 2.0
RESTRICTION: Avoid bundle inheritance (This is also bundle inheritance)
[PROBLEM]
While reading the Symfony doc about how to override any part if a bundle,
I met those lines:
If the controller is a service, see the next section on how to override it. Otherwise, define a new route + controller with the same path associated to the controller you want to override (and make sure that the new route is loaded before the bundle one).
And somehow felt overjoyed seeing how the doc was still as incomplete as ever on some of the most important sections... Right, this part got no code example, can't even be sure of what to do.
Would someone be kind enough to give me an example on how to override the FosUserBundle? Just one section like the login part will be enough. As the same logic will apply for the other sections.
Also, as a side questions:
Is it worth using FosUserBundle?
Is there a bundle easier to use than FosUserBundle?
Wouldn't it be more worth and faster to make my own logic to handle login?
What I understand : simply create your controller and then add a route for it in your configuration with the same path as the one you want to override, making sure it's loaded before.
For example, to override the login action:
// AppBundle\Controller\UserController.php
/**
* #route("/login", name="login_override")
* #param Request $request
* #return Response
*/
public function loginAction(Request $request)
{
/** #var $session Session */
$session = $request->getSession();
$authErrorKey = Security::AUTHENTICATION_ERROR;
$lastUsernameKey = Security::LAST_USERNAME;
// get the error if any (works with forward and redirect -- see below)
if ($request->attributes->has($authErrorKey)) {
$error = $request->attributes->get($authErrorKey);
} elseif (null !== $session && $session->has($authErrorKey)) {
$error = $session->get($authErrorKey);
$session->remove($authErrorKey);
} else {
$error = null;
}
if (!$error instanceof AuthenticationException) {
$error = null; // The value does not come from the security component.
}
// last username entered by the user
$lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);
$tokenManager = $this->container->get('security.csrf.token_manager');
$csrfToken = $tokenManager
? $tokenManager->getToken('authenticate')->getValue()
: null;
return $this->render('#FOSUser/Security/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
'csrf_token' => $csrfToken,
));
}
#app\config\routing.yml
app:
resource: '#AppBundle/Controller/'
type: annotation
fos_user:
resource: "#FOSUserBundle/Resources/config/routing/all.xml"

Handling Symfony Collection violations for user data

I'm writing an API that will take in a JSON string, parse it and return the requested data. I'm using Symfony's Validation component to do this, but I'm having some issues when validating arrays.
For example, if I have this data:
{
"format": {
"type": "foo"
}
}
Then I can quite easily validate this with PHP code like this:
$constraint = new Assert\Collection(array(
"fields" => array(
"format" => new Assert\Collection(array(
"fields" => array(
"type" => new Assert\Choice(["foo", "bar"])
)
))
)
));
$violations = $validator->validate($data, $constraint);
foreach ($violations as $v) {
echo $v->getMessage();
}
If type is neither foo, nor bar, then I get a violation. Even if type is something exotic like a DateTime object, I still get a violation. Easy!
But if I set my data to this:
{
"format": "uh oh"
}
Then instead of getting a violation (because Assert\Collection expects an array), I get a nasty PHP message:
Fatal error: Uncaught Symfony\Component\Validator\Exception\UnexpectedTypeException: Expected argument of type "array or Traversable and ArrayAccess", "string" given [..]
If there a neat way to handle things like this, without needing to try / catch and handle the error manually, and without having to double up on validation (e.g. one validation to check if format is an array, then another validation to check if type is valid)?
Gist with the full code is here: https://gist.github.com/Grayda/fec0ed7487641645304dee668f2163ac
I'm using Symfony 4
As far as I can see, all built-in validators throw an exception when they are expecting an array but receive something else, so you'll have to write your own validator. You can create a custom validator that first checks if the field is an array, and only then runs the rest of the validators.
The constraint:
namespace App\Validation;
use Symfony\Component\Validator\Constraints\Composite;
/**
* #Annotation
* #Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class IfArray extends Composite
{
public $message = 'This field should be an array.';
public $constraints = array();
public function getDefaultOption()
{
return 'constraints';
}
public function getRequiredOptions()
{
return array('constraints');
}
protected function getCompositeOption()
{
return 'constraints';
}
}
And the validator:
namespace App\Validation;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class IfArrayValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof IfArray) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\IfArray');
}
if (null === $value) {
return;
}
if (!is_array($value) && !$value instanceof \Traversable) {
$this->context->buildViolation($constraint->message)
->addViolation();
return;
}
$context = $this->context;
$validator = $context->getValidator()->inContext($context);
$validator->validate($value, $constraint->constraints);
}
}
Note that this is very similar to the All constraint, with the major difference being that if !is_array($value) && !$value instanceof \Traversable is true, the code will add a violation instead of throwing an exception.
The new constraint can now be used like this:
$constraint = new Assert\Collection(array(
"fields" => array(
"format" => new IfArray(array(
"constraints" => new Assert\Collection(array(
"fields" => array(
"type" => new Assert\Choice(["foo", "bar"])
)
))
)),
)
));

Adding Captcha to Symfony2 Login Page

I am new to Symfony2 but read about it very much.
First of all, I am using symfony 2.1.7. And FOSUserBundle for user settings. I have already override fos_user-login template, with username and password. But I want to add a captcha for log in. I have seen GregwarCaptchaBundle, and according to document, new field should be added to FormType. And my question comes: Where is the symfony or FOSUserBundle login form type, that i can add this new field, or override it? There exists ChangePasswordFormType, ProfileFormType... etc. but no LoginFOrmType. May be it is so obvious but i did not get the point, Any help is welcomed please
QUESTION IS EDITED WITH A SOLUTION SOMEHOW
Take a look at the comments below that Patt helped me.
I have created a new form type with _username, _password and captcha fields. When naming for username and password begins with an underscore is enough for 'login_check' routing and Symfony authentication. However Symfony uses a listener for login process.
Which is UsernamePasswordFormAuthenticationListenerclass. Although i've added captcha field in the Form type, it is always ignored during login process.(It is rendered on the page, but the field is never validated, it is simply ignored.)
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username', 'email', array('label' => 'form.username', 'translation_domain' => 'FOSUserBundle')) // TODO: user can login with email by inhibit the user to enter username
->add('_password', 'password', array(
'label' => 'form.current_password',
'translation_domain' => 'FOSUserBundle',
'mapped' => false,
'constraints' => new UserPassword()))
->add('captcha', 'captcha');
}
As i mentioned above UsernamePasswordFormAuthenticationListener class gets the form input values and then redirects you:
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'captcha' => 'captcha',
'intention' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfProvider = $csrfProvider;
}
captcha field is added.
protected function attemptAuthentication(Request $request)
{
if ($this->options['post_only'] && 'post' !== strtolower($request->getMethod())) {
if (null !== $this->logger) {
$this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod()));
}
return null;
}
if (null !== $this->csrfProvider) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
// check here the captcha value
$userCaptcha = $request->get($this->options['captcha'], null, true);
$dummy = $request->getSession()->get('gcb_captcha');
$sessionCaptcha = $dummy['phrase'];
// if captcha is not correct, throw exception
if ($userCaptcha !== $sessionCaptcha) {
throw new BadCredentialsException('Captcha is invalid');
}
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
$request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
}
Now, i have captcha on login screen.
Playing with symfony code is not a good way, i know. If i find out some way to override and call my own function, i'll post it.
ANOTHER USEFUL ANSWER
I found another answer that might be useful
[link]Is there any sort of "pre login" event or similar?
Following this solution, I have simply override UsernamePasswordFormAuthenticationListenerclass and override security listener security.authentication.listener.form.class parameter. Here goes the code:
namespace TCAT\StaffBundle\Listener;
use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener as BaseListener; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Log\LoggerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException;
class StaffLoginFormListener extends BaseListener
{
private $csrfProvider;
/**
* {#inheritdoc}
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options
= array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'captcha' => 'captcha',
'intention' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfProvider = $csrfProvider;
}
/**
* {#inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($this->options['post_only'] && 'post' !== strtolower($request->getMethod())) {
if (null !== $this->logger) {
$this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod()));
}
return null;
}
if (null !== $this->csrfProvider) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
// throw new BadCredentialsException('Bad credentials');
$userCaptcha = $request->get($this->options['captcha'], null, true);
$dummy = $request->getSession()->get('gcb_captcha');
$sessionCaptcha = $dummy['phrase'];
if ($userCaptcha !== $sessionCaptcha) {
throw new BadCredentialsException('Captcha is invalid');
}
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
$request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
}
}
and add security.authentication.listener.form.class: TCAT\StaffBundle\Listener\StaffLoginFormListener line to the app/config/paramaters.yml
BTW i can check my captcha value. I hope it all work for you.
Adding Captcha to Symfony2 Login Page
I am not sure this is a great idea. But it's doable.
Where is the symfony or FOSUserBundle login form type?
There is no form type for the login. The form is directly embed in the template as you can see in login.html.twig.
How could you do it?
You could totally create one but you would have to customize the SecurityController so that you send your form to the template.
The procedure would be something like that:
1. Create your custom loginFormType (that's where you can add your captcha in the builder).
2. Override the SecurityController (you could take a look here to see something similar). You need to override the loginAction method so that you can pass the form to your template here.
3. Override login.html.twig to render the form passed from your controller
Edit: Answer to your comment
How can you access to your form in a controller that extends
ContainerAware?
I highly recommend this reading to see how you can move away from the base controller. Now, how can you do this?
Well, you have 2 options:
OPTION 1: EASY WAY
$form = $this->createForm(new LoginFormType(), null);
becomes:
$form = $this->get('form.factory')->create(new LoginFormType(), $null);
OPTION 2: REGISTER FORM AS A SERVICE
1. Create your formType (normal procedure): loginFormType
2. Define your form as a service acme_user.login.form. You have a great example here (In the 1.2 version of FOSUserBundle, both registration and profile forms were registered as services, so this gives you a perfect example of how it's done).
3. You can now use your form inside your controller extending ContainerAware. See here.
$form = $this->container->get('acme_user.login.form');
In response to : Playing with symfony code is not a good way, i know. If i find out some way to override and call my own function, i'll post it.
To override the "UsernamePasswordFormAuthenticationListenerclass" you must copy the listner file in your bundle and change the config.yml file to load th new one :
parameters:
security.authentication.listener.form.class: Acme\YourBundle\Security\UsernamePasswordFormAuthenticationListener
Also the namespace in the copied file must be changed to the correct one :
namespace Acme\YourBundle\Security;
The last thing is adding "AbstractAuthenticationListener" in the use part to be loaded correctly :
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;

Resources