2022-01-09 12:50:54 +00:00
< ? 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 ));
2022-01-26 13:26:52 +00:00
$new_fields = [
'token' => [ 'type' => 'VARCHAR(255)' , 'default' => '' ],
'token_validity' => [ 'type' => 'DATETIME' , 'default' => '2000-01-01 00:00:00' ]
];
2022-01-26 13:42:51 +00:00
foreach ( $this -> fields as $field => $field_name ){
$new_fields [ $field_name ] = [ 'type' => ( $field == 'phone' ? 'VARCHAR(30)' : 'VARCHAR(255)' ), 'default' => '""' ];
}
2022-01-26 13:26:52 +00:00
foreach ( $new_fields as $field_name => $field_props ){
2022-01-09 12:50:54 +00:00
$query = " SELECT " . $field_name . " FROM " . $this -> rc -> config -> get ( 'pr_users_table' );
$result = $this -> db -> query ( $query );
if ( ! $result ) {
2022-01-26 13:42:51 +00:00
$query = " ALTER TABLE " . $this -> rc -> config -> get ( 'pr_users_table' ) . " ADD " . $field_name . " " . $field_props [ 'type' ] . " CHARACTER SET utf8 NOT NULL " . " DEFAULT " . $field_props [ 'default' ] ;
2022-01-09 12:50:54 +00:00
$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 (
2022-01-10 09:13:54 +00:00
'password_recovery.newpassword' ,
'password_recovery.newpassword_confirm' ,
'password_recovery.question' ,
'password_recovery.answer' ,
'password_recovery.code' ,
2022-01-09 12:50:54 +00:00
'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 )];
}
}
}
2022-01-09 19:50:31 +00:00
if ( ! $this -> rc -> config -> get ( 'pr_use_question' )) {
unset ( $new_fields [ 'question' ]);
unset ( $new_fields [ 'answer' ]);
}
2022-01-09 12:50:54 +00:00
$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 );
}
}
}
?>