diff --git a/Controller/PasswordController.php b/Controller/PasswordController.php
index f6ea24bea..71382262e 100644
--- a/Controller/PasswordController.php
+++ b/Controller/PasswordController.php
@@ -10,6 +10,12 @@ use Chill\MainBundle\Entity\User;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Psr\Log\LoggerInterface;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Validator\Constraints\Callback;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Chill\MainBundle\Security\PasswordRecover\RecoverPasswordHelper;
+use Symfony\Component\HttpFoundation\Response;
+use Chill\MainBundle\Security\PasswordRecover\TokenManager;
class PasswordController extends Controller
{
@@ -31,14 +37,30 @@ class PasswordController extends Controller
*/
protected $chillLogger;
+ /**
+ *
+ * @var RecoverPasswordHelper
+ */
+ protected $recoverPasswordHelper;
+
+ /**
+ *
+ * @var TokenManager
+ */
+ protected $tokenManager;
+
public function __construct(
LoggerInterface $chillLogger,
UserPasswordEncoderInterface $passwordEncoder,
+ RecoverPasswordHelper $recoverPasswordHelper,
+ TokenManager $tokenManager,
TranslatorInterface $translator
) {
$this->chillLogger = $chillLogger;
$this->passwordEncoder = $passwordEncoder;
$this->translator = $translator;
+ $this->tokenManager = $tokenManager;
+ $this->recoverPasswordHelper = $recoverPasswordHelper;
}
/**
@@ -97,14 +119,148 @@ class PasswordController extends Controller
*/
private function passwordForm(User $user)
{
- return $this->createForm(
+ return $this
+ ->createForm(
UserPasswordType::class,
[],
- [ 'user' => $this->getUser() ]
+ [ 'user' => $user ]
)
->add('submit', SubmitType::class, array('label' => 'Change password'))
;
}
+ public function recoverAction(Request $request)
+ {
+ $query = $request->query;
+ $username = $query->get(TokenManager::USERNAME_CANONICAL);
+ $hash = $query->getAlnum(TokenManager::HASH);
+ $token = $query->getAlnum(TokenManager::TOKEN);
+ $timestamp = $query->getInt(TokenManager::TIMESTAMP);
+ $user = $this->getDoctrine()->getRepository(User::class)
+ ->findOneByUsernameCanonical($username);
+
+ if (NULL === $user) {
+ throw $this->createNotFoundException(sprintf('User %s not found', $username));
+ }
+
+ if (TRUE !== $this->tokenManager->verify($hash, $token, $user, $timestamp)) {
+ return new Response("Invalid token", Response::HTTP_FORBIDDEN);
+ }
+
+ $form = $this->passwordForm($user);
+ $form->remove('actual_password');
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $password = $form->get('new_password')->getData();
+ $user->setPassword($this->passwordEncoder->encodePassword($user, $password));
+ // logging for prod
+ $this
+ ->chillLogger
+ ->notice(
+ 'setting new password for user',
+ array(
+ 'user' => $user->getUsername()
+ )
+ );
+
+ $this->getDoctrine()->getManager()->flush();
+
+ return $this->redirectToRoute('password_request_recover_changed');
+ }
+
+ return $this->render('@ChillMain/Password/recover_password_form.html.twig', [
+ 'form' => $form->createView()
+ ]);
+
+ }
+
+ public function changeConfirmedAction()
+ {
+ return $this->render('@ChillMain/Password/recover_password_changed.html.twig');
+ }
+
+ public function requestRecoverAction(Request $request)
+ {
+ $form = $this->requestRecoverForm();
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ /* @var $qb \Doctrine\ORM\QueryBuilder */
+ $qb = $this->getDoctrine()->getManager()
+ ->createQueryBuilder();
+ $qb->select('u')
+ ->from(User::class, 'u')
+ ->where($qb->expr()->eq('u.usernameCanonical', 'UNACCENT(LOWER(:pattern))'))
+ ->orWhere($qb->expr()->eq('u.emailCanonical', 'UNACCENT(LOWER(:pattern))' ))
+ ->setParameter('pattern', $form->get('username_or_email')->getData())
+ ;
+
+ $user = $qb->getQuery()->getSingleResult();
+
+ if (empty($user->getEmail())) {
+ $this->addFlash('error', $this->translator->trans('This account does not have an email address. '
+ . 'Please ask your administrator to renew your password.'));
+ } else {
+ $this->recoverPasswordHelper->sendRecoverEmail($user,
+ (new \DateTimeImmutable('now'))->add(new \DateInterval('PT30M')));
+
+ // logging for prod
+ $this
+ ->chillLogger
+ ->notice(
+ 'Sending an email for password recovering',
+ array(
+ 'user' => $user->getUsername()
+ )
+ );
+
+ return $this->redirectToRoute('password_request_recover_confirm');
+ }
+ }
+
+ return $this->render('@ChillMain/Password/request_recover_password.html.twig', [
+ 'form' => $form->createView()
+ ]);
+ }
+
+ public function requestRecoverConfirmAction()
+ {
+ return $this->render('@ChillMain/Password/request_recover_password_confirm.html.twig');
+ }
+
+ protected function requestRecoverForm()
+ {
+ $builder = $this->createFormBuilder();
+ $builder
+ ->add('username_or_email', TextType::class, [
+ 'label' => 'Username or email',
+ 'constraints' => [
+ new Callback([
+ 'callback' => function($pattern, ExecutionContextInterface $context, $payload) {
+ $qb = $this->getDoctrine()->getManager()
+ ->createQueryBuilder();
+ $qb->select('COUNT(u)')
+ ->from(User::class, 'u')
+ ->where($qb->expr()->eq('u.usernameCanonical', 'UNACCENT(LOWER(:pattern))'))
+ ->orWhere($qb->expr()->eq('u.emailCanonical', 'UNACCENT(LOWER(:pattern))' ))
+ ->setParameter('pattern', $pattern)
+ ;
+
+ if ((int) $qb->getQuery()->getSingleScalarResult() !== 1) {
+ $context->addViolation('This username or email does not exists');
+ }
+ }
+ ])
+ ]
+ ])
+ ->add('submit', SubmitType::class, [
+ 'label' => 'Request recover'
+ ]);
+
+ return $builder->getForm();
+ }
}
diff --git a/DependencyInjection/ChillMainExtension.php b/DependencyInjection/ChillMainExtension.php
index df0686f34..13a335108 100644
--- a/DependencyInjection/ChillMainExtension.php
+++ b/DependencyInjection/ChillMainExtension.php
@@ -79,6 +79,9 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$container->setParameter('chill_main.pagination.item_per_page',
$config['pagination']['item_per_page']);
+ $container->setParameter('chill_main.notifications',
+ $config['notifications']);
+
// add the key 'widget' without the key 'enable'
$container->setParameter('chill_main.widgets',
isset($config['widgets']['homepage']) ?
@@ -100,6 +103,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$loader->load('services/fixtures.yml');
$loader->load('services/menu.yml');
$loader->load('services/security.yml');
+ $loader->load('services/notification.yml');
}
public function getConfiguration(array $config, ContainerBuilder $container)
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 52fce914f..c573a92be 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -67,6 +67,24 @@ class Configuration implements ConfigurationInterface
->end() // end of integer 'item_per_page'
->end() // end of children
->end() // end of pagination
+ ->arrayNode('notifications')
+ ->children()
+ ->scalarNode('from_email')
+ ->cannotBeEmpty()
+ ->end()
+ ->scalarNode('from_name')
+ ->cannotBeEmpty()
+ ->end()
+ ->enumNode('scheme')
+ ->cannotBeEmpty()
+ ->values(['http', 'https'])
+ ->defaultValue('https')
+ ->end()
+ ->scalarNode('host')
+ ->cannotBeEmpty()
+ ->end()
+ ->end()
+ ->end() // end of notifications
->arrayNode('widgets')
->canBeEnabled()
->canBeUnset()
diff --git a/Notification/Mailer.php b/Notification/Mailer.php
new file mode 100644
index 000000000..2b784b3a1
--- /dev/null
+++ b/Notification/Mailer.php
@@ -0,0 +1,168 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
-
-
+
+
+
+