Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context problem with Identical validator and UniqueObject validator #196

Open
Orkin opened this issue Mar 26, 2015 · 3 comments
Open

Context problem with Identical validator and UniqueObject validator #196

Orkin opened this issue Mar 26, 2015 · 3 comments

Comments

@Orkin
Copy link
Member

Orkin commented Mar 26, 2015

Hello, I have one problem when I use Identical validator and UniqueObject validator. In fact for UniqueObject we have to use a context with the id of the object but there is an other context for identical validator (by default data of the InputFilter)

class UserInputFilter extends InputFilter
{

    /**
     * @var EntityManagerInterface
     */
    private $objectManager;

    public function __construct(EntityManagerInterface $objectManager)
    {
        $this->objectManager = $objectManager;

        $this->add(
            [
                'name'       => 'email',
                'required'   => true,
                'filters'    => [
                    [
                        'name' => 'StringTrim',
                    ]
                ],
                'validators' => [
                    [
                        'name' => 'EmailAddress',
                    ],
                ],
            ]
        );

        $this->add(
            [
                'name'       => 'confirm_email',
                'required'   => true,
                'filters'    => [
                    [
                        'name' => 'StringTrim',
                    ]
                ],
                'validators' => [
                    [
                        'name' => 'EmailAddress',
                    ],
                ],
            ]
        );

        // Add the unique validator so that emails are unique
        $inputConfirmEmail = $this->get('confirm_email');
        /** @var ValidatorChain $validators */
        $validators = $inputConfirmEmail->getValidatorChain();
        $validators->attach(new Identical('email'));

        $inputEmail = $this->get('email');
        /** @var ValidatorChain $validators */
        $validators = $inputEmail->getValidatorChain();
        $validators->attach(
            new UniqueObject(
               [
                    'use_context'       => true,
                    'object_repository' => $this->objectManager->getRepository('User\Entity\User'),
                    'object_manager'    => $this->objectManager,
                    'fields'            => 'email',
                ]
            )
        );
    }
}

In controller :

// we assume that $user the current user authenticated represented by a doctrine entity
$data = $this->validateIncomingData(UserInputFilter::class, [
                'email',
                'confirm_email',
            ], ['id' => $user->getId()]
        );

With this (we assume that email and confirm_email are equals) Identical validator is not valid because I force the context with user id and when I don't use the context Identical validator is valid because the default context is passed but UniqueObject throw exception because the context id is missing.

I think it's really really bad to have id in request content for security and in my case it will retrieve with the access token.
ValidateIncomingData retrieve by itself the request content we can't merge id context with request content.
Maybe it will be great to add an other parameter to the plugin ValidateIncomingData like "merge_with_default_context" by default to false and when we have this both validator use like this :

$data = $this->validateIncomingData(UserInputFilter::class . '_update', [
                'email',
                'confirm_email',
            ], ['id' => $user->getId()], true
        );

And ValidateIncomingData merge request content with ['id' => $user->getId()] but maybe we have an other problem in validation where data are filtered in the inputFilter ...

@bakura10 @Ocramius what do you think ? Maybe an other idea ?

@bakura10
Copy link
Member

Hi,

For the context, here is what I'm doing in my controller:

public function put(array $params)
    {
        if (!($user = $this->userService->getById($params['user_id']))) {
            throw new NotFoundException();
        }

        $data = $this->validateIncomingData(UserInputFilter::class, [], $user);
        $this->hydrateObject(UserHydrator::class, $user, $data);
        $this->userService->update($user);

        // ...
    }

As you can see I give the full user object as a context.

Then my input filter overrides the "isValid" method:

namespace Account\InputFilter;
use Account\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use DoctrineModule\Validator\NoObjectExists;
use DoctrineModule\Validator\UniqueObject;
use Zend\Filter\FilterPluginManager;
use Zend\Filter\StringToLower;
use Zend\Filter\StringTrim;
use Zend\Filter\StripTags;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\InputFilterInterface;
use Zend\Validator\EmailAddress;
use Zend\Validator\StringLength;
use Zend\Validator\ValidatorChain;
/**
 * Validate and filter the data of a user
 *
 * @author  Michaël Gallego <mic.gallego@gmail.com>
 * @licence MIT
 */
class UserInputFilter extends InputFilter
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;
    /**
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->add([
            'name'       => 'email',
            'required'   => true,
            'validators' => [
                [
                    'name'    => EmailAddress::class,
                    'options' => [
                        'messages' => [
                            EmailAddress::INVALID_FORMAT => 'Email address is invalid'
                        ]
                    ]
                ],
                [
                    'name'    => StringLength::class,
                    'options' => ['max' => 255]
                ],
                [
                    'name'    => NoObjectExists::class,
                    'options' => [
                        'object_repository' => $entityManager->getRepository(User::class),
                        'fields'            => ['email'],
                        'messages'          => [
                            NoObjectExists::ERROR_OBJECT_FOUND => 'User with this email address already exists'
                        ]
                    ]
                ]
            ],
            'filters' => [
                ['name' => StringTrim::class],
                ['name' => StringToLower::class]
            ]
        ]);
    }
    /**
     * @param  mixed $context
     * @return bool
     */
    public function isValid($context = null)
    {
        // If the context is an object, we do additional things as this is an update
        if ($context instanceof User) {
            $this->data['id'] = $context->getId();
            $this->prepareInputFilterForUpdate($context);
        }
        return parent::isValid($context);
    }
    /**
     * @param User $user
     */
    private function prepareInputFilterForUpdate(User $user)
    {
        /** @var \Zend\InputFilter\InputInterface $passwordInput */
        $passwordInput = $this->inputs['password'];
        $passwordInput->setRequired(false);
        // For the email, we need to copy all the existing validators, but remove the "NoObjectExists" to replace
        // it by a "UniqueObject" filter
        /** @var \Zend\InputFilter\InputInterface $emailInput */
        $emailInput     = $this->inputs['email'];
        $validatorChain = $emailInput->getValidatorChain();
        $newValidatorChain = new ValidatorChain();
        foreach ($validatorChain->getValidators() as $validator) {
            $instance = $validator['instance'];
            if ($instance instanceof NoObjectExists) {
                continue;
            }
            $newValidatorChain->attach($instance);
        }
        // Finally, attach the new one
        $newValidatorChain->attachByName(UniqueObject::class, [
            'object_repository' => $this->entityManager->getRepository(User::class),
            'object_manager'    => $this->entityManager,
            'use_context'       => true,
            'fields'            => ['email'],
            'messages'          => [
                UniqueObject::ERROR_OBJECT_NOT_UNIQUE => 'User with this email address already exists'
            ]
        ]);
        $emailInput->setValidatorChain($newValidatorChain);
    }
}

I admit this is a bit hacky, but until input filter supports a better way to context, this is the best one.

@Orkin
Copy link
Member Author

Orkin commented Mar 30, 2015

Ok it's good for some king of context but how doing this with Identical validator. Both email and confirm_email need to check data from the request and not for the entity. So my problem is to merge context "user id" with default context data retrieve from the request :/.

@bartek75
Copy link

Hi, how do you get entity manager injected into you filter class?
When i call validateIncomingData i got exception "Argument 1 passed to UserInputFilter::__construct() must implement interface Doctrine\ORM\EntityManagerInterface, none given"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants