/** execute the selected login procedure
 *
 * The login process is controlled via the parameter 'login'
 * provided by the user via 'index.php?login=N or via the
 * 'action' property in a HTML-form. These numbers correspond to the
 * LOGIN_PROCEDURE_* constants defined near the top of this file.
 * Here's a reminder:
 *
 * 1. LOGIN_PROCEDURE_NORMAL this is the usual procedure for logging in
 * 2. LOGIN_PROCEDURE_CHANGE_PASSWORD this is the procedure to change the user's password
 * 3. LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER this is phase 1 of the 'forgot password' procedure
 * 4. LOGIN_PROCEDURE_SEND_BYPASS this is phase 2 of the 'forgot password' procedure
 *
 * Note that this routine only returns to the caller after either a succesful
 * regular login (i.e. after completing LOGIN_PROCEDURE_NORMAL). All the
 * other variants and error conditions yield another screen and an immediate exit and
 * hence no return to caller. If this routine returns, it returns the user_id
 * of the authenticated user (the primary key into the users table). It is up to the caller to
 * retrieve additional information about this user; any information read from the database
 * during login is discarded. This prevents password hashes still lying around.
 * 
 * Note that a successful login has the side effect of garbage collection:
 * whenever we experience a successful login any obsolete sessions are removed.
 * This makes sure that locked records eventually will be unlocked, once the corresponding
 * session no longer exists. The garbage collection routine is also called from
 * the PHP session handler every once in a while, but here we make 100% sure that
 * garbage is collected at least at every login. (Note: obsolete sessions should not
 * be a problem for visitors that are not logged in, because you have to be logged in
 * to be able to lock a record.)
 *
 * @param int $procedure the login procedure to execute
 * @param string $message the message to display when showing the login dialog
 * @return void|int no return on error, otherwise the user_id of the authenticated user
 * @uses $CFG
 * @uses dbsession_setup()
 * @uses dbsession_garbage_collection()
 */
function was_login($procedure = LOGIN_PROCEDURE_SHOWLOGIN, $message = '')
{
    global $CFG;
    // get rid of the cookie (if we received one) and the corresponding session;
    // the user's browser should NOT present us with a cookie during the login procedures.
    if (isset($_COOKIE[$CFG->session_name])) {
        was_logout();
        exit;
    }
    // If this IP-address is currently blacklisted, tell the visitor access is denied
    if (login_is_blacklisted($_SERVER['REMOTE_ADDR'])) {
        show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('access_denied', 'loginlib'));
        exit;
    }
    switch (intval($procedure)) {
        case LOGIN_PROCEDURE_NORMAL:
            if (isset($_POST['login_username']) && isset($_POST['login_password'])) {
                $username = magic_unquote($_POST['login_username']);
                $password = magic_unquote($_POST['login_password']);
                $user = authenticate_user(BY_PASSWORD, $username, $password);
                if ($user !== FALSE) {
                    login_failure_reset($_SERVER['REMOTE_ADDR']);
                    if (db_bool_is(FALSE, $user['bypass_mode'])) {
                        // valid credentials and not in a bypass mode: start session and return user_id
                        require_once $CFG->progdir . '/lib/dbsessionlib.php';
                        dbsession_setup($CFG->session_name);
                        $session_key = dbsession_create($user['user_id'], $_SERVER['REMOTE_ADDR']);
                        session_id($session_key);
                        session_start();
                        $_SESSION['session_id'] = dbsession_get_session_id($session_key);
                        $user_id = intval($user['user_id']);
                        $_SESSION['user_id'] = $user_id;
                        $_SESSION['redirect'] = $user['redirect'];
                        $_SESSION['language_key'] = $user['language_key'];
                        $_SESSION['remote_addr'] = $_SERVER['REMOTE_ADDR'];
                        $_SESSION['salt'] = $CFG->salt;
                        // allow for extra check on rogue sessions
                        $_SESSION['username'] = $username;
                        logger('login: \'' . $username . '\' (' . $user_id . '): success', WLOG_INFO, $user_id);
                        // now that we logged on successfully, make sure that obsolete sessions
                        // will not bother us (or other logged in users). Note the 900 seconds minimum duration;
                        // $time_out = max(900,intval(ini_get('session.gc_maxlifetime')));
                        global $CFG;
                        $time_out = max(900, intval($CFG->session_expiry));
                        dbsession_garbage_collection($time_out);
                        return $user_id;
                        // SUCCESS! User is logged in, tell caller!
                    } else {
                        show_login(LOGIN_PROCEDURE_CHANGE_PASSWORD, t('must_change_password', 'loginlib'));
                        exit;
                    }
                }
                // Invalid credentials; pretend we're busy (slow user down), increment failure count...
                $failure_count = login_failure_increment($_SERVER['REMOTE_ADDR'], LOGIN_PROCEDURE_NORMAL, $username);
                login_failure_delay($_SERVER['REMOTE_ADDR']);
                if ($failure_count < intval($CFG->login_max_failures)) {
                    show_login(LOGIN_PROCEDURE_NORMAL, t('invalid_credentials_please_retry', 'loginlib'));
                } elseif ($failure_count == intval($CFG->login_max_failures)) {
                    show_login(LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER, t('do_you_want_to_try_forgot_password_procedure', 'loginlib'));
                } else {
                    login_failure_blacklist_address($_SERVER['REMOTE_ADDR'], 60 * intval($CFG->login_blacklist_interval), $username);
                    show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('contact_webmaster_for_new_password', 'loginlib'));
                }
            } else {
                show_login(LOGIN_PROCEDURE_NORMAL);
            }
            exit;
            break;
        case LOGIN_PROCEDURE_CHANGE_PASSWORD:
            if (isset($_POST['login_username']) && isset($_POST['login_password']) && isset($_POST['login_new_password1']) && isset($_POST['login_new_password2'])) {
                $username = magic_unquote($_POST['login_username']);
                $password = magic_unquote($_POST['login_password']);
                $new_password1 = magic_unquote($_POST['login_new_password1']);
                $new_password2 = magic_unquote($_POST['login_new_password2']);
                $user = authenticate_user(BY_PASSWORD, $username, $password);
                //
                // step 1 - perform some checks on the proposed new passwords
                //
                if ($user !== FALSE) {
                    // user authenticated: we can now also check re-use of existing passwords
                    $salt = $user['salt'];
                    $password_hash = $user['password_hash'];
                    $bypass_hash = $user['bypass_hash'];
                } else {
                    // user not authenticated so we cannot check for re-use of existing passwords
                    $salt = '';
                    $password_hash = '';
                    $bypass_hash = '';
                }
                if (!acceptable_new_password($new_password1, $new_password2, $salt, $password_hash, $bypass_hash)) {
                    show_login(LOGIN_PROCEDURE_CHANGE_PASSWORD, t('invalid_new_passwords', 'loginlib', array('{MIN_LENGTH}' => MINIMUM_PASSWORD_LENGTH, '{MIN_LOWER}' => MINIMUM_PASSWORD_LOWERCASE, '{MIN_UPPER}' => MINIMUM_PASSWORD_UPPERCASE, '{MIN_DIGIT}' => MINIMUM_PASSWORD_DIGITS)));
                    exit;
                }
                //
                // step 2 - if authenticated, actually change password and reset failure counters/blacklists
                //
                if ($user !== FALSE) {
                    // allow the user in:
                    //  - start new session,
                    //  - immediately write/close it,
                    //  - send user an email about success with changing password,
                    //  - and finally leave the user with message box on screen
                    //
                    login_change_password($user['user_id'], $new_password1);
                    login_failure_reset($_SERVER['REMOTE_ADDR']);
                    require_once $CFG->progdir . '/lib/dbsessionlib.php';
                    dbsession_setup($CFG->session_name);
                    $session_key = dbsession_create($user['user_id'], $_SERVER['REMOTE_ADDR']);
                    session_id($session_key);
                    session_start();
                    $_SESSION['session_id'] = dbsession_get_session_id($session_key);
                    $user_id = intval($user['user_id']);
                    $_SESSION['user_id'] = $user_id;
                    $_SESSION['redirect'] = $user['redirect'];
                    $_SESSION['language_key'] = $user['language_key'];
                    $_SESSION['remote_addr'] = $_SERVER['REMOTE_ADDR'];
                    $_SESSION['salt'] = $CFG->salt;
                    // allow for extra check on rogue sessions
                    $_SESSION['username'] = $username;
                    session_write_close();
                    // save the session
                    login_send_confirmation($user);
                    logger('login: \'' . $username . '\' (' . $user_id . '), change password: success', WLOG_INFO, $user_id);
                    show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('password_changed', 'loginlib'));
                    exit;
                }
                // Invalid credentials; pretend we're busy (slow user down), increment failure count...
                $failure_count = login_failure_increment($_SERVER['REMOTE_ADDR'], LOGIN_PROCEDURE_CHANGE_PASSWORD, $username);
                login_failure_delay($_SERVER['REMOTE_ADDR']);
                if ($failure_count < intval($CFG->login_max_failures)) {
                    show_login(LOGIN_PROCEDURE_CHANGE_PASSWORD, t('invalid_credentials_please_retry', 'loginlib'));
                } elseif ($failure_count == intval($CFG->login_max_failures)) {
                    show_login(LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER, t('too_many_login_attempts', 'loginlib'));
                } else {
                    login_failure_blacklist_address($_SERVER['REMOTE_ADDR'], 60 * intval($CFG->login_blacklist_interval), $username);
                    show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('too_many_change_password_attempts', 'loginlib'));
                }
            } else {
                show_login(LOGIN_PROCEDURE_CHANGE_PASSWORD);
            }
            exit;
            break;
        case LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER:
            if (isset($_POST['login_username']) && isset($_POST['login_email'])) {
                $username = magic_unquote($_POST['login_username']);
                $email = magic_unquote($_POST['login_email']);
                $user = authenticate_user(BY_EMAIL, $username, $email);
                if ($user !== FALSE) {
                    if (login_send_laissez_passer($user)) {
                        show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('see_mail_for_further_instructions', 'loginlib'));
                    } else {
                        show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('failure_sending_laissez_passer_mail', 'loginlib'));
                    }
                    exit;
                } else {
                    // Not authenticated; pretend we're busy (slow user down), increment failure count...
                    $failure_count = login_failure_increment($_SERVER['REMOTE_ADDR'], LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER, $username);
                    login_failure_delay($_SERVER['REMOTE_ADDR']);
                    if ($failure_count < intval($CFG->login_max_failures)) {
                        show_login(LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER, t('invalid_username_email_please_retry', 'loginlib'));
                    } elseif ($failure_count == intval($CFG->login_max_failures)) {
                        show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('too_many_login_attempts', 'loginlib'));
                    } else {
                        login_failure_blacklist_address($_SERVER['REMOTE_ADDR'], 60 * intval($CFG->login_blacklist_interval), $username);
                        show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('too_many_login_attempts', 'loginlib'));
                    }
                    exit;
                }
            } else {
                show_login(LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER);
            }
            exit;
            break;
        case LOGIN_PROCEDURE_SEND_BYPASS:
            if (isset($_GET['code']) && isset($_GET['username'])) {
                $laissez_passer = magic_unquote($_GET['code']);
                $username = magic_unquote($_GET['username']);
            } elseif (isset($_POST['login_username']) && isset($_POST['login_laissez_passer'])) {
                $laissez_passer = magic_unquote($_POST['login_laissez_passer']);
                $username = magic_unquote($_POST['login_username']);
            } else {
                show_login(LOGIN_PROCEDURE_SEND_BYPASS);
                exit;
            }
            // still here? Then we check the laissez_passer and send a second email to the user
            $user = authenticate_user(BY_LAISSEZ_PASSER, $username, $laissez_passer);
            if ($user !== FALSE) {
                login_failure_reset($_SERVER['REMOTE_ADDR']);
                if (login_send_bypass($user)) {
                    show_login(LOGIN_PROCEDURE_NORMAL, t('see_mail_for_new_temporary_password', 'loginlib'));
                } else {
                    show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('failure_sending_temporary_password', 'loginlib'));
                }
            } else {
                $failure_count = login_failure_increment($_SERVER['REMOTE_ADDR'], LOGIN_PROCEDURE_SEND_BYPASS, $username);
                login_failure_delay($_SERVER['REMOTE_ADDR']);
                if ($failure_count < intval($CFG->login_max_failures)) {
                    show_login(LOGIN_PROCEDURE_SEND_BYPASS, t('invalid_laissez_passer_please_retry', 'loginlib'));
                } elseif ($failure_count == intval($CFG->login_max_failures)) {
                    show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('too_many_login_attempts', 'loginlib'));
                } else {
                    login_failure_blacklist_address($_SERVER['REMOTE_ADDR'], 60 * intval($CFG->login_blacklist_interval), $username);
                    show_login(LOGIN_PROCEDURE_MESSAGE_BOX, t('too_many_login_attempts', 'loginlib'));
                }
            }
            exit;
            break;
        case LOGIN_PROCEDURE_SHOWLOGIN:
            show_login(LOGIN_PROCEDURE_NORMAL, $message);
            exit;
            break;
        default:
            show_login(LOGIN_PROCEDURE_NORMAL);
            exit;
            break;
    }
}
 /** save basic properties of user account
  *
  * @param int $user_id the account to save (pkey in users table)
  * @uses $WAS_SCRIPT_NAME
  */
 function user_save_basic($user_id)
 {
     global $WAS_SCRIPT_NAME;
     $user_id = intval($user_id);
     //
     // 2 -- validate the data
     //
     $invalid = FALSE;
     $dialogdef = $this->get_dialogdef_edit_user($user_id);
     //
     // 2A -- check for generic errors (string too short, number too small, etc)
     if (!dialog_validate($dialogdef)) {
         $invalid = TRUE;
     }
     // 2B -- additional check: unique username
     $record = db_select_single_record('users', 'user_id', array('username' => $dialogdef['username']['value']));
     if ($record !== FALSE && intval($record['user_id']) != $user_id) {
         // Oops, a record with that username already exists and it's not us. Go flag error
         ++$dialogdef['username']['errors'];
         $fname = $this->get_fname($dialogdef['username']);
         $dialogdef['username']['error_messages'][] = t('validate_not_unique', '', array('{FIELD}' => $fname));
         $invalid = TRUE;
     }
     // 2C -- additional check: valid password
     $password1 = $dialogdef['user_password1']['value'];
     $password2 = $dialogdef['user_password2']['value'];
     if (!empty($password1) || !empty($password2)) {
         if ($password1 != $password2) {
             $params = array('{FIELD1}' => $this->get_fname($dialogdef['user_password1']), '{FIELD2}' => $this->get_fname($dialogdef['user_password2']));
             ++$dialogdef['user_password1']['errors'];
             ++$dialogdef['user_password2']['errors'];
             $dialogdef['user_password1']['error_messages'][] = t('validate_different_passwords', '', $params);
             $dialogdef['user_password1']['value'] = '';
             $dialogdef['user_password2']['value'] = '';
             $invalid = TRUE;
         } elseif (!acceptable_new_password($password1, $password2)) {
             $params = array('{MIN_LENGTH}' => MINIMUM_PASSWORD_LENGTH, '{MIN_LOWER}' => MINIMUM_PASSWORD_LOWERCASE, '{MIN_UPPER}' => MINIMUM_PASSWORD_UPPERCASE, '{MIN_DIGIT}' => MINIMUM_PASSWORD_DIGITS, '{FIELD}' => $this->get_fname($dialogdef['user_password1']));
             ++$dialogdef['user_password1']['errors'];
             $dialogdef['user_password1']['error_messages'][] = t('validate_bad_password', '', $params);
             $params['{FIELD}'] = $this->get_fname($dialogdef['user_password2']);
             ++$dialogdef['user_password2']['errors'];
             $dialogdef['user_password1']['value'] = '';
             $dialogdef['user_password2']['value'] = '';
             $invalid = TRUE;
         }
     }
     // 2D -- if there were any errors go redo dialog while keeping data already entered
     if ($invalid) {
         foreach ($dialogdef as $k => $item) {
             if (isset($item['errors']) && $item['errors'] > 0) {
                 $this->output->add_message($item['error_messages']);
             }
         }
         $params = $this->get_user_names($user_id);
         $this->output->add_content('<h2>' . t('usermanager_edit_user_header', 'admin', $params) . '</h2>');
         $this->output->add_content(t('usermanager_edit_user_explanation', 'admin', $params));
         $href = href($WAS_SCRIPT_NAME, $this->a_params(TASK_USER_SAVE, $user_id));
         if ($dialogdef !== FALSE) {
             $this->output->add_content(dialog_quickform($href, $dialogdef));
             $this->show_breadcrumbs_user($user_id);
         } else {
             $this->output->add_message(t('error_retrieving_data', 'admin'));
             $this->show_menu_user($user_id, TASK_USER_EDIT);
         }
         return;
     }
     // 3 -- Now actually save the data which we just validated
     $fields = array('username' => $dialogdef['username']['value'], 'bypass_mode' => FALSE, 'bypass_hash' => NULL, 'bypass_expiry' => NULL, 'full_name' => $dialogdef['user_fullname']['value'], 'email' => $dialogdef['user_email']['value'], 'is_active' => $dialogdef['user_is_active']['value'] == 1 ? TRUE : FALSE, 'redirect' => $dialogdef['user_redirect']['value'], 'language_key' => $dialogdef['user_language_key']['value'], 'editor' => $dialogdef['user_editor']['value'], 'skin' => $dialogdef['user_skin']['value']);
     if (!empty($password1)) {
         $new_salt = password_salt();
         $new_password = $password1;
         $fields['salt'] = $new_salt;
         $fields['password_hash'] = password_hash($new_salt, $new_password);
     }
     $params = array('{USERNAME}' => $dialogdef['username']['value'], '{FULL_NAME}' => $dialogdef['user_fullname']['value']);
     if (db_update('users', $fields, array('user_id' => $user_id)) === FALSE) {
         $this->output->add_message(t('usermanager_save_user_failure', 'admin', $params));
         logger("usermanager->user_save(): error saving data user '{$user_id}': " . db_errormessage());
     } else {
         $this->output->add_message(t('usermanager_save_user_success', 'admin', $params));
         logger("usermanager->user_save(): success saving changes to '{$user_id}' in 'users'", WLOG_DEBUG);
     }
     $this->users_overview();
     return;
 }