Editing User with error wrongly changes the app.us

2020-07-17 06:40发布

We use the Symfony2 framework and FOSUserBundle for our users. So We have our own UserBundle that inherits from FOSUserBundle. The problem is : When we send the form for editing a user with a wrong password, the app.user.username that is displayed in the header changes when it shouldn't since the form isn't correct. Then when we reload the page, the app.user.username has the normal username (since it hasn't changed in the database). The question is, why does the app.user.username changes temporarily for the value of the form (edit user) that we just sent with a wrong password ?

we use the normal routing from FOSUser :

fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"

fos_user_profile:
    resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
    prefix: /profile

fos_user_register:
    resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
    prefix: /register

fos_user_resetting:
    resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
    prefix: /resetting

fos_user_change_password:
    resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
    prefix: /profile

And the default Controller from FOSUser :

public function editAction(Request $request)
{
    $user = $this->getUser();
    if (!is_object($user) || !$user instanceof UserInterface) {
        throw new AccessDeniedException('This user does not have access to this section.');
    }

    /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */
    $dispatcher = $this->get('event_dispatcher');

    $event = new GetResponseUserEvent($user, $request);
    $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_INITIALIZE, $event);

    if (null !== $event->getResponse()) {
        return $event->getResponse();
    }

    /** @var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */
    $formFactory = $this->get('fos_user.profile.form.factory');

    $form = $formFactory->createForm();
    $form->setData($user);

    $form->handleRequest($request);

    if ($form->isValid()) {
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->get('fos_user.user_manager');

        $event = new FormEvent($form, $request);
        $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_SUCCESS, $event);

        $userManager->updateUser($user);

        if (null === $response = $event->getResponse()) {
            $url = $this->generateUrl('fos_user_profile_show');
            $response = new RedirectResponse($url);
        }

        $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_COMPLETED, new FilterUserResponseEvent($user, $request, $response));

        return $response;
    }

    return $this->render('FOSUserBundle:Profile:edit.html.twig', array(
        'form' => $form->createView()
    ));
}

Here is the part of code that is displayed in the header that should NOT change when submitting the form with a wrong password. (app.user.username)

<ul class="nav navbar-nav navbar-right">
    {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
    <li>
        <a href="{{ path('fos_user_profile_show') }}">{{ app.user.username }}
        </a>
    </li>
    <li>
        <a href="{{ path('fos_user_security_logout') }}">
                {{ 'layout.logout'|trans({}, 'FOSUserBundle') }}
        </a>
     </li>
     {% else %}
     <li>
        <a href="{{ path('fos_user_registration_register') }}">{{ 'layout.register'|trans({}, 'FOSUserBundle') }}</a>
     </li>
     <li>
        <a href="{{ path('fos_user_security_login') }}">{{ 'layout.login'|trans({}, 'FOSUserBundle') }}</a>
      </li>
      {% endif %}
</ul>

And Here is the code of the formulaire that is displayed :

<div class="well">

{{ form_start(form, {'attr': {'class': 'form-horizontal'}}) }}

{{ form_errors(form) }}

<div class="form-group">
  {# Génération du label username. #}
  {{ form_label(form.username, 'register.form.username'|trans, {'label_attr': {'class': 'col-sm-3 control-label'}}) }}

  {# Affichage des erreurs pour ce champ précis. #}
  {{ form_errors(form.username) }}

  <div class="col-sm-4">
    {{ form_widget(form.username, {'attr': {'class': 'form-control'}}) }}
  </div>
</div>

  <div class="form-group">
  {# Génération du label adresse. #}
  {{ form_label(form.address, 'register.form.address'|trans, {'label_attr': {'class': 'col-sm-3 control-label'}}) }}

  {# Affichage des erreurs pour ce champ précis. #}
  {{ form_errors(form.address) }}

  <div class="col-sm-4">
    {{ form_widget(form.address, {'attr': {'class': 'form-control'}}) }}
  </div>
</div>

  <div class="form-group">
  {# Génération du label nom du contact. #}
  {{ form_label(form.contactName, 'register.form.contact_name'|trans, {'label_attr': {'class': 'col-sm-3 control-label'}}) }}

  {# Affichage des erreurs pour ce champ précis. #}
  {{ form_errors(form.contactName) }}

  <div class="col-sm-4">
    {{ form_widget(form.contactName, {'attr': {'class': 'form-control'}}) }}
  </div>
</div>

  <div class="form-group">
  {# Génération du label email. #}
  {{ form_label(form.email, 'register.form.email'|trans, {'label_attr': {'class': 'col-sm-3 control-label'}}) }}

  {# Affichage des erreurs pour ce champ précis. #}
  {{ form_errors(form.email) }}

  <div class="col-sm-4">
    {{ form_widget(form.email, {'attr': {'class': 'form-control'}}) }}
  </div>
  </div>

  <div class="form-group">
  {# Génération du label numéro de téléphone. #}
  {{ form_label(form.phone, 'register.form.phone'|trans, {'label_attr': {'class': 'col-sm-3 control-label'}}) }}

  {# Affichage des erreurs pour ce champ précis. #}
  {{ form_errors(form.phone) }}

  <div class="col-sm-4">
    {{ form_widget(form.phone, {'attr': {'class': 'form-control'}}) }}
  </div>
  </div>

  <div class="form-group">
  {# Génération du label mot de passe. #}
  {{ form_label(form.current_password, 'register.form.password'|trans, {'label_attr': {'class': 'col-sm-3 control-label'}}) }}

  {# Affichage des erreurs pour ce champ précis. #}
  {{ form_errors(form.current_password) }}

  <div class="col-sm-4">
    {{ form_widget(form.current_password, {'attr': {'class': 'form-control'}}) }}
  </div>
</div>

{# Pour le bouton, pas de label ni d'erreur, on affiche juste le widget #}
{{ form_widget(form.saveButton, {'attr': {'class': 'btn btn-primary'},  'label': 'profile.edit.submit'|trans }) }}

{# Génération automatique des champs pas encore écrits.
 Dans cet exemple, ce serait le champ CSRF (géré automatiquement par Symfony !)
 et tous les champs cachés (type « hidden »). #}
{{ form_rest(form) }}

{# Fermeture de la balise <form> du formulaire HTML #}
{{ form_end(form) }}

</div>

And here is the FormType that we user for this particular form :

public function buildForm(FormBuilderInterface $builder, array $options)
{
    // add your custom field
    $builder->add('contactName', 'text')
            ->add('phone', 'text')
            ->add('address', 'text')
            ->add('saveButton', 'submit');
}

The FormType inherits from ProfileFormType (Default in FOSUserBundle) thanks to a getParent() function.

Thank you for your help figuring out what is wrong. We don't understand why the app.user.username changes in the header since the form is sent with a wrong password and the data in the database isn't changed and sorry for the comments that are in French in the code :).

3条回答
女痞
2楼-- · 2020-07-17 06:49

This is happening by design. At the moment you call $form->handleRequest($request) the user entity is updated with the submitted form data. When you access app.user you are in fact accessing the same user entity. The docs have some more detail about this

查看更多
手持菜刀,她持情操
3楼-- · 2020-07-17 06:52

You can use FOSUserBundle-EventListener. (LoginListener|ProfileEditListener) I save the username in the session-context and print them out in twig-template instead of app.user.username.

It works fine.

<?php
// LoginListener.php
namespace <YOUR_PROJECT>\<YOUR_BUNDLE>Bundle\EventListener;

use <YOUR_PROJECT>\<YOUR_BUNDLE>Bundle\Entity\User;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\UserEvent;

use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Http\SecurityEvents;

class LoginListener implements EventSubscriberInterface
{
    /**
     * @var UrlGeneratorInterface
     */
    private $router;

    /**
     * @param UrlGeneratorInterface $router
     */
    public function __construct(UrlGeneratorInterface $router)
    {
        $this->router = $router;
    }

    /**
     * {@inheritDoc}
     */
    public static function getSubscribedEvents()
    {
        return array(
            FOSUserEvents::SECURITY_IMPLICIT_LOGIN => 'onImplicitLogin',
            SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
        );
    }

    /**
     * @param UserEvent $event
     */
    public function onImplicitLogin(UserEvent $event)
    {
        // not implemented yet
    }

    /**
     * @param InteractiveLoginEvent $event
     */
    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        $session = new Session();
        $user = $event->getAuthenticationToken()->getUser();

        if ($user instanceof User) {
            $session->set('username', $user->getUsername());
        }
    }
}

-

<?php
// ProfileEditListener
namespace <YOUR_PROJECT>\<YOUR_BUNDLE>Bundle\EventListener;

use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Event\UserEvent;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\SecurityContext;

class ProfileEditListener implements EventSubscriberInterface
{
    /**
     * @var UrlGeneratorInterface
     */
    private $router;

    /**
     * @param UrlGeneratorInterface $router
     */
    public function __construct(UrlGeneratorInterface $router)
    {
        $this->router = $router;
    }

    /**
     * {@inheritDoc}
     */
    public static function getSubscribedEvents()
    {
        return array(
            FOSUserEvents::PROFILE_EDIT_INITIALIZE => 'onProfileInitialize',
            FOSUserEvents::PROFILE_EDIT_SUCCESS => 'onProfileEdit',
            FOSUserEvents::PROFILE_EDIT_COMPLETED => 'onProfileCompleted'
        );
    }

    /**
     * @param UserEvent $userEvent
     */
    public function onProfileInitialize(UserEvent $userEvent) {
        $session = new Session();
        $session->set('username', $userEvent->getUser()->getUsername());
    }

    /**
     * @param FormEvent $event
     */
    public function onProfileEdit(FormEvent $event)
    {
        // not implemented yet
    }

    /**
     * @param UserEvent $userEvent
     */
    public function onProfileCompleted(UserEvent $userEvent)
    {
        $session = new Session();
        $session->set('username', $userEvent->getUser()->getUsername());
    }
}

Put this into your services.yml of your Bundle

services:
    <YOUR_PROJECT>_user.profile_edit:
        class: <YOUR_PROJECT>\<YOUR_BUNDLE>Bundle\EventListener\ProfileEditListener
        arguments: [@router]
        tags:
            - { name: kernel.event_subscriber }
    <YOUR_PROJECT>_user.login_manager:
        class: <YOUR_PROJECT>\<YOUR_BUNDLE>Bundle\EventListener\LoginListener
        arguments: [@router]
        tags:
            - { name: kernel.event_subscriber }

And this into templates:

{{ app.session.get('username') }}

I think it is only a work around, i had the same question today.

An other idea is, that you remove the session-attribute in "onProfileCompleted". Then you ask in twig template if session-attribute is set; if not you use the app.user.username, if true you print out app.session.get('....')... i think tomorrow i will change to this story; because it is more sf2 complicated (..app.user.username).

Hope it helps and will make you creative ;)

Regards

查看更多
我欲成王,谁敢阻挡
4楼-- · 2020-07-17 07:04

This works for me:

1) Add currentUsername property and lifecycle callbacks into User entity class

namespace AppBundle\Entity;

use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
* Class User
* @package AppBundle\Entity
*
* @ORM\Entity(repositoryClass="AppBundle\Repositories\User")
* @ORM\Table(name="site_user")
* @ORM\HasLifecycleCallbacks()
*/
class User extends BaseUser
{
    /**
     * @var int
     *
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\Id()
     * @ORM\GeneratedValue()
     */
    protected $id;

    /**
     * @var string
     */
    public $currentUsername;

    /**
     * @ORM\PostLoad()
     * @ORM\PostUpdate()
     * @ORM\PostPersist()
     */
    public function syncCurrentUsername()
    {
        $this->currentUsername = $this->username;
    }

    /**
     * @return string
     */
    public function getCurrentUsername()
    {
        return $this->currentUsername;
    }

}

2) And then in template use:

{{ app.user.getCurrentUsername() }}

3) In some cases maybe you should need to override serialize and deserialize methods and add new property (currentUsername) to it.

查看更多
登录 后发表回答