summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Controller/Auth.php17
-rw-r--r--app/Controller/Captcha.php29
-rw-r--r--app/Controller/Config.php3
-rw-r--r--app/Controller/PasswordReset.php120
-rw-r--r--app/Controller/User.php14
-rw-r--r--app/Core/Base.php2
-rw-r--r--app/Helper/App.php12
-rw-r--r--app/Locale/bs_BA/translations.php12
-rw-r--r--app/Locale/cs_CZ/translations.php12
-rw-r--r--app/Locale/da_DK/translations.php12
-rw-r--r--app/Locale/de_DE/translations.php12
-rw-r--r--app/Locale/es_ES/translations.php12
-rw-r--r--app/Locale/fi_FI/translations.php12
-rw-r--r--app/Locale/fr_FR/translations.php12
-rw-r--r--app/Locale/hu_HU/translations.php12
-rw-r--r--app/Locale/id_ID/translations.php12
-rw-r--r--app/Locale/it_IT/translations.php12
-rw-r--r--app/Locale/ja_JP/translations.php12
-rw-r--r--app/Locale/nb_NO/translations.php12
-rw-r--r--app/Locale/nl_NL/translations.php12
-rw-r--r--app/Locale/pl_PL/translations.php12
-rw-r--r--app/Locale/pt_BR/translations.php12
-rw-r--r--app/Locale/pt_PT/translations.php12
-rw-r--r--app/Locale/ru_RU/translations.php12
-rw-r--r--app/Locale/sr_Latn_RS/translations.php12
-rw-r--r--app/Locale/sv_SE/translations.php12
-rw-r--r--app/Locale/th_TH/translations.php12
-rw-r--r--app/Locale/tr_TR/translations.php12
-rw-r--r--app/Locale/zh_CN/translations.php12
-rw-r--r--app/Model/PasswordReset.php93
-rw-r--r--app/Schema/Mysql.php24
-rw-r--r--app/Schema/Postgres.php24
-rw-r--r--app/Schema/Sqlite.php20
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php4
-rw-r--r--app/ServiceProvider/ClassProvider.php4
-rw-r--r--app/ServiceProvider/RouteProvider.php4
-rw-r--r--app/Template/auth/index.php11
-rw-r--r--app/Template/config/application.php10
-rw-r--r--app/Template/password_reset/change.php16
-rw-r--r--app/Template/password_reset/create.php17
-rw-r--r--app/Template/password_reset/email.php6
-rw-r--r--app/Template/user/password_reset.php26
-rw-r--r--app/Template/user/sidebar.php3
-rw-r--r--app/Validator/Base.php36
-rw-r--r--app/Validator/PasswordResetValidator.php98
45 files changed, 825 insertions, 32 deletions
diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php
index cd1dd167..07e66070 100644
--- a/app/Controller/Auth.php
+++ b/app/Controller/Auth.php
@@ -2,8 +2,6 @@
namespace Kanboard\Controller;
-use Gregwar\Captcha\CaptchaBuilder;
-
/**
* Authentication controller
*
@@ -62,21 +60,6 @@ class Auth extends Base
}
/**
- * Display captcha image
- *
- * @access public
- */
- public function captcha()
- {
- $this->response->contentType('image/jpeg');
-
- $builder = new CaptchaBuilder;
- $builder->build();
- $this->sessionStorage->captcha = $builder->getPhrase();
- $builder->output();
- }
-
- /**
* Redirect the user after the authentication
*
* @access private
diff --git a/app/Controller/Captcha.php b/app/Controller/Captcha.php
new file mode 100644
index 00000000..fcf081ea
--- /dev/null
+++ b/app/Controller/Captcha.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Gregwar\Captcha\CaptchaBuilder;
+
+/**
+ * Captcha Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Captcha extends Base
+{
+ /**
+ * Display captcha image
+ *
+ * @access public
+ */
+ public function image()
+ {
+ $this->response->contentType('image/jpeg');
+
+ $builder = new CaptchaBuilder;
+ $builder->build();
+ $this->sessionStorage->captcha = $builder->getPhrase();
+ $builder->output();
+ }
+}
diff --git a/app/Controller/Config.php b/app/Controller/Config.php
index c7097da3..4aee8553 100644
--- a/app/Controller/Config.php
+++ b/app/Controller/Config.php
@@ -40,6 +40,9 @@ class Config extends Base
$values = $this->request->getValues();
switch ($redirect) {
+ case 'application':
+ $values += array('password_reset' => 0);
+ break;
case 'project':
$values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0);
break;
diff --git a/app/Controller/PasswordReset.php b/app/Controller/PasswordReset.php
new file mode 100644
index 00000000..ebc1f77a
--- /dev/null
+++ b/app/Controller/PasswordReset.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Password Reset Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class PasswordReset extends Base
+{
+ /**
+ * Show the form to reset the password
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $this->checkActivation();
+
+ $this->response->html($this->template->layout('password_reset/create', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'no_layout' => true,
+ )));
+ }
+
+ /**
+ * Validate and send the email
+ */
+ public function save()
+ {
+ $this->checkActivation();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->passwordResetValidator->validateCreation($values);
+
+ if ($valid) {
+ $this->sendEmail($values['username']);
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Show the form to set a new password
+ */
+ public function change(array $values = array(), array $errors = array())
+ {
+ $this->checkActivation();
+
+ $token = $this->request->getStringParam('token');
+ $user_id = $this->passwordReset->getUserIdByToken($token);
+
+ if ($user_id !== false) {
+ $this->response->html($this->template->layout('password_reset/change', array(
+ 'token' => $token,
+ 'errors' => $errors,
+ 'values' => $values,
+ 'no_layout' => true,
+ )));
+ }
+
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+
+ /**
+ * Set the new password
+ */
+ public function update(array $values = array(), array $errors = array())
+ {
+ $this->checkActivation();
+
+ $token = $this->request->getStringParam('token');
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->passwordResetValidator->validateModification($values);
+
+ if ($valid) {
+ $user_id = $this->passwordReset->getUserIdByToken($token);
+
+ if ($user_id !== false) {
+ $this->user->update(array('id' => $user_id, 'password' => $values['password']));
+ $this->passwordReset->disable($user_id);
+ }
+
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+
+ $this->change($values, $errors);
+ }
+
+ /**
+ * Send the email
+ */
+ private function sendEmail($username)
+ {
+ $token = $this->passwordReset->create($username);
+
+ if ($token !== false) {
+ $user = $this->user->getByUsername($username);
+
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ t('Password Reset for Kanboard'),
+ $this->template->render('password_reset/email', array('token' => $token))
+ );
+ }
+ }
+
+ /**
+ * Check feature availability
+ */
+ private function checkActivation()
+ {
+ if ($this->config->get('password_reset', 0) == 0) {
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+ }
+}
diff --git a/app/Controller/User.php b/app/Controller/User.php
index 8b6df44c..2a811219 100644
--- a/app/Controller/User.php
+++ b/app/Controller/User.php
@@ -173,6 +173,20 @@ class User extends Base
}
/**
+ * Display last password reset
+ *
+ * @access public
+ */
+ public function passwordReset()
+ {
+ $user = $this->getUser();
+ $this->response->html($this->layout('user/password_reset', array(
+ 'tokens' => $this->passwordReset->getAll($user['id']),
+ 'user' => $user,
+ )));
+ }
+
+ /**
* Display last connections
*
* @access public
diff --git a/app/Core/Base.php b/app/Core/Base.php
index 905c7375..f32c1442 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -69,6 +69,7 @@ use Pimple\Container;
* @property \Kanboard\Model\Link $link
* @property \Kanboard\Model\Notification $notification
* @property \Kanboard\Model\OverdueNotification $overdueNotification
+ * @property \Kanboard\Model\PasswordReset $passwordReset
* @property \Kanboard\Model\Project $project
* @property \Kanboard\Model\ProjectActivity $projectActivity
* @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
@@ -112,6 +113,7 @@ use Pimple\Container;
* @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
* @property \Kanboard\Model\UserMetadata $userMetadata
* @property \Kanboard\Model\Webhook $webhook
+ * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Psr\Log\LoggerInterface $logger
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
diff --git a/app/Helper/App.php b/app/Helper/App.php
index 2015d896..0593795f 100644
--- a/app/Helper/App.php
+++ b/app/Helper/App.php
@@ -13,6 +13,18 @@ use Kanboard\Core\Base;
class App extends Base
{
/**
+ * Get config variable
+ *
+ * @access public
+ * @param string $param
+ * @return mixed
+ */
+ public function config($param)
+ {
+ return $this->config->get($param);
+ }
+
+ /**
* Make sidebar menu active
*
* @access public
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index 777810cd..dc8fc548 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index 70ea873b..8ed054fc 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 37504d66..35ae3036 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 69efb309..264b8aa7 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index e28e1757..e091d1ec 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 76f312fd..3543963f 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index f2e7dda2..016c16d7 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -1089,4 +1089,16 @@ return array(
'Disable two-factor authentication' => 'Désactiver l\'authentification à deux-facteurs',
'Enable two-factor authentication' => 'Activer l\'authentification à deux-facteurs',
'There is no integration registered at the moment.' => 'Il n\'y a aucune intégration enregistrée pour le moment.',
+ 'Password Reset for Kanboard' => 'Réinitialisation du mot de passe pour Kanboard',
+ 'Forgot password?' => 'Mot de passe oublié ?',
+ 'Enable "Forget Password"' => 'Activer la fonctionnalité « Mot de passe oublié »',
+ 'Password Reset' => 'Réinitialisation du mot de passe',
+ 'New password' => 'Nouveau mot de passe',
+ 'Change Password' => 'Changer de mot de passe',
+ 'To reset your password click on this link:' => 'Pour réinitialiser votre mot de passe cliquer sur ce lien :',
+ 'Last Password Reset' => 'Dernières réinitialisation de mot de passe',
+ 'The password has never been reinitialized.' => 'Le mot de passe n\'a jamais été réinitialisé.',
+ 'Creation' => 'Création',
+ 'Expiration' => 'Expiration',
+ 'Password reset history' => 'Historique de la réinitialisation du mot de passe',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 54c4ba40..bbca9914 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index ab296437..57479050 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 7d8df85f..6b1d0b0b 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index f1d20c49..22783e1e 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 67dac98c..cff7e11b 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index c52c20ea..e4f64cfb 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index dc95fbe1..5e70633d 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 7b291f26..fd994e6c 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 44003b1b..b1652dd9 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 4ad5b593..8964b46f 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index 07efed98..fe2401b7 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 46159d8b..a8746d67 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 0706163d..2e29348d 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 3b576485..89b3b33b 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index f5de4725..161d8d2f 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ // 'Forgot password?' => '',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ // 'Last Password Reset' => '',
+ // 'The password has never been reinitialized.' => '',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ // 'Password reset history' => '',
);
diff --git a/app/Model/PasswordReset.php b/app/Model/PasswordReset.php
new file mode 100644
index 00000000..c2d7dde9
--- /dev/null
+++ b/app/Model/PasswordReset.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Password Reset Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class PasswordReset extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'password_reset';
+
+ /**
+ * Token duration (30 minutes)
+ *
+ * @var string
+ */
+ const DURATION = 1800;
+
+ /**
+ * Get all tokens
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getAll($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_creation')->limit(100)->findAll();
+ }
+
+ /**
+ * Generate a new reset token for a user
+ *
+ * @access public
+ * @param string $username
+ * @param integer $expiration
+ * @return boolean|string
+ */
+ public function create($username, $expiration = 0)
+ {
+ $user_id = $this->db->table(User::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id');
+
+ if (! $user_id) {
+ return false;
+ }
+
+ $token = $this->token->getToken();
+
+ $result = $this->db->table(self::TABLE)->insert(array(
+ 'token' => $token,
+ 'user_id' => $user_id,
+ 'date_expiration' => $expiration ?: time() + self::DURATION,
+ 'date_creation' => time(),
+ 'ip' => $this->request->getIpAddress(),
+ 'user_agent' => $this->request->getUserAgent(),
+ 'is_active' => 1,
+ ));
+
+ return $result ? $token : false;
+ }
+
+ /**
+ * Get user id from the token
+ *
+ * @access public
+ * @param string $token
+ * @return integer
+ */
+ public function getUserIdByToken($token)
+ {
+ return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_active', 1)->gte('date_expiration', time())->findOneColumn('user_id');
+ }
+
+ /**
+ * Disable all tokens for a user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function disable($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->update(array('is_active' => 0));
+ }
+}
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index b42e9661..c98e083e 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,25 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 100;
+const VERSION = 101;
+
+function version_101(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE password_reset (
+ token VARCHAR(80) PRIMARY KEY,
+ user_id INT NOT NULL,
+ date_expiration INT NOT NULL,
+ date_creation INT NOT NULL,
+ ip VARCHAR(45) NOT NULL,
+ user_agent VARCHAR(255) NOT NULL,
+ is_active TINYINT(1) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+
+ $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
+}
function version_100(PDO $pdo)
{
@@ -1063,7 +1081,7 @@ function version_12(PDO $pdo)
CREATE TABLE remember_me (
id INT NOT NULL AUTO_INCREMENT,
user_id INT,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
@@ -1079,7 +1097,7 @@ function version_12(PDO $pdo)
id INT NOT NULL AUTO_INCREMENT,
auth_type VARCHAR(25),
user_id INT,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
date_creation INT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 65128eb5..961d8f4d 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,25 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 80;
+const VERSION = 81;
+
+function version_81(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE password_reset (
+ token VARCHAR(80) PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ date_expiration INTEGER NOT NULL,
+ date_creation INTEGER NOT NULL,
+ ip VARCHAR(45) NOT NULL,
+ user_agent VARCHAR(255) NOT NULL,
+ is_active BOOLEAN NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ ");
+
+ $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
+}
function version_80(PDO $pdo)
{
@@ -983,7 +1001,7 @@ function version_1(PDO $pdo)
CREATE TABLE remember_me (
id SERIAL PRIMARY KEY,
user_id INTEGER,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
@@ -996,7 +1014,7 @@ function version_1(PDO $pdo)
id SERIAL PRIMARY KEY,
auth_type VARCHAR(25),
user_id INTEGER,
- ip VARCHAR(40),
+ ip VARCHAR(45),
user_agent VARCHAR(255),
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index f430c00b..f1be0cf1 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,25 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 92;
+const VERSION = 93;
+
+function version_93(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE password_reset (
+ token TEXT PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ date_expiration INTEGER NOT NULL,
+ date_creation INTEGER NOT NULL,
+ ip TEXT NOT NULL,
+ user_agent TEXT NOT NULL,
+ is_active INTEGER NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ ");
+
+ $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
+}
function version_92(PDO $pdo)
{
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
index 6e7c49d9..7617ba95 100644
--- a/app/ServiceProvider/AuthenticationProvider.php
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -126,7 +126,9 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC));
$acl->add('Oauth', array('google', 'github', 'gitlab'), Role::APP_PUBLIC);
- $acl->add('Auth', array('login', 'check', 'captcha'), Role::APP_PUBLIC);
+ $acl->add('Auth', array('login', 'check'), Role::APP_PUBLIC);
+ $acl->add('Captcha', '*', Role::APP_PUBLIC);
+ $acl->add('PasswordReset', '*', Role::APP_PUBLIC);
$acl->add('Webhook', '*', Role::APP_PUBLIC);
$acl->add('Task', 'readonly', Role::APP_PUBLIC);
$acl->add('Board', 'readonly', Role::APP_PUBLIC);
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 2e207aa5..e206ef68 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -32,6 +32,7 @@ class ClassProvider implements ServiceProviderInterface
'Link',
'Notification',
'OverdueNotification',
+ 'PasswordReset',
'Project',
'ProjectActivity',
'ProjectAnalytic',
@@ -84,6 +85,9 @@ class ClassProvider implements ServiceProviderInterface
'UserFilterAutoCompleteFormatter',
'GroupAutoCompleteFormatter',
),
+ 'Validator' => array(
+ 'PasswordResetValidator',
+ ),
'Core' => array(
'DateParser',
'Helper',
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
index e58d50c7..ce66090b 100644
--- a/app/ServiceProvider/RouteProvider.php
+++ b/app/ServiceProvider/RouteProvider.php
@@ -205,6 +205,10 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('oauth/gitlab', 'oauth', 'gitlab');
$container['route']->addRoute('login', 'auth', 'login');
$container['route']->addRoute('logout', 'auth', 'logout');
+
+ // PasswordReset
+ $container['route']->addRoute('forgot-password', 'PasswordReset', 'create');
+ $container['route']->addRoute('forgot-password/change/:token', 'PasswordReset', 'change');
}
return $container;
diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php
index 2f75b113..a1059d6f 100644
--- a/app/Template/auth/index.php
+++ b/app/Template/auth/index.php
@@ -19,17 +19,22 @@
<?php if (isset($captcha) && $captcha): ?>
<?= $this->form->label(t('Enter the text below'), 'captcha') ?>
- <img src="<?= $this->url->href('auth', 'captcha') ?>"/>
- <?= $this->form->text('captcha', $values, $errors, array('required')) ?>
+ <img src="<?= $this->url->href('Captcha', 'image') ?>"/>
+ <?= $this->form->text('captcha', array(), $errors, array('required')) ?>
<?php endif ?>
<?php if (REMEMBER_ME_AUTH): ?>
- <?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br/>
+ <?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br>
<?php endif ?>
<div class="form-actions">
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
</div>
+ <?php if ($this->app->config('password_reset') == 1): ?>
+ <div class="reset-password">
+ <?= $this->url->link(t('Forgot password?'), 'PasswordReset', 'create') ?>
+ </div>
+ <?php endif ?>
</form>
<?php endif ?>
diff --git a/app/Template/config/application.php b/app/Template/config/application.php
index 7d4c811d..ec7d8462 100644
--- a/app/Template/config/application.php
+++ b/app/Template/config/application.php
@@ -7,19 +7,21 @@
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Application URL'), 'application_url') ?>
- <?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?><br/>
+ <?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?>
<p class="form-help"><?= t('Example: http://example.kanboard.net/ (used by email notifications)') ?></p>
<?= $this->form->label(t('Language'), 'application_language') ?>
- <?= $this->form->select('application_language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('application_language', $languages, $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'application_timezone') ?>
- <?= $this->form->select('application_timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('application_timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Date format'), 'application_date_format') ?>
- <?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?><br/>
+ <?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?>
<p class="form-help"><?= t('ISO format is always accepted, example: "%s" and "%s"', date('Y-m-d'), date('Y_m_d')) ?></p>
+ <?= $this->form->checkbox('password_reset', t('Enable "Forget Password"'), 1, $values['password_reset'] == 1) ?>
+
<?= $this->form->label(t('Custom Stylesheet'), 'application_stylesheet') ?>
<?= $this->form->textarea('application_stylesheet', $values, $errors) ?><br/>
diff --git a/app/Template/password_reset/change.php b/app/Template/password_reset/change.php
new file mode 100644
index 00000000..310f0f97
--- /dev/null
+++ b/app/Template/password_reset/change.php
@@ -0,0 +1,16 @@
+<div class="form-login">
+ <h2><?= t('Password Reset') ?></h2>
+ <form method="post" action="<?= $this->url->href('PasswordReset', 'update', array('token' => $token)) ?>">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('New password'), 'password') ?>
+ <?= $this->form->password('password', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Confirmation'), 'confirmation') ?>
+ <?= $this->form->password('confirmation', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue"/>
+ </div>
+ </form>
+</div> \ No newline at end of file
diff --git a/app/Template/password_reset/create.php b/app/Template/password_reset/create.php
new file mode 100644
index 00000000..ef958011
--- /dev/null
+++ b/app/Template/password_reset/create.php
@@ -0,0 +1,17 @@
+<div class="form-login">
+ <h2><?= t('Password Reset') ?></h2>
+ <form method="post" action="<?= $this->url->href('PasswordReset', 'save') ?>">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Username'), 'username') ?>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?>
+
+ <?= $this->form->label(t('Enter the text below'), 'captcha') ?>
+ <img src="<?= $this->url->href('Captcha', 'image') ?>"/>
+ <?= $this->form->text('captcha', array(), $errors, array('required')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue"/>
+ </div>
+ </form>
+</div> \ No newline at end of file
diff --git a/app/Template/password_reset/email.php b/app/Template/password_reset/email.php
new file mode 100644
index 00000000..62788b49
--- /dev/null
+++ b/app/Template/password_reset/email.php
@@ -0,0 +1,6 @@
+<p><?= t('To reset your password click on this link:') ?></p>
+
+<p><?= $this->url->to('PasswordReset', 'change', array('token' => $token), '', true) ?></p>
+
+<hr>
+Kanboard \ No newline at end of file
diff --git a/app/Template/user/password_reset.php b/app/Template/user/password_reset.php
new file mode 100644
index 00000000..b4c9a0c4
--- /dev/null
+++ b/app/Template/user/password_reset.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <h2><?= t('Last Password Reset') ?></h2>
+</div>
+
+<?php if (empty($tokens)): ?>
+ <p class="alert"><?= t('The password has never been reinitialized.') ?></p>
+<?php else: ?>
+ <table class="table-small table-fixed">
+ <tr>
+ <th class="column-20"><?= t('Creation') ?></th>
+ <th class="column-20"><?= t('Expiration') ?></th>
+ <th class="column-5"><?= t('Active') ?></th>
+ <th class="column-15"><?= t('IP address') ?></th>
+ <th><?= t('User agent') ?></th>
+ </tr>
+ <?php foreach ($tokens as $token): ?>
+ <tr>
+ <td><?= dt('%B %e, %Y at %k:%M %p', $token['date_creation']) ?></td>
+ <td><?= dt('%B %e, %Y at %k:%M %p', $token['date_expiration']) ?></td>
+ <td><?= $token['is_active'] == 0 ? t('No') : t('Yes') ?></td>
+ <td><?= $this->e($token['ip']) ?></td>
+ <td><?= $this->e($token['user_agent']) ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index 7756126e..9f745568 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -19,6 +19,9 @@
<li <?= $this->app->checkMenuSelection('user', 'sessions') ?>>
<?= $this->url->link(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
</li>
+ <li <?= $this->app->checkMenuSelection('user', 'passwordReset') ?>>
+ <?= $this->url->link(t('Password reset history'), 'user', 'passwordReset', array('user_id' => $user['id'])) ?>
+ </li>
<?php endif ?>
<?= $this->hook->render('template:user:sidebar:information') ?>
diff --git a/app/Validator/Base.php b/app/Validator/Base.php
new file mode 100644
index 00000000..6c56e2fd
--- /dev/null
+++ b/app/Validator/Base.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Kanboard\Validator;
+
+/**
+ * Base Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class Base extends \Kanboard\Core\Base
+{
+ /**
+ * Execute multiple validators
+ *
+ * @access public
+ * @param array $validators List of validators
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function executeValidators(array $validators, array $values)
+ {
+ $result = false;
+ $errors = array();
+
+ foreach ($validators as $method) {
+ list($result, $errors) = $this->$method($values);
+
+ if (! $result) {
+ break;
+ }
+ }
+
+ return array($result, $errors);
+ }
+}
diff --git a/app/Validator/PasswordResetValidator.php b/app/Validator/PasswordResetValidator.php
new file mode 100644
index 00000000..6f21cbca
--- /dev/null
+++ b/app/Validator/PasswordResetValidator.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use Gregwar\Captcha\CaptchaBuilder;
+
+/**
+ * Password Reset Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class PasswordResetValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ return $this->executeValidators(array('validateFields', 'validateCaptcha'), $values);
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('password', t('The password is required')),
+ new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
+ new Validators\Required('confirmation', t('The confirmation is required')),
+ new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors(),
+ );
+ }
+
+ /**
+ * Validate fields
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ protected function validateFields(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('captcha', t('This value is required')),
+ new Validators\Required('username', t('The username is required')),
+ new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors(),
+ );
+ }
+
+ /**
+ * Validate captcha
+ *
+ * @access protected
+ * @param array $values Form values
+ * @return boolean
+ */
+ protected function validateCaptcha(array $values)
+ {
+ $result = true;
+ $errors = array();
+
+ if (! isset($this->sessionStorage->captcha)) {
+ $result = false;
+ } else {
+ $builder = new CaptchaBuilder;
+ $builder->setPhrase($this->sessionStorage->captcha);
+ $result = $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
+
+ if (! $result) {
+ $errors['captcha'] = array(t('Invalid captcha'));
+ }
+ }
+
+ return array($result, $errors);;
+ }
+}