From 299faf5402b5e0998e0e6893568db6aee1d82250 Mon Sep 17 00:00:00 2001 From: alf Date: Sun, 9 Jan 2022 15:50:54 +0300 Subject: [PATCH] ver 1.0 --- .gitignore | 1 + README.md | 16 +- bin/sendsms.sh | 54 ++ composer.json | 24 + config.inc.php.dist | 65 +++ lib/password.php | 132 +++++ lib/send.php | 195 +++++++ localization/en_US.inc | 58 ++ .../en_US/alert_for_admin_to_reset_pw.html | 2 + localization/en_US/reset_pw_body.html | 3 + localization/ru_RU.inc | 58 ++ .../ru_RU/alert_for_admin_to_reset_pw.html | 2 + localization/ru_RU/reset_pw_body.html | 3 + password_recovery.js | 94 ++++ password_recovery.php | 510 ++++++++++++++++++ .../default/templates/new_password_form.html | 71 +++ .../templates/recovery_password_form.html | 32 ++ .../elastic/templates/new_password_form.html | 71 +++ .../templates/recovery_password_form.html | 32 ++ 19 files changed, 1422 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100755 bin/sendsms.sh create mode 100644 composer.json create mode 100644 config.inc.php.dist create mode 100644 lib/password.php create mode 100644 lib/send.php create mode 100644 localization/en_US.inc create mode 100644 localization/en_US/alert_for_admin_to_reset_pw.html create mode 100644 localization/en_US/reset_pw_body.html create mode 100644 localization/ru_RU.inc create mode 100644 localization/ru_RU/alert_for_admin_to_reset_pw.html create mode 100644 localization/ru_RU/reset_pw_body.html create mode 100644 password_recovery.js create mode 100644 password_recovery.php create mode 100644 skins/default/templates/new_password_form.html create mode 100644 skins/default/templates/recovery_password_form.html create mode 100644 skins/elastic/templates/new_password_form.html create mode 100644 skins/elastic/templates/recovery_password_form.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e9e3af --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.inc.php diff --git a/README.md b/README.md index db6a079..cc1d340 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ #### Password Recovery Plugin for Roundcube -Plugin that adds functionality so that a user can create a new password if the original is lost. + +Plugin that adds functionality so that a user can +create a new password if the original is lost. + +To restore the password, the user is asked a secret question, +and/or a confirmation code is sent to an additional email address +and SMS to the phone. + +It is recommended that you use the "SMSTools" package to send SMS. + +When checking and saving a new password, +the password is encrypted using the MD5-Crypt method. +The password is written directly to the Postfix database (mailbox table). + +The Password plugin can also be used when configured accordingly. diff --git a/bin/sendsms.sh b/bin/sendsms.sh new file mode 100755 index 0000000..605e5af --- /dev/null +++ b/bin/sendsms.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# only for Russia!!! +COUNTRY_CODE="7" + +USER="smsd" +GROUP="smsd" +SPOOLDIR="/var/spool/sms/outgoing/" + +if [ -z "$*" ]; then + echo "Usage: ./sendsms.sh \"phone number\" \"message\"" + exit -1 +fi + +DST=$1 +MSG=$2 + +if [[ -z "${DST}" ]]; then + echo "No destination phone number" + exit -1 +fi + +if [[ -z "${MSG}" ]]; then + echo "No message" + exit -1 +fi + +if [[ $DST == +* ]]; then + DST=${DST:1:11} +fi + +if [[ ${#DST} == 10 ]]; then + DST="$COUNTRY_CODE$DST" +elif [[ ${#DST} == 11 && $DST == 8* ]]; then + DST="$COUNTRY_CODE${DST:1:10}" +fi + +if [[ ${#DST} != 11 ]]; then + echo "Error in destination phone number" + exit -1 +fi + +SMS=$(mktemp /tmp/sms_XXXXXXX) +chown :${GROUP} ${SMS} +chmod 0666 ${SMS} + +echo "To: ${DST}" >> $SMS +echo "" >> $SMS +echo $MSG >> $SMS + +mv ${SMS} ${SPOOLDIR} + +echo 1 +exit 1 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9fa232f --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "AlfnRU/password_recovery", + "type": "roundcube-plugin", + "description": "Plugin that adds functionality so that a user can create a new password if the original is lost.", + "homepage": "https://github.com/AlfnRU/roundcube-password_recovery/", + "license": "GPL", + "version": "1.0", + "authors": [ + { + "name": "Alexander Alferov", + "email": "a@alfn.ru", + "role": "Lead" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "roundcube/plugin-installer": ">=0.1.6" + } +} diff --git a/config.inc.php.dist b/config.inc.php.dist new file mode 100644 index 0000000..c981f7b --- /dev/null +++ b/config.inc.php.dist @@ -0,0 +1,65 @@ + 'email_other', + 'phone' => 'phone', + 'question' => 'question', + 'answer' => 'answer', +]; + +// Admin email (this account will receive alerts when an user does not have an alternative email and phone) +$config['pr_admin_email'] = 'postmaster@your.domain.com'; + +// Use secret question/answer to confirmation password recovery +$config['pr_use_question'] = false; + +// Use message with code to confirmation password recovery +$config['pr_use_confirm_code'] = true; + +// Minimum length of new password +$config['pr_password_minimum_length'] = 8; + +// Confirmation code length +$config['pr_confirm_code_length'] = 6; + +// Maximum number of attempts to send confirmation code +$config['pr_confirm_code_count_max'] = 3; + +// Confirmation code duration (in minutes) +$config['pr_confirm_code_validity_time'] = 30; + +// SMTP settings +// $config['pr_default_smtp_server'] = 'tls://your.domain.com'; +// $config['pr_default_smtp_user'] = 'no-reply@your.domain.com'; +// $config['pr_default_smtp_pass'] = 'YOUR_SMTP_USER_PASSWORD'; +$config['pr_default_smtp_server'] = 'localhost'; +$config['pr_default_smtp_user'] = ''; +$config['pr_default_smtp_pass'] = ''; + +// Full path to SMS send function +// This function must accept 2 parameters: phone number and message, +// and return true on success or false on failure +// +// Example of send SMS function using Clickatell HTTP API - see /lib/send.php +// +$config['pr_sms_send_function'] = dirname(__FILE__) . '/bin/sendsms.sh'; + +// Enables logging of password changes into /logs/password.log +$config['pr_password_log'] = true; + +// Set to TRUE if you need write debug messages into /log/console.log +$config['pr_debug'] = false; + +?> diff --git a/lib/password.php b/lib/password.php new file mode 100644 index 0000000..9b07f13 --- /dev/null +++ b/lib/password.php @@ -0,0 +1,132 @@ +pr = $pr_plugin; + $this->rc = $pr_plugin->rc; + } + + function _check_strength($passwd) + { + $min_score = ($this->pr->use_password ? $this->rc->config->get('password_minimum_score') : $this->rc->config->get('pr_password_minimum_score')); + + if (!$min_score) { + return; + } + + if ($this->pr->use_password && ($driver = $this->_load_driver('strength')) && method_exists($driver, 'check_strength')) { + list($score, $reason) = $driver->check_strength($passwd); + } else { + $score = (!preg_match("/[0-9]/", $passwd) || !preg_match("/[^A-Za-z0-9]/", $passwd)) ? 1 : 5; + } + + if ($score < $min_score) { + return $this->pr->gettext('password_check_failed') . (!empty($reason) ? " $reason" : ''); + } + } + + function _save($passwd, $username) + { + if ($res = $this->_check_strength($passwd)) { + return $res; + } + + if (!($driver = $this->_load_driver())) { + return $this->pr->gettext('write_failed'); + } + + $result = $driver->save('', $passwd, $username); + $message = ''; + + if (is_array($result)) { + $message = $result['message']; + $result = $result['code']; + } + + switch ($result) { + case PASSWORD_SUCCESS: + return PASSWORD_SUCCESS; + case PASSWORD_CRYPT_ERROR: + $reason = $this->pr->gettext('crypt_error'); + break; + case PASSWORD_CONNECT_ERROR: + $reason = $this->pr->gettext('connect_error'); + break; + case PASSWORD_IN_HISTORY: + $reason = $this->pr->gettext('password_in_history'); + break; + case PASSWORD_CONSTRAINT_VIOLATION: + $reason = $this->pr->gettext('password_const_viol'); + break; + case PASSWORD_ERROR: + default: + $reason = $this->pr->gettext('write_failed'); + } + + if ($message) { + $reason .= ' ' . $message; + } + + return $reason; + } + + function _load_driver($type = 'password') + { + if (!($type && $driver = $this->rc->config->get('password_' . $type . '_driver'))) { + $driver = $this->rc->config->get('password_driver', 'sql'); + } + + if (empty($this->drivers[$type])) { + $class = "rcube_{$driver}_password"; + $file = __DIR__ . "/../../password/drivers/$driver.php"; + + if (!file_exists($file)) { + rcube::raise_error([ + 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Password plugin: Driver file does not exist ($file)" + ], true, false + ); + return false; + } + + include_once $file; + + if (!class_exists($class, false) || (!method_exists($class, 'save') && !method_exists($class, 'check_strength'))) { + rcube::raise_error([ + 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Password plugin: Broken driver $driver" + ], true, false + ); + return false; + } + + $this->drivers[$type] = new $class; + } + + return $this->drivers[$type]; + } + +} + +?> diff --git a/lib/send.php b/lib/send.php new file mode 100644 index 0000000..86dc6ea --- /dev/null +++ b/lib/send.php @@ -0,0 +1,195 @@ +pr = $pr_plugin; + $this->rc = $pr_plugin->rc; + $this->user = $pr_plugin->user; + } + + // Send SMS over Clickatell API + function send_sms_clickatell($to, $message) { + $clickatell_api_id = 'CHANGEME'; + $clickatell_user = 'CHANGEME'; + $clickatell_password = 'CHANGEME'; + $clickatell_sender = 'CHANGEME'; + + $url = 'https://api.clickatell.com/http/sendmsg?api_id=%s&user=%s&password=%s&to=%s&from=%s&text=%s'; + $url = sprintf($url, $clickatell_api_id, $clickatell_user, $clickatell_password, $to, $clickatell_sender, urlencode($message)); + $result = file_get_contents($url); + return $result !== false; + } + + + + // Send SMS + function send_sms($to, $message) { + $ret = false; + $to = escapeshellarg($to); + $message = escapeshellarg($message); + $sms_send_function = $this->rc->config->get('pr_sms_send_function'); + if ($sms_send_function) { + if (is_file($sms_send_function)) { + $ret = (int) exec("$sms_send_function $to $message"); + } else if (is_callable($sms_send_function)) { + $ret = $sms_send_function($to, $message); + } + } + return $ret !== false || $ret > 0; + } + + // Send E-Mail + function send_email($to, $from, $subject, $body) { + $ctb = md5(rand() . microtime()); + $subject = "=?UTF-8?B?".base64_encode($subject)."?="; + + $headers = "Return-Path: $from\r\n"; + $headers .= "MIME-Version: 1.0\r\n"; + $headers .= "Content-Type: multipart/alternative; boundary=\"=_$ctb\"\r\n"; + $headers .= "Date: " . date('r', time()) . "\r\n"; + $headers .= "From: $from\r\n"; + $headers .= "To: $to\r\n"; + $headers .= "Subject: $subject\r\n"; + $headers .= "Reply-To: $from\r\n"; + + $txt_body = "--=_$ctb"; + $txt_body .= "\r\n"; + $txt_body .= "Content-Transfer-Encoding: 7bit\r\n"; + $txt_body .= "Content-Type: text/plain; charset=" . $this->rc->config->get('default_charset', RCUBE_CHARSET) . "\r\n"; + + $h2t = new rcube_html2text($body, false, true, 0); + $txt = rcube_mime::wordwrap($h2t->get_text(), $this->rc->config->get('line_length', 75), "\r\n"); + $txt = wordwrap($txt, 998, "\r\n", true); + $txt_body .= "$txt\r\n"; + $txt_body .= "--=_$ctb"; + $txt_body .= "\r\n"; + + $msg_body = "Content-Type: multipart/alternative; boundary=\"=_$ctb\"\r\n\r\n"; + $msg_body .= $txt_body; + $msg_body .= "Content-Transfer-Encoding: quoted-printable\r\n"; + $msg_body .= "Content-Type: text/html; charset=" . $this->rc->config->get('default_charset', RCUBE_CHARSET) . "\r\n\r\n"; + $msg_body .= str_replace("=","=3D",$body); + $msg_body .= "\r\n\r\n"; + $msg_body .= "--=_$ctb--"; + $msg_body .= "\r\n\r\n"; + + // send message + if (!is_object($this->rc->smtp)) { + $this->rc->smtp_init(true); + } + + if($this->rc->config->get('smtp_pass') == "%p") { + $this->rc->config->set('smtp_server', $this->rc->config->get('pr_default_smtp_server')); + $this->rc->config->set('smtp_user', $this->rc->config->get('pr_default_smtp_user')); + $this->rc->config->set('smtp_pass', $this->rc->config->get('pr_default_smtp_pass')); + } + + $this->rc->smtp->connect(); + if($this->rc->smtp->send_mail($from, $to, $headers, $msg_body)) { + return true; + } else { + rcube::write_log('errors', 'response:' . print_r($this->rc->smtp->get_response(),true)); + rcube::write_log('errors', 'errors:' . print_r($this->rc->smtp->get_error(),true)); + return false; + } + } + + // Send message to administrator + function send_alert_to_admin($user_requesting_new_password) { + $file = dirname(__FILE__) . "/../localization/" . $this->rc->user->language . "/alert_for_admin_to_reset_pw.html"; + $body = strtr(file_get_contents($file), array('[USER]' => $user_requesting_new_password)); + $subject = $this->pr->gettext('email_subject_admin'); + return $this->send_email( + $this->rc->config->get('pr_admin_email'), + $this->get_email_from($user_requesting_new_password), + $subject, + $body + ); + } + + // Send code to user + function send_confirm_code_to_user() { + $send_email = false; + $send_sms = false; + $confirm_code = $this->generate_confirm_code(); + + if ($confirm_code && $this->pr->set_user_props(['token'=>$confirm_code])) { + // send EMail + if ($this->user['have_altemail']) { + $file = dirname(__FILE__) . "/../localization/" . $this->rc->user->language . "/reset_pw_body.html"; + $link = "http://{$_SERVER['SERVER_NAME']}/?_task=login&_action=plugin.password_recovery&_username=". $this->user['username']; + $body = strtr(file_get_contents($file), ['[LINK]' => $link, '[CODE]' => $confirm_code]); + $subject = $this->pr->gettext('email_subject'); + + $send_email = $this->send_email( + $this->user['altemail'], + $this->get_email_from($this->rc->config->get('pr_admin_email')), + $subject, + $body + ); + } + + // send SMS + if ($this->user['have_phone']) { + $send_sms = $this->send_sms( + $this->user['phone'], + $this->pr->gettext('code') . ": " . $confirm_code + ); + } + + // log & message + if ($send_email || $send_sms) { + $log = "Send password recovery code [". $confirm_code . "] for '" . $this->user['username'] . "'"; + $message = $this->pr->gettext('check_account'); + if ($send_email) { + $log .= " to alt email: '" . $this->user['altemail'] . "'"; + $message .= $this->pr->gettext('check_email'); + } + if ($send_sms) { + if ($send_email) { + $log .= " and"; + $message .= $this->pr->gettext('and'); + } + $log .= " to phone: '" . $this->user['phone'] . "'"; + $message .= $this->pr->gettext('check_sms'); + } + $this->pr->logging($log); + } else { + $this->pr->set_user_props(['token'=>'', 'token_validity'=>'']); + } + } else { + $message = $this->pr->gettext('write_failed'); + } + + return [ + 'send' => ($send_email || $send_sms), + 'message' => $message + ]; + } + + // Generate and return a random code + function generate_confirm_code() { + $code_length = (int) $this->rc->config->get('pr_confirm_code_length', 6); + $code = ""; + $possible = "0123456789"; + while (strlen($code) < $code_length) { + $random = random_int(0, strlen($possible)-1); + $char = substr($possible, $random, 1); + $code .= $char; + $possible = str_replace($char,"",$possible); //removing the used character from the possible + } + return $code; + } + + function get_email_from($email) { + $parts = explode('@',$email); + return 'no-reply@'.$parts[1]; + } +} + +?> diff --git a/localization/en_US.inc b/localization/en_US.inc new file mode 100644 index 0000000..2f77655 --- /dev/null +++ b/localization/en_US.inc @@ -0,0 +1,58 @@ + diff --git a/localization/en_US/alert_for_admin_to_reset_pw.html b/localization/en_US/alert_for_admin_to_reset_pw.html new file mode 100644 index 0000000..4f8a2b6 --- /dev/null +++ b/localization/en_US/alert_for_admin_to_reset_pw.html @@ -0,0 +1,2 @@ +

User [USER] requested a password change but they do not have an phone and alternate email address registered.

+

Please change the password or register an phone and/or alternative email for this user.

diff --git a/localization/en_US/reset_pw_body.html b/localization/en_US/reset_pw_body.html new file mode 100644 index 0000000..0205a36 --- /dev/null +++ b/localization/en_US/reset_pw_body.html @@ -0,0 +1,3 @@ +

Hello!

+

You have requested password recovery.

+

Confirmation code: [CODE]

diff --git a/localization/ru_RU.inc b/localization/ru_RU.inc new file mode 100644 index 0000000..4c5f45d --- /dev/null +++ b/localization/ru_RU.inc @@ -0,0 +1,58 @@ + diff --git a/localization/ru_RU/alert_for_admin_to_reset_pw.html b/localization/ru_RU/alert_for_admin_to_reset_pw.html new file mode 100644 index 0000000..fd06d6c --- /dev/null +++ b/localization/ru_RU/alert_for_admin_to_reset_pw.html @@ -0,0 +1,2 @@ +

Пользователь [USER] запросил(а) сброс пароля, но у него/неё не найдены параметры для восстановления пароля.

+

Пожалуйста, поменяйте пароль пользователю или настройте дополнительный адрес электронной почты и/или телефон для него/неё.

diff --git a/localization/ru_RU/reset_pw_body.html b/localization/ru_RU/reset_pw_body.html new file mode 100644 index 0000000..3b61c6d --- /dev/null +++ b/localization/ru_RU/reset_pw_body.html @@ -0,0 +1,3 @@ +

Здравствуйте!

+

Вы запросили восстановление пароля.

+

Код подтверждения: [CODE]

diff --git a/password_recovery.js b/password_recovery.js new file mode 100644 index 0000000..a755c0a --- /dev/null +++ b/password_recovery.js @@ -0,0 +1,94 @@ + +if (window.rcmail) { + rcmail.addEventListener('init', function(evt) { + var loginform = $('#login-form'); + if (loginform) { + loginform.append('' + rcmail.gettext('forgot_password','password_recovery') + ''); + } + + var newpasswordform = $('#new-password-form'); + if (newpasswordform && rcmail.env.pr_use_confirm_code) { + newpasswordform.append('' + rcmail.gettext('renew_code','password_recovery') + ''); + } + + rcmail.register_command('plugin.password_recovery.cancel', function() { + rcmail.http_request('plugin.password_recovery', { '_a':'cancel' }); + }, true); + + rcmail.register_command('plugin.password_recovery.reset', function() { + var input_username = rcube_find_object('_username'); + if (input_username && input_username.value == '') { + rcmail.alert_dialog(rcmail.get_label('no_username', 'password_recovery'), function() { + input_username.focus(); + return true; + }); + } + else { + rcmail.gui_objects.recoverypasswordform.submit(); + } + }, true); + + rcmail.register_command('plugin.password_recovery.save', function() { + var input_code = rcube_find_object('_code'), + input_answer = rcube_find_object('_answer'), + input_newpassword = rcube_find_object('_newpassword'), + input_newpassword_confirm = rcube_find_object('_newpassword_confirm'); + + if (rcmail.env.pr_use_confirm_code && input_code && input_code.value == '') { + rcmail.alert_dialog(rcmail.get_label('no_code', 'password_recovery'), function() { + input_code.focus(); + return true; + }); + } + else if (rcmail.env.pr_use_question && input_answer && input_answer.value == '') { + rcmail.alert_dialog(rcmail.get_label('no_answer', 'password_recovery'), function() { + input_answer.focus(); + return true; + }); + } + else if (input_newpassword && input_newpassword.value == '') { + rcmail.alert_dialog(rcmail.get_label('no_password', 'password_recovery'), function() { + input_newpassword.focus(); + return true; + }); + } + else if (input_newpassword_confirm && input_newpassword_confirm.value == '') { + rcmail.alert_dialog(rcmail.get_label('no_password_confirm', 'password_recovery'), function() { + input_newpassword_confirm.focus(); + return true; + }); + } + else if (input_newpassword && input_newpassword_confirm && input_newpassword.value != input_newpassword_confirm.value) { + rcmail.alert_dialog(rcmail.get_label('password_inconsistency', 'password_recovery'), function() { + input_newpassword.focus(); + return true; + }); + } + else if (input_newpassword && input_newpassword.value.length < rcmail.env.pr_password_minimum_length) { + rcmail.alert_dialog(rcmail.get_label('password_too_short', 'password_recovery').replace('%d', minimum_length), function() { + input_newpassword.focus(); + return true; + }); + } + else { + rcmail.gui_objects.newpasswordform.submit(); + } + }, true); + + $('input:not(:hidden)').first().focus(); + }); +} + +function forgot_password() { + var url = "./?_task=login&_action=plugin.password_recovery"; +/* var input_user = rcube_find_object('_user'); + if (input_user && input_user.value != '') { + url = url + "&_u=" + input_user.value; + }*/ + document.location.href = url; +} + +function renew_confirm_code() { + rcmail.http_request('plugin.password_recovery', { '_a':'renew', '_username':rcmail.env.pr_username }); +} + diff --git a/password_recovery.php b/password_recovery.php new file mode 100644 index 0000000..6227272 --- /dev/null +++ b/password_recovery.php @@ -0,0 +1,510 @@ +rc = rcmail::get_instance(); + $this->load_config(); + + if (!$this->rc->config->get('pr_use_confirm_code') && !$this->rc->config->get('pr_use_question')) + return; + + $this->init_ui(); + $this->add_texts('localization/'); + + if ($this->rc->task == 'login' || $this->rc->task == 'logout') { + $this->add_hook('render_page', [$this, 'add_labels_to_login_page']); + $this->add_hook('startup', [$this, 'startup']); + $this->register_action('plugin.get_confirm_code_count', [$this, 'get_confirm_code_count']); + } else if ($this->rc->task == 'mail') { + $this->add_hook('render_page', [$this, 'add_labels_to_mail_page']); + $this->add_hook('messages_list', [$this, 'check_identities']); + } else if ($this->rc->task == 'settings') { + $this->add_hook('identity_form', [$this, 'identity_form']); + $this->add_hook('identity_update', [$this, 'identity_update']); + } + + $this->include_script('password_recovery.js'); + } + + /******************* + * STARTUP + *******************/ + + function init_ui() { + if (!$this->fields) $this->fields = $this->rc->config->get('pr_fields'); + if (!$this->db) $this->get_dbh(); + if (!$this->user) $this->get_user_props(); + + $this->use_password = ($this->rc->config->get('pr_use_password_plugin') && $this->rc->plugins->load_plugin('password', true)); + + foreach($this->fields as $field => $field_name){ + $query = "SELECT " . $field_name . " FROM " . $this->rc->config->get('pr_users_table'); + $result = $this->db->query($query); + if (!$result) { + $type = ($field == 'phone' ? 'VARCHAR(30)' : 'VARCHAR(255)'); + $query = "ALTER TABLE " . $this->rc->config->get('pr_users_table') . " ADD " . $field_name . " " . $type . " CHARACTER SET utf8 NOT NULL"; + $result = $this->db->query($query); + } + } + + require_once $this->home . '/lib/send.php'; + $this->send = new password_recovery_send($this); + + require_once $this->home . '/lib/password.php'; + $this->pwd = new password_recovery_pwd($this); + } + + function startup($p) { + if ($this->rc->action != 'plugin.password_recovery' || !isset($_SESSION['temp'])) + return $p; + + switch ($this->get_action()) { + case 'init': + $this->recovery_password_form(); + break; + + case 'renew': + $this->renew_confirm_code(); + break; + + case 'new': + $this->new_password_form(); + break; + + case 'reset': + $this->reset_password(); + break; + + case 'save': + $this->save_password(); + break; + + case 'cancel': + $this->rc->kill_session(); + $this->rc->output->command('redirect', './'); + break; + } + return $p; + } + + function add_labels_to_login_page($p) { + if ($p['template'] == 'login') { + $this->rc->output->add_label('password_recovery.forgot_password'); + } + return $p; + } + + function add_labels_to_mail_page($p) { + $this->rc->output->add_label('password_recovery.no_identities'); + $this->rc->output->add_script('rcmail.message_time = 10000;'); + return $p; + } + + /******************* + * PASSWORD + *******************/ + + // Creating form for reset password + private function recovery_password_form() { + $this->rc->output->add_label( + 'password_recovery.recovery_password', + 'password_recovery.no_username' + ); + + $this->rc->output->set_pagetitle($this->gettext('recovery_password')); + $this->rc->output->add_gui_object('recoverypasswordform', 'recovery-password-form'); + $this->rc->output->send('password_recovery.recovery_password_form'); + } + + // Creating form for new password + private function new_password_form() { + $this->rc->output->add_label( + 'password_recovery.recovery_password', + 'password_recovery.renew_code', + 'password_recovery.count_send_code_maximum', + 'password_recovery.no_code', + 'password_recovery.no_answer', + 'password_recovery.no_password', + 'password_recovery.no_password_confirm', + 'password_recovery.password_inconsistency', + 'password_recovery.password_too_short' + ); + + $this->rc->output->set_pagetitle($this->gettext('recovery_password')); + $this->rc->output->add_gui_object('newpasswordform', 'new-password-form'); + + $password_minimum_length = ($this->use_password ? $this->rc->config->get('password_minimum_length',8) : $this->rc->config->get('pr_password_minimum_length',8)); + + $this->rc->output->set_env('pr_username', $this->user['username']); + $this->rc->output->set_env('pr_question', $this->user['question']); + $this->rc->output->set_env('pr_use_question', (bool) ($this->rc->config->get('pr_use_question') && $this->user['have_answer'])); + $this->rc->output->set_env('pr_use_confirm_code', (bool) ($this->rc->config->get('pr_use_confirm_code') && $this->user['have_code'])); + $this->rc->output->set_env('pr_password_minimum_length', (int) $password_minimum_length); + + $this->rc->output->send('password_recovery.new_password_form'); + } + + // Renew and send confirmation code to user (to alternative email and phone) + private function renew_confirm_code() { + if ($this->get_confirm_code_count() < $this->rc->config->get('pr_confirm_code_count_max')) { + $result = $this->send->send_confirm_code_to_user(); + if ($result['send']) { + $this->update_confirm_code_count(1); + $message = $result['message']; + $type = 'confirmation'; + } else { + $message = $this->gettext('send_failed') . "\n" . $result['message']; + $type = 'error'; + } + } else { + $message = $this->gettext('count_send_code_maximum'); + $type = 'error'; + } + $this->rc->output->command('display_message', $message, $type); + $this->rc->output->send('plugin'); + } + + // Creating and send confirmation code to user (to alternative email and phone) or send message to administrator + private function reset_password() { + // kill remember_me cookies + setcookie ('rememberme_user', '', time()-3600); + setcookie ('rememberme_pass', '', time()-3600); + + $allow_answer = ($this->rc->config->get('pr_use_question') && $this->user['have_answer']); + $allow_code = ($this->rc->config->get('pr_use_confirm_code') && $this->user['have_code']); + + if (!$this->user['username']) { + $message = $this->gettext('user_not_found'); + $type = 'error'; + } else if (!$allow_answer && !$allow_code) { + $this->send->send_alert_to_admin($this->user['username']); + $message = $this->gettext('sent_to_admin'); + $type = 'error'; + } else if ($allow_code) { + $result['send'] = false; + if ($this->user['token_validity'] && !$this->user['token_expired']) { + $this->update_confirm_code_count(1); + $result['send'] = true; + $message = $this->gettext('check_account_notice'); + $type = 'notice'; + } else { + $result = $this->send->send_confirm_code_to_user(); + if ($result['send']) { + $this->update_confirm_code_count(1); + $message = $result['message']; + $type = 'confirmation'; + } else { + $message = $this->gettext('send_failed') . "\n" . $result['message']; + $type = 'error'; + } + } + } + + $this->logging("Password recovery request for '" . $this->user['username'] . "' (IP: " . rcube_utils::remote_addr() . ")"); + if ($message) { + $this->rc->output->command('display_message', $message, $type); + } + + if ($type == 'error') { + $this->recovery_password_form(); + } else { + $this->new_password_form(); + } + } + + // Save new password to DB + private function save_password() { + $params = rcube_utils::request2param(rcube_utils::INPUT_POST); + $this->debug("Save new password: " . print_r($params, true)); + + if ($this->rc->config->get('pr_use_question') && $this->user['have_answer'] && $this->user['answer'] != $params['answer']) { + $message = $this->gettext('answer_failed'); + $type = 'error'; + } else if ($this->rc->config->get('pr_use_confirm_code') && $this->user['token_expired']) { + $message = $this->gettext('code_expired'); + $type = 'error'; + } else if ($this->rc->config->get('pr_use_confirm_code') && $this->user['token'] != $params['code']) { + $message = $this->gettext('code_failed'); + $type = 'error'; + } else { + // props to save + $save = ['token'=>'', 'token_validity'=>'']; + + // check allowed characters according to the configured 'password_charset' option + // by converting the password entered by the user to this charset and back to UTF-8 + $rc_charset = strtoupper($this->rc->output->get_charset()); + $orig_pwd = $params['newpassword']; + $chk_pwd = rcube_charset::convert($orig_pwd, $rc_charset, 'UTF-8'); + $chk_pwd = rcube_charset::convert($chk_pwd, 'UTF-8', $rc_charset); + + // We're doing this for consistence with Roundcube core + $newpassword = rcube_charset::convert($params['newpassword'], $rc_charset, 'UTF-8'); + + if ($chk_pwd != $orig_pwd || preg_match('/[\x00-\x1F\x7F]/', $newpassword)) { + $message = $this->gettext('password_forbidden'); + $type = 'error'; + } else if (!$this->use_password && ($chk_strength = $this->pwd->_check_strength($newpassword))) { + $message = $chk_strength; + $type = 'error'; + } else { + if ($this->use_password) { + $result = $this->pwd->_save($newpassword, $this->user['username']); + if ($result != 0) { + $message = $this->gettext('write_failed') . ": " . $result; + $type = 'error'; + } + } else { + $save['password'] = crypt($newpassword, '$1$' . rcube_utils::random_bytes(9)); + } + + if ($type != 'error' && $this->set_user_props($save)) { + $this->logging("Save new password for '" . $this->user['username'] . "' (IP: " . rcube_utils::remote_addr() . ")"); + $message = $this->gettext('password_changed'); + $type = 'confirmation'; + } else if (!$this->use_password) { + $message = $this->gettext('password_not_changed'); + $type = 'error'; + } + } + } + + $this->rc->output->command('display_message', $message, $type); + + if ($type != 'error') { + $this->rc->kill_session(); + $this->rc->output->command('redirect', './', 2); +// $this->rc->output->send('login'); + } + } + + /******************* + * IDENTITIES + *******************/ + + // Verifying the user identities needed to recover the password + function check_identities() { + if (!isset($_SESSION['show_notice_identities']) && !$this->user['have_altemail'] && !$this->user['have_phone']) { + $link = "". $this->gettext('click_here') .""; + $this->rc->output->command('display_message', sprintf($this->gettext('no_identities'), $link), 'notice'); + $_SESSION['show_notice_identities'] = true; + } + } + + // Handler for 'identity_form' hook (executed on identities form create) + function identity_form($p) { + if (isset($p['form']['addressing']) && !empty($p['record']['identity_id'])) { + $new_fields = []; + foreach ($p['form']['addressing']['content'] as $col => $colprop) { + $new_fields[$col] = $colprop; + if ($col == 'email') { + // add ext fields after 'email' + foreach ($this->fields as $field => $field_name){ + $new_fields[$field] = ['type' => 'text', 'size' => 40, 'label' => $this->gettext($field)]; + } + } + } + $p['form']['addressing']['content'] = $new_fields; + + if($this->user['username']){ + foreach ($this->fields as $field => $field_name){ + $p['record'][$field] = $this->user[$field]; + } + } + } + return $p; + } + + // Handler for identity_update hook (executed on identities form submit) + function identity_update($p) { + $save = []; + foreach ($this->fields as $field => $field_name) { + $save[$field] = rcube_utils::get_input_value("_".$field,rcube_utils::INPUT_POST); + } + + foreach ($save as $par => $val) { + if ((!$this->user['username'] && empty($val)) || ($this->user['username'] && $val == $this->user[$par])) { + unset($save[$par]); + } + } + + if ($save['altemail']) { + $save['altemail'] = rcube_utils::idn_to_ascii($save['altemail']); + if (empty($save['altemail'])) { + $this->rc->output->command('display_message', $this->gettext('altemail_cleared'), 'confirmation'); + } else if($save['altemail'] == $p['record']['email']) { + unset($save['altemail']); + $p['abort'] = true; + $p['message'] = $this->gettext('altemail_match_primary'); + return $p; + } else if(!rcube_utils::check_email($save['altemail'])) { + unset($save['altemail']); + $p['abort'] = true; + $p['message'] = $this->gettext('altemail_invalid'); + return $p; + } + } + + if (count($save)) { + $this->debug("Save user identities: " . print_r($save, true)); + $this->set_user_props($save); + } + + return $p; + } + + // Return array - user props (username must be user@domain.ltd) + function get_user_props($username = null, $with_alias = true) { + if (!$username) { + $username = ($this->rc->get_user_name() ? $this->rc->get_user_name() : rcube_utils::get_input_value("_username", rcube_utils::INPUT_GPC)); + } + + $ret = []; + $user = trim(urldecode($username)); + if ($user) { + // get user row + $query = "SELECT u.user_id, u.username, i.email" . + " FROM users u" . + " INNER JOIN identities i ON i.user_id = u.user_id" . + " WHERE username=?"; + + $result = $this->rc->db->query($query, $user); + + if ($result && ($arr = $this->rc->db->fetch_assoc($result))) { + $fields = []; + foreach ($this->fields as $field => $field_name) { + $fields[] = $field_name . " as " . $field; + } + $query = "SELECT " . implode(",",$fields) . ", token, token_validity, token='' OR token_validity < NOW() as token_expired" . + " FROM " . $this->rc->config->get('pr_users_table') . + " WHERE username=?"; + + $result = $this->db->query($query, $arr['email']); + $ret = array_merge($arr, $this->db->fetch_assoc($result)); + } else { + // for alias (with users_alias plugin) + if ($with_alias && $this->rc->plugins->load_plugin('users_alias', true)) { + $users_alias = new users_alias($this->api); + $result = $users_alias->alias2user(['user' => $user]); + if ($result['user']) { + $ret = $this->get_user_props($result['user'], false); + } + } + } + + $_SESSION['username'] = $ret['username']; //for password plugin + + $ret['have_altemail'] = ($ret['altemail'] && !empty($ret['altemail'])); + $ret['have_phone'] = ($ret['phone'] && !empty($ret['phone'])); + $ret['have_code'] = ($ret['have_altemail'] || $ret['have_phone']); + $ret['have_answer'] = ($ret['question'] && !empty($ret['question']) && $ret['answer'] && !empty($ret['answer'])); + } + + $this->user = $ret; + return $ret; + } + + // Save user props to DB + function set_user_props($props) { + $fields = []; + foreach ($this->fields as $field => $field_name) { + if (isset($props[$field])) { + $fields[] = $field_name . " = '" . $props[$field] . "'"; + } + } + + if (isset($props['token'])) { + if (empty($props['token'])) { + $code_validity_time = 0; + } else { + $code_validity_time = (int) $this->rc->config->get('pr_confirm_code_validity_time', 30); + } + $fields[] = "token = '" . $props['token'] . "', token_validity = NOW() + INTERVAL " . $code_validity_time . " MINUTE"; + } + + if ($props['password']) { + $fields[] = "password = '" . $props['password'] . "'"; + } + + if (count($fields)) { + $query = "UPDATE " . $this->rc->config->get('pr_users_table') . " SET " . implode(",",$fields) . " WHERE username=?"; + $this->db->query($query, $this->user['username']); + $this->get_user_props(); //update user props + $this->debug("Update user '" . $this->user['username'] . "' props: " . print_r($fields, true)); + return $this->db->affected_rows() == 1; + } + return false; + } + + function update_confirm_code_count($plus = 0) { + $count = $this->get_confirm_code_count() + $plus; + $_SESSION['pr_confirm_code_count'] = $count; + } + + function get_confirm_code_count() { + if (!isset($_SESSION['pr_confirm_code_count'])) { + $_SESSION['pr_confirm_code_count'] = 0; + } + return $_SESSION['pr_confirm_code_count']; + } + + /******************* + * service functions + *******************/ + + function get_dbh() { + if (!$this->db) { + if ($dsn = $this->rc->config->get('pr_db_dsn')) { + $this->db = rcube_db::factory($dsn); + $this->db->set_debug((bool)$this->rc->config->get('sql_debug')); + } + else { + $this->db = $this->rc->get_dbh(); + } + } + return $this->db; + } + + function get_action() { + $action = rcube_utils::get_input_value('_a', rcube_utils::INPUT_GPC); + if (!$action || empty($action)) { + $action = 'init'; + } + return $action; + } + + function logging($text) { + if ($this->rc->config->get('pr_password_log') == true) { + rcube::write_log('password', $text); + } + } + + function debug($text) { + if ($this->rc->config->get('pr_debug') == true) { + $msg = (is_array($text) ? print_r($text, true) : $text); + rcube::write_log('console', $msg); + } + } +} + +?> diff --git a/skins/default/templates/new_password_form.html b/skins/default/templates/new_password_form.html new file mode 100644 index 0000000..3b32a34 --- /dev/null +++ b/skins/default/templates/new_password_form.html @@ -0,0 +1,71 @@ + + +


+
+ + + + diff --git a/skins/default/templates/recovery_password_form.html b/skins/default/templates/recovery_password_form.html new file mode 100644 index 0000000..5a2172e --- /dev/null +++ b/skins/default/templates/recovery_password_form.html @@ -0,0 +1,32 @@ + + +


+
+ + + + diff --git a/skins/elastic/templates/new_password_form.html b/skins/elastic/templates/new_password_form.html new file mode 100644 index 0000000..3b32a34 --- /dev/null +++ b/skins/elastic/templates/new_password_form.html @@ -0,0 +1,71 @@ + + +


+
+ + + + diff --git a/skins/elastic/templates/recovery_password_form.html b/skins/elastic/templates/recovery_password_form.html new file mode 100644 index 0000000..5b37437 --- /dev/null +++ b/skins/elastic/templates/recovery_password_form.html @@ -0,0 +1,32 @@ + + +


+
+ + + +