From fcdaee569bd27dcc7170cc332b0a1986a81834bb Mon Sep 17 00:00:00 2001 From: Stefan Hagspiel Date: Tue, 18 Feb 2020 12:02:16 +0100 Subject: [PATCH 1/8] implement username/email login identifier --- CHANGELOG-2.2.x.md | 3 + .../12_Customers/03_Registration_Service.md | 8 +- .../12_Customers/04_Registration_Types.md | 34 ++++++++ .../12_Customers/05_Company_Extension.md | 41 ++++++++++ docs/03_Development/12_Customers/README.md | 5 +- .../Context/Transform/CustomerContext.php | 13 ++++ .../Customer/RegistrationService.php | 13 +++- .../CustomerSecurityValidationListener.php | 77 +++++++++++++++++++ .../CoreBundle/Resources/config/services.yml | 1 + .../Resources/config/services/customer.yml | 1 + .../Resources/config/services/listeners.yml | 8 ++ .../Security/ObjectUserProvider.php | 20 ++++- .../DependencyInjection/Configuration.php | 1 + .../CoreShopCustomerExtension.php | 2 + .../Form/Type/CustomerLoginType.php | 19 ++++- .../Pimcore/Repository/CustomerRepository.php | 30 +++++++- .../Resources/config/services/form.yml | 6 ++ .../Resources/config/validation/Customer.yml | 10 +-- .../Resources/translations/validators.de.yml | 5 +- .../Resources/translations/validators.en.yml | 5 +- .../install/pimcore/translations.yml | 6 ++ .../Resources/config/services/validators.yml | 2 + .../Constraints/UniqueEntityValidator.php | 49 +++++++++++- .../Component/Customer/Model/Customer.php | 24 ++++-- .../CustomerRepositoryInterface.php | 32 +++++++- 25 files changed, 378 insertions(+), 37 deletions(-) create mode 100644 CHANGELOG-2.2.x.md create mode 100644 docs/03_Development/12_Customers/04_Registration_Types.md create mode 100644 docs/03_Development/12_Customers/05_Company_Extension.md create mode 100644 src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php diff --git a/CHANGELOG-2.2.x.md b/CHANGELOG-2.2.x.md new file mode 100644 index 0000000000..6c62aa9758 --- /dev/null +++ b/CHANGELOG-2.2.x.md @@ -0,0 +1,3 @@ +# Within 2.2 + +## 2.2.0 diff --git a/docs/03_Development/12_Customers/03_Registration_Service.md b/docs/03_Development/12_Customers/03_Registration_Service.md index 670ad2e1ad..b2c58d607b 100644 --- a/docs/03_Development/12_Customers/03_Registration_Service.md +++ b/docs/03_Development/12_Customers/03_Registration_Service.md @@ -1,12 +1,10 @@ # CoreShop Customer Registration Service - -CoreShop already implements a registration Service which handles creating a Customer with Addresses. The Registration Service implements the Interface ```CoreShop\Bundle\CoreBundle\Customer``` and CoreShop implements it using the service ```coreshop.customer.registration_service``` +CoreShop already implements a registration Service which handles creating a Customer with Addresses. +The Registration Service implements the Interface `CoreShop\Bundle\CoreBundle\Customer` and CoreShop implements it using the service `coreshop.customer.registration_service`. ## Usage - To use the Service, you need to pass a Customer, Address, additional Formdata and if Registration is Guest or Customer. - -In our example, we gonna do that from a Controller with a FormTypes +In our example, we gonna do that from a Controller with a FormType. ```php $customer = $this->getCustomer(); diff --git a/docs/03_Development/12_Customers/04_Registration_Types.md b/docs/03_Development/12_Customers/04_Registration_Types.md new file mode 100644 index 0000000000..f01dfc394e --- /dev/null +++ b/docs/03_Development/12_Customers/04_Registration_Types.md @@ -0,0 +1,34 @@ +# CoreShop Customer Registration Types +By default, a customer needs to provide a unique and valid email address to pass a registration. + +## Register By Email +> This is the default setting! + +To switch to registration by a unique and valid email address, you need set the identifier: + +```yaml +core_shop_customer: + login_identifier: 'email' +``` + +## Register By Username +First, you need to make sure your customer object provides a `username` field. +By default, coreshop **does not** install this field to prevent unnecessary confusion. +To implement the username field, just open your class editor and add a text field called `username` and you're good to go! + +To switch to registration by a unique username, you need change the identifier: + +```yaml +core_shop_customer: + login_identifier: 'username' +``` + +## Security + +### Form (Frontend) +CoreShop comes with a preinstalled constraints which will tell your customer, if an email address or username - depending on your settings - is valid or not. + +### Backend / API +Plus, if you're going to update a customer by API or Backend, coreshop also checks if you're customer entity has unique data. + +> Note: Both checks only applies to non-guests entities! \ No newline at end of file diff --git a/docs/03_Development/12_Customers/05_Company_Extension.md b/docs/03_Development/12_Customers/05_Company_Extension.md new file mode 100644 index 0000000000..a48c3d55fd --- /dev/null +++ b/docs/03_Development/12_Customers/05_Company_Extension.md @@ -0,0 +1,41 @@ +# CoreShop Customer Company Extension +The Company Entity allows you to append customers to a given company. +After a customer has been connected to a company by using the 1to1 relation `company`, it's possible to share addresses between company and the self-assigned addresses. + +## Access Types +> Note! This is only available if a customer already is connected to a valid company! + +### Own Only +If set, the customer can create, edit and delete own addresses and choose them in checkout as well. This is the default behaviour. + +### Company Only +If set, the customer can create, edit und delete company addresses and choose them in checkout as well. He's not able to add addresses to himself. + +### Own And Company +If set, the customer can create, edit and delete company and private addresses and choose them in checkout as well. + +Plus, the `own_and_company` mode allows the customer to define and modify the allocation of the address. +To do so, coreshop renders an additional choice type to the address creation/modification form. + +**Note**: If a customer switches the allocation after it has been created, the address also physically gets moved to its desired location. +In this example, the customer changes the allocation from `own` to `company`: + +Before: +```yaml +- company A + - addresses + - customer A + - addresses + - address A +``` + +After: +```yaml +- company A + - addresses + - address A + - customer A + - addresses +``` + +Read more about this feature [here](https://github.com/coreshop/CoreShop/issues/1266). \ No newline at end of file diff --git a/docs/03_Development/12_Customers/README.md b/docs/03_Development/12_Customers/README.md index 364f6f3014..2fd8f13bc7 100644 --- a/docs/03_Development/12_Customers/README.md +++ b/docs/03_Development/12_Customers/README.md @@ -1,7 +1,8 @@ # CoreShop Customer - This guide should lead you through how CoreShop handles Customer Information. 1. [Create, Read, Update, Delete](./01_CRUD.md) 2. [Customer Context](./02_Context.md) - 3. [Registration Service](./03_Registration_Service.md) \ No newline at end of file + 3. [Registration Service](./03_Registration_Service.md) + 3. [Registration Types](./04_Registration_Types.md) + 4. [Company Extension](./05_Company_Extension.md) \ No newline at end of file diff --git a/src/CoreShop/Behat/Context/Transform/CustomerContext.php b/src/CoreShop/Behat/Context/Transform/CustomerContext.php index 94ddfb73c0..9bb7cd0c1b 100644 --- a/src/CoreShop/Behat/Context/Transform/CustomerContext.php +++ b/src/CoreShop/Behat/Context/Transform/CustomerContext.php @@ -44,4 +44,17 @@ public function getCustomerByEmail($email) return $customer; } + + /** + * @Transform /^customer "([^"]+)"$/ + * @Transform /^username "([^"]+)"$/ + */ + public function getCustomerByUsername($username) + { + $customer = $this->customerRepository->findCustomerByUsername($username); + + Assert::isInstanceOf($customer, CustomerInterface::class); + + return $customer; + } } diff --git a/src/CoreShop/Bundle/CoreBundle/Customer/RegistrationService.php b/src/CoreShop/Bundle/CoreBundle/Customer/RegistrationService.php index 2ad8cfb04c..489d3bc718 100644 --- a/src/CoreShop/Bundle/CoreBundle/Customer/RegistrationService.php +++ b/src/CoreShop/Bundle/CoreBundle/Customer/RegistrationService.php @@ -60,6 +60,11 @@ final class RegistrationService implements RegistrationServiceInterface */ private $addressFolder; + /** + * @var string + */ + private $loginIdentifier; + /** * @param CustomerRepositoryInterface $customerRepository * @param ObjectServiceInterface $objectService @@ -68,6 +73,7 @@ final class RegistrationService implements RegistrationServiceInterface * @param string $customerFolder * @param string $guestFolder * @param string $addressFolder + * @param string $loginIdentifier */ public function __construct( CustomerRepositoryInterface $customerRepository, @@ -76,7 +82,8 @@ public function __construct( LocaleContextInterface $localeContext, $customerFolder, $guestFolder, - $addressFolder + $addressFolder, + $loginIdentifier ) { $this->customerRepository = $customerRepository; $this->objectService = $objectService; @@ -85,6 +92,7 @@ public function __construct( $this->customerFolder = $customerFolder; $this->guestFolder = $guestFolder; $this->addressFolder = $addressFolder; + $this->loginIdentifier = $loginIdentifier; } /** @@ -96,7 +104,8 @@ public function registerCustomer( $formData, $isGuest = false ) { - $existingCustomer = $this->customerRepository->findCustomerByEmail($customer->getEmail()); + $loginIdentifierValue = $this->loginIdentifier === 'email' ? $customer->getEmail() : $customer->getUsername(); + $existingCustomer = $this->customerRepository->findUniqueByLoginIdentifier($this->loginIdentifier, $loginIdentifierValue, false); if ($existingCustomer instanceof CustomerInterface && !$existingCustomer->getIsGuest()) { throw new CustomerAlreadyExistsException(); diff --git a/src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php b/src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php new file mode 100644 index 0000000000..71e4b6e33c --- /dev/null +++ b/src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php @@ -0,0 +1,77 @@ +customerRepository = $customerRepository; + $this->className = $className; + $this->loginIdentifier = $loginIdentifier; + } + + /** + * @param DataObjectEvent $event + * + * @throws ValidationException + */ + public function checkCustomerSecurityDataBeforeUpdate(DataObjectEvent $event) + { + $object = $event->getObject(); + + if (!$object instanceof CustomerInterface) { + return; + } + + if ($object->getIsGuest() === true) { + return; + } + + $identifierValue = $this->loginIdentifier === 'email' ? $object->getEmail() : $object->getUsername(); + $customer = $this->customerRepository->findUniqueByLoginIdentifier($this->loginIdentifier, $identifierValue, false); + + if ($customer instanceof CustomerInterface) { + throw new ValidationException(sprintf('%s "%s" is already used. Please use another one.', ucfirst($this->loginIdentifier), $identifierValue)); + } + } +} diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml index 633f9b1627..18d6f771c2 100755 --- a/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml +++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml @@ -57,6 +57,7 @@ services: arguments: - '@coreshop.repository.customer' - '%coreshop.model.customer.class%' + - '%coreshop.customer.security.login_identifier%' coreshop.security.customer.password_encoder_factory: class: Pimcore\Security\Encoder\Factory\UserAwareEncoderFactory diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/services/customer.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/customer.yml index 9b0faf2a6b..55a792b930 100644 --- a/src/CoreShop/Bundle/CoreBundle/Resources/config/services/customer.yml +++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/customer.yml @@ -16,6 +16,7 @@ services: - '%coreshop.folder.customer%' - '%coreshop.folder.guest%' - '%coreshop.folder.address%' + - '%coreshop.customer.security.login_identifier%' coreshop.customer.login_service: '@CoreShop\Bundle\CoreBundle\Customer\CustomerLoginService' CoreShop\Bundle\CoreBundle\Customer\CustomerLoginServiceInterface: '@CoreShop\Bundle\CoreBundle\Customer\CustomerLoginService' diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/services/listeners.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/listeners.yml index 54937c872b..e376192bf6 100644 --- a/src/CoreShop/Bundle/CoreBundle/Resources/config/services/listeners.yml +++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/listeners.yml @@ -72,6 +72,14 @@ services: - { name: kernel.event_listener, event: pimcore.dataobject.preDelete, method: checkCustomerOrdersBeforeDeletion } - { name: kernel.event_listener, event: pimcore.dataobject.deleteInfo, method: checkCustomerDeletionAllowed } + CoreShop\Bundle\CoreBundle\EventListener\CustomerSecurityValidationListener: + arguments: + - '@coreshop.repository.customer' + - '%coreshop.model.customer.class%' + - '%coreshop.customer.security.login_identifier%' + tags: + - { name: kernel.event_listener, event: pimcore.dataobject.preUpdate, method: checkCustomerSecurityDataBeforeUpdate } + CoreShop\Bundle\CoreBundle\EventListener\QuantityRangeUnitValidationListener: arguments: - '@coreshop.repository.product_unit_definition' diff --git a/src/CoreShop/Bundle/CoreBundle/Security/ObjectUserProvider.php b/src/CoreShop/Bundle/CoreBundle/Security/ObjectUserProvider.php index 2a55fd1d54..ad27a4eb89 100644 --- a/src/CoreShop/Bundle/CoreBundle/Security/ObjectUserProvider.php +++ b/src/CoreShop/Bundle/CoreBundle/Security/ObjectUserProvider.php @@ -31,27 +31,39 @@ class ObjectUserProvider implements UserProviderInterface */ protected $className; + /** + * @var string + */ + protected $loginIdentifier; + /** * @param CustomerRepositoryInterface $customerRepository * @param string $className + * @param string $loginIdentifier */ - public function __construct(CustomerRepositoryInterface $customerRepository, $className) + public function __construct( + CustomerRepositoryInterface $customerRepository, + $className, + $loginIdentifier + ) { $this->customerRepository = $customerRepository; $this->className = $className; + $this->loginIdentifier = $loginIdentifier; } /** * {@inheritdoc} */ - public function loadUserByUsername($emailAddress) + public function loadUserByUsername($userNameOrEmailAddress) { - $customer = $this->customerRepository->findCustomerByEmail($emailAddress); + $customer = $this->customerRepository->findUniqueByLoginIdentifier($this->loginIdentifier, $userNameOrEmailAddress, false); + if ($customer instanceof CustomerInterface) { return $customer; } - throw new UsernameNotFoundException(sprintf('User with email address %s was not found', $emailAddress)); + throw new UsernameNotFoundException(sprintf('User with email address or username "%s" was not found', $userNameOrEmailAddress)); } /** diff --git a/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/Configuration.php b/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/Configuration.php index d9ebfcd236..baf25c969f 100644 --- a/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/Configuration.php +++ b/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/Configuration.php @@ -36,6 +36,7 @@ public function getConfigTreeBuilder() $rootNode ->children() ->scalarNode('driver')->defaultValue(CoreShopResourceBundle::DRIVER_DOCTRINE_ORM)->end() + ->enumNode('login_identifier')->values(['email', 'username'])->defaultValue('email')->end() ->end(); $this->addStack($rootNode); diff --git a/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/CoreShopCustomerExtension.php b/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/CoreShopCustomerExtension.php index 873f0a2069..5d48bc5097 100644 --- a/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/CoreShopCustomerExtension.php +++ b/src/CoreShop/Bundle/CustomerBundle/DependencyInjection/CoreShopCustomerExtension.php @@ -42,6 +42,8 @@ public function load(array $config, ContainerBuilder $container) $this->registerStack('coreshop', $config['stack'], $container); } + $container->setParameter('coreshop.customer.security.login_identifier', $config['login_identifier']); + $loader->load('services.yml'); $container diff --git a/src/CoreShop/Bundle/CustomerBundle/Form/Type/CustomerLoginType.php b/src/CoreShop/Bundle/CustomerBundle/Form/Type/CustomerLoginType.php index 8961e2f3c4..0eabb090ef 100644 --- a/src/CoreShop/Bundle/CustomerBundle/Form/Type/CustomerLoginType.php +++ b/src/CoreShop/Bundle/CustomerBundle/Form/Type/CustomerLoginType.php @@ -20,20 +20,35 @@ class CustomerLoginType extends AbstractType { + /** + * @var string + */ + protected $loginIdentifier; + + /** + * @param string $loginIdentifier + */ + public function __construct($loginIdentifier) + { + $this->loginIdentifier = $loginIdentifier; + } + /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { + $loginIdentifierLabel = sprintf('coreshop.form.login.%s', $this->loginIdentifier); + $builder ->add('_username', TextType::class, [ - 'label' => 'coreshop.form.login.username', + 'label' => $loginIdentifierLabel, ]) ->add('_password', PasswordType::class, [ 'label' => 'coreshop.form.login.password', ]) ->add('_remember_me', CheckboxType::class, [ - 'label' => 'coreshop.form.login.remember_me', + 'label' => 'coreshop.form.login.remember_me', 'required' => false, ]); } diff --git a/src/CoreShop/Bundle/CustomerBundle/Pimcore/Repository/CustomerRepository.php b/src/CoreShop/Bundle/CustomerBundle/Pimcore/Repository/CustomerRepository.php index 899e9f21bb..213a6c6804 100644 --- a/src/CoreShop/Bundle/CustomerBundle/Pimcore/Repository/CustomerRepository.php +++ b/src/CoreShop/Bundle/CustomerBundle/Pimcore/Repository/CustomerRepository.php @@ -53,12 +53,12 @@ public function findByNewsletterToken($newsletterToken) /** * {@inheritdoc} */ - public function findUniqueByEmail($email, $isGuest) + public function findUniqueByLoginIdentifier(string $identifier, $value, $isGuest) { $list = $this->getList(); - $conditions = ['email = ?']; - $conditionsValues = [$email]; + $conditions = [sprintf('%s = ?', $identifier)]; + $conditionsValues = [$value]; $conditionsValues[] = $isGuest ? 1 : 0; if (!$isGuest) { @@ -79,6 +79,22 @@ public function findUniqueByEmail($email, $isGuest) return null; } + /** + * {@inheritdoc} + */ + public function findUniqueByEmail($email, $isGuest) + { + return $this->findUniqueByLoginIdentifier('email', $email, $isGuest); + } + + /** + * {@inheritdoc} + */ + public function findUniqueByUsername($username, $isGuest) + { + return $this->findUniqueByLoginIdentifier('username', $username, $isGuest); + } + /** * {@inheritdoc} */ @@ -94,4 +110,12 @@ public function findCustomerByEmail($email) { return $this->findUniqueByEmail($email, false); } + + /** + * {@inheritdoc} + */ + public function findCustomerByUsername($username) + { + return $this->findUniqueByUsername($username, false); + } } diff --git a/src/CoreShop/Bundle/CustomerBundle/Resources/config/services/form.yml b/src/CoreShop/Bundle/CustomerBundle/Resources/config/services/form.yml index d5e098729b..4a154ee6ff 100644 --- a/src/CoreShop/Bundle/CustomerBundle/Resources/config/services/form.yml +++ b/src/CoreShop/Bundle/CustomerBundle/Resources/config/services/form.yml @@ -14,6 +14,12 @@ services: tags: - { name: form.type } + CoreShop\Bundle\CustomerBundle\Form\Type\CustomerLoginType: + arguments: + - '%coreshop.customer.security.login_identifier%' + tags: + - { name: form.type } + CoreShop\Bundle\CustomerBundle\Form\Type\ChangePasswordType: tags: - { name: form.type } diff --git a/src/CoreShop/Bundle/CustomerBundle/Resources/config/validation/Customer.yml b/src/CoreShop/Bundle/CustomerBundle/Resources/config/validation/Customer.yml index c4268e17b3..44de9d970d 100644 --- a/src/CoreShop/Bundle/CustomerBundle/Resources/config/validation/Customer.yml +++ b/src/CoreShop/Bundle/CustomerBundle/Resources/config/validation/Customer.yml @@ -15,15 +15,13 @@ CoreShop\Component\Customer\Model\Customer: constraints: - \CoreShop\Bundle\ResourceBundle\Validator\Constraints\UniqueEntity: groups: ['coreshop'] - fields: ['email'] + fields: ['container.getParameter("coreshop.customer.security.login_identifier")'] values: {isGuest: [false, null]} - message: 'coreshop.form.customer.email.unique' + message: 'container.getParameter("coreshop.customer.security.login_identifier") == "email" ? "coreshop.form.customer.email.unique" : "coreshop.form.customer.username.unique"' allowSameEntity: true - \CoreShop\Bundle\ResourceBundle\Validator\Constraints\UniqueEntity: groups: ['coreshop_customer_guest'] - fields: ['email'] + fields: ['container.getParameter("coreshop.customer.security.login_identifier")'] values: {isGuest: [false, null]} - message: 'coreshop.form.guest.email_is_customer' - - + message: 'container.getParameter("coreshop.customer.security.login_identifier") == "email" ? "coreshop.form.guest.email_is_customer" : "coreshop.form.guest.username_is_customer"' diff --git a/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.de.yml b/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.de.yml index c47ff87e5b..1a6d1d69fc 100644 --- a/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.de.yml +++ b/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.de.yml @@ -1,10 +1,13 @@ coreshop: form: customer: + username: + unique: 'Dieser Benutzername wird bereits verwendet.' email: unique: 'Diese E-Mail-Adresse wird bereits verwendet.' must_match: 'E-Mail-Adressen müssen übereinstimmen.' password: must_match: 'Passwörter müssen übereinstimmen.' guest: - email_is_customer: 'Diese E-Mail-Adresse wird bereits verwendet. Bitte melden Sie sich an oder benutzen Sie die "Passwort zurücksetzen" Funktion.' \ No newline at end of file + email_is_customer: 'Diese E-Mail-Adresse wird bereits verwendet. Bitte melden Sie sich an oder benutzen Sie die "Passwort zurücksetzen" Funktion.' + username_is_customer: 'Dieser Benutzername wird bereits verwendet. Bitte melden Sie sich an oder benutzen Sie die "Passwort zurücksetzen" Funktion.' \ No newline at end of file diff --git a/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.en.yml b/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.en.yml index b56a199e67..2c965895cc 100644 --- a/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.en.yml +++ b/src/CoreShop/Bundle/CustomerBundle/Resources/translations/validators.en.yml @@ -1,10 +1,13 @@ coreshop: form: customer: + username: + unique: 'This username is already used.' email: unique: 'This email is already used.' must_match: 'The email fields must match.' password: must_match: 'The password fields must match.' guest: - email_is_customer: 'This email is already registered. Please login or use the "reset password" feature.' \ No newline at end of file + email_is_customer: 'This email is already registered. Please login or use the "reset password" feature.' + username_is_customer: 'This username is already registered. Please login or use the "reset password" feature.' \ No newline at end of file diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml b/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml index 8f7fe30a19..40b44c23cf 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml @@ -1151,6 +1151,12 @@ translations: en: 'Username' de: 'Benutzername' + coreshop.form.login.email: + languages: + de_CH: 'E-Mail-Adresse' + en: 'Email' + de: 'E-Mail-Adresse' + coreshop.form.login.password: languages: de_CH: 'Passwort' diff --git a/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml b/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml index 121d01f797..6a42f191ce 100644 --- a/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml +++ b/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml @@ -3,5 +3,7 @@ services: public: true CoreShop\Bundle\ResourceBundle\Validator\Constraints\UniqueEntityValidator: + arguments: + - '@service_container' tags: - { name: validator.constraint_validator, alias: coreshop.unique_entity } diff --git a/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php b/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php index 12b38f4596..170294fef9 100644 --- a/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php +++ b/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php @@ -15,6 +15,9 @@ use CoreShop\Component\Resource\Exception\UnexpectedTypeException; use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\Concrete; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\SyntaxError; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -22,6 +25,48 @@ final class UniqueEntityValidator extends ConstraintValidator { + /** + * @var ExpressionLanguage + */ + protected $expressionLanguage; + + /** + * @var ContainerInterface + */ + protected $container; + + /** + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + $this->expressionLanguage = new ExpressionLanguage(); + $this->container = $container; + } + + /** + * @param mixed $value + * + * @return mixed + */ + protected function evaluateExpression($value) + { + if (is_array($value)) { + foreach ($value as $i => $item) { + $value[$i] = $this->evaluateExpression($item); + } + + return $value; + } + + try { + return $this->expressionLanguage->evaluate($value, ['container' => $this->container,]); + } catch (SyntaxError $e) { + // do not throw any exception but simple return value instead + return $value; + } + } + /** * @param Concrete $entity * @param Constraint $constraint @@ -38,7 +83,7 @@ public function validate($entity, Constraint $constraint) throw new UnexpectedTypeException($constraint->fields, 'array'); } - $fields = (array)$constraint->fields; + $fields = $this->evaluateExpression((array)$constraint->fields); if (0 === count($fields)) { throw new ConstraintDefinitionException('At least one field has to be specified.'); @@ -108,7 +153,7 @@ public function validate($entity, Constraint $constraint) return; } - $this->context->buildViolation($constraint->message) + $this->context->buildViolation($this->evaluateExpression($constraint->message)) ->atPath($errorPath) ->setParameter('{{ value }}', $criteria[$fields[0]]) ->setInvalidValue($criteria[$fields[0]]) diff --git a/src/CoreShop/Component/Customer/Model/Customer.php b/src/CoreShop/Component/Customer/Model/Customer.php index 292d6b3106..54f5747478 100644 --- a/src/CoreShop/Component/Customer/Model/Customer.php +++ b/src/CoreShop/Component/Customer/Model/Customer.php @@ -120,6 +120,22 @@ public function setEmail($email) throw new ImplementedByPimcoreException(__CLASS__, __METHOD__); } + /** + * {@inheritdoc} + */ + public function getUsername() + { + throw new ImplementedByPimcoreException(__CLASS__, __METHOD__); + } + + /** + * @param string $username + */ + public function setUsername($username) + { + throw new ImplementedByPimcoreException(__CLASS__, __METHOD__); + } + /** * {@inheritdoc} */ @@ -244,14 +260,6 @@ public function getRoles() return array_unique($roles); } - /** - * {@inheritdoc} - */ - public function getUsername() - { - return $this->getEmail(); - } - /** * {@inheritdoc} */ diff --git a/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php b/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php index 888fdffa83..4f7c2bebd3 100644 --- a/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php +++ b/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php @@ -36,7 +36,18 @@ public function findByResetToken($resetToken); public function findByNewsletterToken($newsletterToken); /** - * Find Customer by email. + * Find Customer by Identifier. + * + * @param string $identifier + * @param $value + * @param $isGuest + * + * @return mixed + */ + public function findUniqueByLoginIdentifier(string $identifier, $value, $isGuest); + + /** + * Find Customer by Email. * * @param string $email * @param bool $isGuest @@ -45,6 +56,16 @@ public function findByNewsletterToken($newsletterToken); */ public function findUniqueByEmail($email, $isGuest); + /** + * Find Customer by Username. + * + * @param string $username + * @param bool $isGuest + * + * @return CustomerInterface|null + */ + public function findUniqueByUsername($username, $isGuest); + /** * Find Guest Customer by Email. * @@ -62,4 +83,13 @@ public function findGuestByEmail($email); * @return CustomerInterface|null */ public function findCustomerByEmail($email); + + /** + * Find Customer by Username. + * + * @param string $username + * + * @return CustomerInterface|null + */ + public function findCustomerByUsername($username); } From f54bd25ab3fcc2f6d493526b76d71881c6f2384a Mon Sep 17 00:00:00 2001 From: Stefan Hagspiel Date: Tue, 18 Feb 2020 13:26:54 +0100 Subject: [PATCH 2/8] also respect reset password data --- .../Form/Type/RequestResetPasswordType.php | 22 +++++++-- .../Controller/RegisterController.php | 45 ++++++++++++++++--- .../install/pimcore/translations.yml | 6 +++ 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/CoreShop/Bundle/CustomerBundle/Form/Type/RequestResetPasswordType.php b/src/CoreShop/Bundle/CustomerBundle/Form/Type/RequestResetPasswordType.php index 6b23e5712b..89c7125c74 100644 --- a/src/CoreShop/Bundle/CustomerBundle/Form/Type/RequestResetPasswordType.php +++ b/src/CoreShop/Bundle/CustomerBundle/Form/Type/RequestResetPasswordType.php @@ -14,7 +14,9 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; class RequestResetPasswordType extends AbstractType { @@ -23,10 +25,12 @@ class RequestResetPasswordType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder - ->add('email', EmailType::class, [ - 'label' => 'coreshop.form.customer.email', - ]); + $identifier = $options['reset_identifier']; + $typeClass = $identifier === 'email' ? EmailType::class : TextType::class; + + $builder->add($identifier, $typeClass, [ + 'label' => sprintf('coreshop.form.customer.%s', $identifier) + ]); } /** @@ -36,4 +40,14 @@ public function getBlockPrefix() { return 'coreshop_request_reset_password'; } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('reset_identifier', 'email'); + $resolver->setAllowedTypes('reset_identifier', 'string'); + $resolver->setAllowedValues('reset_identifier', ['email', 'username']); + } } diff --git a/src/CoreShop/Bundle/FrontendBundle/Controller/RegisterController.php b/src/CoreShop/Bundle/FrontendBundle/Controller/RegisterController.php index 3d6059117c..19c399dcad 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Controller/RegisterController.php +++ b/src/CoreShop/Bundle/FrontendBundle/Controller/RegisterController.php @@ -19,8 +19,11 @@ use CoreShop\Bundle\CustomerBundle\Form\Type\ResetPasswordType; use CoreShop\Component\Address\Model\AddressInterface; use CoreShop\Component\Customer\Model\CustomerInterface; +use CoreShop\Component\Customer\Repository\CustomerRepositoryInterface; use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class RegisterController extends FrontendController @@ -28,7 +31,7 @@ class RegisterController extends FrontendController /** * @param Request $request * - * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response + * @return RedirectResponse|Response */ public function registerAction(Request $request) { @@ -81,25 +84,26 @@ public function registerAction(Request $request) /** * @param Request $request * - * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response + * @return RedirectResponse|Response */ public function passwordResetRequestAction(Request $request) { - $form = $this->get('form.factory')->createNamed('', RequestResetPasswordType::class); + $resetIdentifier = $this->getParameter('coreshop.customer.security.login_identifier'); + $form = $this->get('form.factory')->createNamed('', RequestResetPasswordType::class, null, ['reset_identifier' => $resetIdentifier]); if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true)) { $handledForm = $form->handleRequest($request); if ($handledForm->isValid()) { - $passwordReset = $handledForm->getData(); + $passwordResetData = $handledForm->getData(); - $customer = $this->get('coreshop.repository.customer')->findCustomerByEmail($passwordReset['email']); + $customer = $this->getCustomerRepository()->findUniqueByLoginIdentifier($resetIdentifier, $passwordResetData[$resetIdentifier], false); if (!$customer instanceof CustomerInterface) { return $this->redirectToRoute('coreshop_index'); } - $customer->setPasswordResetHash(hash('md5', $customer->getId() . $customer->getEmail() . mt_rand() . time())); + $customer->setPasswordResetHash($this->generateResetPasswordHash($customer)); $customer->save(); $resetLink = $this->generateCoreShopUrl(null, 'coreshop_customer_password_reset', ['token' => $customer->getPasswordResetHash()], UrlGeneratorInterface::ABSOLUTE_URL); @@ -118,12 +122,17 @@ public function passwordResetRequestAction(Request $request) ]); } + /** + * @param Request $request + * + * @return RedirectResponse|Response + */ public function passwordResetAction(Request $request) { $resetToken = $request->get('token'); if ($resetToken) { - $customer = $this->get('coreshop.repository.customer')->findByResetToken($resetToken); + $customer = $this->getCustomerRepository()->findByResetToken($resetToken); $form = $this->get('form.factory')->createNamed('', ResetPasswordType::class); @@ -153,6 +162,14 @@ public function passwordResetAction(Request $request) return $this->redirectToRoute('coreshop_index'); } + /** + * @return CustomerRepositoryInterface + */ + protected function getCustomerRepository() + { + return $this->get('coreshop.repository.customer'); + } + /** * @return bool|CustomerInterface|null */ @@ -165,4 +182,18 @@ protected function getCustomer() return null; } + + /** + * @param CustomerInterface $customer + * + * @return string + */ + protected function generateResetPasswordHash(CustomerInterface $customer) + { + $resetIdentifier = $this->getParameter('coreshop.customer.security.login_identifier'); + + $userKey = $resetIdentifier === 'email' ? $customer->getEmail() : $customer->getUsername(); + + return hash('md5', $customer->getId() . $userKey . mt_rand() . time()); + } } diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml b/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml index 40b44c23cf..9fd444158a 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/install/pimcore/translations.yml @@ -1067,6 +1067,12 @@ translations: en: 'Last Name' de: 'Nachname' + coreshop.form.customer.username: + languages: + de_CH: 'Benutzername' + en: 'Username' + de: 'Benutzername' + coreshop.form.customer.email: languages: de_CH: 'E-Mail-Adresse' From ac825b5dd0624218287cf61617cdadc998f53229 Mon Sep 17 00:00:00 2001 From: Stefan Hagspiel Date: Tue, 18 Feb 2020 13:27:36 +0100 Subject: [PATCH 3/8] fix migration class updater --- .../Bundle/CoreBundle/Migrations/Version20200211120529.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CoreShop/Bundle/CoreBundle/Migrations/Version20200211120529.php b/src/CoreShop/Bundle/CoreBundle/Migrations/Version20200211120529.php index 8cad8352c1..35fbda8dfd 100644 --- a/src/CoreShop/Bundle/CoreBundle/Migrations/Version20200211120529.php +++ b/src/CoreShop/Bundle/CoreBundle/Migrations/Version20200211120529.php @@ -97,7 +97,7 @@ public function up(Schema $schema) } if (!$classUpdater->hasField('addressAccessType')) { - $classUpdater->insertFieldBefore('addresses', $addressAccessTypeField); + $classUpdater->insertFieldAfter('addresses', $addressAccessTypeField); $saveClass = true; } From 3313d6f3b66227c4904e8bfa88a5cf4b2b5b5e50 Mon Sep 17 00:00:00 2001 From: Stefan Hagspiel Date: Tue, 18 Feb 2020 16:44:06 +0100 Subject: [PATCH 4/8] use coreshop expression language --- .../ResourceBundle/Resources/config/services/validators.yml | 1 + .../Validator/Constraints/UniqueEntityValidator.php | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml b/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml index 6a42f191ce..4f61d697f0 100644 --- a/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml +++ b/src/CoreShop/Bundle/ResourceBundle/Resources/config/services/validators.yml @@ -4,6 +4,7 @@ services: CoreShop\Bundle\ResourceBundle\Validator\Constraints\UniqueEntityValidator: arguments: + - '@coreshop.expression_language' - '@service_container' tags: - { name: validator.constraint_validator, alias: coreshop.unique_entity } diff --git a/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php b/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php index 170294fef9..73f25f28d3 100644 --- a/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php +++ b/src/CoreShop/Bundle/ResourceBundle/Validator/Constraints/UniqueEntityValidator.php @@ -36,11 +36,12 @@ final class UniqueEntityValidator extends ConstraintValidator protected $container; /** + * @param ExpressionLanguage $expressionLanguage * @param ContainerInterface $container */ - public function __construct(ContainerInterface $container) + public function __construct(ExpressionLanguage $expressionLanguage, ContainerInterface $container) { - $this->expressionLanguage = new ExpressionLanguage(); + $this->expressionLanguage = $expressionLanguage; $this->container = $container; } From 58a2a374fe1f893d74dc194c088d0940d5518b18 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 18 Feb 2020 17:11:07 +0100 Subject: [PATCH 5/8] Update docs/03_Development/12_Customers/04_Registration_Types.md Co-Authored-By: Jacob Dreesen --- docs/03_Development/12_Customers/04_Registration_Types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/03_Development/12_Customers/04_Registration_Types.md b/docs/03_Development/12_Customers/04_Registration_Types.md index f01dfc394e..aec59faf8c 100644 --- a/docs/03_Development/12_Customers/04_Registration_Types.md +++ b/docs/03_Development/12_Customers/04_Registration_Types.md @@ -29,6 +29,6 @@ core_shop_customer: CoreShop comes with a preinstalled constraints which will tell your customer, if an email address or username - depending on your settings - is valid or not. ### Backend / API -Plus, if you're going to update a customer by API or Backend, coreshop also checks if you're customer entity has unique data. +Plus, if you're going to update a customer by API or Backend, coreshop also checks if your customer entity has unique data. -> Note: Both checks only applies to non-guests entities! \ No newline at end of file +> Note: Both checks only applies to non-guests entities! From cdb3efe84279cc0c1b44c7d4e98c8a7089ed9a65 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 18 Feb 2020 17:11:42 +0100 Subject: [PATCH 6/8] Update docs/03_Development/12_Customers/04_Registration_Types.md Co-Authored-By: Jacob Dreesen --- docs/03_Development/12_Customers/04_Registration_Types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03_Development/12_Customers/04_Registration_Types.md b/docs/03_Development/12_Customers/04_Registration_Types.md index aec59faf8c..28c35a03a3 100644 --- a/docs/03_Development/12_Customers/04_Registration_Types.md +++ b/docs/03_Development/12_Customers/04_Registration_Types.md @@ -26,7 +26,7 @@ core_shop_customer: ## Security ### Form (Frontend) -CoreShop comes with a preinstalled constraints which will tell your customer, if an email address or username - depending on your settings - is valid or not. +CoreShop comes with a preinstalled constraint which will tell your customer, if an email address or username - depending on your settings - is valid or not. ### Backend / API Plus, if you're going to update a customer by API or Backend, coreshop also checks if your customer entity has unique data. From a74954ee393828226a87c4787250c7f62caf61ca Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 18 Feb 2020 17:12:13 +0100 Subject: [PATCH 7/8] Update 04_Registration_Types.md --- docs/03_Development/12_Customers/04_Registration_Types.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/03_Development/12_Customers/04_Registration_Types.md b/docs/03_Development/12_Customers/04_Registration_Types.md index 28c35a03a3..aa84d0f0f1 100644 --- a/docs/03_Development/12_Customers/04_Registration_Types.md +++ b/docs/03_Development/12_Customers/04_Registration_Types.md @@ -31,4 +31,5 @@ CoreShop comes with a preinstalled constraint which will tell your customer, if ### Backend / API Plus, if you're going to update a customer by API or Backend, coreshop also checks if your customer entity has unique data. -> Note: Both checks only applies to non-guests entities! +> Note: Both checks only apply to non-guest entities! + From 515ad24ae237f04948d8c8e51033a1cf9ba08005 Mon Sep 17 00:00:00 2001 From: Stefan Hagspiel Date: Tue, 18 Feb 2020 17:57:12 +0100 Subject: [PATCH 8/8] exclude own id on duplicate check --- .../CustomerSecurityValidationListener.php | 14 +++++++++++--- .../Repository/CustomerRepositoryInterface.php | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php b/src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php index 71e4b6e33c..bbc99f337b 100644 --- a/src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php +++ b/src/CoreShop/Bundle/CoreBundle/EventListener/CustomerSecurityValidationListener.php @@ -68,10 +68,18 @@ public function checkCustomerSecurityDataBeforeUpdate(DataObjectEvent $event) } $identifierValue = $this->loginIdentifier === 'email' ? $object->getEmail() : $object->getUsername(); - $customer = $this->customerRepository->findUniqueByLoginIdentifier($this->loginIdentifier, $identifierValue, false); - if ($customer instanceof CustomerInterface) { - throw new ValidationException(sprintf('%s "%s" is already used. Please use another one.', ucfirst($this->loginIdentifier), $identifierValue)); + $listing = $this->customerRepository->getList(); + $listing->setUnpublished(true); + $listing->addConditionParam(sprintf('%s = ?', $this->loginIdentifier), $identifierValue); + $listing->addConditionParam('o_id != ?', $object->getId()); + + $objects = $listing->getObjects(); + + if (count($objects) === 0) { + return; } + + throw new ValidationException(sprintf('%s "%s" is already used. Please use another one.', ucfirst($this->loginIdentifier), $identifierValue)); } } diff --git a/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php b/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php index 4f7c2bebd3..0aecca42bc 100644 --- a/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php +++ b/src/CoreShop/Component/Customer/Repository/CustomerRepositoryInterface.php @@ -39,8 +39,8 @@ public function findByNewsletterToken($newsletterToken); * Find Customer by Identifier. * * @param string $identifier - * @param $value - * @param $isGuest + * @param string $value + * @param bool $isGuest * * @return mixed */