<?php
/*
	Question2Answer by Gideon Greenspan and contributors
	http://www.question2answer.org/

	Description: User management (application level) for creating/modifying users


	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	More about this license: http://www.question2answer.org/license.php
*/

if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
	header('Location: ../../');
	exit;
}

@define('QA_MIN_PASSWORD_LEN', 4);
@define('QA_NEW_PASSWORD_LEN', 8); // when resetting password


/**
 * Return $errors fields for any invalid aspect of user-entered $handle (username) and $email. Works by calling through
 * to all filter modules and also rejects existing values in database unless they belongs to $olduser (if set).
 * @param $handle
 * @param $email
 * @param $olduser
 * @return array
 */
function qa_handle_email_filter(&$handle, &$email, $olduser = null)
{
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'util/string.php';

	$errors = array();

	// sanitise 4-byte Unicode
	$handle = qa_remove_utf8mb4($handle);

	$filtermodules = qa_load_modules_with('filter', 'filter_handle');

	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->filter_handle($handle, $olduser);
		if (isset($error)) {
			$errors['handle'] = $error;
			break;
		}
	}

	if (!isset($errors['handle'])) { // first test through filters, then check for duplicates here
		$handleusers = qa_db_user_find_by_handle($handle);
		if (count($handleusers) && ((!isset($olduser['userid'])) || (array_search($olduser['userid'], $handleusers) === false)))
			$errors['handle'] = qa_lang('users/handle_exists');
	}

	$filtermodules = qa_load_modules_with('filter', 'filter_email');

	$error = null;
	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->filter_email($email, $olduser);
		if (isset($error)) {
			$errors['email'] = $error;
			break;
		}
	}

	if (!isset($errors['email'])) {
		$emailusers = qa_db_user_find_by_email($email);
		if (count($emailusers) && ((!isset($olduser['userid'])) || (array_search($olduser['userid'], $emailusers) === false)))
			$errors['email'] = qa_lang('users/email_exists');
	}

	return $errors;
}


/**
 * Make $handle valid and unique in the database - if $allowuserid is set, allow it to match that user only
 * @param $handle
 * @return string
 */
function qa_handle_make_valid($handle)
{
	require_once QA_INCLUDE_DIR . 'util/string.php';
	require_once QA_INCLUDE_DIR . 'db/maxima.php';
	require_once QA_INCLUDE_DIR . 'db/users.php';

	if (!strlen($handle))
		$handle = qa_lang('users/registered_user');

	$handle = preg_replace('/[\\@\\+\\/]/', ' ', $handle);

	for ($attempt = 0; $attempt <= 99; $attempt++) {
		$suffix = $attempt ? (' ' . $attempt) : '';
		$tryhandle = qa_substr($handle, 0, QA_DB_MAX_HANDLE_LENGTH - strlen($suffix)) . $suffix;

		$filtermodules = qa_load_modules_with('filter', 'filter_handle');
		foreach ($filtermodules as $filtermodule) {
			// filter first without worrying about errors, since our goal is to get a valid one
			$filtermodule->filter_handle($tryhandle, null);
		}

		$haderror = false;

		foreach ($filtermodules as $filtermodule) {
			$error = $filtermodule->filter_handle($tryhandle, null); // now check for errors after we've filtered
			if (isset($error))
				$haderror = true;
		}

		if (!$haderror) {
			$handleusers = qa_db_user_find_by_handle($tryhandle);
			if (!count($handleusers))
				return $tryhandle;
		}
	}

	qa_fatal_error('Could not create a valid and unique handle from: ' . $handle);
}


/**
 * Return an array with a single element (key 'password') if user-entered $password is valid, otherwise an empty array.
 * Works by calling through to all filter modules.
 * @param $password
 * @param $olduser
 * @return array
 */
function qa_password_validate($password, $olduser = null)
{
	$error = null;
	$filtermodules = qa_load_modules_with('filter', 'validate_password');

	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->validate_password($password, $olduser);
		if (isset($error))
			break;
	}

	if (!isset($error)) {
		$minpasslen = max(QA_MIN_PASSWORD_LEN, 1);
		if (qa_strlen($password) < $minpasslen)
			$error = qa_lang_sub('users/password_min', $minpasslen);
	}

	if (isset($error))
		return array('password' => $error);

	return array();
}


/**
 * Create a new user (application level) with $email, $password, $handle and $level.
 * Set $confirmed to true if the email address has been confirmed elsewhere.
 * Handles user points, notification and optional email confirmation.
 * @param $email
 * @param $password
 * @param $handle
 * @param int $level
 * @param bool $confirmed
 * @return mixed
 */
function qa_create_new_user($email, $password, $handle, $level = QA_USER_LEVEL_BASIC, $confirmed = false)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'db/points.php';
	require_once QA_INCLUDE_DIR . 'app/options.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';
	require_once QA_INCLUDE_DIR . 'app/cookies.php';

	$userid = qa_db_user_create($email, $password, $handle, $level, qa_remote_ip_address());
	qa_db_points_update_ifuser($userid, null);
	qa_db_uapprovecount_update();

	if ($confirmed)
		qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, true);

	if (qa_opt('show_notice_welcome'))
		qa_db_user_set_flag($userid, QA_USER_FLAGS_WELCOME_NOTICE, true);

	$custom = qa_opt('show_custom_welcome') ? trim(qa_opt('custom_welcome')) : '';

	if (qa_opt('confirm_user_emails') && $level < QA_USER_LEVEL_EXPERT && !$confirmed) {
		$confirm = strtr(qa_lang('emails/welcome_confirm'), array(
			'^url' => qa_get_new_confirm_url($userid, $handle),
		));

		if (qa_opt('confirm_user_required'))
			qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_CONFIRM, true);

	} else
		$confirm = '';

	// we no longer use the 'approve_user_required' option to set QA_USER_FLAGS_MUST_APPROVE; this can be handled by the Permissions settings

	qa_send_notification($userid, $email, $handle, qa_lang('emails/welcome_subject'), qa_lang('emails/welcome_body'), array(
		'^password' => isset($password) ? qa_lang('main/hidden') : qa_lang('users/password_to_set'), // v 1.6.3: no longer email out passwords
		'^url' => qa_opt('site_url'),
		'^custom' => strlen($custom) ? ($custom . "\n\n") : '',
		'^confirm' => $confirm,
	));

	qa_report_event('u_register', $userid, $handle, qa_cookie_get(), array(
		'email' => $email,
		'level' => $level,
	));

	return $userid;
}


/**
 * Delete $userid and all their votes and flags. Their posts will become anonymous.
 * Handles recalculations of votes and flags for posts this user has affected.
 * @param $userid
 * @return mixed
 */
function qa_delete_user($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'db/votes.php';
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'db/post-update.php';
	require_once QA_INCLUDE_DIR . 'db/points.php';

	$postids = qa_db_uservoteflag_user_get($userid); // posts this user has flagged or voted on, whose counts need updating

	qa_db_user_delete($userid);
	qa_db_uapprovecount_update();
	qa_db_userpointscount_update();

	foreach ($postids as $postid) { // hoping there aren't many of these - saves a lot of new SQL code...
		qa_db_post_recount_votes($postid);
		qa_db_post_recount_flags($postid);
	}

	$postuserids = qa_db_posts_get_userids($postids);

	foreach ($postuserids as $postuserid) {
		qa_db_points_update_ifuser($postuserid, array('avoteds', 'qvoteds', 'upvoteds', 'downvoteds'));
	}
}


/**
 * Set a new email confirmation code for the user and send it out
 * @param $userid
 * @return mixed
 */
function qa_send_new_confirm($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'db/selects.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';

	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));

	$emailcode = qa_db_user_rand_emailcode();

	if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/confirm_subject'), qa_lang('emails/confirm_body'), array(
			'^url' => qa_get_new_confirm_url($userid, $userinfo['handle'], $emailcode),
			'^code' => $emailcode,
	))) {
		qa_fatal_error('Could not send email confirmation');
	}
}


/**
 * Set a new email confirmation code for the user and return the corresponding link. If the email code is also sent then that value
 * is used. Otherwise, a new email code is generated
 * @param $userid
 * @param $handle
 * @param $emailcode
 * @return mixed|string
 */
function qa_get_new_confirm_url($userid, $handle, $emailcode = null)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'db/users.php';

	if (!isset($emailcode)) {
		$emailcode = qa_db_user_rand_emailcode();
	}
	qa_db_user_set($userid, 'emailcode', $emailcode);

	return qa_path_absolute('confirm', array('c' => $emailcode, 'u' => $handle));
}


/**
 * Complete the email confirmation process for the user
 * @param $userid
 * @param $email
 * @param $handle
 * @return mixed
 */
function qa_complete_confirm($userid, $email, $handle)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'app/cookies.php';

	qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, true);
	qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_CONFIRM, false);
	qa_db_user_set($userid, 'emailcode', ''); // to prevent re-use of the code

	qa_report_event('u_confirmed', $userid, $handle, qa_cookie_get(), array(
		'email' => $email,
	));
}


/**
 * Set the user level of user $userid with $handle to $level (one of the QA_USER_LEVEL_* constraints in /qa-include/app/users.php)
 * Pass the previous user level in $oldlevel. Reports the appropriate event, assumes change performed by the logged in user.
 * @param $userid
 * @param $handle
 * @param $level
 * @param $oldlevel
 */
function qa_set_user_level($userid, $handle, $level, $oldlevel)
{
	require_once QA_INCLUDE_DIR . 'db/users.php';

	qa_db_user_set($userid, 'level', $level);
	qa_db_uapprovecount_update();

	if ($level >= QA_USER_LEVEL_APPROVED) {
		// no longer necessary as QA_USER_FLAGS_MUST_APPROVE is deprecated, but kept for posterity
		qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_APPROVE, false);
	}

	qa_report_event('u_level', qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), array(
		'userid' => $userid,
		'handle' => $handle,
		'level' => $level,
		'oldlevel' => $oldlevel,
	));
}


/**
 * Set the status of user $userid with $handle to blocked if $blocked is true, otherwise to unblocked. Reports the appropriate
 * event, assumes change performed by the logged in user.
 * @param $userid
 * @param $handle
 * @param $blocked
 */
function qa_set_user_blocked($userid, $handle, $blocked)
{
	require_once QA_INCLUDE_DIR . 'db/users.php';

	qa_db_user_set_flag($userid, QA_USER_FLAGS_USER_BLOCKED, $blocked);
	qa_db_uapprovecount_update();

	qa_report_event($blocked ? 'u_block' : 'u_unblock', qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), array(
		'userid' => $userid,
		'handle' => $handle,
	));
}


/**
 * Start the 'I forgot my password' process for $userid, sending reset code
 * @param $userid
 * @return mixed
 */
function qa_start_reset_user($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'app/options.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';
	require_once QA_INCLUDE_DIR . 'db/selects.php';

	qa_db_user_set($userid, 'emailcode', qa_db_user_rand_emailcode());

	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));

	if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/reset_subject'), qa_lang('emails/reset_body'), array(
		'^code' => $userinfo['emailcode'],
		'^url' => qa_path_absolute('reset', array('c' => $userinfo['emailcode'], 'e' => $userinfo['email'])),
	))) {
		qa_fatal_error('Could not send reset password email');
	}
}


/**
 * Successfully finish the 'I forgot my password' process for $userid, sending new password
 *
 * @deprecated This function has been replaced by qa_finish_reset_user since Q2A 1.8
 * @param $userid
 * @return mixed
 */
function qa_complete_reset_user($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'util/string.php';
	require_once QA_INCLUDE_DIR . 'app/options.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';
	require_once QA_INCLUDE_DIR . 'app/cookies.php';
	require_once QA_INCLUDE_DIR . 'db/selects.php';

	$password = qa_random_alphanum(max(QA_MIN_PASSWORD_LEN, QA_NEW_PASSWORD_LEN));

	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));

	if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/new_password_subject'), qa_lang('emails/new_password_body'), array(
		'^password' => $password,
		'^url' => qa_opt('site_url'),
	))) {
		qa_fatal_error('Could not send new password - password not reset');
	}

	qa_db_user_set_password($userid, $password); // do this last, to be safe
	qa_db_user_set($userid, 'emailcode', ''); // so can't be reused

	qa_report_event('u_reset', $userid, $userinfo['handle'], qa_cookie_get(), array(
		'email' => $userinfo['email'],
	));
}


/**
 * Successfully finish the 'I forgot my password' process for $userid, cleaning the emailcode field and logging in the user
 * @param mixed $userId The userid identifiying the user who will have the password reset
 * @param string $newPassword The new password for the user
 * @return void
 */
function qa_finish_reset_user($userId, $newPassword)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	// For qa_db_user_set_password(), qa_db_user_set()
	require_once QA_INCLUDE_DIR . 'db/users.php';

	// For qa_set_logged_in_user()
	require_once QA_INCLUDE_DIR . 'app/options.php';

	// For qa_cookie_get()
	require_once QA_INCLUDE_DIR . 'app/cookies.php';

	// For qa_db_select_with_pending(), qa_db_user_account_selectspec()
	require_once QA_INCLUDE_DIR . 'db/selects.php';

	// For qa_set_logged_in_user()
	require_once QA_INCLUDE_DIR . 'app/users.php';

	qa_db_user_set_password($userId, $newPassword);

	qa_db_user_set($userId, 'emailcode', ''); // to prevent re-use of the code

	$userInfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userId, true));

	qa_set_logged_in_user($userId, $userInfo['handle'], false, $userInfo['sessionsource']); // reinstate this specific session

	qa_report_event('u_reset', $userId, $userInfo['handle'], qa_cookie_get(), array(
		'email' => $userInfo['email'],
	));
}

/**
 * Flush any information about the currently logged in user, so it is retrieved from database again
 */
function qa_logged_in_user_flush()
{
	global $qa_cached_logged_in_user;

	$qa_cached_logged_in_user = null;
}


/**
 * Set the avatar of $userid to the image in $imagedata, and remove $oldblobid from the database if not null
 * @param $userid
 * @param $imagedata
 * @param $oldblobid
 * @return bool
 */
function qa_set_user_avatar($userid, $imagedata, $oldblobid = null)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'util/image.php';

	$imagedata = qa_image_constrain_data($imagedata, $width, $height, qa_opt('avatar_store_size'));

	if (isset($imagedata)) {
		require_once QA_INCLUDE_DIR . 'app/blobs.php';

		$newblobid = qa_create_blob($imagedata, 'jpeg', null, $userid, null, qa_remote_ip_address());

		if (isset($newblobid)) {
			qa_db_user_set($userid, 'avatarblobid', $newblobid);
			qa_db_user_set($userid, 'avatarwidth', $width);
			qa_db_user_set($userid, 'avatarheight', $height);
			qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_AVATAR, true);
			qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_GRAVATAR, false);

			if (isset($oldblobid))
				qa_delete_blob($oldblobid);

			return true;
		}
	}

	return false;
}
