diff --git a/Controller/AbstractController.php b/Controller/AbstractController.php index c7ac6de7..4fec7637 100644 --- a/Controller/AbstractController.php +++ b/Controller/AbstractController.php @@ -75,9 +75,14 @@ protected function getWebspaceKey() */ protected function setUserPasswordAndSalt(User $user, Form $form) { + $plainPassword = $form->get('plainPassword')->getData(); + if (null === $plainPassword) { + return $user; + } + $salt = $this->get('sulu_security.salt_generator')->getRandomSalt(); $encoder = $this->get('security.encoder_factory')->getEncoder($user); - $password = $encoder->encodePassword($form->get('plainPassword')->getData(), $salt); + $password = $encoder->encodePassword($plainPassword, $salt); $user->setPassword($password); $user->setSalt($salt); @@ -108,7 +113,7 @@ protected function checkAutoLogin($type) * * @return Response */ - protected function renderTemplate($type, $data) + protected function renderTemplate($type, $data = []) { return $this->render( $this->getCommunityManager($this->getWebspaceKey())->getConfigTypeProperty( diff --git a/Controller/EmailConfirmationController.php b/Controller/EmailConfirmationController.php new file mode 100644 index 00000000..224eb405 --- /dev/null +++ b/Controller/EmailConfirmationController.php @@ -0,0 +1,50 @@ +get('sulu_community.email_confirmation.repository'); + + $success = false; + $token = $repository->findByToken($request->get('token')); + + if ($token !== null) { + $user = $token->getUser(); + $user->setEmail($user->getContact()->getMainEmail()); + $this->get('doctrine.orm.entity_manager')->remove($token); + $this->saveEntities(); + + $success = true; + } + + return $this->renderTemplate(self::TYPE, ['success' => $success]); + } +} diff --git a/Controller/ProfileController.php b/Controller/ProfileController.php new file mode 100644 index 00000000..db99757d --- /dev/null +++ b/Controller/ProfileController.php @@ -0,0 +1,174 @@ +getCommunityManager($this->getWebspaceKey()); + + $user = $this->getUser(); + + // Create Form + $form = $this->createForm( + $communityManager->getConfigTypeProperty(self::TYPE, Configuration::FORM_TYPE), + $user, + $communityManager->getConfigTypeProperty(self::TYPE, Configuration::FORM_TYPE_OPTIONS) + ); + + $form->handleRequest($request); + $success = false; + + // Handle Form Success + if ($form->isSubmitted() && $form->isValid()) { + // Set Password and Salt + $user = $this->setUserPasswordAndSalt($form->getData(), $form); + + if (!$user->getLocale()) { + $user->setLocale($request->getLocale()); + } + + $this->saveAvatar($form, $user, $request->getLocale()); + + // Register User + $communityManager->saveProfile($user); + $this->saveEntities(); + + // Redirect + $redirectTo = $communityManager->getConfigTypeProperty(self::TYPE, Configuration::REDIRECT_TO); + + if ($redirectTo) { + return $this->redirect($redirectTo); + } + + $success = true; + } + + return $this->renderTemplate( + self::TYPE, + [ + 'form' => $form->createView(), + 'success' => $success, + ] + ); + } + + /** + * Save media and set avatar on user. + * + * @param Form $form + * @param User $user + * @param string $locale + */ + private function saveAvatar(Form $form, User $user, $locale) + { + $uploadedFile = $form->get('contact')->get('avatar')->getData(); + if (null === $uploadedFile) { + return; + } + + $systemCollectionManager = $this->get('sulu_media.system_collections.manager'); + $mediaManager = $this->get('sulu_media.media_manager'); + + $collection = $systemCollectionManager->getSystemCollection('sulu_contact.contact'); + $avatar = $user->getContact()->getAvatar(); + + $apiMedia = $mediaManager->save( + $uploadedFile, + [ + 'id' => (null !== $avatar ? $avatar->getId() : null), + 'locale' => $locale, + 'title' => $user->getFullName(), + 'collection' => $collection, + ], + $user->getId() + ); + + $user->getContact()->setAvatar($apiMedia->getEntity()); + } + + /** + * {@inheritdoc} + * + * @return User + */ + public function getUser() + { + /** @var User $user */ + $user = parent::getUser(); + + if (null === $user->getContact()->getMainAddress()) { + $this->addAddress($user); + } + + if (0 === count($user->getContact()->getNotes())) { + $this->addNote($user); + } + + return $user; + } + + /** + * Add address to user. + * + * @param User $user + */ + private function addAddress(User $user) + { + $entityManager = $this->get('doctrine.orm.entity_manager'); + + $address = new Address(); + $address->setPrimaryAddress(true); + $address->setAddressType($entityManager->getRepository(AddressType::class)->find(1)); + $contactAddress = new ContactAddress(); + $contactAddress->setAddress($address); + $contactAddress->setContact($user->getContact()); + + $user->getContact()->addContactAddress($contactAddress); + } + + /** + * Add note to user. + * + * @param User $user + */ + private function addNote(User $user) + { + $note = new Note(); + $user->getContact()->addNote($note); + + $this->get('doctrine.orm.entity_manager')->persist($note); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 20ff0af0..cb9e2260 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -14,6 +14,7 @@ use Sulu\Bundle\CommunityBundle\Form\Type\CompletionType; use Sulu\Bundle\CommunityBundle\Form\Type\PasswordForgetType; use Sulu\Bundle\CommunityBundle\Form\Type\PasswordResetType; +use Sulu\Bundle\CommunityBundle\Form\Type\ProfileType; use Sulu\Bundle\CommunityBundle\Form\Type\RegistrationType; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -44,6 +45,8 @@ class Configuration implements ConfigurationInterface const TYPE_BLACKLISTED = 'blacklisted'; const TYPE_BLACKLIST_CONFIRMED = 'blacklist_confirmed'; const TYPE_BLACKLIST_DENIED = 'blacklist_denied'; + const TYPE_PROFILE = 'profile'; + const TYPE_EMAIL_CONFIRMATION = 'email_confirmation'; public static $TYPES = [ self::TYPE_LOGIN, @@ -89,8 +92,8 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Login configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:login.html.twig')->end() - ->scalarNode(self::EMBED_TEMPLATE)->defaultValue('SuluCommunityBundle:community:login-embed.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Login:login.html.twig')->end() + ->scalarNode(self::EMBED_TEMPLATE)->defaultValue('SuluCommunityBundle:Login:login-embed.html.twig')->end() ->end() ->end() // Registration @@ -98,7 +101,7 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Registration configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:registration.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Registration:form.html.twig')->end() ->scalarNode(self::FORM_TYPE)->defaultValue(RegistrationType::class)->end() ->arrayNode(self::FORM_TYPE_OPTIONS) ->addDefaultsIfNotSet() @@ -111,7 +114,44 @@ public function getConfigTreeBuilder() ->children() ->scalarNode(self::EMAIL_SUBJECT)->defaultValue('Registration')->end() ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue(null)->end() - ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:community:registration-email.html.twig')->end() + ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:Registration:email.html.twig')->end() + ->end() + ->end() + ->end() + ->end() + // Registration + ->arrayNode(self::TYPE_PROFILE) + ->addDefaultsIfNotSet() + ->children() + // Registration configuration + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Profile:form.html.twig')->end() + ->scalarNode(self::FORM_TYPE)->defaultValue(ProfileType::class)->end() + ->arrayNode(self::FORM_TYPE_OPTIONS) + ->addDefaultsIfNotSet() + ->end() + ->scalarNode(self::REDIRECT_TO)->defaultValue(null)->end() + ->arrayNode(self::EMAIL) + ->addDefaultsIfNotSet() + ->children() + ->scalarNode(self::EMAIL_SUBJECT)->defaultValue(null)->end() + ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue(null)->end() + ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue(null)->end() + ->end() + ->end() + ->end() + ->end() + // Email change + ->arrayNode(self::TYPE_EMAIL_CONFIRMATION) + ->addDefaultsIfNotSet() + ->children() + // Email change configuration + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:EmailConfirmation:success.html.twig')->end() + ->arrayNode(self::EMAIL) + ->addDefaultsIfNotSet() + ->children() + ->scalarNode(self::EMAIL_SUBJECT)->defaultValue('E-Mail change')->end() + ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue(null)->end() + ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:EmailConfirmation:email.html.twig')->end() ->end() ->end() ->end() @@ -121,7 +161,7 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Confirmation configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:confirmation.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Confirmation:message.html.twig')->end() ->scalarNode(self::ACTIVATE_USER)->defaultValue(true)->end() ->scalarNode(self::AUTO_LOGIN)->defaultValue(true)->end() ->scalarNode(self::REDIRECT_TO)->defaultValue(null)->end() @@ -140,12 +180,11 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Blacklisted configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:blacklist.html.twig')->end() ->arrayNode(self::EMAIL) ->addDefaultsIfNotSet() ->children() ->scalarNode(self::EMAIL_SUBJECT)->defaultValue('Blacklisted')->end() - ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue('SuluCommunityBundle:community:blacklisted-email.html.twig')->end() + ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue('SuluCommunityBundle:Blacklist:email.html.twig')->end() ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue(null)->end() ->end() ->end() @@ -156,7 +195,7 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Denied configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:blacklist-denied.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Blacklist:denied.html.twig')->end() ->scalarNode(self::DELETE_USER)->defaultTrue()->end() ->arrayNode(self::EMAIL) ->addDefaultsIfNotSet() @@ -173,13 +212,13 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Confirmed configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:blacklist-confirmed.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Blacklist:confirmed.html.twig')->end() ->arrayNode(self::EMAIL) ->addDefaultsIfNotSet() ->children() ->scalarNode(self::EMAIL_SUBJECT)->defaultValue('Registration')->end() ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue(null)->end() - ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:community:registration-email.html.twig')->end() + ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:Password:email.html.twig')->end() ->end() ->end() ->end() @@ -189,7 +228,7 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Password Forget configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:password-forget.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Password:forget.html.twig')->end() ->scalarNode(self::FORM_TYPE)->defaultValue(PasswordForgetType::class)->end() ->arrayNode(self::FORM_TYPE_OPTIONS) ->addDefaultsIfNotSet() @@ -200,7 +239,7 @@ public function getConfigTreeBuilder() ->children() ->scalarNode(self::EMAIL_SUBJECT)->defaultValue('Password Forget')->end() ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue(null)->end() - ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:community:password-forget-email.html.twig')->end() + ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:Password:forget-email.html.twig')->end() ->end() ->end() ->end() @@ -210,7 +249,7 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() // Password Forget configuration - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:password-reset.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Password:reset.html.twig')->end() ->scalarNode(self::FORM_TYPE)->defaultValue(PasswordResetType::class)->end() ->arrayNode(self::FORM_TYPE_OPTIONS) ->addDefaultsIfNotSet() @@ -222,7 +261,7 @@ public function getConfigTreeBuilder() ->children() ->scalarNode(self::EMAIL_SUBJECT)->defaultValue('Password Reset')->end() ->scalarNode(self::EMAIL_ADMIN_TEMPLATE)->defaultValue(null)->end() - ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:community:password-reset-email.html.twig')->end() + ->scalarNode(self::EMAIL_USER_TEMPLATE)->defaultValue('SuluCommunityBundle:Password:reset-email.html.twig')->end() ->end() ->end() ->end() @@ -233,7 +272,7 @@ public function getConfigTreeBuilder() ->children() // Completion Configuration ->scalarNode(self::SERVICE)->defaultValue(null)->end() - ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:community:completion.html.twig')->end() + ->scalarNode(self::TEMPLATE)->defaultValue('SuluCommunityBundle:Completion:form.html.twig')->end() ->scalarNode(self::FORM_TYPE)->defaultValue(CompletionType::class)->end() ->arrayNode(self::FORM_TYPE_OPTIONS) ->addDefaultsIfNotSet() diff --git a/Entity/EmailConfirmationToken.php b/Entity/EmailConfirmationToken.php new file mode 100644 index 00000000..82fc7db5 --- /dev/null +++ b/Entity/EmailConfirmationToken.php @@ -0,0 +1,87 @@ +user = $user; + } + + /** + * Returns id. + * + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * Returns token. + * + * @return string + */ + public function getToken() + { + return $this->token; + } + + /** + * Set token. + * + * @param string $token + * + * @return $this + */ + public function setToken($token) + { + $this->token = $token; + + return $this; + } + + /** + * Returns user. + * + * @return UserInterface + */ + public function getUser() + { + return $this->user; + } +} diff --git a/Entity/EmailConfirmationTokenRepository.php b/Entity/EmailConfirmationTokenRepository.php new file mode 100644 index 00000000..7ad7ec4f --- /dev/null +++ b/Entity/EmailConfirmationTokenRepository.php @@ -0,0 +1,59 @@ +findOneBy(['token' => $token]); + } catch (NonUniqueResultException $e) { + return; + } catch (NoResultException $e) { + return; + } + } + + /** + * Return email-confirmation for given token. + * + * @param UserInterface $user + * + * @return EmailConfirmationToken + */ + public function findByUser($user) + { + try { + return $this->findOneBy(['user' => $user]); + } catch (NonUniqueResultException $e) { + return; + } catch (NoResultException $e) { + return; + } + } +} diff --git a/EventListener/EmailConfirmationListener.php b/EventListener/EmailConfirmationListener.php new file mode 100644 index 00000000..53b52b90 --- /dev/null +++ b/EventListener/EmailConfirmationListener.php @@ -0,0 +1,99 @@ +mailFactory = $mailFactory; + $this->entityManager = $entityManager; + $this->emailConformationRepository = $emailConformationRepository; + $this->tokenGenerator = $tokenGenerator; + } + + /** + * Send confirmation-email if email-address has changed. + * + * @param CommunityEvent $event + */ + public function sendConfirmationOnEmailChange(CommunityEvent $event) + { + $user = $event->getUser(); + if ($user->getEmail() === $user->getContact()->getMainEmail()) { + return; + } + + $entity = $this->emailConformationRepository->findByUser($user); + $token = $this->tokenGenerator->generateToken(); + if (null === $entity) { + $entity = new EmailConfirmationToken($user); + $this->entityManager->persist($entity); + } + + $entity->setToken($token); + $this->entityManager->flush(); + + $this->mailFactory->sendEmails( + Mail::create( + $event->getConfigProperty(Configuration::EMAIL_FROM), + $event->getConfigProperty(Configuration::EMAIL_TO), + $event->getConfigTypeProperty(Configuration::TYPE_EMAIL_CONFIRMATION, Configuration::EMAIL) + )->setUserEmail($user->getContact()->getMainEmail()), + $user, + ['token' => $entity->getToken()] + ); + } +} diff --git a/EventListener/MailListener.php b/EventListener/MailListener.php index 88a64fb7..049a3234 100644 --- a/EventListener/MailListener.php +++ b/EventListener/MailListener.php @@ -84,6 +84,16 @@ public function sendCompletionEmails(CommunityEvent $event) $this->sendTypeEmails($event, Configuration::TYPE_COMPLETION); } + /** + * Send notification email for profile save. + * + * @param CommunityEvent $event + */ + public function sendNotificationSaveProfile(CommunityEvent $event) + { + $this->sendTypeEmails($event, Configuration::TYPE_PROFILE); + } + /** * Send emails for specific type. * diff --git a/Form/Type/ProfileAddressType.php b/Form/Type/ProfileAddressType.php new file mode 100644 index 00000000..712e50f5 --- /dev/null +++ b/Form/Type/ProfileAddressType.php @@ -0,0 +1,53 @@ +add('primaryAddress', 'hidden', ['data' => 1]); + + $builder->add('street', TextType::class, ['required' => false]); + $builder->add('number', TextType::class, ['required' => false]); + $builder->add('zip', TextType::class, ['required' => false]); + $builder->add('city', TextType::class, ['required' => false]); + $builder->add('country', EntityType::class, ['class' => Country::class, 'property' => 'name']); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'data_class' => Address::class, + 'validation_groups' => ['profile'], + ] + ); + } +} diff --git a/Form/Type/ProfileContactAddressType.php b/Form/Type/ProfileContactAddressType.php new file mode 100644 index 00000000..15e45e81 --- /dev/null +++ b/Form/Type/ProfileContactAddressType.php @@ -0,0 +1,47 @@ +add('address', new $options['address_type'](), $options['address_type_options']); + $builder->add('main', 'hidden', [ + 'required' => false, + 'data' => 1, + ]); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'data_class' => ContactAddress::class, + 'address_type' => ProfileAddressType::class, + 'address_type_options' => ['label' => false], + 'validation_groups' => ['profile'], + ] + ); + } +} diff --git a/Form/Type/ProfileContactType.php b/Form/Type/ProfileContactType.php new file mode 100644 index 00000000..b19fc420 --- /dev/null +++ b/Form/Type/ProfileContactType.php @@ -0,0 +1,88 @@ +add( + 'formOfAddress', + ChoiceType::class, + [ + 'choices' => [ + 'contact.contacts.formOfAddress.male', + 'contact.contacts.formOfAddress.female', + ], + 'translation_domain' => 'backend', + 'expanded' => true, + ] + ); + + $builder->add('first_name', TextType::class); + $builder->add('last_name', TextType::class); + $builder->add('main_email', EmailType::class); + $builder->add('avatar', FileType::class, ['mapped' => false, 'required' => false]); + + $builder->add( + 'contactAddresses', + CollectionType::class, + [ + 'label' => false, + 'type' => $options['contact_address_type'], + 'options' => $options['contact_address_type_options'], + ] + ); + $builder->add( + 'notes', + CollectionType::class, + [ + 'label' => false, + 'type' => $options['note_type'], + 'options' => $options['note_type_options'], + ] + ); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'data_class' => Contact::class, + 'contact_address_type' => ProfileContactAddressType::class, + 'contact_address_type_options' => ['label' => false], + 'note_type' => ProfileNoteType::class, + 'note_type_options' => ['label' => false], + 'validation_groups' => ['profile'], + ] + ); + } +} diff --git a/Form/Type/ProfileNoteType.php b/Form/Type/ProfileNoteType.php new file mode 100644 index 00000000..99e32e2c --- /dev/null +++ b/Form/Type/ProfileNoteType.php @@ -0,0 +1,55 @@ +add('value', TextType::class, ['required' => false, 'label' => 'Note']); + $builder->get('value')->addViewTransformer( + new CallbackTransformer( + function ($value) { + return $value; + }, + function ($value) { + if ($value === null) { + return ''; + } + + return $value; + } + ) + ); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(['data_class' => Note::class]); + } +} diff --git a/Form/Type/ProfileType.php b/Form/Type/ProfileType.php new file mode 100644 index 00000000..88b3bd46 --- /dev/null +++ b/Form/Type/ProfileType.php @@ -0,0 +1,50 @@ +add('plainPassword', PasswordType::class, ['mapped' => false, 'required' => false]); + $builder->add('contact', $options['contact_type'], $options['contact_type_options']); + $builder->add('submit', SubmitType::class); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'data_class' => User::class, + 'contact_type' => ProfileContactType::class, + 'contact_type_options' => ['label' => false], + 'validation_groups' => ['profile'], + ] + ); + } +} diff --git a/Form/Type/RegistrationContactType.php b/Form/Type/RegistrationContactType.php index ad929842..2c18dc05 100644 --- a/Form/Type/RegistrationContactType.php +++ b/Form/Type/RegistrationContactType.php @@ -13,6 +13,7 @@ use Sulu\Bundle\ContactBundle\Entity\Contact; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -26,8 +27,8 @@ class RegistrationContactType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('first_name', 'text'); - $builder->add('last_name', 'text'); + $builder->add('first_name', TextType::class); + $builder->add('last_name', TextType::class); } /** diff --git a/Mail/Mail.php b/Mail/Mail.php index c9965077..2d5ac436 100644 --- a/Mail/Mail.php +++ b/Mail/Mail.php @@ -48,6 +48,11 @@ public static function create($from, $to, $config) */ private $to; + /** + * @var string + */ + private $userEmail; + /** * @var string */ @@ -128,4 +133,29 @@ public function getAdminTemplate() { return $this->adminTemplate; } + + /** + * Returns user-email. + * + * @return string + */ + public function getUserEmail() + { + return $this->userEmail; + } + + /** + * Set user-email. + * This setting overwrite the user-email. + * + * @param string $userEmail + * + * @return $this + */ + public function setUserEmail($userEmail) + { + $this->userEmail = $userEmail; + + return $this; + } } diff --git a/Mail/MailFactory.php b/Mail/MailFactory.php index 515e15a0..b6f15552 100644 --- a/Mail/MailFactory.php +++ b/Mail/MailFactory.php @@ -53,6 +53,9 @@ public function __construct(\Swift_Mailer $mailer, EngineInterface $engine, Tran public function sendEmails(Mail $mail, BaseUser $user, $parameters = []) { $email = $user->getEmail(); + if ($mail->getUserEmail()) { + $email = $mail->getUserEmail(); + } $data = array_merge($parameters, ['user' => $user]); // Send User Email diff --git a/Manager/CommunityManager.php b/Manager/CommunityManager.php index 9e3c5759..253cd7c1 100644 --- a/Manager/CommunityManager.php +++ b/Manager/CommunityManager.php @@ -36,6 +36,7 @@ class CommunityManager implements CommunityManagerInterface const EVENT_PASSWORD_FORGOT = 'sulu.community.password_forgot'; const EVENT_PASSWORD_RESETED = 'sulu.community.password_reseted'; const EVENT_COMPLETED = 'sulu.community.completed'; + const EVENT_SAVE_PROFILE = 'sulu.community.save_profile'; /** * @var array @@ -241,10 +242,7 @@ public function passwordReset(User $user) } /** - * Send email to user and admin by type. - * - * @param $type - * @param BaseUser $user + * {@inheritdoc} */ public function sendEmails($type, BaseUser $user) { @@ -259,7 +257,19 @@ public function sendEmails($type, BaseUser $user) } /** - * @return array + * {@inheritdoc} + */ + public function saveProfile(BaseUser $user) + { + // Event + $event = new CommunityEvent($user, $this->config); + $this->eventDispatcher->dispatch(self::EVENT_SAVE_PROFILE, $event); + + return $user; + } + + /** + * {@inheritdoc} */ public function getConfig() { diff --git a/Manager/CommunityManagerInterface.php b/Manager/CommunityManagerInterface.php index 87d18706..6075092d 100644 --- a/Manager/CommunityManagerInterface.php +++ b/Manager/CommunityManagerInterface.php @@ -120,4 +120,13 @@ public function getConfigTypeProperty($type, $property); * @param BaseUser $user */ public function sendEmails($type, BaseUser $user); + + /** + * Save profile for given user. + * + * @param BaseUser $user + * + * @return BaseUser + */ + public function saveProfile(BaseUser $user); } diff --git a/Manager/UserManager.php b/Manager/UserManager.php index 647d1706..75392b0b 100644 --- a/Manager/UserManager.php +++ b/Manager/UserManager.php @@ -103,6 +103,8 @@ public function createUser(User $user, $webspaceKey, $roleName) $contact->setLastName(''); } + $contact->setMainEmail($user->getEmail()); + // Create and Add User Role $userRole = $this->createUserRole($user, $webspaceKey, $roleName); $user->addUserRole($userRole); diff --git a/Resources/config/doctrine/BlacklistItem.orm.xml b/Resources/config/doctrine/BlacklistItem.orm.xml index 58a4c8a3..cdb6d1d1 100644 --- a/Resources/config/doctrine/BlacklistItem.orm.xml +++ b/Resources/config/doctrine/BlacklistItem.orm.xml @@ -4,6 +4,10 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> + + + + diff --git a/Resources/config/doctrine/BlacklistUser.orm.xml b/Resources/config/doctrine/BlacklistUser.orm.xml index 905cb060..7b3af992 100644 --- a/Resources/config/doctrine/BlacklistUser.orm.xml +++ b/Resources/config/doctrine/BlacklistUser.orm.xml @@ -4,6 +4,10 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> + + + + diff --git a/Resources/config/doctrine/EmailConfirmationToken.orm.xml b/Resources/config/doctrine/EmailConfirmationToken.orm.xml new file mode 100644 index 00000000..b4d25bf3 --- /dev/null +++ b/Resources/config/doctrine/EmailConfirmationToken.orm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/Resources/config/routing_website.xml b/Resources/config/routing_website.xml index 3bcbcf70..fa7f0f77 100644 --- a/Resources/config/routing_website.xml +++ b/Resources/config/routing_website.xml @@ -40,5 +40,13 @@ SuluCommunityBundle:Login:index + + SuluCommunityBundle:Profile:index + + + + SuluCommunityBundle:EmailConfirmation:index + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 6f01498d..31f200fa 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -11,6 +11,7 @@ Sulu\Bundle\CommunityBundle\Manager\CommunityManager::EVENT_PASSWORD_FORGOT Sulu\Bundle\CommunityBundle\Manager\CommunityManager::EVENT_PASSWORD_RESETED Sulu\Bundle\CommunityBundle\Manager\CommunityManager::EVENT_COMPLETED + Sulu\Bundle\CommunityBundle\Manager\CommunityManager::EVENT_SAVE_PROFILE @@ -22,8 +23,8 @@ - - + + @@ -38,15 +39,22 @@ - + - - - - - + + + + + + @@ -60,16 +68,16 @@ priority="51"/> - + - - - - + + + + - - + + @@ -105,11 +113,29 @@ SuluCommunityBundle:BlacklistUser + + + + + + + + + + + + + SuluCommunityBundle:EmailConfirmationToken + + - - - + + + diff --git a/Resources/config/validation.xml b/Resources/config/validation.xml index 88d75d2d..3d80b451 100644 --- a/Resources/config/validation.xml +++ b/Resources/config/validation.xml @@ -11,6 +11,7 @@ @@ -18,6 +19,7 @@ @@ -25,6 +27,7 @@ @@ -33,6 +36,7 @@ @@ -50,6 +54,7 @@ @@ -58,6 +63,7 @@ @@ -69,6 +75,7 @@ diff --git a/Resources/views/community/blacklist-confirmed.html.twig b/Resources/views/Blacklist/confirmed.html.twig similarity index 100% rename from Resources/views/community/blacklist-confirmed.html.twig rename to Resources/views/Blacklist/confirmed.html.twig diff --git a/Resources/views/community/blacklist-denied.html.twig b/Resources/views/Blacklist/denied.html.twig similarity index 100% rename from Resources/views/community/blacklist-denied.html.twig rename to Resources/views/Blacklist/denied.html.twig diff --git a/Resources/views/community/blacklisted-email.html.twig b/Resources/views/Blacklist/email.html.twig similarity index 100% rename from Resources/views/community/blacklisted-email.html.twig rename to Resources/views/Blacklist/email.html.twig diff --git a/Resources/views/community/completion.html.twig b/Resources/views/Completion/form.html.twig similarity index 100% rename from Resources/views/community/completion.html.twig rename to Resources/views/Completion/form.html.twig diff --git a/Resources/views/community/confirmation.html.twig b/Resources/views/Confirmation/message.html.twig similarity index 100% rename from Resources/views/community/confirmation.html.twig rename to Resources/views/Confirmation/message.html.twig diff --git a/Resources/views/EmailConfirmation/email.html.twig b/Resources/views/EmailConfirmation/email.html.twig new file mode 100644 index 00000000..508260c9 --- /dev/null +++ b/Resources/views/EmailConfirmation/email.html.twig @@ -0,0 +1,2 @@ +{% set confirmUrl = url('sulu_community.email_confirmation', { token: token }) %} +{{ confirmUrl }} diff --git a/Resources/views/EmailConfirmation/success.html.twig b/Resources/views/EmailConfirmation/success.html.twig new file mode 100644 index 00000000..56ac778a --- /dev/null +++ b/Resources/views/EmailConfirmation/success.html.twig @@ -0,0 +1,15 @@ +{% extends "master.html.twig" %} + +{% block content %} +

{{ 'Profile'|trans }}

+ + {% if success %} +

+ {{ 'email_confirmation_finished'|trans }} +

+ {% else %} +

+ {{ 'email_confirmation_failed'|trans }} +

+ {% endif %} +{% endblock %} diff --git a/Resources/views/Login/login-embed.html.twig b/Resources/views/Login/login-embed.html.twig new file mode 100644 index 00000000..0a2ccaa8 --- /dev/null +++ b/Resources/views/Login/login-embed.html.twig @@ -0,0 +1,22 @@ +{% if app.user %} + {% set media = null %} + {% if app.user.contact.avatar is not null %} + {% set media = sulu_resolve_media(app.user.contact.avatar, request.locale) %} + {% endif %} + + + {% if media is not null %} + + {% endif %} + + {{ app.user.username|default('No username'|trans) }} + + + + {{ 'Logout'|trans }} + +{% else %} + + {{ 'Login'|trans }} + +{% endif %} diff --git a/Resources/views/community/login.html.twig b/Resources/views/Login/login.html.twig similarity index 100% rename from Resources/views/community/login.html.twig rename to Resources/views/Login/login.html.twig diff --git a/Resources/views/community/password-forget-email.html.twig b/Resources/views/Password/forget-email.html.twig similarity index 100% rename from Resources/views/community/password-forget-email.html.twig rename to Resources/views/Password/forget-email.html.twig diff --git a/Resources/views/community/password-forget.html.twig b/Resources/views/Password/forget.html.twig similarity index 100% rename from Resources/views/community/password-forget.html.twig rename to Resources/views/Password/forget.html.twig diff --git a/Resources/views/community/password-reset-email.html.twig b/Resources/views/Password/reset-email.html.twig similarity index 100% rename from Resources/views/community/password-reset-email.html.twig rename to Resources/views/Password/reset-email.html.twig diff --git a/Resources/views/community/password-reset.html.twig b/Resources/views/Password/reset.html.twig similarity index 100% rename from Resources/views/community/password-reset.html.twig rename to Resources/views/Password/reset.html.twig diff --git a/Resources/views/Profile/form.html.twig b/Resources/views/Profile/form.html.twig new file mode 100644 index 00000000..9d460ced --- /dev/null +++ b/Resources/views/Profile/form.html.twig @@ -0,0 +1,25 @@ +{% extends "master.html.twig" %} + +{% form_theme form _self %} + +{% block email_widget %} + {% spaceless %} + + {{ dump() }} + + {{ block('form_widget') }} + + {% endspaceless %} +{% endblock %} + +{% block content %} +

{{ 'Profile'|trans }}

+ + {% if success %} +

+ {{ 'save_profile_finished'|trans }} +

+ {% endif %} + + {{ form(form) }} +{% endblock %} diff --git a/Resources/views/community/registration-email.html.twig b/Resources/views/Registration/email.html.twig similarity index 100% rename from Resources/views/community/registration-email.html.twig rename to Resources/views/Registration/email.html.twig diff --git a/Resources/views/community/registration.html.twig b/Resources/views/Registration/form.html.twig similarity index 100% rename from Resources/views/community/registration.html.twig rename to Resources/views/Registration/form.html.twig diff --git a/Resources/views/community/denied-email.html.twig b/Resources/views/community/denied-email.html.twig deleted file mode 100644 index 42171bad..00000000 --- a/Resources/views/community/denied-email.html.twig +++ /dev/null @@ -1 +0,0 @@ -Denied diff --git a/Resources/views/community/login-embed.html.twig b/Resources/views/community/login-embed.html.twig deleted file mode 100644 index 8e5e2e6a..00000000 --- a/Resources/views/community/login-embed.html.twig +++ /dev/null @@ -1,11 +0,0 @@ -{% if app.user %} - {{ app.user.username }} - - - {{ 'Logout'|trans }} - -{% else %} - - {{ 'Login'|trans }} - -{% endif %} diff --git a/Tests/Functional/Controller/EmailConfirmationControllerTest.php b/Tests/Functional/Controller/EmailConfirmationControllerTest.php new file mode 100644 index 00000000..ea134b26 --- /dev/null +++ b/Tests/Functional/Controller/EmailConfirmationControllerTest.php @@ -0,0 +1,107 @@ +purgeDatabase(); + + /** @var EntityManagerInterface $entityManager */ + $entityManager = $this->getEntityManager(); + + $contact = new Contact(); + $contact->setMainEmail('new@sulu.io'); + $contact->setFirstName('Hikaru'); + $contact->setLastName('Sulu'); + + $entityManager->persist($contact); + $entityManager->flush(); + + $this->user = new User(); + $this->user->setEmail('test@sulu.io'); + $this->user->setUsername('test'); + $this->user->setPassword('test'); + $this->user->setSalt('test'); + $this->user->setLocale('de'); + $this->user->setContact($contact); + + $token = new EmailConfirmationToken($this->user); + $token->setToken('123-123-123'); + + $entityManager->persist($this->user); + $entityManager->persist($token); + $entityManager->flush(); + } + + public function testConfirm() + { + $client = $this->createClient( + [ + 'sulu_context' => 'website', + 'environment' => 'dev', + ] + ); + + $crawler = $client->request('GET', '/profile/email-confirmation?token=123-123-123'); + $this->assertHttpStatusCode(200, $client->getResponse()); + + $this->assertCount(1, $crawler->filter('.success')); + $this->assertCount(0, $crawler->filter('.fail')); + + /** @var EntityManagerInterface $entityManager */ + $entityManager = $this->getEntityManager(); + $entityManager->clear(); + + $this->assertNull($entityManager->getRepository(EmailConfirmationToken::class)->findByToken('123-123-123')); + + $user = $entityManager->find(User::class, $this->user->getId()); + $contact = $user->getContact(); + + $this->assertEquals($user->getEmail(), $contact->getMainEmail()); + } + + public function testConfirmWrongToken() + { + $client = $this->createClient( + [ + 'sulu_context' => 'website', + 'environment' => 'dev', + ] + ); + + $crawler = $client->request('GET', '/profile/email-confirmation?token=312-312-312'); + $this->assertHttpStatusCode(200, $client->getResponse()); + + $this->assertCount(0, $crawler->filter('.success')); + $this->assertCount(1, $crawler->filter('.fail')); + + /** @var EntityManagerInterface $entityManager */ + $entityManager = $this->getEntityManager(); + + $this->assertNotNull($entityManager->getRepository(EmailConfirmationToken::class)->findByToken('123-123-123')); + } +} diff --git a/Tests/Functional/Controller/ProfileControllerTest.php b/Tests/Functional/Controller/ProfileControllerTest.php new file mode 100644 index 00000000..343d0faa --- /dev/null +++ b/Tests/Functional/Controller/ProfileControllerTest.php @@ -0,0 +1,107 @@ +purgeDatabase(); + + /** @var EntityManagerInterface $entityManager */ + $entityManager = $this->getEntityManager(); + + $addressType = new AddressType(); + $addressType->setName('Home'); + $addressType->setId(1); + + $metadata = $entityManager->getClassMetadata(get_class($addressType)); + $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE); + + $country = new Country(); + $country->setName('Star Trek'); + $country->setCode('ST'); + $country->setId(1); + + $metadata = $entityManager->getClassMetadata(get_class($country)); + $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE); + + $entityManager->persist($addressType); + $entityManager->persist($country); + $entityManager->flush(); + } + + public function testForm() + { + $client = $this->createClient( + [ + 'sulu_context' => 'website', + 'environment' => 'dev', + ], + [ + 'PHP_AUTH_USER' => 'test', + 'PHP_AUTH_PW' => 'test', + ] + ); + + $crawler = $client->request('GET', '/profile'); + $this->assertHttpStatusCode(200, $client->getResponse()); + + $this->assertCount(1, $crawler->filter('#profile_contact_first_name')); + $this->assertCount(1, $crawler->filter('#profile_contact_last_name')); + $this->assertCount(1, $crawler->filter('#profile_contact_contactAddresses_0_address_street')); + $this->assertCount(1, $crawler->filter('#profile_contact_contactAddresses_0_address_number')); + $this->assertCount(1, $crawler->filter('#profile_contact_contactAddresses_0_address_zip')); + $this->assertCount(1, $crawler->filter('#profile_contact_contactAddresses_0_address_city')); + $this->assertCount(1, $crawler->filter('#profile_contact_contactAddresses_0_address_country')); + $this->assertCount(1, $crawler->filter('#profile_contact_notes_0_value')); + + $form = $crawler->selectButton('profile[submit]')->form( + [ + 'profile[contact][formOfAddress]' => 0, + 'profile[contact][first_name]' => 'Hikaru', + 'profile[contact][last_name]' => 'Sulu', + 'profile[contact][contactAddresses][0][address][street]' => 'Rathausstraße', + 'profile[contact][contactAddresses][0][address][number]' => 16, + 'profile[contact][contactAddresses][0][address][zip]' => 12351, + 'profile[contact][contactAddresses][0][address][city]' => 'USS Excelsior', + 'profile[contact][contactAddresses][0][address][country]' => 1, + 'profile[contact][notes][0][value]' => 'Test', + 'profile[contact][contactAddresses][0][main]' => 1, + 'profile[_token]' => $crawler->filter('#profile__token')->first()->attr('value'), + ]); + $crawler = $client->submit($form); + $this->assertHttpStatusCode(200, $client->getResponse()); + + $this->assertCount(1, $crawler->filter('.success')); + + /** @var UserRepository $repository */ + $repository = $this->getEntityManager()->getRepository(User::class); + $user = $repository->findOneBy(['username' => 'test']); + + $this->assertEquals('Hikaru Sulu', $user->getFullname()); + $this->assertEquals('Rathausstraße', $user->getContact()->getMainAddress()->getStreet()); + $this->assertEquals(16, $user->getContact()->getMainAddress()->getNumber()); + $this->assertEquals(12351, $user->getContact()->getMainAddress()->getZip()); + $this->assertEquals(1, $user->getContact()->getMainAddress()->getCountry()->getId()); + } +} diff --git a/Tests/Unit/Listener/EmailConfirmationListenerTest.php b/Tests/Unit/Listener/EmailConfirmationListenerTest.php new file mode 100644 index 00000000..0fd6f6c0 --- /dev/null +++ b/Tests/Unit/Listener/EmailConfirmationListenerTest.php @@ -0,0 +1,168 @@ +mailFactory = $this->prophesize(MailFactoryInterface::class); + $this->entityManager = $this->prophesize(EntityManagerInterface::class); + $this->repository = $this->prophesize(EmailConfirmationTokenRepository::class); + $this->tokenGenerator = $this->prophesize(TokenGeneratorInterface::class); + + $this->listener = new EmailConfirmationListener( + $this->mailFactory->reveal(), + $this->entityManager->reveal(), + $this->repository->reveal(), + $this->tokenGenerator->reveal() + ); + + $this->event = $this->prophesize(CommunityEvent::class); + $this->user = $this->prophesize(User::class); + $this->contact = $this->prophesize(Contact::class); + $this->token = $this->prophesize(EmailConfirmationToken::class); + + $this->event->getUser()->willReturn($this->user->reveal()); + $this->event->getConfigProperty(Argument::any())->willReturnArgument(0); + $this->event->getConfigTypeProperty(Argument::cetera())->willReturn( + [ + Configuration::EMAIL_SUBJECT => '', + Configuration::EMAIL_USER_TEMPLATE => '', + Configuration::EMAIL_ADMIN_TEMPLATE => '', + ] + ); + $this->user->getContact()->willReturn($this->contact->reveal()); + $this->token->getUser()->willReturn($this->user->reveal()); + } + + public function testSendConfirmation() + { + $this->user->getEmail()->willReturn('test@sulu.io'); + $this->contact->getMainEmail()->willReturn('new@sulu.io'); + $this->repository->findByUser($this->user->reveal())->willReturn(null); + $this->tokenGenerator->generateToken()->willReturn('123-123-123'); + + $this->entityManager->persist( + Argument::that( + function (EmailConfirmationToken $token) { + return $token->getToken() === '123-123-123' && $token->getUser() === $this->user->reveal(); + } + ) + ); + $this->entityManager->flush(); + + $this->mailFactory->sendEmails( + Argument::type(Mail::class), + $this->user->reveal(), + ['token' => '123-123-123'] + )->shouldBeCalled(); + + $this->listener->sendConfirmationOnEmailChange($this->event->reveal()); + } + + public function testSendConfirmationExistingToken() + { + $this->user->getEmail()->willReturn('test@sulu.io'); + $this->contact->getMainEmail()->willReturn('new@sulu.io'); + $this->repository->findByUser($this->user->reveal())->willReturn($this->token->reveal()); + $this->tokenGenerator->generateToken()->willReturn('123-123-123'); + + $this->token->setToken('123-123-123')->shouldBeCalled(); + $this->token->getToken()->willReturn('123-123-123'); + + $this->entityManager->persist(Argument::any())->shouldNotBeCalled(); + $this->entityManager->flush()->shouldBeCalled(); + + $this->mailFactory->sendEmails( + Argument::type(Mail::class), + $this->user->reveal(), + ['token' => '123-123-123'] + )->shouldBeCalled(); + + $this->listener->sendConfirmationOnEmailChange($this->event->reveal()); + } + + public function testSendConfirmationNoChange() + { + $this->user->getEmail()->willReturn('test@sulu.io'); + $this->contact->getMainEmail()->willReturn('test@sulu.io'); + + $this->entityManager->persist(Argument::any())->shouldNotBeCalled(); + $this->entityManager->flush()->shouldNotBeCalled(); + + $this->mailFactory->sendEmails(Argument::cetera())->shouldNotBeCalled(); + + $this->listener->sendConfirmationOnEmailChange($this->event->reveal()); + } +} diff --git a/Tests/app/config/config_website.yml b/Tests/app/config/config_website.yml index 73dd6180..db30fc44 100644 --- a/Tests/app/config/config_website.yml +++ b/Tests/app/config/config_website.yml @@ -9,41 +9,26 @@ sulu_community: sulu_io: ~ security: - session_fixation_strategy: none + acl: + connection: default access_decision_manager: strategy: affirmative - acl: - connection: default - encoders: - Sulu\Bundle\SecurityBundle\Entity\User: - algorithm: sha512 - iterations: 5000 - encode_as_base64: false + Sulu\Bundle\SecurityBundle\Entity\User: plaintext providers: - sulu: - id: sulu_security.user_provider + testprovider: + id: test_user_provider access_control: - - { path: /profile, roles: ROLE_USER } + - { path: /login, roles: IS_AUTHENTICATED_ANONYMOUSLY } firewalls: - sulu_io: - pattern: ^/ + test: + http_basic: ~ anonymous: ~ - form_login: - login_path: sulu_community.login - check_path: sulu_community.login - logout: - path: sulu_community.logout - target: sulu_community.login - remember_me: - secret: '%secret%' - lifetime: 604800 # 1 week in seconds - path: / sulu_security: checker: