diff --git a/src/IDF/Form/Password.php b/src/IDF/Form/Password.php new file mode 100644 index 0000000..d7eacbd --- /dev/null +++ b/src/IDF/Form/Password.php @@ -0,0 +1,86 @@ +fields['account'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Your login or email'), + 'help_text' => __('Provide either your login or your email to recover your password.'), + )); + } + + /** + * Validate that a user with this login or email exists. + */ + public function clean_account() + { + $account = mb_strtolower(trim($this->cleaned_data['account'])); + $sql = new Pluf_SQL('email=%s OR login=%s', + array($account, $account)); + $users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen())); + if ($users->count() == 0) { + throw new Pluf_Form_Invalid(__('Sorry, we cannot find a user with this email address or login. Feel free to try again.')); + } + return $account; + } + + /** + * Send the reminder email. + * + */ + function save($commit=true) + { + if (!$this->isValid()) { + throw new Exception(__('Cannot save the model from an invalid form.')); + } + $account = $this->cleaned_data['account']; + $sql = new Pluf_SQL('email=%s OR login=%s', + array($account, $account)); + $users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen())); + foreach ($users as $user) { + $tmpl = new Pluf_Template('idf/user/passrecovery-email.txt'); + $cr = new Pluf_Crypt(md5(Pluf::f('secret_key'))); + $code = trim($cr->encrypt($user->email.':'.$user->id.':'.time()), + '~'); + $code = substr(md5(Pluf::f('secret_key').$code), 0, 2).$code; + $url = Pluf::f('url_base').Pluf_HTTP_URL_urlForView('IDF_Views::passwordRecovery', array($code), array(), false); + $urlic = Pluf::f('url_base').Pluf_HTTP_URL_urlForView('IDF_Views::passwordRecoveryInputCode', array(), array(), false); + $context = new Pluf_Template_Context(array('url' => Pluf_Template::markSafe($url), + 'urlik' => Pluf_Template::markSafe($urlic), + 'user' => Pluf_Template::markSafe($user), + 'key' => Pluf_Template::markSafe($code))); + $email = new Pluf_Mail(Pluf::f('from_email'), $user->email, + __('Password Recovery - InDefero')); + $email->setReturnPath(Pluf::f('bounce_email', Pluf::f('from_email'))); + $email->addTextMessage($tmpl->render($context)); + $email->sendMail(); + } + } +} diff --git a/src/IDF/Form/PasswordInputKey.php b/src/IDF/Form/PasswordInputKey.php new file mode 100644 index 0000000..56aa621 --- /dev/null +++ b/src/IDF/Form/PasswordInputKey.php @@ -0,0 +1,100 @@ +fields['key'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Your verification key'), + 'initial' => '', + 'widget_attrs' => array( + 'size' => 50, + ), + )); + } + + /** + * Validate the key. + */ + public function clean_key() + { + $this->cleaned_data['key'] = trim($this->cleaned_data['key']); + $error = __('We are sorry but this validation key is not valid. Maybe you should directly copy/paste it from your validation email.'); + if (false === ($cres=self::checkKeyHash($this->cleaned_data['key']))) { + throw new Pluf_Form_Invalid($error); + } + $guser = new Pluf_User(); + $sql = new Pluf_SQL('email=%s AND id=%s', + array($cres[0], $cres[1])); + if ($guser->getCount(array('filter' => $sql->gen())) != 1) { + throw new Pluf_Form_Invalid($error); + } + if ((time() - $cres[2]) > 86400) { + throw new Pluf_Form_Invalid(__('Sorry, but this verification key has expired, please restart the password recovery sequence. For security reasons, the verification key is only valid 24h.')); + } + return $this->cleaned_data['key']; + } + + /** + * Save the model in the database. + * + * @param bool Commit in the database or not. If not, the object + * is returned but not saved in the database. + * @return string Url to redirect to the form. + */ + function save($commit=true) + { + if (!$this->isValid()) { + throw new Exception(__('Cannot save an invalid form.')); + } + return Pluf_HTTP_URL_urlForView('IDF_Views::passwordRecovery', + array($this->cleaned_data['key'])); + } + + /** + * Return false or an array with the email, id and timestamp. + * + * This is a static function to be reused by other forms. + * + * @param string Confirmation key + * @return mixed Either false or array(email, id, timestamp) + */ + public static function checkKeyHash($key) + { + $hash = substr($key, 0, 2); + $encrypted = substr($key, 2); + if ($hash != substr(md5(Pluf::f('secret_key').$encrypted), 0, 2)) { + return false; + } + $cr = new Pluf_Crypt(md5(Pluf::f('secret_key'))); + return split(':', $cr->decrypt($encrypted), 3); + } +} diff --git a/src/IDF/Form/PasswordReset.php b/src/IDF/Form/PasswordReset.php new file mode 100644 index 0000000..bb33001 --- /dev/null +++ b/src/IDF/Form/PasswordReset.php @@ -0,0 +1,135 @@ +user = $extra['user']; + $this->fields['key'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Your verification key'), + 'initial' => $extra['key'], + 'widget' => 'Pluf_Form_Widget_HiddenInput', + )); + $this->fields['password'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Your password'), + 'initial' => '', + 'widget' => 'Pluf_Form_Widget_PasswordInput', + 'help_text' => __('Your password must be hard for other people to find it, but easy for you to remember.'), + 'widget_attrs' => array( + 'maxlength' => 50, + 'size' => 15, + ), + )); + $this->fields['password2'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Confirm your password'), + 'initial' => '', + 'widget' => 'Pluf_Form_Widget_PasswordInput', + 'widget_attrs' => array( + 'maxlength' => 50, + 'size' => 15, + ), + )); + + + } + + /** + * Check the passwords. + */ + public function clean() + { + if ($this->cleaned_data['password'] != $this->cleaned_data['password2']) { + throw new Pluf_Form_Invalid(__('The two passwords must be the same.')); + } + return $this->cleaned_data; + } + + + /** + * Validate the key. + */ + public function clean_key() + { + $this->cleaned_data['key'] = trim($this->cleaned_data['key']); + $error = __('We are sorry but this validation key is not valid. Maybe you should directly copy/paste it from your validation email.'); + if (false === ($cres=IDF_Form_PasswordInputKey::checkKeyHash($this->cleaned_data['key']))) { + throw new Pluf_Form_Invalid($error); + } + $guser = new Pluf_User(); + $sql = new Pluf_SQL('email=%s AND id=%s', + array($cres[0], $cres[1])); + if ($guser->getCount(array('filter' => $sql->gen())) != 1) { + throw new Pluf_Form_Invalid($error); + } + if ((time() - $cres[2]) > 86400) { + throw new Pluf_Form_Invalid(__('Sorry, but this verification key has expired, please restart the password recovery sequence. For security reasons, the verification key is only valid 24h.')); + } + return $this->cleaned_data['key']; + } + + function save($commit=true) + { + if (!$this->isValid()) { + throw new Exception(__('Cannot save an invalid form.')); + } + $this->user->setFromFormData($this->cleaned_data); + if ($commit) { + $this->user->update(); + /** + * [signal] + * + * Pluf_User::passwordUpdated + * + * [sender] + * + * IDF_Form_PasswordReset + * + * [description] + * + * This signal is sent when the user reset his + * password from the password recovery page. + * + * [parameters] + * + * array('user' => $user) + * + */ + $params = array('user' => $this->user); + Pluf_Signal::send('Pluf_User::passwordUpdated', + 'IDF_Form_PasswordReset', $params); + } + return $this->user; + } +} diff --git a/src/IDF/Views.php b/src/IDF/Views.php index 0cb538a..03d8680 100644 --- a/src/IDF/Views.php +++ b/src/IDF/Views.php @@ -176,6 +176,98 @@ class IDF_Views $request); } + /** + * Password recovery. + * + * Request the login or the email of the user and if the login or + * email is available in the database, send an email with a key to + * reset the password. + * + */ + function passwordRecoveryAsk($request, $match) + { + $title = __('Password Recovery'); + if ($request->method == 'POST') { + $form = new IDF_Form_Password($request->POST); + if ($form->isValid()) { + $form->save(); + $url = Pluf_HTTP_URL_urlForView('IDF_Views::passwordRecoveryInputCode'); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new IDF_Form_Password(); + } + return Pluf_Shortcuts_RenderToResponse('idf/user/passrecovery-ask.html', + array('page_title' => $title, + 'form' => $form), + $request); + } + + /** + * If the key is valid, provide a nice form to reset the password + * and automatically login the user. + * + * This is also firing the password change event for the plugins. + */ + public function passwordRecovery($request, $match) + { + $title = __('Password Recovery'); + $key = $match[1]; + // first "check", full check is done in the form. + $email_id = IDF_Form_PasswordInputKey::checkKeyHash($key); + if (false == $email_id) { + $url = Pluf_HTTP_URL_urlForView('IDF_Views::passwordRecoveryInputKey'); + return new Pluf_HTTP_Response_Redirect($url); + } + $user = new Pluf_User($email_id[1]); + $extra = array('key' => $key, + 'user' => $user); + if ($request->method == 'POST') { + $form = new IDF_Form_PasswordReset($request->POST, $extra); + if ($form->isValid()) { + $user = $form->save(); + $request->user = $user; + $request->session->clear(); + $request->session->setData('login_time', gmdate('Y-m-d H:i:s')); + $user->last_login = gmdate('Y-m-d H:i:s'); + $user->update(); + $request->user->setMessage(__('Welcome back! Next time, you can use your broswer options to remember the password.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views::index'); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new IDF_Form_PasswordReset(null, $extra); + } + return Pluf_Shortcuts_RenderToResponse('idf/user/passrecovery.html', + array('page_title' => $title, + 'new_user' => $user, + 'form' => $form), + $request); + + } + + /** + * Just a simple input box to provide the code and redirect to + * passwordRecovery + */ + public function passwordRecoveryInputCode($request, $match) + { + $title = __('Password Recovery'); + if ($request->method == 'POST') { + $form = new IDF_Form_PasswordInputKey($request->POST); + if ($form->isValid()) { + $url = $form->save(); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new IDF_Form_PasswordInputKey(); + } + return Pluf_Shortcuts_RenderToResponse('idf/user/passrecovery-inputkey.html', + array('page_title' => $title, + 'form' => $form), + $request); + } + /** * FAQ. */ diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 09c9f69..5838dd0 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -64,24 +64,6 @@ $ctl[] = array('regex' => '#^/u/(.*)/$#', 'model' => 'IDF_Views_User', 'method' => 'view'); -$ctl[] = array('regex' => '#^/register/$#', - 'base' => $base, - 'priority' => 4, - 'model' => 'IDF_Views', - 'method' => 'register'); - -$ctl[] = array('regex' => '#^/register/k/(.*)/$#', - 'base' => $base, - 'priority' => 4, - 'model' => 'IDF_Views', - 'method' => 'registerConfirmation'); - -$ctl[] = array('regex' => '#^/register/ik/$#', - 'base' => $base, - 'priority' => 4, - 'model' => 'IDF_Views', - 'method' => 'registerInputKey'); - $ctl[] = array('regex' => '#^/logout/$#', 'base' => $base, 'priority' => 4, @@ -435,5 +417,44 @@ $ctl[] = array('regex' => '#^/admin/projects/create/$#', 'model' => 'IDF_Views_Admin', 'method' => 'projectCreate'); +// ---------- UTILITY VIEWS ------------------------------- + +$ctl[] = array('regex' => '#^/register/$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views', + 'method' => 'register'); + +$ctl[] = array('regex' => '#^/register/k/(.*)/$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views', + 'method' => 'registerConfirmation'); + +$ctl[] = array('regex' => '#^/register/ik/$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views', + 'method' => 'registerInputKey'); + +$ctl[] = array('regex' => '#^/password/$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views', + 'method' => 'passwordRecoveryAsk'); + +$ctl[] = array('regex' => '#^/password/ik/$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views', + 'method' => 'passwordRecoveryInputCode'); + +$ctl[] = array('regex' => '#^/password/k/(.*)/$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views', + 'method' => 'passwordRecovery'); + + return $ctl; diff --git a/src/IDF/templates/idf/login_form.html b/src/IDF/templates/idf/login_form.html index aea6df9..624d613 100644 --- a/src/IDF/templates/idf/login_form.html +++ b/src/IDF/templates/idf/login_form.html @@ -15,6 +15,7 @@

,

+| {trans 'I lost my password!'}

+{/block} + diff --git a/src/IDF/templates/idf/user/passrecovery-email.txt b/src/IDF/templates/idf/user/passrecovery-email.txt new file mode 100644 index 0000000..503fdad --- /dev/null +++ b/src/IDF/templates/idf/user/passrecovery-email.txt @@ -0,0 +1,25 @@ +{blocktrans}Hello {$user}, + +You lost your password and wanted to recover it. +To provide a new password for your account, you +just have to follow the provided link. You will +get a simple form to provide a new password. + +{$url} + +Alternatively, go to this page: + +{$urlik} + +and provide the following verification key: + +{$key} + +If you are not the one who requested to reset +your password, simply ignore this email, your +password will not be changed. + +Yours faithfully, +The development team. +{/blocktrans} + diff --git a/src/IDF/templates/idf/user/passrecovery-inputkey.html b/src/IDF/templates/idf/user/passrecovery-inputkey.html new file mode 100644 index 0000000..ef58d88 --- /dev/null +++ b/src/IDF/templates/idf/user/passrecovery-inputkey.html @@ -0,0 +1,40 @@ +{extends "idf/base-simple.html"} +{block body} +{if $form.errors} +
+

{trans 'Oups, we found an error in the form.'}

+{if $form.get_top_errors} +{$form.render_top_errors|unsafe} +{/if} +
+{/if} + +
+ + + + + + + + + +
 {$form.f.key.labelTag}:
+{if $form.f.key.errors}{$form.f.key.fieldErrors}{/if} +{$form.f.key|unsafe} +
  | {trans 'Cancel'} +
+
+{/block} +{block context} +
+

{trans 'Instructions'}

+

{trans 'Use your email software to read your emails and open your verification email. Either click directly on the verification link or copy/paste the verification key in the box and submit the form.'}

+

{trans 'Just after providing the confirmation key, you will be able to reset your password and use again this website fully.'}

+
+{/block} +{block javascript} +{/block} + diff --git a/src/IDF/templates/idf/user/passrecovery.html b/src/IDF/templates/idf/user/passrecovery.html new file mode 100644 index 0000000..1e8a5f7 --- /dev/null +++ b/src/IDF/templates/idf/user/passrecovery.html @@ -0,0 +1,53 @@ +{extends "idf/base-simple.html"} +{block body} +{if $form.errors} +
+

{trans 'Oups, please check the form for errors.'}

+{if $form.get_top_errors} +{$form.render_top_errors|unsafe} +{/if} +{if $form.f.key.errors}{$form.f.key.fieldErrors}{/if} +
+{/if} + +
+ + + + + + + + + + + + + + + + + + + + + +
{trans 'Login:'}{$new_user.login}
{trans 'Email:'}{$new_user.email}
{$form.f.password.labelTag}:{if $form.f.password.errors}{$form.f.password.fieldErrors}{/if} +{$form.f.password|unsafe}
+{$form.f.password.help_text} +
{$form.f.password2.labelTag}:{if $form.f.password2.errors}{$form.f.password2.fieldErrors}{/if} +{$form.f.password2|unsafe} +
  | {trans 'Cancel'} +
{$form.f.key|unsafe} +
+{/block} +{block context} +
+

{trans 'This is the last step, but just be sure to have the cookies enabled to log in afterwards.'}

+
+{/block} +{block javascript} +{/block} +