Implement change password in Symfony2 - forms

What is the best way to implement change password functionality in Symfony2?
Right now I'm using this:
$builder->add('password', 'repeated', array(
'first_name' => 'New password',
'second_name' => 'Confirm new password',
'type' => 'password'
));
It should also contain the current password check for security reasons.
Note: I'm not using FOSUserBundle.

Since Symfony 2.3 you can easily use UserPassword validation constraint.
Acme\UserBundle\Form\Model\ChangePassword.php
namespace Acme\UserBundle\Form\Model;
use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
use Symfony\Component\Validator\Constraints as Assert;
class ChangePassword
{
/**
* #SecurityAssert\UserPassword(
* message = "Wrong value for your current password"
* )
*/
protected $oldPassword;
/**
* #Assert\Length(
* min = 6,
* minMessage = "Password should be at least 6 chars long"
* )
*/
protected $newPassword;
}
Acme\UserBundle\Form\ChangePasswordType.php
namespace Acme\UserBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ChangePasswordType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('oldPassword', 'password');
$builder->add('newPassword', 'repeated', array(
'type' => 'password',
'invalid_message' => 'The password fields must match.',
'required' => true,
'first_options' => array('label' => 'Password'),
'second_options' => array('label' => 'Repeat Password'),
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Form\Model\ChangePassword',
));
}
public function getName()
{
return 'change_passwd';
}
}
Acme\UserBundle\Controller\DemoController.php
namespace Acme\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Acme\UserBundle\Form\ChangePasswordType;
use Acme\UserBundle\Form\Model\ChangePassword;
class DemoController extends Controller
{
public function changePasswdAction(Request $request)
{
$changePasswordModel = new ChangePassword();
$form = $this->createForm(new ChangePasswordType(), $changePasswordModel);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// perform some action,
// such as encoding with MessageDigestPasswordEncoder and persist
return $this->redirect($this->generateUrl('change_passwd_success'));
}
return $this->render('AcmeUserBundle:Demo:changePasswd.html.twig', array(
'form' => $form->createView(),
));
}
}

You have to either create another model with two fields:
one for the current password;
and the other for the new one.
Or add a non-persisted property to your user model like the FOSUserBundle does (see the plainPassword property).
So once you checked both current and new password are valid, you encode the new password and replace the old one with it.

Just add this to your form type:
$builder->add('oldPlainPassword', \Symfony\Component\Form\Extension\Core\Type\PasswordType::class, array(
'constraints' => array(
new \Symfony\Component\Security\Core\Validator\Constraints\UserPassword(),
),
'mapped' => false,
'required' => true,
'label' => 'Current Password',
));

I use a action from my controller:
public function changepasswordAction(Request $request) {
$session = $request->getSession();
if($request->getMethod() == 'POST') {
$old_pwd = $request->get('old_password');
$new_pwd = $request->get('new_password');
$user = $this->getUser();
$encoder = $this->container->get('security.encoder_factory')->getEncoder($user);
$old_pwd_encoded = $encoder->encodePassword($old_pwd, $user->getSalt());
if($user->getPassword() != $old_pwd_encoded) {
$session->getFlashBag()->set('error_msg', "Wrong old password!");
} else {
$new_pwd_encoded = $encoder->encodePassword($new_pwd, $user->getSalt());
$user->setPassword($new_pwd_encoded);
$manager = $this->getDoctrine()->getManager();
$manager->persist($user);
$manager->flush();
$session->getFlashBag()->set('success_msg', "Password change successfully!");
}
return $this->render('#adminlte/profile/change_password.html.twig');
}
return $this->render('#adminlte/profile/change_password.html.twig', array(
));
}

Can't You get old password from User before binding form?
// in action:
$oldpassword = $user->getPassword();
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
if ($form->isValid())
{
// check password here (by hashing new one)

Related

Symfony2 UserPassword constraint gets a NULL password

I want just to allow users to change their password in my application. I have built the a form to edit the password but the validation never pass because the password of the current user in the UserPassword constraint is always NULL:
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien#symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Core\Validator\Constraints;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class UserPasswordValidator extends ConstraintValidator
{
private $securityContext;
private $encoderFactory;
public function __construct(SecurityContextInterface $securityContext, EncoderFactoryInterface $encoderFactory)
{
$this->securityContext = $securityContext;
$this->encoderFactory = $encoderFactory;
}
/**
* {#inheritdoc}
*/
public function validate($password, Constraint $constraint)
{
if (!$constraint instanceof UserPassword) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\UserPassword');
}
$user = $this->securityContext->getToken()->getUser();
if (!$user instanceof UserInterface) {
throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.');
}
$encoder = $this->encoderFactory->getEncoder($user);
//I tried to print $user->getPassword from here and it is always NULL
if (!$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
$this->context->addViolation($constraint->message);
}
}
}
This is fhe form I'm using to change the password:
class UserPasswordEditType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('oldPassword', 'password', array(
'constraints' => array(
new UserPassword(array(
'message' => 'password_current.invalid',
'groups' => 'user-password-edit'
)
),
new NotBlank(array(
'message' => 'not_blank',
'groups' => 'user-password-edit'
))
),
'mapped' => false,
'required' => true,
))
->add('password', 'repeated', array(
'type' => 'password',
'invalid_message' => 'password_repeat.invalid',
'required' => true,
'first_options' => array('label' => 'password.label'),
'second_options' => array('label' => 'password_repeat.label'),
))
->add('save', 'submit', array(
'label' => 'save.label'
));
}
public function getName()
{
return 'user_edit_password';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => array('user-password-edit'),
));
}
}
This is a slice of the security.yml
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
XXX\PrivateApplication\Bundle\UserBundle\Entity\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
chain_provider:
chain:
providers: [in_memory, user_db]
in_memory:
memory:
users:
API_DOC: { password: #aaa, roles: [ 'ROLE_API_DOC' ] }
user_db:
entity: { class: XXX\PrivateApplication\Bundle\UserBundle\Entity\User, property: username }
Why the password of the logged user is always NULL from the constraint? If I print it from the controller it works... I don't use the FOSUserBundle.
Thank you
PS:
I have found a similar question Using Symfony2 UserPassword validator in form type but without replies...
Why the password of the logged user is always NULL from the constraint? If I print it from the controller it works...
The thing is, that in $password is your plain password from the form, not in getPassword()! You retrieve the 'signed in' user from the token of the security context and the encoded password is NULL. That means, that probably also getUsername() is NULL and getRoles() is just anonymous or guest (don't know it right now).
Then the login at all doesn't work and the token is only anonymous.

Symfony2 entity type check box set preferred choices

I have the following form filed in the edit form.
->add('district', 'entity', array(
'class' => 'AdminBundle:Districts',
'query_builder' => function(EntityRepository $repository) {
return $repository->createQueryBuilder('c')
->where('c.status =:status')
->setparameter('status','1');
},
'property' => 'districtName',
'preferred_choices' => array($details->getDistrict()),
'multiple' => TRUE,
'expanded' => TRUE,
'required' => true,
)
)
Output of this is checkboxes. I can check more districts here.
In the edit mode how to set the preferred choices?
OK, you need to use an EventListener against the form. See documentation for more information.
This will allow you to pre set form data
Example ()
/* Form */
namespace Company\YourBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Doctrine\ORM\EntityRepository;
use Company\YourBundle\Form\EventListener\YourEventListener;
class FormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventSubscriber(new YourEventListener($builder->getFormFactory()));
}
public function getName() {
return 'company_formtype';
}
}
/* Event Listener (You may require to pass more data to this class from your form as I have little information to help you with)*/
namespace Company\YourBundle\Form\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class ActivityListener implements EventSubscriberInterface {
private $form;
public function __construct($form) {
$this->form = $form;
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'onPreSetData',
);
}
public function onPreSetData(FormEvent $e) {
$data = $e->getData();
$form = $e->getForm();
if ($form->has('district')) {
$form->remove('district');
}
$form->add($this->form->createNamed('district', 'entity', null, array(
'class' => 'AdminBundle:Districts',
'query_builder' => function(EntityRepository $repository) {
return $repository->createQueryBuilder('c')
->where('c.status =:status')
->setparameter('status','1')},
'property' => 'districtName',
'preferred_choices' => $data['id'] ? /** in edit mode set the preferred **/ ? null,
'multiple' => TRUE,
'expanded' => TRUE,
'required' => true,
));
}
}

Symfony2 - Adding Swiftmailer as a service

I'd like to move my email code from my controller into a service.
I've done the following thus far:
created the entry in services.yml
created a EmailManager.php file inside acme/demobundle/services/EmailManager.php
Could use some help on what needs to go into the EmailManager.php and how to call it in the controller?
services.yml
services:
email_manager:
class: Acme\DemoBundle\Services\EmailManager
arguments: [#request_stack, #mailer]
scope: request
EmailManager.php
<?php
// src/Acme/DemoBundle/Services/EmailManager.php
namespace Acme\DemoBundle\Services;
class EmailManager
{
private $mailer;
private $request;
public function __construct(RequestStack $requestStack, $mailer)
{
$this->request = $requestStack->getCurrentRequest();
$this->mailer = $mailer;
}
What needs to go here? Do I just copy/paste the code from the contactAction below into here?
}
Controller code with contactAction that I would like to move out of the controller into EmailManager service:
/**
* #Route("/", name="contact")
* #Template("AcmeDemoBundle:Default:index.html.twig")
*/
public function contactAction(Request $request)
{
$form = $this->createForm(new ContactType());
if ($request->isMethod('POST')) {
$form->submit($request);
if ($form->isValid()) {
$message = \Swift_Message::newInstance()
->setSubject($form->get('subject')->getData())
->setFrom($form->get('email')->getData())
->setTo('example#gmail.com')
->setBody(
$this->renderView(
'AcmeDemoBundle:Default:index.html.twig',
array(
'ip' => $request->getClientIp(),
'name' => $form->get('name')->getData(),
'message' => $form->get('message')->getData()
)
)
);
$this->get('mailer')->send($message);
$request->getSession()->getFlashBag()->add('success', 'Your email has been sent! Thanks!');
return $this->redirect($this->generateUrl('contact'));
}
}
return array(
'form' => $form->createView()
);
}
ContactType Form
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text', array(
'attr' => array(
'placeholder' => 'What\'s your name?',
'pattern' => '.{2,}' //minlength
)
))
->add('email', 'email', array(
'attr' => array(
'placeholder' => 'So I can get back to you.'
)
))
->add('subject', 'text', array(
'attr' => array(
'placeholder' => 'The subject of your message.',
'pattern' => '.{3,}' //minlength
)
))
->add('message', 'textarea', array(
'attr' => array(
'cols' => 90,
'rows' => 10,
'placeholder' => 'And your message to me...'
)
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$collectionConstraint = new Collection(array(
'name' => array(
new NotBlank(array('message' => 'Name should not be blank.')),
new Length(array('min' => 2))
),
'email' => array(
new NotBlank(array('message' => 'Email should not be blank.')),
new Email(array('message' => 'Invalid email address.'))
),
'subject' => array(
new NotBlank(array('message' => 'Subject should not be blank.')),
new Length(array('min' => 3))
),
'message' => array(
new NotBlank(array('message' => 'Message should not be blank.')),
new Length(array('min' => 5))
)
));
$resolver->setDefaults(array(
'constraints' => $collectionConstraint
));
}
public function getName()
{
return 'contact';
}
}
You can customize this as you see fit, but that's a general idea and a very quick draft to guide you:
public function send($subject, $recipientName, $recipientEmail, $bodyHtml, $bodyText)
{
/* #var $mailer \Swift_Mailer */
if(!$this->mailer->getTransport()->isStarted()){
$this->mailer->getTransport()->start();
}
/* #var $message \Swift_Message */
$message = $this->mailer->createMessage();
$message->setSubject($subject);
$message->setBody($bodyHtml, 'text/html');
$message->addPart($bodyText, 'text/plain', 'UTF8');
$message->addTo($recipientEmail, $recipientName);
$message->setFrom( array('example#gmail.com' => 'Chance') );
$this->mailer->send($message);
$this->mailer->getTransport()->stop();
}
Room for Improvement
You could have:
An email data model that would contain the fields necessary for an email (like $subject, $recipientEmail, ...)
A composer that would compose your email from your request
A sender that would send your email
EMAIL MODEL would look something like this:
/**
* Email Data Model
*/
class Email implements EmailInterface
{
/**
* The text part of the message.
*
* #var string
*/
protected $bodyText;
// etc...etc..
}
You'd have an EmailInterface too:
/**
* Email interface
*/
interface EmailInterface
{
/**
* #return string
*/
public function getBodyText();
// etc...etc..
}
THE SENDER would look like this (if kept inside EmailManager):
public function send(EmailInterface $email)
{
//...
}
THE COMPOSER would look like this (if kept inside EmailManager):
public function composeEmail(Request $request)
{
//...
return $email;
}
Note: Composer and Sender could also be a separate service for better reuse, that's up to you I guess. Here is what they would look like if there were just functions in your EmailManager

Symfony2 - validate a group of fields (and change values) only if one of them is filled

I'm working on profile edit form type. I have problem with how to validate fields oldPassword and password only if one of them are filled. Firstly I set required => false, but I cannot find how turn off validation and turn on only if field oldPassword is filled, or repeated field password. Also, if validation wasn't done, values of these fields will not be processed into DB.
In form type class I have this:
->add('oldPassword', 'password', array('label' => 'Old password', 'required' => false, 'mapped' => false, 'error_bubbling' => true))
->add('password', 'repeated', array('type' => 'password', 'required' => false, 'invalid_message' => 'New passwords must equal', 'first_options' => array('label' => 'New password'), 'second_options' => array('label' => 'Repeat new password'), 'error_bubbling' => true,))
In my entity:
/**
* #ORM\Column(type="string", length=255)
* #Assert\Length(
* min = "4",
* minMessage = "Password must have at least 4 characters"
* )
*/
private $password;
/**
* #SecurityAssert\UserPassword(
* message = "Old password must equal with current"
* )
*/
protected $oldPassword;
And in controller nothing special:
public function profileAction(Request $request)
{
if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
$em = $this->getDoctrine()->getManager();
$user = $this->getUser();
if(!$user)
{
$this->get('session')->getFlashBag()->add(
'error',
'User with ID ' . $user->getId() . ' does not exist'
);
return $this->redirect($this->generateUrl('admin_dashboard'));
}
$form = $this->createForm('user', $user);
$form->handleRequest($request);
if($form->isValid())
{
$em->persist($user);
try {
$em->flush();
} catch (\PDOException $e) {
// sth
}
$this->get('session')->getFlashBag()->add(
'success',
'Profile was successfuly edited'
);
$em->refresh($user);
}
return $this->render('AcmeUserBundle:Admin:profile.html.twig', array('form' => $form->createView()));
}
And now I need that these fields (oldPassword, password) will not be validated if there are not filled and also will not change current values, because there are more fields such as username, email, name, etc.
Is this work for FormType or Controller or both?

Symfony 2 - How to validate Entity Field Type when it is populated using Ajax?

I am confronted to a problem that is driving me crazy for more than 3 days and I do not find any solutions. Nevertheless I found a post on stackoverflow that is EXCACTLY the problem I am facing. Unfortunately the person did manage to find a solution on his down but he or she did not shared it fully. As he explained it perfectly let just copy paste it here below:
By the way it seems that person who created that post only created his account for this problem and never came back since for other things. That it is why I allow myself to ask here again...
I have 2 entities (A and B) with a Many to One relationship between
them.
I create my form with the A entity and i use an entity field (dropdown
list) to display the rows in the B entity. I use a query builder to
filter them. If don't change the values in the list (ie. with ajax),
everything is working fine.
But if I change dynamicly the values in the dropdown, when I submit
the form I have this error "This value is invalid"
It's because the submitted value isn't included in the "array"
returned by the query builder.
It seems that this validation is automatic in symfony for entity field
(I don't use any asserts on this field). I'd like to get rid of this.
But how ?
It seems that I need to implement Form Events. Unfortunatally I do not get it. I read the documentation which is very poor on that subject, read a lot of posts, searched on the Internet but did not found anything.
Here below my personal form type. What I do is the following. I create the first entity field type with the mapped property set to false and filter the entity just to get the departements. Then I create another entity type called localisation. By default I filter the entity to get nothing (''). What I do then to populate it is to use Jquery. But unfortunatelly I am confro,ted to the same problem as the other buddy (see above).
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Doctrine\ORM\EntityRepository;
use Auth\GeoBundle\Form\LocalisationType;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('departement', 'entity', array(
'mapped' => false,
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'departement',
'query_builder' => function ($repository) {
return $repository
->createQueryBuilder('e')
->add('groupBy', 'e.departement')
;
},
));
$builder->add('localisation', 'entity', array(
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'formLabel',
'query_builder' => function ($repository) use ($dpt) {
return $repository
->createQueryBuilder('e')
->where('e.departement = :dpt')
->setParameter('dpt', '')
->add('orderBy', 'e.ville ASC')
;
},
));
//some other fields here...
}
public function getName()
{
return 'auth_user_registration';
}
}
I finally manage to find a solution using the form events. I played with the "tutorial" at http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html and got it working :) Here below the code I used in case somebody interested.
My formType:
<?php
//src/Auth/UserBundle/Form/Type/RegistrationFormType.php
namespace Auth\UserBundle\Form\Type;
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Doctrine\ORM\EntityRepository;
use Auth\GeoBundle\Form\LocalisationType;
use Auth\UserBundle\Form\EventListener\IsAdminFieldSubscriber;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
//NE PAS EFFACER -> exempled e comment ajouter un champ qui n'a rien à voir avec nos entitys
//$builder->add("firstName", "text", array("mapped" => false));
$builder->add('departement', 'genemu_jqueryselect2_entity', array(
'mapped' => false,
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'departement',
'query_builder' => function ($repository) {
return $repository
->createQueryBuilder('e')
->add('groupBy', 'e.departement')
;
},
));
$dpt = "";
$builder->add('localisation', 'genemu_jqueryselect2_entity', array(
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'formLabel',
'query_builder' => function ($repository) use ($dpt) {
return $repository
->createQueryBuilder('e')
->where('e.departement = :dpt')
->setParameter('dpt', $dpt)
->add('orderBy', 'e.ville ASC')
;
},
));
$builder->add('sexe', 'genemu_jqueryselect2_choice', array(
'empty_value' => '',
'choices' => array(
'homme' => 'Homme',
'femme' => 'Femme',
),
'configs' => array(
'minimumResultsForSearch' => 5,
)
));
$builder->add('date_naissance', 'date', array(
'empty_value' => '',
'widget' => 'choice',
'attr' => array('class' => 'input-small'),
'years' => range(1900,2100),
'months' => range(1,12),
'days' => range(1,31),
));
$builder->add('petit_mot');
$subscriber = new IsAdminFieldSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
}
public function getName()
{
return 'auth_user_registration';
}
}
my EventListener:
<?php
//src/Auth/UserBundle/Form/EventListener/isAdminFieldSubscriber.php
namespace Auth\UserBundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
class IsAdminFieldSubscriber implements EventSubscriberInterface
{
/**
* #var FormFactoryInterface
*/
private $factory;
/**
* #param FormFactoryInterface $factory
*/
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_BIND => 'preBind',
);
}
/**
* Called before form data is set
*
* #param DataEvent $event
*/
public function preBind(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$dpt = $data['localisation'];
$form->add($this->factory->createNamed('localisation', 'entity', null, array(
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'formLabel',
'query_builder' => function ($repository) use ($dpt) {
return $repository
->createQueryBuilder('e')
->where('e.id = :dpt_id')
->setParameter('dpt_id', $dpt)
->add('orderBy', 'e.ville ASC')
;
},
)));
}
}
You explain the problem yourself:
"It's because the submitted value isn't included in the "array" returned by the query builder."
You can use data transformers to solve this issue.