aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/Wallabag
diff options
context:
space:
mode:
Diffstat (limited to 'src/Wallabag')
-rw-r--r--src/Wallabag/CoreBundle/Controller/SecurityController.php122
-rw-r--r--src/Wallabag/CoreBundle/Entity/User.php56
-rw-r--r--src/Wallabag/CoreBundle/Form/Type/ForgotPasswordType.php52
-rw-r--r--src/Wallabag/CoreBundle/Form/Type/ResetPasswordType.php34
-rw-r--r--src/Wallabag/CoreBundle/Resources/config/services.yml8
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/Mail/forgotPassword.txt.twig6
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/Security/checkEmail.html.twig17
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/Security/forgotPassword.html.twig31
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig15
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/Security/reset.html.twig35
-rw-r--r--src/Wallabag/CoreBundle/Tests/Controller/SecurityControllerTest.php142
11 files changed, 513 insertions, 5 deletions
diff --git a/src/Wallabag/CoreBundle/Controller/SecurityController.php b/src/Wallabag/CoreBundle/Controller/SecurityController.php
index c2901da2..fe511db5 100644
--- a/src/Wallabag/CoreBundle/Controller/SecurityController.php
+++ b/src/Wallabag/CoreBundle/Controller/SecurityController.php
@@ -2,9 +2,12 @@
2 2
3namespace Wallabag\CoreBundle\Controller; 3namespace Wallabag\CoreBundle\Controller;
4 4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
5use Symfony\Bundle\FrameworkBundle\Controller\Controller; 7use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Symfony\Component\HttpFoundation\Request; 8use Symfony\Component\HttpFoundation\Request;
7use Symfony\Component\Security\Core\SecurityContext; 9use Symfony\Component\Security\Core\SecurityContext;
10use Wallabag\CoreBundle\Form\Type\ResetPasswordType;
8 11
9class SecurityController extends Controller 12class SecurityController extends Controller
10{ 13{
@@ -25,4 +28,123 @@ class SecurityController extends Controller
25 'error' => $error, 28 'error' => $error,
26 )); 29 ));
27 } 30 }
31
32 /**
33 * Request forgot password: show form
34 *
35 * @Route("/forgot-password", name="forgot_password")
36 * @Method({"GET", "POST"})
37 */
38 public function forgotPasswordAction(Request $request)
39 {
40 $form = $this->createForm('forgot_password');
41 $form->handleRequest($request);
42
43 if ($form->isValid()) {
44 $user = $this->getDoctrine()->getRepository('WallabagCoreBundle:User')->findOneByEmail($form->get('email')->getData());
45
46 // generate "hard" token
47 $user->setConfirmationToken(rtrim(strtr(base64_encode(hash('sha256', uniqid(mt_rand(), true), true)), '+/', '-_'), '='));
48 $user->setPasswordRequestedAt(new \DateTime());
49
50 $em = $this->getDoctrine()->getManager();
51 $em->persist($user);
52 $em->flush();
53
54 $message = \Swift_Message::newInstance()
55 ->setSubject('Reset Password')
56 ->setFrom($this->container->getParameter('from_email'))
57 ->setTo($user->getEmail())
58 ->setBody($this->renderView('WallabagCoreBundle:Mail:forgotPassword.txt.twig', array(
59 'username' => $user->getUsername(),
60 'confirmationUrl' => $this->generateUrl('forgot_password_reset', array('token' => $user->getConfirmationToken()), true),
61 )))
62 ;
63 $this->get('mailer')->send($message);
64
65 return $this->redirect($this->generateUrl('forgot_password_check_email',
66 array('email' => $this->getObfuscatedEmail($user->getEmail()))
67 ));
68 }
69
70 return $this->render('WallabagCoreBundle:Security:forgotPassword.html.twig', array(
71 'form' => $form->createView(),
72 ));
73 }
74
75 /**
76 * Tell the user to check his email provider
77 *
78 * @Route("/forgot-password/check-email", name="forgot_password_check_email")
79 * @Method({"GET"})
80 */
81 public function checkEmailAction(Request $request)
82 {
83 $email = $request->query->get('email');
84
85 if (empty($email)) {
86 // the user does not come from the forgotPassword action
87 return $this->redirect($this->generateUrl('forgot_password'));
88 }
89
90 return $this->render('WallabagCoreBundle:Security:checkEmail.html.twig', array(
91 'email' => $email,
92 ));
93 }
94
95 /**
96 * Reset user password
97 *
98 * @Route("/forgot-password/{token}", name="forgot_password_reset")
99 * @Method({"GET", "POST"})
100 */
101 public function resetAction(Request $request, $token)
102 {
103 $user = $this->getDoctrine()->getRepository('WallabagCoreBundle:User')->findOneByConfirmationToken($token);
104
105 if (null === $user) {
106 throw $this->createNotFoundException(sprintf('No user found with token "%s"', $token));
107 }
108
109 $form = $this->createForm(new ResetPasswordType());
110 $form->handleRequest($request);
111
112 if ($form->isValid()) {
113 $user->setPassword($form->get('new_password')->getData());
114
115 $em = $this->getDoctrine()->getManager();
116 $em->persist($user);
117 $em->flush();
118
119 $this->get('session')->getFlashBag()->add(
120 'notice',
121 'The password has been reset successfully'
122 );
123
124 return $this->redirect($this->generateUrl('login'));
125 }
126
127 return $this->render('WallabagCoreBundle:Security:reset.html.twig', array(
128 'token' => $token,
129 'form' => $form->createView(),
130 ));
131 }
132
133 /**
134 * Get the truncated email displayed when requesting the resetting.
135 *
136 * Keeping only the part following @ in the address.
137 *
138 * @param string $email
139 *
140 * @return string
141 */
142 protected function getObfuscatedEmail($email)
143 {
144 if (false !== $pos = strpos($email, '@')) {
145 $email = '...'.substr($email, $pos);
146 }
147
148 return $email;
149 }
28} 150}
diff --git a/src/Wallabag/CoreBundle/Entity/User.php b/src/Wallabag/CoreBundle/Entity/User.php
index f05c8760..6a7619ac 100644
--- a/src/Wallabag/CoreBundle/Entity/User.php
+++ b/src/Wallabag/CoreBundle/Entity/User.php
@@ -78,6 +78,16 @@ class User implements AdvancedUserInterface, \Serializable
78 private $isActive = true; 78 private $isActive = true;
79 79
80 /** 80 /**
81 * @ORM\Column(name="confirmation_token", type="string", nullable=true)
82 */
83 private $confirmationToken;
84
85 /**
86 * @ORM\Column(name="password_requested_at", type="datetime", nullable=true)
87 */
88 private $passwordRequestedAt;
89
90 /**
81 * @var date 91 * @var date
82 * 92 *
83 * @ORM\Column(name="created_at", type="datetime") 93 * @ORM\Column(name="created_at", type="datetime")
@@ -377,4 +387,50 @@ class User implements AdvancedUserInterface, \Serializable
377 { 387 {
378 return $this->config; 388 return $this->config;
379 } 389 }
390
391 /**
392 * Set confirmationToken
393 *
394 * @param string $confirmationToken
395 * @return User
396 */
397 public function setConfirmationToken($confirmationToken)
398 {
399 $this->confirmationToken = $confirmationToken;
400
401 return $this;
402 }
403
404 /**
405 * Get confirmationToken
406 *
407 * @return string
408 */
409 public function getConfirmationToken()
410 {
411 return $this->confirmationToken;
412 }
413
414 /**
415 * Set passwordRequestedAt
416 *
417 * @param \DateTime $passwordRequestedAt
418 * @return User
419 */
420 public function setPasswordRequestedAt($passwordRequestedAt)
421 {
422 $this->passwordRequestedAt = $passwordRequestedAt;
423
424 return $this;
425 }
426
427 /**
428 * Get passwordRequestedAt
429 *
430 * @return \DateTime
431 */
432 public function getPasswordRequestedAt()
433 {
434 return $this->passwordRequestedAt;
435 }
380} 436}
diff --git a/src/Wallabag/CoreBundle/Form/Type/ForgotPasswordType.php b/src/Wallabag/CoreBundle/Form/Type/ForgotPasswordType.php
new file mode 100644
index 00000000..c278b84f
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Form/Type/ForgotPasswordType.php
@@ -0,0 +1,52 @@
1<?php
2namespace Wallabag\CoreBundle\Form\Type;
3
4use Symfony\Component\Form\AbstractType;
5use Symfony\Component\Form\FormBuilderInterface;
6use Symfony\Component\Validator\Constraints;
7use Symfony\Component\Validator\ExecutionContextInterface;
8use Doctrine\Bundle\DoctrineBundle\Registry;
9
10class ForgotPasswordType extends AbstractType
11{
12 private $doctrine = null;
13
14 public function __construct(Registry $doctrine)
15 {
16 $this->doctrine = $doctrine;
17 }
18
19 public function buildForm(FormBuilderInterface $builder, array $options)
20 {
21 $builder
22 ->add('email', 'email', array(
23 'constraints' => array(
24 new Constraints\Email(),
25 new Constraints\NotBlank(),
26 new Constraints\Callback(array(array($this, 'validateEmail'))),
27 ),
28 ))
29 ;
30 }
31
32 public function getName()
33 {
34 return 'forgot_password';
35 }
36
37 public function validateEmail($email, ExecutionContextInterface $context)
38 {
39 $user = $this->doctrine
40 ->getRepository('WallabagCoreBundle:User')
41 ->findOneByEmail($email);
42
43 if (!$user) {
44 $context->addViolationAt(
45 'email',
46 'No user found with this email',
47 array(),
48 $email
49 );
50 }
51 }
52}
diff --git a/src/Wallabag/CoreBundle/Form/Type/ResetPasswordType.php b/src/Wallabag/CoreBundle/Form/Type/ResetPasswordType.php
new file mode 100644
index 00000000..50ae800b
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Form/Type/ResetPasswordType.php
@@ -0,0 +1,34 @@
1<?php
2namespace Wallabag\CoreBundle\Form\Type;
3
4use Symfony\Component\Form\AbstractType;
5use Symfony\Component\Form\FormBuilderInterface;
6use Symfony\Component\Validator\Constraints;
7
8class ResetPasswordType extends AbstractType
9{
10 public function buildForm(FormBuilderInterface $builder, array $options)
11 {
12 $builder
13 ->add('new_password', 'repeated', array(
14 'type' => 'password',
15 'invalid_message' => 'The password fields must match.',
16 'required' => true,
17 'first_options' => array('label' => 'New password'),
18 'second_options' => array('label' => 'Repeat new password'),
19 'constraints' => array(
20 new Constraints\Length(array(
21 'min' => 8,
22 'minMessage' => 'Password should by at least 8 chars long',
23 )),
24 new Constraints\NotBlank(),
25 ),
26 ))
27 ;
28 }
29
30 public function getName()
31 {
32 return 'change_passwd';
33 }
34}
diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml
index c734a3a5..062e1651 100644
--- a/src/Wallabag/CoreBundle/Resources/config/services.yml
+++ b/src/Wallabag/CoreBundle/Resources/config/services.yml
@@ -22,9 +22,17 @@ services:
22 - @security.context 22 - @security.context
23 - %theme% # default theme from parameters.yml 23 - %theme% # default theme from parameters.yml
24 24
25 # custom form type
25 wallabag_core.form.type.config: 26 wallabag_core.form.type.config:
26 class: Wallabag\CoreBundle\Form\Type\ConfigType 27 class: Wallabag\CoreBundle\Form\Type\ConfigType
27 arguments: 28 arguments:
28 - %liip_theme.themes% 29 - %liip_theme.themes%
29 tags: 30 tags:
30 - { name: form.type, alias: config } 31 - { name: form.type, alias: config }
32
33 wallabag_core.form.type.forgot_password:
34 class: Wallabag\CoreBundle\Form\Type\ForgotPasswordType
35 arguments:
36 - @doctrine
37 tags:
38 - { 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 00000000..631bcb88
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/Mail/forgotPassword.txt.twig
@@ -0,0 +1,6 @@
1Hello {{username}}!
2
3To reset your password - please visit {{confirmationUrl}}
4
5Regards,
6Wallabag 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 00000000..056d65b5
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/Security/checkEmail.html.twig
@@ -0,0 +1,17 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{% trans %}Forgot password{% endtrans %}{% endblock %}
4
5{% block body_class %}login{% endblock %}
6
7{% block menu %}{% endblock %}
8
9{% block content %}
10 <form>
11 <fieldset class="w500p center">
12 <h2 class="mbs txtcenter">{% trans %}Forgot password{% endtrans %}</h2>
13
14 <p>{{ 'An email has been sent to %email%. It contains a link you must click to reset your password.'|trans({'%email%': email}) }}</p>
15 </fieldset>
16 </form>
17{% 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 00000000..4476ea7b
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/Security/forgotPassword.html.twig
@@ -0,0 +1,31 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{% trans %}Forgot password{% endtrans %}{% endblock %}
4
5{% block body_class %}login{% endblock %}
6
7{% block menu %}{% endblock %}
8
9{% block content %}
10 <form action="{{ path('forgot_password') }}" method="post" name="forgotPasswordform">
11 <fieldset class="w500p center">
12 <h2 class="mbs txtcenter">{% trans %}Forgot password{% endtrans %}</h2>
13
14 {{ form_errors(form) }}
15
16 <p>Enter your email address below and we'll send you password reset instructions.</p>
17
18 <div class="row">
19 {{ form_label(form.email) }}
20 {{ form_errors(form.email) }}
21 {{ form_widget(form.email) }}
22 </div>
23
24 <div class="row mts txtcenter">
25 <button type="submit">Send me reset instructions</button>
26 </div>
27 </fieldset>
28
29 {{ form_rest(form) }}
30 </form>
31{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig b/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig
index eb8f08c8..f669574e 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 @@
5{% block body_class %}login{% endblock %} 5{% block body_class %}login{% endblock %}
6 6
7{% block menu %}{% endblock %} 7{% block menu %}{% endblock %}
8{% block messages %}{% endblock %}
8 9
9{% block content %} 10{% block content %}
10 {% if error %}
11 <div>{{ error.message }}</div>
12 {% endif %}
13
14 <form action="{{ path('login_check') }}" method="post" name="loginform"> 11 <form action="{{ path('login_check') }}" method="post" name="loginform">
15 <fieldset class="w500p center"> 12 <fieldset class="w500p center">
16 <h2 class="mbs txtcenter">{% trans %}Login to wallabag{% endtrans %}</h2> 13 <h2 class="mbs txtcenter">{% trans %}Login to wallabag{% endtrans %}</h2>
14 {% if error %}
15 <div>{{ error.message }}</div>
16 {% endif %}
17
18 {% for flashMessage in app.session.flashbag.get('notice') %}
19 <p>{{ flashMessage }}</p>
20 {% endfor %}
17 21
18 <div class="row"> 22 <div class="row">
19 <label class="col w150p" for="username">{% trans %}Username{% endtrans %}</label> 23 <label class="col w150p" for="username">{% trans %}Username{% endtrans %}</label>
@@ -26,7 +30,8 @@
26 </div> 30 </div>
27 31
28 <div class="row mts txtcenter"> 32 <div class="row mts txtcenter">
29 <button type="submit">login</button> 33 <button type="submit">Login</button>
34 <a href="{{ path('forgot_password') }}" class="small">Forgot your password?</a>
30 </div> 35 </div>
31 </fieldset> 36 </fieldset>
32 </form> 37 </form>
diff --git a/src/Wallabag/CoreBundle/Resources/views/Security/reset.html.twig b/src/Wallabag/CoreBundle/Resources/views/Security/reset.html.twig
new file mode 100644
index 00000000..fda88af2
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Resources/views/Security/reset.html.twig
@@ -0,0 +1,35 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2
3{% block title %}{% trans %}Change password{% endtrans %}{% endblock %}
4
5{% block body_class %}login{% endblock %}
6
7{% block menu %}{% endblock %}
8
9{% block content %}
10 <form action="{{ path('forgot_password_reset', {'token': token}) }}" method="post" name="loginform">
11 <fieldset class="w500p center">
12 <h2 class="mbs txtcenter">{% trans %}Change password{% endtrans %}</h2>
13
14 {{ form_errors(form) }}
15
16 <div class="row">
17 {{ form_label(form.new_password.first) }}
18 {{ form_errors(form.new_password.first) }}
19 {{ form_widget(form.new_password.first) }}
20 </div>
21
22 <div class="row">
23 {{ form_label(form.new_password.second) }}
24 {{ form_errors(form.new_password.second) }}
25 {{ form_widget(form.new_password.second) }}
26 </div>
27
28 <div class="row mts txtcenter">
29 <button type="submit">Change password</button>
30 </div>
31 </fieldset>
32
33 {{ form_rest(form) }}
34 </form>
35{% endblock %}
diff --git a/src/Wallabag/CoreBundle/Tests/Controller/SecurityControllerTest.php b/src/Wallabag/CoreBundle/Tests/Controller/SecurityControllerTest.php
index 54cf5073..1dd05f89 100644
--- a/src/Wallabag/CoreBundle/Tests/Controller/SecurityControllerTest.php
+++ b/src/Wallabag/CoreBundle/Tests/Controller/SecurityControllerTest.php
@@ -3,6 +3,8 @@
3namespace Wallabag\CoreBundle\Tests\Controller; 3namespace Wallabag\CoreBundle\Tests\Controller;
4 4
5use Wallabag\CoreBundle\Tests\WallabagTestCase; 5use Wallabag\CoreBundle\Tests\WallabagTestCase;
6use Symfony\Component\Filesystem\Filesystem;
7use Symfony\Component\Finder\Finder;
6 8
7class SecurityControllerTest extends WallabagTestCase 9class SecurityControllerTest extends WallabagTestCase
8{ 10{
@@ -37,4 +39,144 @@ class SecurityControllerTest extends WallabagTestCase
37 39
38 $this->assertContains('Bad credentials', $client->getResponse()->getContent()); 40 $this->assertContains('Bad credentials', $client->getResponse()->getContent());
39 } 41 }
42
43 public function testForgotPassword()
44 {
45 $client = $this->getClient();
46
47 $crawler = $client->request('GET', '/forgot-password');
48
49 $this->assertEquals(200, $client->getResponse()->getStatusCode());
50
51 $this->assertContains('Forgot password', $client->getResponse()->getContent());
52
53 $form = $crawler->filter('button[type=submit]');
54
55 $this->assertCount(1, $form);
56
57 return array(
58 'form' => $form->form(),
59 'client' => $client,
60 );
61 }
62
63 /**
64 * @depends testForgotPassword
65 */
66 public function testSubmitForgotPasswordFail($parameters)
67 {
68 $form = $parameters['form'];
69 $client = $parameters['client'];
70
71 $data = array(
72 'forgot_password[email]' => 'baggy',
73 );
74
75 $client->submit($form, $data);
76
77 $this->assertEquals(200, $client->getResponse()->getStatusCode());
78 $this->assertContains('No user found with this email', $client->getResponse()->getContent());
79 }
80
81 /**
82 * @depends testForgotPassword
83 *
84 * Instead of using collector which slow down the test suite
85 * http://symfony.com/doc/current/cookbook/email/testing.html
86 *
87 * Use a different way where Swift store email as file
88 */
89 public function testSubmitForgotPassword($parameters)
90 {
91 $form = $parameters['form'];
92 $client = $parameters['client'];
93
94 $spoolDir = $client->getKernel()->getContainer()->getParameter('swiftmailer.spool.default.file.path');
95
96 // cleanup pool dir
97 $filesystem = new Filesystem();
98 $filesystem->remove($spoolDir);
99
100 // to use `getCollector` since `collect: false` in config_test.yml
101 $client->enableProfiler();
102
103 $data = array(
104 'forgot_password[email]' => 'bobby@wallabag.org',
105 );
106
107 $client->submit($form, $data);
108
109 $this->assertEquals(302, $client->getResponse()->getStatusCode());
110
111 $crawler = $client->followRedirect();
112
113 $this->assertContains('An email has been sent to', $client->getResponse()->getContent());
114
115 // find every files (ie: emails) inside the spool dir except hidden files
116 $finder = new Finder();
117 $finder
118 ->in($spoolDir)
119 ->ignoreDotFiles(true)
120 ->files();
121
122 $this->assertCount(1, $finder, 'Only one email has been sent');
123
124 foreach ($finder as $file) {
125 $message = unserialize(file_get_contents($file));
126
127 $this->assertInstanceOf('Swift_Message', $message);
128 $this->assertEquals('Reset Password', $message->getSubject());
129 $this->assertEquals('no-reply@wallabag.org', key($message->getFrom()));
130 $this->assertEquals('bobby@wallabag.org', key($message->getTo()));
131 $this->assertContains(
132 'To reset your password - please visit',
133 $message->getBody()
134 );
135 }
136 }
137
138 public function testReset()
139 {
140 $client = $this->getClient();
141 $user = $client->getContainer()
142 ->get('doctrine.orm.entity_manager')
143 ->getRepository('WallabagCoreBundle:User')
144 ->findOneByEmail('bobby@wallabag.org');
145
146 $crawler = $client->request('GET', '/forgot-password/'.$user->getConfirmationToken());
147
148 $this->assertEquals(200, $client->getResponse()->getStatusCode());
149 $this->assertCount(2, $crawler->filter('input[type=password]'));
150 $this->assertCount(1, $form = $crawler->filter('button[type=submit]'));
151 $this->assertCount(1, $form);
152
153 $data = array(
154 'change_passwd[new_password][first]' => 'mypassword',
155 'change_passwd[new_password][second]' => 'mypassword',
156 );
157
158 $client->submit($form->form(), $data);
159
160 $this->assertEquals(302, $client->getResponse()->getStatusCode());
161 $this->assertContains('login', $client->getResponse()->headers->get('location'));
162 }
163
164 public function testResetBadToken()
165 {
166 $client = $this->getClient();
167
168 $client->request('GET', '/forgot-password/UIZOAU29UE902IEPZO');
169
170 $this->assertEquals(404, $client->getResponse()->getStatusCode());
171 }
172
173 public function testCheckEmailWithoutEmail()
174 {
175 $client = $this->getClient();
176
177 $client->request('GET', '/forgot-password/check-email');
178
179 $this->assertEquals(302, $client->getResponse()->getStatusCode());
180 $this->assertContains('forgot-password', $client->getResponse()->headers->get('location'));
181 }
40} 182}