<?php

/*
	Question2Answer (c) Gideon Greenspan

	http://www.question2answer.org/


	File: qa-include/qa-app-users.php
	Version: See define()s at top of qa-include/qa-base.php
	Description: User management (application level) for basic user operations


	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_USER_LEVEL_BASIC', 0);
	define('QA_USER_LEVEL_APPROVED', 10);
	define('QA_USER_LEVEL_EXPERT', 20);
	define('QA_USER_LEVEL_EDITOR', 50);
	define('QA_USER_LEVEL_MODERATOR', 80);
	define('QA_USER_LEVEL_ADMIN', 100);
	define('QA_USER_LEVEL_SUPER', 120);

	define('QA_USER_FLAGS_EMAIL_CONFIRMED', 1);
	define('QA_USER_FLAGS_USER_BLOCKED', 2);
	define('QA_USER_FLAGS_SHOW_AVATAR', 4);
	define('QA_USER_FLAGS_SHOW_GRAVATAR', 8);
	define('QA_USER_FLAGS_NO_MESSAGES', 16);
	define('QA_USER_FLAGS_NO_MAILINGS', 32);
	define('QA_USER_FLAGS_WELCOME_NOTICE', 64);
	define('QA_USER_FLAGS_MUST_CONFIRM', 128);
	define('QA_USER_FLAGS_NO_WALL_POSTS', 256);
	define('QA_USER_FLAGS_MUST_APPROVE', 512);

	define('QA_FIELD_FLAGS_MULTI_LINE', 1);
	define('QA_FIELD_FLAGS_LINK_URL', 2);
	define('QA_FIELD_FLAGS_ON_REGISTER', 4);

	@define('QA_FORM_EXPIRY_SECS', 86400); // how many seconds a form is valid for submission
	@define('QA_FORM_KEY_LENGTH', 32);


	if (QA_FINAL_EXTERNAL_USERS) {

	//	If we're using single sign-on integration (WordPress or otherwise), load PHP file for that

		if (defined('QA_FINAL_WORDPRESS_INTEGRATE_PATH'))
			require_once QA_INCLUDE_DIR.'qa-external-users-wp.php';
		else
			require_once QA_EXTERNAL_DIR.'qa-external-users.php';


	//	Access functions for user information

		function qa_get_logged_in_user_cache()
	/*
		Return array of information about the currently logged in user, cache to ensure only one call to external code
	*/
		{
			global $qa_cached_logged_in_user;

			if (!isset($qa_cached_logged_in_user)) {
				$user=qa_get_logged_in_user();
				$qa_cached_logged_in_user=isset($user) ? $user : false; // to save trying again
			}

			return @$qa_cached_logged_in_user;
		}


		function qa_get_logged_in_user_field($field)
	/*
		Return $field of the currently logged in user, or null if not available
	*/
		{
			$user=qa_get_logged_in_user_cache();

			return @$user[$field];
		}


		function qa_get_logged_in_userid()
	/*
		Return the userid of the currently logged in user, or null if none
	*/
		{
			return qa_get_logged_in_user_field('userid');
		}


		function qa_get_logged_in_points()
	/*
		Return the number of points of the currently logged in user, or null if none is logged in
	*/
		{
			global $qa_cached_logged_in_points;

			if (!isset($qa_cached_logged_in_points)) {
				require_once QA_INCLUDE_DIR.'qa-db-selects.php';

				$qa_cached_logged_in_points=qa_db_select_with_pending(qa_db_user_points_selectspec(qa_get_logged_in_userid(), true));
			}

			return $qa_cached_logged_in_points['points'];
		}


		function qa_get_external_avatar_html($userid, $size, $padding=false)
	/*
		Return HTML to display for the avatar of $userid, constrained to $size pixels, with optional $padding to that size
	*/
		{
			if (function_exists('qa_avatar_html_from_userid'))
				return qa_avatar_html_from_userid($userid, $size, $padding);
			else
				return null;
		}


	} else {

		function qa_start_session()
	/*
		Open a PHP session if one isn't opened already
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			@ini_set('session.gc_maxlifetime', 86400); // worth a try, but won't help in shared hosting environment
			@ini_set('session.use_trans_sid', false); // sessions need cookies to work, since we redirect after login
			@ini_set('session.cookie_domain', QA_COOKIE_DOMAIN);

			if (!isset($_SESSION))
				session_start();
		}


		function qa_session_var_suffix()
	/*
		Returns a suffix to be used for names of session variables to prevent them being shared between multiple Q2A sites on the same server
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			$prefix=defined('QA_MYSQL_USERS_PREFIX') ? QA_MYSQL_USERS_PREFIX : QA_MYSQL_TABLE_PREFIX;

			return md5(QA_FINAL_MYSQL_HOSTNAME.'/'.QA_FINAL_MYSQL_USERNAME.'/'.QA_FINAL_MYSQL_PASSWORD.'/'.QA_FINAL_MYSQL_DATABASE.'/'.$prefix);
		}


		function qa_session_verify_code($userid)
	/*
		Returns a verification code used to ensure that a user session can't be generated by another PHP script running on the same server
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			return sha1($userid.'/'.QA_MYSQL_TABLE_PREFIX.'/'.QA_FINAL_MYSQL_DATABASE.'/'.QA_FINAL_MYSQL_PASSWORD.'/'.QA_FINAL_MYSQL_USERNAME.'/'.QA_FINAL_MYSQL_HOSTNAME);
		}


		function qa_set_session_cookie($handle, $sessioncode, $remember)
	/*
		Set cookie in browser for username $handle with $sessioncode (in database).
		Pass true if user checked 'Remember me' (either now or previously, as learned from cookie).
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			// if $remember is true, store in browser for a month, otherwise store only until browser is closed
			setcookie('qa_session', $handle.'/'.$sessioncode.'/'.($remember ? 1 : 0), $remember ? (time()+2592000) : 0, '/', QA_COOKIE_DOMAIN);
		}


		function qa_clear_session_cookie()
	/*
		Remove session cookie from browser
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			setcookie('qa_session', false, 0, '/', QA_COOKIE_DOMAIN);
		}


		function qa_set_session_user($userid, $source)
	/*
		Set the session variables to indicate that $userid is logged in from $source
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			$suffix=qa_session_var_suffix();

			$_SESSION['qa_session_userid_'.$suffix]=$userid;
			$_SESSION['qa_session_source_'.$suffix]=$source;
			$_SESSION['qa_session_verify_'.$suffix]=qa_session_verify_code($userid);
				// prevents one account on a shared server being able to create a log in a user to Q2A on another account on same server
		}


		function qa_clear_session_user()
	/*
		Clear the session variables indicating that a user is logged in
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			$suffix=qa_session_var_suffix();

			unset($_SESSION['qa_session_userid_'.$suffix]);
			unset($_SESSION['qa_session_source_'.$suffix]);
			unset($_SESSION['qa_session_verify_'.$suffix]);
		}


		function qa_set_logged_in_user($userid, $handle='', $remember=false, $source=null)
	/*
		Call for successful log in by $userid and $handle or successful log out with $userid=null.
		$remember states if 'Remember me' was checked in the login form.
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			require_once QA_INCLUDE_DIR.'qa-app-cookies.php';

			qa_start_session();

			if (isset($userid)) {
				qa_set_session_user($userid, $source);

				// PHP sessions time out too quickly on the server side, so we also set a cookie as backup.
				// Logging in from a second browser will make the previous browser's 'Remember me' no longer
				// work - I'm not sure if this is the right behavior - could see it either way.

				require_once QA_INCLUDE_DIR.'qa-db-selects.php';

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

				// if we have logged in before, and are logging in the same way as before, we don't need to change the sessioncode/source
				// this means it will be possible to automatically log in (via cookies) to the same account from more than one browser

				if (empty($userinfo['sessioncode']) || ($source!==$userinfo['sessionsource'])) {
					$sessioncode=qa_db_user_rand_sessioncode();
					qa_db_user_set($userid, 'sessioncode', $sessioncode);
					qa_db_user_set($userid, 'sessionsource', $source);
				} else
					$sessioncode=$userinfo['sessioncode'];

				qa_db_user_logged_in($userid, qa_remote_ip_address());
				qa_set_session_cookie($handle, $sessioncode, $remember);

				qa_report_event('u_login', $userid, $userinfo['handle'], qa_cookie_get());

			} else {
				$olduserid=qa_get_logged_in_userid();
				$oldhandle=qa_get_logged_in_handle();

				qa_clear_session_cookie();
				qa_clear_session_user();

				qa_report_event('u_logout', $olduserid, $oldhandle, qa_cookie_get());
			}
		}


		function qa_log_in_external_user($source, $identifier, $fields)
	/*
		Call to log in a user based on an external identity provider $source with external $identifier
		A new user is created based on $fields if it's a new combination of $source and $identifier
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			require_once QA_INCLUDE_DIR.'qa-db-users.php';

			$users=qa_db_user_login_find($source, $identifier);
			$countusers=count($users);

			if ($countusers>1)
				qa_fatal_error('External login mapped to more than one user'); // should never happen

			if ($countusers) // user exists so log them in
				qa_set_logged_in_user($users[0]['userid'], $users[0]['handle'], false, $source);

			else { // create and log in user
				require_once QA_INCLUDE_DIR.'qa-app-users-edit.php';

				qa_db_user_login_sync(true);

				$users=qa_db_user_login_find($source, $identifier); // check again after table is locked

				if (count($users)==1) {
					qa_db_user_login_sync(false);
					qa_set_logged_in_user($users[0]['userid'], $users[0]['handle'], false, $source);

				} else {
					$handle=qa_handle_make_valid(@$fields['handle']);

					if (strlen(@$fields['email'])) { // remove email address if it will cause a duplicate
						$emailusers=qa_db_user_find_by_email($fields['email']);
						if (count($emailusers)) {
							qa_redirect('login', array('e' => $fields['email'], 'ee' => '1'));
							unset($fields['email']);
							unset($fields['confirmed']);
						}
					}

					$userid=qa_create_new_user((string)@$fields['email'], null /* no password */, $handle,
						isset($fields['level']) ? $fields['level'] : QA_USER_LEVEL_BASIC, @$fields['confirmed']);

					qa_db_user_login_add($userid, $source, $identifier);
					qa_db_user_login_sync(false);

					$profilefields=array('name', 'location', 'website', 'about');

					foreach ($profilefields as $fieldname)
						if (strlen(@$fields[$fieldname]))
							qa_db_user_profile_set($userid, $fieldname, $fields[$fieldname]);

					if (strlen(@$fields['avatar']))
						qa_set_user_avatar($userid, $fields['avatar']);

					qa_set_logged_in_user($userid, $handle, false, $source);
				}
			}
		}


		function qa_get_logged_in_userid()
	/*
		Return the userid of the currently logged in user, or null if none logged in
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			global $qa_logged_in_userid_checked;

			$suffix=qa_session_var_suffix();

			if (!$qa_logged_in_userid_checked) { // only check once
				qa_start_session(); // this will load logged in userid from the native PHP session, but that's not enough

				$sessionuserid=@$_SESSION['qa_session_userid_'.$suffix];

				if (isset($sessionuserid)) // check verify code matches
					if (@$_SESSION['qa_session_verify_'.$suffix] != qa_session_verify_code($sessionuserid))
						qa_clear_session_user();

				if (!empty($_COOKIE['qa_session'])) {
					@list($handle, $sessioncode, $remember)=explode('/', $_COOKIE['qa_session']);

					if ($remember)
						qa_set_session_cookie($handle, $sessioncode, $remember); // extend 'remember me' cookies each time

					$sessioncode=trim($sessioncode); // trim to prevent passing in blank values to match uninitiated DB rows

					// Try to recover session from the database if PHP session has timed out
					if ( (!isset($_SESSION['qa_session_userid_'.$suffix])) && (!empty($handle)) && (!empty($sessioncode)) ) {
						require_once QA_INCLUDE_DIR.'qa-db-selects.php';

						$userinfo=qa_db_single_select(qa_db_user_account_selectspec($handle, false)); // don't get any pending

						if (strtolower(trim($userinfo['sessioncode'])) == strtolower($sessioncode))
							qa_set_session_user($userinfo['userid'], $userinfo['sessionsource']);
						else
							qa_clear_session_cookie(); // if cookie not valid, remove it to save future checks
					}
				}

				$qa_logged_in_userid_checked=true;
			}

			return @$_SESSION['qa_session_userid_'.$suffix];
		}


		function qa_get_logged_in_source()
	/*
		Get the source of the currently logged in user, from call to qa_log_in_external_user() or null if logged in normally
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			$userid=qa_get_logged_in_userid();
			$suffix=qa_session_var_suffix();

			if (isset($userid))
				return @$_SESSION['qa_session_source_'.$suffix];
		}


		function qa_get_logged_in_user_field($field)
	/*
		Return $field of the currently logged in user, cache to ensure only one call to external code
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			global $qa_cached_logged_in_user;

			$userid=qa_get_logged_in_userid();

			if (isset($userid) && !isset($qa_cached_logged_in_user)) {
				require_once QA_INCLUDE_DIR.'qa-db-selects.php';
				$qa_cached_logged_in_user=qa_db_get_pending_result('loggedinuser', qa_db_user_account_selectspec($userid, true));

				if (!isset($qa_cached_logged_in_user)) { // the user can no longer be found (should be because they're deleted)
					qa_clear_session_user();
					qa_fatal_error('The logged in user cannot be found');
						// it's too late here to proceed because the caller may already be branching based on whether someone is logged in
				}
			}

			return @$qa_cached_logged_in_user[$field];
		}


		function qa_get_logged_in_points()
	/*
		Return the number of points of the currently logged in user, or null if none is logged in
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			return qa_get_logged_in_user_field('points');
		}


		function qa_get_mysql_user_column_type()
	/*
		Return column type to use for users (if not using single sign-on integration)
	*/
		{
			return 'INT UNSIGNED';
		}


		function qa_get_one_user_html($handle, $microformats=false, $favorited=false)
	/*
		Return HTML to display for user with username $handle, with microformats if $microformats is true. Set $favorited to true to show the user as favorited.
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			return strlen($handle) ? ('<a href="'.qa_path_html('user/'.$handle).'" class="qa-user-link'
				.($favorited ? ' qa-user-favorited' : '').($microformats ? ' url nickname' : '').'">'.qa_html($handle).'</a>') : '';
		}


		function qa_get_user_avatar_html($flags, $email, $handle, $blobid, $width, $height, $size, $padding=false)
	/*
		Return HTML to display for the user's avatar, constrained to $size pixels, with optional $padding to that size
		Pass the user's fields $flags, $email, $handle, and avatar $blobid, $width and $height
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			require_once QA_INCLUDE_DIR.'qa-app-format.php';

			if (qa_opt('avatar_allow_gravatar') && ($flags & QA_USER_FLAGS_SHOW_GRAVATAR))
				$html=qa_get_gravatar_html($email, $size);
			elseif (qa_opt('avatar_allow_upload') && (($flags & QA_USER_FLAGS_SHOW_AVATAR)) && isset($blobid))
				$html=qa_get_avatar_blob_html($blobid, $width, $height, $size, $padding);
			elseif ( (qa_opt('avatar_allow_gravatar')||qa_opt('avatar_allow_upload')) && qa_opt('avatar_default_show') && strlen(qa_opt('avatar_default_blobid')) )
				$html=qa_get_avatar_blob_html(qa_opt('avatar_default_blobid'), qa_opt('avatar_default_width'), qa_opt('avatar_default_height'), $size, $padding);
			else
				$html=null;

			return (isset($html) && strlen($handle)) ? ('<a href="'.qa_path_html('user/'.$handle).'" class="qa-avatar-link">'.$html.'</a>') : $html;
		}


		function qa_get_user_email($userid)
	/*
		Return email address for user $userid (if not using single sign-on integration)
	*/
		{
			$userinfo=qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));

			return $userinfo['email'];
		}


		function qa_user_report_action($userid, $action)
	/*
		Called after a database write $action performed by a user $userid
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			require_once QA_INCLUDE_DIR.'qa-db-users.php';

			qa_db_user_written($userid, qa_remote_ip_address());
		}


		function qa_user_level_string($level)
	/*
		Return textual representation of the user $level
	*/
		{
			if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

			if ($level>=QA_USER_LEVEL_SUPER)
				$string='users/level_super';
			elseif ($level>=QA_USER_LEVEL_ADMIN)
				$string='users/level_admin';
			elseif ($level>=QA_USER_LEVEL_MODERATOR)
				$string='users/level_moderator';
			elseif ($level>=QA_USER_LEVEL_EDITOR)
				$string='users/level_editor';
			elseif ($level>=QA_USER_LEVEL_EXPERT)
				$string='users/level_expert';
			elseif ($level>=QA_USER_LEVEL_APPROVED)
				$string='users/approved_user';
			else
				$string='users/registered_user';

			return qa_lang($string);
		}


		function qa_get_login_links($rooturl, $tourl)
	/*
		Return an array of links to login, register, email confirm and logout pages (if not using single sign-on integration)
	*/
		{
			return array(
				'login' => qa_path('login', isset($tourl) ? array('to' => $tourl) : null, $rooturl),
				'register' => qa_path('register', isset($tourl) ? array('to' => $tourl) : null, $rooturl),
				'confirm' => qa_path('confirm', null, $rooturl),
				'logout' => qa_path('logout', null, $rooturl),
			);
		}

	} // end of: if (QA_FINAL_EXTERNAL_USERS) { ... } else { ... }


	function qa_is_logged_in()
/*
	Return whether someone is logged in at the moment
*/
	{
		$userid=qa_get_logged_in_userid();
		return isset($userid);
	}


	function qa_get_logged_in_handle()
/*
	Return displayable handle/username of currently logged in user, or null if none
*/
	{
		return qa_get_logged_in_user_field(QA_FINAL_EXTERNAL_USERS ? 'publicusername' : 'handle');
	}


	function qa_get_logged_in_email()
/*
	Return email of currently logged in user, or null if none
*/
	{
		return qa_get_logged_in_user_field('email');
	}


	function qa_get_logged_in_level()
/*
	Return level of currently logged in user, or null if none
*/
	{
		return qa_get_logged_in_user_field('level');
	}


	function qa_get_logged_in_flags()
/*
	Return flags (see QA_USER_FLAGS_*) of currently logged in user, or null if none
*/
	{
		if (QA_FINAL_EXTERNAL_USERS)
			return qa_get_logged_in_user_field('blocked') ? QA_USER_FLAGS_USER_BLOCKED : 0;
		else
			return qa_get_logged_in_user_field('flags');
	}


	function qa_get_logged_in_levels()
/*
	Return an array of all the specific (e.g. per category) level privileges for the logged in user, retrieving from the database if necessary
*/
	{
		require_once QA_INCLUDE_DIR.'qa-db-selects.php';

		return qa_db_get_pending_result('userlevels', qa_db_user_levels_selectspec(qa_get_logged_in_userid(), true));
	}


	function qa_userids_to_handles($userids)
/*
	Return an array mapping each userid in $userids to that user's handle (public username), or to null if not found
*/
	{
		if (QA_FINAL_EXTERNAL_USERS)
			$rawuseridhandles=qa_get_public_from_userids($userids);

		else {
			require_once QA_INCLUDE_DIR.'qa-db-users.php';
			$rawuseridhandles=qa_db_user_get_userid_handles($userids);
		}

		$gotuseridhandles=array();
		foreach ($userids as $userid)
			$gotuseridhandles[$userid]=@$rawuseridhandles[$userid];

		return $gotuseridhandles;
	}


	function qa_userid_to_handle($userid)
/*
	Return an string mapping the received userid to that user's handle (public username), or to null if not found
*/
	{
		$handles=qa_userids_to_handles(array($userid));
		return empty($handles) ? null : $handles[$userid];
	}


	function qa_handles_to_userids($handles, $exactonly=false)
/*
	Return an array mapping each handle in $handles the user's userid, or null if not found. If $exactonly is true then
	$handles must have the correct case and accents. Otherwise, handles are case- and accent-insensitive, and the keys
	of the returned array will match the $handles provided, not necessary those in the DB.
*/
	{
		require_once QA_INCLUDE_DIR.'qa-util-string.php';

		if (QA_FINAL_EXTERNAL_USERS)
			$rawhandleuserids=qa_get_userids_from_public($handles);

		else {
			require_once QA_INCLUDE_DIR.'qa-db-users.php';
			$rawhandleuserids=qa_db_user_get_handle_userids($handles);
		}

		$gothandleuserids=array();

		if ($exactonly) { // only take the exact matches
			foreach ($handles as $handle)
				$gothandleuserids[$handle]=@$rawhandleuserids[$handle];

		} else { // normalize to lowercase without accents, and then find matches
			$normhandleuserids=array();
			foreach ($rawhandleuserids as $handle => $userid)
				$normhandleuserids[qa_string_remove_accents(qa_strtolower($handle))]=$userid;

			foreach ($handles as $handle)
				$gothandleuserids[$handle]=@$normhandleuserids[qa_string_remove_accents(qa_strtolower($handle))];
		}

		return $gothandleuserids;
	}


	function qa_handle_to_userid($handle)
/*
	Return the userid corresponding to $handle (not case- or accent-sensitive)
*/
	{
		if (QA_FINAL_EXTERNAL_USERS)
			$handleuserids=qa_get_userids_from_public(array($handle));

		else {
			require_once QA_INCLUDE_DIR.'qa-db-users.php';
			$handleuserids=qa_db_user_get_handle_userids(array($handle));
		}

		if (count($handleuserids)==1)
			return reset($handleuserids); // don't use $handleuserids[$handle] since capitalization might be different

		return null;
	}


	function qa_user_level_for_categories($categoryids)
/*
	Return the level of the logged in user for a post with $categoryids (expressing the full hierarchy to the final category)
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		require_once QA_INCLUDE_DIR.'qa-app-updates.php';

		$level=qa_get_logged_in_level();

		if (count($categoryids)) {
			$userlevels=qa_get_logged_in_levels();

			$categorylevels=array(); // create a map
			foreach ($userlevels as $userlevel)
				if ($userlevel['entitytype']==QA_ENTITY_CATEGORY)
					$categorylevels[$userlevel['entityid']]=$userlevel['level'];

			foreach ($categoryids as $categoryid)
				$level=max($level, @$categorylevels[$categoryid]);
		}

		return $level;
	}


	function qa_user_level_for_post($post)
/*
	Return the level of the logged in user for $post, as retrieved from the database
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		if (strlen(@$post['categoryids']))
			return qa_user_level_for_categories(explode(',', $post['categoryids']));

		return null;
	}


	function qa_user_level_maximum()
/*
	Return the maximum possible level of the logged in user in any context (i.e. for any category)
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		$level=qa_get_logged_in_level();

		$userlevels=qa_get_logged_in_levels();
		foreach ($userlevels as $userlevel)
			$level=max($level, $userlevel['level']);

		return $level;
	}


	function qa_user_post_permit_error($permitoption, $post, $limitaction=null, $checkblocks=true)
/*
	Check whether the logged in user has permission to perform $permitoption on post $post (from the database)
	Other parameters and the return value are as for qa_user_permit_error(...)
*/
	{
		return qa_user_permit_error($permitoption, $limitaction, qa_user_level_for_post($post), $checkblocks);
	}


	function qa_user_maximum_permit_error($permitoption, $limitaction=null, $checkblocks=true)
/*
	Check whether the logged in user would have permittion to perform $permitoption in any context (i.e. for any category)
	Other parameters and the return value are as for qa_user_permit_error(...)
*/
	{
		return qa_user_permit_error($permitoption, $limitaction, qa_user_level_maximum(), $checkblocks);
	}


	function qa_user_permit_error($permitoption=null, $limitaction=null, $userlevel=null, $checkblocks=true)
/*
	Check whether the logged in user has permission to perform $permitoption. If $permitoption is null, this simply
	checks whether the user is blocked. Optionally provide an $limitaction (see top of qa-app-limits.php) to also check
	against user or IP rate limits. You can pass in a QA_USER_LEVEL_* constant in $userlevel to consider the user at a
	different level to usual (e.g. if they are performing this action in a category for which they have elevated
	privileges). To ignore the user's blocked status, set $checkblocks to false.

	Possible results, in order of priority (i.e. if more than one reason, the first will be given):
	'level' => a special privilege level (e.g. expert) or minimum number of points is required
	'login' => the user should login or register
	'userblock' => the user has been blocked
	'ipblock' => the ip address has been blocked
	'confirm' => the user should confirm their email address
	'approve' => the user needs to be approved by the site admins
	'limit' => the user or IP address has reached a rate limit (if $limitaction specified)
	false => the operation can go ahead
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		require_once QA_INCLUDE_DIR.'qa-app-limits.php';

		$userid=qa_get_logged_in_userid();
		if (!isset($userlevel))
			$userlevel=qa_get_logged_in_level();

		$flags=qa_get_logged_in_flags();
		if (!$checkblocks)
			$flags&=~QA_USER_FLAGS_USER_BLOCKED;

		$error=qa_permit_error($permitoption, $userid, $userlevel, $flags);

		if ($checkblocks && (!$error) && qa_is_ip_blocked())
			$error='ipblock';

		if ((!$error) && isset($userid) && ($flags & QA_USER_FLAGS_MUST_CONFIRM) && qa_opt('confirm_user_emails'))
			$error='confirm';

		if ((!$error) && isset($userid) && ($flags & QA_USER_FLAGS_MUST_APPROVE) && qa_opt('moderate_users'))
			$error='approve';

		if (isset($limitaction) && !$error)
			if (qa_user_limits_remaining($limitaction)<=0)
				$error='limit';

		return $error;
	}


	function qa_permit_error($permitoption, $userid, $userlevel, $userflags, $userpoints=null)
/*
	Check whether $userid (null for no user) can perform $permitoption. Result as for qa_user_permit_error(...).
	If appropriate, pass the user's level in $userlevel, flags in $userflags and points in $userpoints.
	If $userid is currently logged in, you can set $userpoints=null to retrieve them only if necessary.
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		$permit=isset($permitoption) ? qa_opt($permitoption) : QA_PERMIT_ALL;

		if (isset($userid) && (($permit==QA_PERMIT_POINTS) || ($permit==QA_PERMIT_POINTS_CONFIRMED) || ($permit==QA_PERMIT_APPROVED_POINTS)) ) {
				// deal with points threshold by converting as appropriate

			if ( (!isset($userpoints)) && ($userid==qa_get_logged_in_userid()) )
				$userpoints=qa_get_logged_in_points(); // allow late retrieval of points (to avoid unnecessary DB query when using external users)

			if ($userpoints>=qa_opt($permitoption.'_points'))
				$permit=($permit==QA_PERMIT_APPROVED_POINTS) ? QA_PERMIT_APPROVED :
					(($permit==QA_PERMIT_POINTS_CONFIRMED) ? QA_PERMIT_CONFIRMED : QA_PERMIT_USERS); // convert if user has enough points
			else
				$permit=QA_PERMIT_EXPERTS; // otherwise show a generic message so they're not tempted to collect points just for this
		}

		return qa_permit_value_error($permit, $userid, $userlevel, $userflags);
	}


	function qa_permit_value_error($permit, $userid, $userlevel, $userflags)
/*
	Check whether $userid of level $userlevel with $userflags can reach the permission level in $permit
	(generally retrieved from an option, but not always). Result as for qa_user_permit_error(...).
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		if ($permit>=QA_PERMIT_ALL)
			$error=false;

		elseif ($permit>=QA_PERMIT_USERS)
			$error=isset($userid) ? false : 'login';

		elseif ($permit>=QA_PERMIT_CONFIRMED) {
			if (!isset($userid))
				$error='login';

			elseif (
				QA_FINAL_EXTERNAL_USERS || // not currently supported by single sign-on integration
				($userlevel>=QA_PERMIT_APPROVED) || // if user approved or assigned to a higher level, no need
				($userflags & QA_USER_FLAGS_EMAIL_CONFIRMED) || // actual confirmation
				(!qa_opt('confirm_user_emails')) // if this option off, we can't ask it of the user
			)
				$error=false;

			else
				$error='confirm';

		} elseif ($permit>=QA_PERMIT_APPROVED) {
			if (!isset($userid))
				$error='login';

			elseif (
				($userlevel>=QA_USER_LEVEL_APPROVED) || // user has been approved
				(!qa_opt('moderate_users')) // if this option off, we can't ask it of the user
			)
				$error=false;

			else
				$error='approve';

		} elseif ($permit>=QA_PERMIT_EXPERTS)
			$error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_EXPERT)) ? false : 'level';

		elseif ($permit>=QA_PERMIT_EDITORS)
			$error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_EDITOR)) ? false : 'level';

		elseif ($permit>=QA_PERMIT_MODERATORS)
			$error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_MODERATOR)) ? false : 'level';

		elseif ($permit>=QA_PERMIT_ADMINS)
			$error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_ADMIN)) ? false : 'level';

		else
			$error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_SUPER)) ? false : 'level';

		if (isset($userid) && ($userflags & QA_USER_FLAGS_USER_BLOCKED) && ($error!='level'))
			$error='userblock';

		return $error;
	}


	function qa_user_captcha_reason($userlevel=null)
/*
	Return whether a captcha is required for posts submitted by the current user. You can pass in a QA_USER_LEVEL_*
	constant in $userlevel to consider the user at a different level to usual (e.g. if they are performing this action
	in a category for which they have elevated privileges).

	Possible results:
	'login' => captcha required because the user is not logged in
	'approve' => captcha required because the user has not been approved
	'confirm' => captcha required because the user has not confirmed their email address
	false => captcha is not required
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		$reason=false;
		if (!isset($userlevel))
			$userlevel=qa_get_logged_in_level();

		if ($userlevel < QA_USER_LEVEL_APPROVED) { // approved users and above aren't shown captchas
			$userid=qa_get_logged_in_userid();

			if (qa_opt('captcha_on_anon_post') && !isset($userid))
				$reason='login';
			elseif (qa_opt('moderate_users') && qa_opt('captcha_on_unapproved'))
				$reason='approve';
			elseif (qa_opt('confirm_user_emails') && qa_opt('captcha_on_unconfirmed') && !(qa_get_logged_in_flags() & QA_USER_FLAGS_EMAIL_CONFIRMED) )
				$reason='confirm';
		}

		return $reason;
	}


	function qa_user_use_captcha($userlevel=null)
/*
	Return whether a captcha should be presented to the logged in user for writing posts. You can pass in a
	QA_USER_LEVEL_* constant in $userlevel to consider the user at a different level to usual.
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		return qa_user_captcha_reason($userlevel)!=false;
	}


	function qa_user_moderation_reason($userlevel=null)
/*
	Return whether moderation is required for posts submitted by the current user. You can pass in a QA_USER_LEVEL_*
constant in $userlevel to consider the user at a different level to usual (e.g. if they are performing this action
in a category for which they have elevated privileges).

	Possible results:
	'login' => moderation required because the user is not logged in
	'approve' => moderation required because the user has not been approved
	'confirm' => moderation required because the user has not confirmed their email address
	'points' => moderation required because the user has insufficient points
	false => moderation is not required
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		$reason=false;
		if (!isset($userlevel))
			$userlevel=qa_get_logged_in_level();

		if (
			($userlevel < QA_USER_LEVEL_EXPERT) && // experts and above aren't moderated
			qa_user_permit_error('permit_moderate') // if the user can approve posts, no point in moderating theirs
		) {
			$userid=qa_get_logged_in_userid();

			if (isset($userid)) {
				if (qa_opt('moderate_users') && qa_opt('moderate_unapproved') && ($userlevel<QA_USER_LEVEL_APPROVED))
					$reason='approve';
				elseif (qa_opt('confirm_user_emails') && qa_opt('moderate_unconfirmed') && !(qa_get_logged_in_flags() & QA_USER_FLAGS_EMAIL_CONFIRMED) )
					$reason='confirm';
				elseif (qa_opt('moderate_by_points') && (qa_get_logged_in_points() < qa_opt('moderate_points_limit')))
					$reason='points';

			} elseif (qa_opt('moderate_anon_post'))
				$reason='login';
		}

		return $reason;
	}


	function qa_user_userfield_label($userfield)
/*
	Return the label to display for $userfield as retrieved from the database, using default if no name set
*/
	{
		if (isset($userfield['content']))
			return $userfield['content'];

		else {
			$defaultlabels=array(
				'name' => 'users/full_name',
				'about' => 'users/about',
				'location' => 'users/location',
				'website' => 'users/website',
			);

			if (isset($defaultlabels[$userfield['title']]))
				return qa_lang($defaultlabels[$userfield['title']]);
		}

		return '';
	}


	function qa_set_form_security_key()
/*
	Set or extend the cookie in browser of non logged-in users which identifies them for the purposes of form security (anti-CSRF protection)
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		global $qa_form_key_cookie_set;

		if ( (!qa_is_logged_in()) && !@$qa_form_key_cookie_set) {
			$qa_form_key_cookie_set=true;

			if (strlen(@$_COOKIE['qa_key'])!=QA_FORM_KEY_LENGTH) {
				require_once QA_INCLUDE_DIR.'qa-util-string.php';
				$_COOKIE['qa_key']=qa_random_alphanum(QA_FORM_KEY_LENGTH);
			}

			setcookie('qa_key', $_COOKIE['qa_key'], time()+2*QA_FORM_EXPIRY_SECS, '/', QA_COOKIE_DOMAIN); // extend on every page request
		}
	}


	function qa_calc_form_security_hash($action, $timestamp)
/*
	Return the form security (anti-CSRF protection) hash for an $action (any string), that can be performed within
	QA_FORM_EXPIRY_SECS of $timestamp (in unix seconds) by the current user.
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		$salt=qa_opt('form_security_salt');

		if (qa_is_logged_in())
			return sha1($salt.'/'.$action.'/'.$timestamp.'/'.qa_get_logged_in_userid().'/'.qa_get_logged_in_user_field('passsalt'));
		else
			return sha1($salt.'/'.$action.'/'.$timestamp.'/'.@$_COOKIE['qa_key']); // lower security for non logged in users - code+cookie can be transferred
	}


	function qa_get_form_security_code($action)
/*
	Return the full form security (anti-CSRF protection) code for an $action (any string) performed within
	QA_FORM_EXPIRY_SECS of now by the current user.
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		qa_set_form_security_key();

		$timestamp=qa_opt('db_time');

		return (int)qa_is_logged_in().'-'.$timestamp.'-'.qa_calc_form_security_hash($action, $timestamp);
	}


	function qa_check_form_security_code($action, $value)
/*
	Return whether $value matches the expected form security (anti-CSRF protection) code for $action (any string) and
	that the code has not expired (if more than QA_FORM_EXPIRY_SECS have passed). Logs causes for suspicion.
*/
	{
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

		$reportproblems=array();
		$silentproblems=array();

		if (!isset($value))
			$silentproblems[]='code missing';

		else if (!strlen($value))
			$silentproblems[]='code empty';

		else {
			$parts=explode('-', $value);

			if (count($parts)==3) {
				$loggedin=$parts[0];
				$timestamp=$parts[1];
				$hash=$parts[2];
				$timenow=qa_opt('db_time');

				if ($timestamp>$timenow)
					$reportproblems[]='time '.($timestamp-$timenow).'s in future';
				elseif ($timestamp<($timenow-QA_FORM_EXPIRY_SECS))
					$silentproblems[]='timeout after '.($timenow-$timestamp).'s';

				if (qa_is_logged_in()) {
					if (!$loggedin)
						$silentproblems[]='now logged in';

				} else {
					if ($loggedin)
						$silentproblems[]='now logged out';

					else {
						$key=@$_COOKIE['qa_key'];

						if (!isset($key))
							$silentproblems[]='key cookie missing';
						elseif (!strlen($key))
							$silentproblems[]='key cookie empty';
						else if (strlen($key)!=QA_FORM_KEY_LENGTH)
							$reportproblems[]='key cookie '.$key.' invalid';
					}
				}

				if (empty($silentproblems) && empty($reportproblems))
					if (strtolower(qa_calc_form_security_hash($action, $timestamp))!=strtolower($hash))
						$reportproblems[]='code mismatch';

			} else
				$reportproblems[]='code '.$value.' malformed';
		}

		if (count($reportproblems))
			@error_log(
				'PHP Question2Answer form security violation for '.$action.
				' by '.(qa_is_logged_in() ? ('userid '.qa_get_logged_in_userid()) : 'anonymous').
				' ('.implode(', ', array_merge($reportproblems, $silentproblems)).')'.
				' on '.@$_SERVER['REQUEST_URI'].
				' via '.@$_SERVER['HTTP_REFERER']
			);

		return (empty($silentproblems) && empty($reportproblems));
	}


/*
	Omit PHP closing tag to help avoid accidental output
*/
