Symfony 3, populating token and refreshing user
Asked Answered
N

2

12

repository with issue

I have a form for entity User with email field:

->add('email', EmailType::class, [
                'constraints' => [
                    new NotBlank(),
                    new Email([
                        'checkMX' => true,
                    ])
                ],
                'required' => true
            ])

when i'm editing email to something like [email protected] and submit form it shows me error "This value is not a valid email address." THat's ok, but after that symfony populate wrong email into token and when i'm going to any other page or just reload page, i'm getting this:

WARNING security Username could not be found in the selected user provider.

i think question is: why symfony populate wrong Email that failed validation into token and how i could prevent it?

controller:

public function meSettingsAction(Request $request)
    {

        $user = $this->getUser();
        $userUnSubscribed = $this->getDoctrine()->getRepository('AppBundle:UserUnsubs')->findOneBy(
            [
                'email' => $user->getEmail(),
            ]
        );

        $form = $this->createForm(UserSettingsType::class, $user);
        $form->get('subscribed')->setData(!(bool)$userUnSubscribed);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            /**
             * @var $user User
             */
            $user = $form->getData();

            /** @var UploadedFile $avatar */
            $avatar = $request->files->get('user_settings')['photo'];

            $em = $this->getDoctrine()->getManager();

            if ($avatar) {
                $avatar_content = file_get_contents($avatar->getRealPath());
                $avatarName = uniqid().'.jpg';
                $oldAvatar = $user->getPhoto();
                $user
                    ->setState(User::PHOTO_STATE_UNCHECKED)
                    ->setPhoto($avatarName);
                $gearmanClient = $this->get('gearman.client');
                $gearmanClient->doBackgroundDependsOnEnv(
                    'avatar_content_upload',
                    serialize(['content' => $avatar_content, 'avatarName' => $avatarName, 'oldAvatar' => $oldAvatar])
                );
            }

            $subscribed = $form->get('subscribed')->getData();
            if ((bool)$userUnSubscribed && $subscribed) {
                $em->remove($userUnSubscribed);
            } elseif (!(bool)$userUnSubscribed && !$subscribed) {
                $userUnSubscribed = new UserUnsubs();
                $userUnSubscribed->setEmail($form->get('email')->getData())->setTs(time());
                $em->persist($userUnSubscribed);
            }
            $user->setLastTs(time());
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

            $this->get('user.manager')->refresh($user);

            return $this->redirectToRoute('me');
        }

        return $this->render(
            ':user:settings.html.twig',
            [
                'form' => $form->createView(),
            ]
        );
    }

UPD: it works fine if i change in OAuthProvider:

/**
 * @param \Symfony\Component\Security\Core\User\UserInterface $user
 *
 * @return \Symfony\Component\Security\Core\User\UserInterface
 */
public function refreshUser(UserInterface $user)
{
    return $this->loadUserByUsername($user->getName());
}

to:

/**
 * @param \Symfony\Component\Security\Core\User\UserInterface $user
 *
 * @return \Symfony\Component\Security\Core\User\UserInterface
 */
public function refreshUser(UserInterface $user)
{
    return $this->userManager($user->getId());
}

but it seems to be dirty hack.

Thanks.

Now answered 8/6, 2017 at 10:38 Comment(8)
can you show your controller?Huneycutt
@AlessandroMinoccheri i've added controller code to question.Now
why is refreshUser() even called if the email is wrong? Please add some more information about your serurity.yml config.Unwashed
@Unwashed I don't know why it's calling. And problem is in another thing, reread question.Now
What Entity is your UserSettingsType linked to ? What are your Users ? A simple implementation of the UserInterface or something coming from an external bundle like FOSUserBundle ? As already requested, can we get your security.yml ? We really need more info to be able to reproduce the issue.Winnipegosis
@Winnipegosis UserSettingsType using User entity - simple implementation UserInterface, EquatableInterface. I'm not using FOSUserBundle. What information do you need from my security.yml?Now
@Now all from security.yml, but mainly about user providers, user checkers, anything that could be the cause for your issue. I do not understand how you can issue a bounty on a question not providing all the necessary information event though requested multiple times and than expecting to get an answer.Unwashed
@Unwashed i've added example code to reproduce issue. Look into question, now you could try.Now
U
2

This is a tricky one, thanks to the repository it was easier to isolate the problem. You are binding the user object form the authentication token to the createForm() method. After the

$form->handleRequest($request)

call the email off the token user object is updated.

I first thought to solve this by implementing the EquatableInterface.html in the User entity but this did not work, as the compared object already had the wrong email address set.

It may also be useful to implement the EquatableInterface interface, which defines a method to check if the user is equal to the current user. This interface requires an isEqualTo() method.)

Than I thought about forcing a reload of the user from the db and resetting the security token, but in the it came to my mind, that it might be sufficient to just refresh the current user object from the database in case the form fails:

$this->get('doctrine')->getManager()->refresh($this->getUser());`

In your controller, this would solve your issue.

/**
 * @Route("/edit_me", name="edit")
 * @Security("has_role('ROLE_USER')")
 */
public function editMyselfAction(Request $request) {
    $form = $this->createForm(User::class, $this->getUser());

    if ($request->isMethod(Request::METHOD_POST)) {
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();
        } else {
            $this->get('doctrine')->getManager()->refresh($this->getUser());
        }
    }

    return $this->render(':security:edit.html.twig',['form' => $form->createView()]);
}

Alternative solution

The issue at the Symfony repository resulted in some valuable input about Avoiding Entities in Forms and Decoupling Your Security User which provides a more complex approach for a solution to your problem.

Unwashed answered 17/6, 2017 at 16:23 Comment(5)
thanks for your attention, but this is dirty hack :D i've already posted an issue to symfony/symfony repository on github and provide them my repository. That is not normal behavior of populating token. I have already another dirty hack, that was described in my answer. Thanks again)Now
Well, to be honest I do not consider this a dirty hack but a necessary solution because how you bind the $user from the token object to the form. This is expected behavior. If you want to avoid it, you should create a new empty user object, initialize it with needed data from the currently logged in user and bind it to the form. If the form is valid you manually map the submitted user object to the user object from the token.Unwashed
just for the sake of completeness. The issue you have posted on the symfony repository is considered a SRP and not a bug. So this is a valid answer to your question.Unwashed
You persuaded me)Now
I have updated the answer, let me know if you want to expand on the approach of creating a Data Transfer Object or decoupling the security user topic.Unwashed
K
3

Your user token seems to be updated by the form, even if the email constraint stop the flush.

Can you check if your form past the isValid function ? You can maybe try to avoid it with an event listener or a validator.

With an event SUBMIT you should be able to check the email integrity, and then add a FormError to avoid the refreshUser.

Kv answered 13/6, 2017 at 12:49 Comment(2)
Are you serious ? I understand why you're still stuck on it, we are here to help you for free, be polite please.Kv
i have already mentioned that was updating after submitting the form, also if form isn't valid. As you can see in code : ` if ($form->isSubmitted() && $form->isValid()) {` there is checking validators, and for is not populating data in entity if form is not valid.Now
U
2

This is a tricky one, thanks to the repository it was easier to isolate the problem. You are binding the user object form the authentication token to the createForm() method. After the

$form->handleRequest($request)

call the email off the token user object is updated.

I first thought to solve this by implementing the EquatableInterface.html in the User entity but this did not work, as the compared object already had the wrong email address set.

It may also be useful to implement the EquatableInterface interface, which defines a method to check if the user is equal to the current user. This interface requires an isEqualTo() method.)

Than I thought about forcing a reload of the user from the db and resetting the security token, but in the it came to my mind, that it might be sufficient to just refresh the current user object from the database in case the form fails:

$this->get('doctrine')->getManager()->refresh($this->getUser());`

In your controller, this would solve your issue.

/**
 * @Route("/edit_me", name="edit")
 * @Security("has_role('ROLE_USER')")
 */
public function editMyselfAction(Request $request) {
    $form = $this->createForm(User::class, $this->getUser());

    if ($request->isMethod(Request::METHOD_POST)) {
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();
        } else {
            $this->get('doctrine')->getManager()->refresh($this->getUser());
        }
    }

    return $this->render(':security:edit.html.twig',['form' => $form->createView()]);
}

Alternative solution

The issue at the Symfony repository resulted in some valuable input about Avoiding Entities in Forms and Decoupling Your Security User which provides a more complex approach for a solution to your problem.

Unwashed answered 17/6, 2017 at 16:23 Comment(5)
thanks for your attention, but this is dirty hack :D i've already posted an issue to symfony/symfony repository on github and provide them my repository. That is not normal behavior of populating token. I have already another dirty hack, that was described in my answer. Thanks again)Now
Well, to be honest I do not consider this a dirty hack but a necessary solution because how you bind the $user from the token object to the form. This is expected behavior. If you want to avoid it, you should create a new empty user object, initialize it with needed data from the currently logged in user and bind it to the form. If the form is valid you manually map the submitted user object to the user object from the token.Unwashed
just for the sake of completeness. The issue you have posted on the symfony repository is considered a SRP and not a bug. So this is a valid answer to your question.Unwashed
You persuaded me)Now
I have updated the answer, let me know if you want to expand on the approach of creating a Data Transfer Object or decoupling the security user topic.Unwashed

© 2022 - 2024 — McMap. All rights reserved.