diff --git a/app/config/config_dev.yml b/app/config/config_dev.yml index efaf396e9..205e0f66b 100644 --- a/app/config/config_dev.yml +++ b/app/config/config_dev.yml @@ -44,5 +44,11 @@ monolog: assetic: use_controller: true -#swiftmailer: -# delivery_address: me@example.com +swiftmailer: + # see http://mailcatcher.me/ + transport: smtp + host: 'localhost' + port: 1025 + username: null + password: null + diff --git a/app/config/config_test.yml b/app/config/config_test.yml index a6ead1e82..00a6bc578 100644 --- a/app/config/config_test.yml +++ b/app/config/config_test.yml @@ -13,7 +13,9 @@ web_profiler: intercept_redirects: false swiftmailer: - disable_delivery: true + # to be able to read emails sent + spool: + type: file doctrine: dbal: diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index 449697505..8f9670113 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -41,3 +41,4 @@ parameters: items_on_page: 12 theme: baggy language: en_US + from_email: no-reply@wallabag.org diff --git a/app/config/security.yml b/app/config/security.yml index e06c89672..90903f310 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -59,4 +59,5 @@ security: - { path: ^/api/salt, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/forgot-password, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/, roles: ROLE_USER } diff --git a/src/Wallabag/CoreBundle/Controller/SecurityController.php b/src/Wallabag/CoreBundle/Controller/SecurityController.php index c2901da2e..5007307af 100644 --- a/src/Wallabag/CoreBundle/Controller/SecurityController.php +++ b/src/Wallabag/CoreBundle/Controller/SecurityController.php @@ -2,9 +2,12 @@ namespace Wallabag\CoreBundle\Controller; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\SecurityContext; +use Wallabag\CoreBundle\Form\Type\ResetPasswordType; class SecurityController extends Controller { @@ -25,4 +28,123 @@ class SecurityController extends Controller 'error' => $error, )); } + + /** + * Request forgot password: show form + * + * @Route("/forgot-password", name="forgot_password") + * @Method({"GET", "POST"}) + */ + public function forgotPasswordAction(Request $request) + { + $form = $this->createForm('forgot_password'); + $form->handleRequest($request); + + if ($form->isValid()) { + $user = $this->getDoctrine()->getRepository('WallabagCoreBundle:User')->findOneByEmail($form->get('email')->getData()); + + // generate "hard" token + $user->setConfirmationToken(rtrim(strtr(base64_encode(hash('sha256', uniqid(mt_rand(), true), true)), '+/', '-_'), '=')); + $user->setPasswordRequestedAt(new \DateTime()); + + $em = $this->getDoctrine()->getManager(); + $em->persist($user); + $em->flush(); + + $message = \Swift_Message::newInstance() + ->setSubject('Reset Password') + ->setFrom($this->container->getParameter('from_email')) + ->setTo($user->getEmail()) + ->setBody($this->renderView('WallabagCoreBundle:Mail:forgotPassword.txt.twig', array( + 'username' => $user->getUsername(), + 'confirmationUrl' => $this->generateUrl('forgot_password_reset', array('token' => $user->getConfirmationToken()), true), + ))) + ; + $this->get('mailer')->send($message); + + return $this->redirect($this->generateUrl('forgot_password_check_email', + array('email' => $this->getObfuscatedEmail($user->getEmail())) + )); + } + + return $this->render('WallabagCoreBundle:Security:forgotPassword.html.twig', array( + 'form' => $form->createView(), + )); + } + + /** + * Tell the user to check his email provider + * + * @Route("/forgot-password/check-email", name="forgot_password_check_email") + * @Method({"GET"}) + */ + public function checkEmailAction(Request $request) + { + $email = $request->query->get('email'); + + if (empty($email)) { + // the user does not come from the forgotPassword action + return $this->redirect($this->generateUrl('forgot_password')); + } + + return $this->render('WallabagCoreBundle:Security:checkEmail.html.twig', array( + 'email' => $email, + )); + } + + /** + * Reset user password + * + * @Route("/forgot-password/{token}", name="forgot_password_reset") + * @Method({"GET", "POST"}) + */ + public function resetAction(Request $request, $token) + { + $user = $this->getDoctrine()->getRepository('WallabagCoreBundle:User')->findOneByConfirmationToken($token); + + if (null === $user) { + $this->createNotFoundException(sprintf('No user found with token "%s"', $token)); + } + + $form = $this->createForm(new ResetPasswordType()); + $form->handleRequest($request); + + if ($form->isValid()) { + $user->setPassword($form->get('new_password')->getData()); + + $em = $this->getDoctrine()->getManager(); + $em->persist($user); + $em->flush(); + + $this->get('session')->getFlashBag()->add( + 'notice', + 'The password has been reset successfully' + ); + + return $this->redirect($this->generateUrl('login')); + } + + return $this->render('WallabagCoreBundle:Security:reset.html.twig', array( + 'token' => $token, + 'form' => $form->createView(), + )); + } + + /** + * Get the truncated email displayed when requesting the resetting. + * + * Keeping only the part following @ in the address. + * + * @param string $email + * + * @return string + */ + protected function getObfuscatedEmail($email) + { + if (false !== $pos = strpos($email, '@')) { + $email = '...'.substr($email, $pos); + } + + return $email; + } } diff --git a/src/Wallabag/CoreBundle/Entity/User.php b/src/Wallabag/CoreBundle/Entity/User.php index f05c8760e..6a7619ac2 100644 --- a/src/Wallabag/CoreBundle/Entity/User.php +++ b/src/Wallabag/CoreBundle/Entity/User.php @@ -77,6 +77,16 @@ class User implements AdvancedUserInterface, \Serializable */ private $isActive = true; + /** + * @ORM\Column(name="confirmation_token", type="string", nullable=true) + */ + private $confirmationToken; + + /** + * @ORM\Column(name="password_requested_at", type="datetime", nullable=true) + */ + private $passwordRequestedAt; + /** * @var date * @@ -377,4 +387,50 @@ class User implements AdvancedUserInterface, \Serializable { return $this->config; } + + /** + * Set confirmationToken + * + * @param string $confirmationToken + * @return User + */ + public function setConfirmationToken($confirmationToken) + { + $this->confirmationToken = $confirmationToken; + + return $this; + } + + /** + * Get confirmationToken + * + * @return string + */ + public function getConfirmationToken() + { + return $this->confirmationToken; + } + + /** + * Set passwordRequestedAt + * + * @param \DateTime $passwordRequestedAt + * @return User + */ + public function setPasswordRequestedAt($passwordRequestedAt) + { + $this->passwordRequestedAt = $passwordRequestedAt; + + return $this; + } + + /** + * Get passwordRequestedAt + * + * @return \DateTime + */ + public function getPasswordRequestedAt() + { + return $this->passwordRequestedAt; + } } diff --git a/src/Wallabag/CoreBundle/Form/Type/ForgotPasswordType.php b/src/Wallabag/CoreBundle/Form/Type/ForgotPasswordType.php new file mode 100644 index 000000000..c278b84fa --- /dev/null +++ b/src/Wallabag/CoreBundle/Form/Type/ForgotPasswordType.php @@ -0,0 +1,52 @@ +doctrine = $doctrine; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('email', 'email', array( + 'constraints' => array( + new Constraints\Email(), + new Constraints\NotBlank(), + new Constraints\Callback(array(array($this, 'validateEmail'))), + ), + )) + ; + } + + public function getName() + { + return 'forgot_password'; + } + + public function validateEmail($email, ExecutionContextInterface $context) + { + $user = $this->doctrine + ->getRepository('WallabagCoreBundle:User') + ->findOneByEmail($email); + + if (!$user) { + $context->addViolationAt( + 'email', + 'No user found with this email', + array(), + $email + ); + } + } +} diff --git a/src/Wallabag/CoreBundle/Form/Type/ResetPasswordType.php b/src/Wallabag/CoreBundle/Form/Type/ResetPasswordType.php new file mode 100644 index 000000000..50ae800b4 --- /dev/null +++ b/src/Wallabag/CoreBundle/Form/Type/ResetPasswordType.php @@ -0,0 +1,34 @@ +add('new_password', 'repeated', array( + 'type' => 'password', + 'invalid_message' => 'The password fields must match.', + 'required' => true, + 'first_options' => array('label' => 'New password'), + 'second_options' => array('label' => 'Repeat new password'), + 'constraints' => array( + new Constraints\Length(array( + 'min' => 8, + 'minMessage' => 'Password should by at least 8 chars long', + )), + new Constraints\NotBlank(), + ), + )) + ; + } + + public function getName() + { + return 'change_passwd'; + } +} diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index c734a3a57..062e1651f 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.yml +++ b/src/Wallabag/CoreBundle/Resources/config/services.yml @@ -22,9 +22,17 @@ services: - @security.context - %theme% # default theme from parameters.yml + # custom form type wallabag_core.form.type.config: class: Wallabag\CoreBundle\Form\Type\ConfigType arguments: - %liip_theme.themes% tags: - { name: form.type, alias: config } + + wallabag_core.form.type.forgot_password: + class: Wallabag\CoreBundle\Form\Type\ForgotPasswordType + arguments: + - @doctrine + tags: + - { name: form.type, alias: forgot_password } diff --git a/src/Wallabag/CoreBundle/Resources/views/Mail/forgotPassword.txt.twig b/src/Wallabag/CoreBundle/Resources/views/Mail/forgotPassword.txt.twig new file mode 100644 index 000000000..631bcb887 --- /dev/null +++ b/src/Wallabag/CoreBundle/Resources/views/Mail/forgotPassword.txt.twig @@ -0,0 +1,6 @@ +Hello {{username}}! + +To reset your password - please visit {{confirmationUrl}} + +Regards, +Wallabag bot diff --git a/src/Wallabag/CoreBundle/Resources/views/Security/checkEmail.html.twig b/src/Wallabag/CoreBundle/Resources/views/Security/checkEmail.html.twig new file mode 100644 index 000000000..056d65b50 --- /dev/null +++ b/src/Wallabag/CoreBundle/Resources/views/Security/checkEmail.html.twig @@ -0,0 +1,17 @@ +{% extends "WallabagCoreBundle::layout.html.twig" %} + +{% block title %}{% trans %}Forgot password{% endtrans %}{% endblock %} + +{% block body_class %}login{% endblock %} + +{% block menu %}{% endblock %} + +{% block content %} +
+{% endblock %} diff --git a/src/Wallabag/CoreBundle/Resources/views/Security/forgotPassword.html.twig b/src/Wallabag/CoreBundle/Resources/views/Security/forgotPassword.html.twig new file mode 100644 index 000000000..4476ea7b3 --- /dev/null +++ b/src/Wallabag/CoreBundle/Resources/views/Security/forgotPassword.html.twig @@ -0,0 +1,31 @@ +{% extends "WallabagCoreBundle::layout.html.twig" %} + +{% block title %}{% trans %}Forgot password{% endtrans %}{% endblock %} + +{% block body_class %}login{% endblock %} + +{% block menu %}{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig b/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig index eb8f08c82..f669574e8 100644 --- a/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig @@ -5,15 +5,19 @@ {% block body_class %}login{% endblock %} {% block menu %}{% endblock %} +{% block messages %}{% endblock %} {% block content %} - {% if error %} -