511 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			PHP
		
	
	
	
			
		
		
	
	
			511 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			PHP
		
	
	
	
| <?php
 | |
| /**
 | |
|  * Password recovery
 | |
|  *
 | |
|  * Plugin to reset an account password
 | |
|  *
 | |
|  * @version 1.0
 | |
|  * @original_author Alexander Alferov
 | |
|  *
 | |
|  * @url https://github.com/AlfnRU/roundcube-password_recovery
 | |
|  */
 | |
| 
 | |
| class password_recovery extends rcube_plugin {
 | |
| 
 | |
|     public $task = 'login|logout|settings|mail';
 | |
|     public $rc;
 | |
|     public $db;
 | |
|     public $user;
 | |
|     public $use_password;
 | |
|     public $pwd_conf;
 | |
|     private $pwd;
 | |
|     private $fields;
 | |
|     private $send;
 | |
| 
 | |
|     function init() {
 | |
|         $this->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 = "<a href='./?_task=settings&_action=identities'>". $this->gettext('click_here') ."</a>";
 | |
|             $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);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| ?>
 |