In my Symfony2 application I created a custom form to edit objects of a User class. Users have a password property which contains a hash of the user's password. For obvious reasons, I do not want to echo this property's value into a field. However, I want a password field on my form so that when editing a user it is possible to change the user's password.
This password field should behave as follows:
If the user has a password set, then the field should contain ********.
If the user has no password set, then the field should be empty.
(It turns out the previous two points are impossible to achieve with my current architecture, so instead I am going for:) When the page is loaded, the field should be empty, regardless of whether the user has a password set.
If the field is posted with content, then the user's password should be set to the hashed value of the field.
If the field is posted empty, the user's password should not be changed and, more importantly, not cleared.
I thought of implementing this with a custom data transformer. However, the data transformer does not provide me with a way to skip updating the user's password property when the password field is posted empty.
Where do I need to extend the framework to add custom logic deciding which fields should be updated?
UPDATE
This is the legacy code I am trying to replace:
/* SomeController.php */
$pass = $request->get('password');
if (strlen($pass) >= 5 && strlen($pass) <= 16) {
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($pass, $user->getSalt());
$user->setPassword($password);
}
I can live with removing the string length checks. All I want to check is whether something has been entered.
As you can see, I can not simply move this code to a data transformer as it needs to access the $user which is the user we are currently editing. I don't think it is a good idea to create a service providing this value.
Just insert a control directly into your entity method and use data transformer (as you have insight)
So your entity will be
class User
{
//some properties and methods here
public function setPassword($pwd = null) {
if (null !== $pwd) {
$this->password = //do actions here like hash or whatever
}
//don't update the password
}
}
If you want to take advantage only of DataTransformers, you could still do what you need that way
use Symfony\Component\DependencyInjection\ContainerInterface;
class PasswordTransformer implements DataTransformerInterface
{
private $ci;
public function __construct(ContainerInterface $ci) {
$this->ci = $ci;
}
//...
public function reverseTransform($form_password) {
if (!$form_password) {
//return password already stored
return $this->ci->get('security.context')
->getToken()
->getUser()
->getPassword();
}
}
}
Of course you need to inject service_container service into you data transformer (or better, you should inject it into your form type's selector and pass to DataTransformer constructor as follows:
services:
your.bundle.password_selector_type:
class: Your\Bundle\Form\Type\PasswordSelectorType
arguments: ["#security.context"]
tags:
- { name: form.type, alias: password_selector_type }
For the form part, you should take a look a this widget.
http://symfony.com/doc/current/reference/forms/types/repeated.html
It provides an easy way to ask and treat confirmation on a field (it also hide values with stars when you set type to password).
$builder->add('password', 'repeated', array(
'type' => 'password',
'invalid_message' => 'The password fields must match.',
'options' => array('attr' => array('class' => 'password-field')),
'required' => true,
'first_options' => array('label' => 'Password'),
'second_options' => array('label' => 'Repeat Password')));
It will check first and second options to be equal. If that's the case then your form will be considered as valid. Otherwise, invalid_message will be displayed and first field will set to the content typed by user while confirmation field (second option) will be emptied.
You can add some logic afterwards like hashing the password to finally persist your entity. (Extracting it in a form handler would be a good practice).
Here is what I came up with for now but I am not happy with the solution as it involves custom form processing in the controller. However, so far this is the only way I have found to make it work.
My form class adds an unmapped field for the user's password:
class UserType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('username')
->add('displayName')
->add('password', 'password', ['mapped' => false]);
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'MyCompany\MyBundle\Entity\User'));
}
public function getName() {
return 'user';
}
}
This field is then processed manually in my controller class:
class UserAdminController extends Controller {
public function editUserAction($userId, Request $request) {
$user = $this->getDoctrine()->getRepository('MyCompanyMyBundle:User')->findOneById($userId);
$form = $this->createForm('user', $user);
$form->handleRequest($request);
if ($form->isValid()) {
$newPassword = $form['password']->getData();
if ($newPassword !== "") {
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($newPassword, $user->getSalt());
$user->setPassword($password);
}
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
}
return $this->render(
"MyCompanyMyBundle:Admin/Management/User:Edit.html.php",
[
"form" => $form->createView()
]
);
}
}
Related
I am working on a simple reset password system with Symfony4.
I don't know why isPasswordValid() always return false?
I am using Bcrypt to Hash the password
Here is some code of Security.yaml :
security:
encoders:
App\Entity\User:
algorithm: bcrypt
I don't know why isPasswordValid() Always returns false.
I manually tried this:
$pass="000000000";
dump($encoder->isPasswordValid($user, $pass));
die();
and it dump false..
Here is the function I wrote on the controller:
/**
* #Route("/password", name="change_pass", methods={"GET","POST"})
* #IsGranted("ROLE_USER")
*/
public function edit(Request $request,UserPasswordEncoderInterface $encoder): Response
{
$user = $this->getUser();
$form = $this->createForm(ResetPassType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$oldPassword = $request->request->get('reset_pass')['oldPassword'];
$newPassword = $user->getPassword();
if ($encoder->isPasswordValid($user, $oldPassword)) {
$hash = $encoder->encodePassword($user,$newPassword);
$user->setPassword($hash);
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', 'Your password is succesfully changed');
}else {
$this->addFlash('fail', 'old password is wrong');
}
}
$this->getDoctrine()->getManager()->refresh($user);
return $this->render('consultant/changepass.html.twig', [
'form' => $form->createView(),
]);
}
Here is the form ResetPassType :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('oldPassword', PasswordType::class, [
'mapped' => false,])
->add('password',PasswordType::class)
;
}
Short answer: you are using the new password as the old password's hash.
Explanation:
I guess you bound your form to your user class.
The password field in your form is mapped, meaning it updates your user's password property.
Therefore, as the property should contain the hash of the "old password" but contains your new password, it can't validate.
Solution and improvements:
The quickest solution would be to "unmap" the password field or to remove the data_class form option, and to replace a line in your controller:
// before: $newPassword = $user->getPassword();
$newPassword = $form->get('password')->getData();
I'd suggest to:
unbind the form to your user class
use the UserPassword validation constraint for your oldPassword field
fetch the form data using $form->getData() instead of accessing the $request manually
You will change your form because if you keep the field password, the password of the user will be changed by form too, so the right way is :
-in your Form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('oldPassword', PasswordType::class, [
'mapped' => false,])
->add('newPassword',PasswordType::class, [
'mapped' => false,])
;
}
and then in your controller change the way you get the new password
$oldPassword = $request->request->get('reset_pass')['oldPassword'];
$newPassword = $request->request->get('reset_pass')['newPassword'];
Have fun :)
I executed the make:crud command for generate some files for the entity user.
All works like a charm, but I have one problem when I edit a user.
When I edit a user, I can :
change the password and save
or
don't modify the password and save
From the generated user 'edit' controller:
/**
* #Route("/{id}/edit", name="user_edit", methods={"GET","POST"})
*/
public function edit(Request $request, User $user): Response
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// HERE : How can I check if the password was changed ?
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('user_index', [
'id' => $user->getId(),
]);
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
'title' => 'edit userr'
]);
}
How can I check if the password was changed ? The user variable contains the new password value...
If the password is new, I must use the encoder service. If not, I just must update the user with new data
For this it helps to have a class variable in your User that is not written to the database. In your form you can then use this variable to temporarily store the updated password in, that you then encode and remove. This is what the eraseCredentials()-method in the UserInterface is for.
For example in your User you could have
class User implements UserInterface
{
private $plainPassword;
// ...
public function setPlainPassword(string $plainPassword)
{
$this->plainPassword = $plainPassword;
}
public function getPlainPassword()
{
return $this->plainPassword;
}
public function eraseCredentials()
{
$this->plainPassword = null;
}
}
Notice how private $plainPassword does not have any ORM-annotations, that means it will not be stored in the database. You can however use validation constraints, e.g. if you want to make sure that passwords have a minimum length or a certain complexity. You will still require the original password field that stores the encrypted password.
You then add this field to your user update-form instead of the actual password field. In your controller you can then only check if the new plainPassword-field was filled out and then read the value, encode it and replace the actual password field.
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
if ($user->getPlainPassword() !== null) {
$user->setPassword($this->userPasswordEncoder->encode(
$user->getPlainPassword(),
$user
);
}
// ...
Another way of doing this, without adding this "helper"-property to the user is using an unmapped form field:
# UserForm
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('plainPassword', PasswordType::class, ['mapped' => false])
;
}
The controller will look similar, only you fetch the data from the unmapped field instead of from the user:
$form->get('plainPassword')->getData();
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 test#gmail.com1 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.
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.
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.
I have a Users system which allows account creation, login and logout procedures.
I'm currently working on an Edit Profile, which allows editing of the full name, username etc.
For this procedure, I would like the password to be not required, however, it needs to be required for the create and login procedures.
Is there a way to support this in Laravel?
I thought about detecting the presence of an Input::get('password') and if not, passing through the original password, however, this would re-hash
So, to summarise, I would like the password field to be required on create, but not on update procedures. Alternatively, if there's another way to achieve the same end, then I'm open to it.
For reference, I'm using JeffreyWay/Laravel-Model-Validators which handles the validation within the save procedure.
Probably you cannot do this using this package. However you can create such functionality manually, for example creating 2 methods that chooses required fields for validation, a piece of User class:
protected static $rules
= [
'name' => 'required|min:3|max:60',
'password' => [
'required',
'min:10'
],
];
public static function validateRegistration($data, $translation)
{
return self::validate($data, $translation);
}
public static function validateUpdate($data, $translation)
{
$rules = self::$rules;
if (trim($data['password']) == '') {
unset($rules['password']); // unsetting unwanted password rule
}
return self::validate($data, $translation, $rules);
}
public static function validate($data, $translation, $rules = null)
{
if (is_null($rules)) {
$rules = self::$rules;
}
$v = Validator::make($data, $rules, $translation);
if ($v->fails()) {
self::$errors = $v->messages()->toArray();
return false;
}
return true;
}
I got this code:
public function actionJoin() {
$user = new RUser;
if (isset($_POST['RUser']))
$user->attributes = $_POST['RUser'];
$this->render('join',
array(
'user' => $user
)
);
}
Which will not yet allow user to register. What I want to know is how to send data back to user. I mean, if user form haven't passed verification I have to send some data back, so there is no need for user to re-enter it again.
I can do so with this:
$user->mail = $_POST['RUser']['mail'];
But it's looks like dropping back to plain PHP and not using powers of the framework here.
Addition. Publishing RUser class, if needed:
class RUser extends CFormModel
{
public $mail;
public $alias;
public function safeAttributes()
{
return array(
'mail', 'alias'
);
}
}
Which version of Yii you use.
In Yii 1.1, there are no safeAttributes. You use the followings,
public function rules()
{
return array(
array('mail, alias', 'safe'),
);
}