<?php
/*
 * e107 website system
 *
 * Copyright (C) 2001-2008 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
 * Handler - user-related functions
 *
 * $Source: /cvs_backup/e107_0.8/e107_handlers/user_handler.php,v $
 * $Revision: 1.10 $
 * $Date: 2009-06-12 20:41:34 $
 * $Author: e107steved $
 *
*/


/*
USER HANDLER CLASS - manages login and various user functions

Vetting routines TODO:
	user_sess processing
	user_image processing
	user_xup processing - nothing special?
*/


if (!defined('e107_INIT')) { exit; }

// Codes for `user_ban` field (not all used ATM)
define('USER_VALIDATED',0);
define('USER_BANNED',1);
define('USER_REGISTERED_NOT_VALIDATED',2);
define('USER_EMAIL_BOUNCED', 3);
define('USER_BOUNCED_RESET', 4);
define('USER_TEMPORARY_ACCOUNT', 5);


define('PASSWORD_E107_MD5',0);
define('PASSWORD_E107_SALT',1);

define('PASSWORD_E107_ID','$E$');			// E107 salted


define('PASSWORD_INVALID', FALSE);
define('PASSWORD_VALID',TRUE);
define ('PASSWORD_DEFAULT_TYPE',PASSWORD_E107_MD5);
//define ('PASSWORD_DEFAULT_TYPE',PASSWORD_E107_SALT);

// Required language file - if not loaded elsewhere, uncomment next line
//include_lan(e_LANGUAGEDIR.e_LANGUAGE.'/lan_user.php');

class UserHandler
{
	var $userVettingInfo = array();
	var $preferred = PASSWORD_DEFAULT_TYPE;			// Preferred password format
	var $passwordOpts = 0;							// Copy of pref
	var $passwordEmail = FALSE;						// True if can use email address to log in
	var $otherFields = array();

	// Constructor
	function UserHandler()
	{
	  global $pref;

/*
	Table of vetting methods for user data - lists every field whose value could be set manually.
	Valid 'vetMethod' values (use comma separated list for multiple vetting):
		0 - Null method
		1 - Check for duplicates
		2 - Check against $pref['signup_disallow_text']

	Index is the destination field name. If the source index name is different, specify 'srcName' in the array.

	Possible processing options:
		'dbClean'		- 'sanitising' method for final value:
							- 'toDB' - passes final value through $tp->toDB()
							- 'intval' - converts to an integer
							- 'image'  - checks image for size
							- 'avatar' - checks an image in the avatars directory
		'stripTags'		- strips HTML tags from the value (not an error if there are some)
		'minLength'		- minimum length (in utf-8 characters) for the string
		'maxLength'		- minimum length (in utf-8 characters) for the string
		'longTrim'		- if set, and the string exceeds maxLength, its trimmed
		'enablePref'	- value is processed only if the named $pref evaluates to true; otherwise any input is discarded without error
*/
	$this->userVettingInfo = array(
		'user_name' => array('niceName'=> LAN_USER_01, 'fieldType' => 'string', 'vetMethod' => '1,2', 'vetParam' => 'signup_disallow_text', 'srcName' => 'username', 'stripTags' => TRUE, 'stripChars' => '/&nbsp;|\#|\=|\$/', fixedBlock => 'anonymous', 'minLength' => 2, 'maxLength' => varset($pref['displayname_maxlength'],15)),				// Display name
		'user_loginname' => array('niceName'=> LAN_USER_02, 'fieldType' => 'string', 'vetMethod' => '1', 'vetParam' => '', 'srcName' => 'loginname', 'stripTags' => TRUE, 'stripChars' => '/&nbsp;|\#|\=|\$/', 'minLength' => 2, 'maxLength' => varset($pref['loginname_maxlength'],30)),			// User name
		'user_login' => array('niceName'=> LAN_USER_03, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'realname', 'dbClean' => 'toDB'),				// Real name (no real vetting)
		'user_customtitle' => array('niceName'=> LAN_USER_04, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'customtitle', 'dbClean' => 'toDB', 'enablePref' => 'signup_option_customtitle'),		// No real vetting
		'user_password' => array('niceName'=> LAN_USER_05, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'password1', 'dataType' => 2, 'minLength' => varset($pref['signup_pass_len'],1)),
		'user_sess' => array('niceName'=> LAN_USER_06, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'stripChars' => "#\"|'|(|)#", 'dbClean' => 'image', 'imagePath' => e_FILE.'public/avatars/', 'maxHeight' => varset($pref['im_height'], 100), 'maxWidth' => varset($pref['im_width'], 120)),				// Photo
		'user_image' => array('niceName'=> LAN_USER_07, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'image', 'stripChars' => "#\"|'|(|)#", 'dbClean' => 'avatar', 'maxHeight' => varset($pref['im_height'], 100), 'maxWidth' => varset($pref['im_width'], 120)),				// Avatar
		'user_email' => array('niceName'=> LAN_USER_08, 'fieldType' => 'string', 'vetMethod' => '1,3', 'vetParam' => '', 'srcName' => 'email', 'dbClean' => 'toDB'),
		'user_signature' => array('niceName'=> LAN_USER_09, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'signature', 'dbClean' => 'toDB'),
		'user_hideemail' => array('niceName'=> LAN_USER_10, 'fieldType' => 'int', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'hideemail', 'dbClean' => 'intval'),
		'user_xup' => array('niceName'=> LAN_USER_11, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'user_xup', 'dbClean' => 'toDB'),
		'user_class' => array('niceName'=> LAN_USER_12, 'fieldType' => 'string', 'vetMethod' => '0', 'vetParam' => '', 'srcName' => 'class', 'dataType' => '1')
	);

		$this->otherFields = array(
			'user_join'			=> LAN_USER_14,
			'user_lastvisit'	=> LAN_USER_15,
			'user_currentvisit'	=> LAN_USER_16,
			'user_comments'		=> LAN_USER_17,
			'user_ip'			=> LAN_USER_18,
			'user_ban'			=> LAN_USER_19,
			'user_prefs'		=> LAN_USER_20,
			'user_visits'		=> LAN_USER_21,
			'user_admin'		=> LAN_USER_22,
			'user_perms'		=> LAN_USER_23,
			'user_pwchange'		=> LAN_USER_24
//			user_chats int(10) unsigned NOT NULL default '0',
			);
		$this->otherFieldTypes = array(
			'user_join'			=> 'int',
			'user_lastvisit'	=> 'int',
			'user_currentvisit'	=> 'int',
			'user_comments'		=> 'int',
			'user_ip'			=> 'string',
			'user_ban'			=> 'int',
			'user_prefs'		=> 'string',
			'user_visits'		=> 'int',
			'user_admin'		=> 'int',
			'user_perms'		=> 'string',
			'user_pwchange'		=> 'int'
			);

	  $this->passwordOpts = varset($pref['passwordEncoding'],0);
	  $this->passwordEmail = varset($pref['allowEmailLogin'],FALSE);
	  switch ($this->passwordOpts)
	  {
	    case 1 :
		case 2 :
		  $this->preferred = PASSWORD_E107_SALT;
		  break;
		case 0 :
		default :
		  $this->preferred = PASSWORD_E107_MD5;
		  $this->passwordOpts = 0;		// In case it got set to some stupid value
		  break;
	  }
	  return FALSE;
	}


	// Given plaintext password and login name, generate password string to store in DB
	function HashPassword($password, $login_name, $force='')
	{
	  if ($force == '') $force = $this->preferred;
	  switch ($force)
	  {
		case PASSWORD_E107_MD5 :
		  return md5($password);

		case PASSWORD_E107_SALT :
		  return PASSWORD_E107_ID.md5(md5($password).$login_name);
		  break;
	  }
	  return FALSE;
	}


	// Verify existing plaintext password against a stored hash value (which defines the encoding format and any 'salt')
	// Return PASSWORD_INVALID if invalid password
	// Return PASSWORD_VALID if valid password
	// Return a new hash to store if valid password but non-preferred encoding
	function CheckPassword($password, $login_name, $stored_hash)
	{
	  if (strlen(trim($password)) == 0) return PASSWORD_INVALID;
	  if (($this->passwordOpts <= 1) && (strlen($stored_hash) == 32))
	  {	// Its simple md5 encoding
		if (md5($password) !== $stored_hash) return PASSWORD_INVALID;
		if ($this->preferred == PASSWORD_E107_MD5) return PASSWORD_VALID;
		return $this->HashPassword($password);		// Valid password, but non-preferred encoding; return the new hash
	  }

	  // Allow the salted password even if disabled - for those that do try to go back!
//  	  if (($this->passwordOpts >= 1) && (strlen($stored_hash) == 35) && (substr($stored_hash,0,3) == PASSWORD_E107_ID))
  	  if ((strlen($stored_hash) == 35) && (substr($stored_hash,0,3) == PASSWORD_E107_ID))
	  {	// Its the standard E107 salted hash
		$hash = $this->HashPassword($password, $login_name, PASSWORD_E107_SALT);
		if ($hash === FALSE) return PASSWORD_INVALID;
		return ($hash == $stored_hash) ? PASSWORD_VALID : PASSWORD_INVALID;
	  }

	  return PASSWORD_INVALID;
	}


	// Verifies a standard response to a CHAP challenge
	function CheckCHAP($challenge, $response, $login_name, $stored_hash )
	{
	  if (strlen($challenge) != 40) return PASSWORD_INVALID;
	  if (strlen($response) != 32) return PASSWORD_INVALID;
	  $valid_ret = PASSWORD_VALID;
	  if (strlen($stored_hash) == 32)
	  {	// Its simple md5 password storage
		$stored_hash = PASSWORD_E107_ID.md5($stored_hash.$login_name);			// Convert to the salted format always used by CHAP
		if ($this->passwordOpts != PASSWORD_E107_MD5) $valid_ret = $stored_response;
	  }
	  $testval = md5(substr($stored_hash,strlen(PASSWORD_E107_ID)).$challenge);
	  if ($testval == $response) return $valid_ret;
	  return PASSWORD_INVALID;
	}



	// Checks whether the user has to validate a user setting change by entering password (basically, if that field affects the
	// stored password value)
	// Returns TRUE if change required, FALSE otherwise
	function isPasswordRequired($fieldName)
	{
		if ($this->preferred == PASSWORD_E107_MD5) return FALSE;
		switch ($fieldName)
		{
			case 'user_email' :
				return $this->passwordEmail;
			case 'user_loginname' :
				return TRUE;
		}
		return FALSE;
	}


	// Determines whether its necessary to store a separate password for email address validation
	function needEmailPassword()
	{
		if ($this->preferred == PASSWORD_E107_MD5) return FALSE;
		if ($this->passwordEmail) return TRUE;
		return FALSE;
	}


	// Checks whether the password value can be converted to the current default
	// Returns TRUE if conversion possible.
	// Returns FALSE if conversion not possible, or not needed
	function canConvert($password)
	{
	  if ($this->preferred == PASSWORD_E107_MD5) return FALSE;
	  if (strlen($password) == 32) return TRUE;		// Can convert from md5 to salted
	  return FALSE;
	}


	// Given md5-encoded password and login name, generate password string to store in DB
	function ConvertPassword($password, $login_name)
	{
	  if ($this->canConvert($password) === FALSE) return $password;
	  return PASSWORD_E107_ID.md5($password.$login_name);
	}



	// Generates a random user login name according to some pattern.
	// Checked for uniqueness.
	function generateUserLogin($pattern, $seed='')
	{
	  $ul_sql = new db;
	  if (strlen($pattern) < 6) $pattern = '##....';
	  do
	  {
		$newname = $this->generateRandomString($pattern, $seed);
	  } while ($ul_sql->db_Select('user','user_id',"`user_loginname`='{$newname}'"));
	  return $newname;
	}



	// Generates a random string - for user login name, password etc, according to some pattern.
	// Checked for uniqueness.
	// Pattern format:
	//		# - an alpha character
	//		. - a numeric character
	//		* - an alphanumeric character
	//		^ - next character from seed
	//		alphanumerics are included 'as is'
	function generateRandomString($pattern, $seed='')
	{
	  if (strlen($pattern) < 6) $pattern = '##....';
	  $newname = '';
	  $seed_ptr = 0;			// Next character of seed (if used)
	  for ($i = 0; $i < strlen($pattern); $i++)
	  {
		$c = $pattern[$i];
		switch ($c)
		{
		  case '#' :		// Alpha only (upper and lower case)
			do
			{
			  $t = chr(rand(65,122));
			} while (!ctype_alpha($t));
			$newname .= $t;
			break;
		  case '.' :		// Numeric only
			do
			{
			  $t = chr(rand(48,57));
			} while (!ctype_digit($t));
			$newname .= $t;
			break;
		  case '*' :		// Alphanumeric
			do
			{
			  $t = chr(rand(48,122));
			} while (!ctype_alnum($t));
			$newname .= $t;
			break;
		  case '^' :		// Next character from seed
			if ($seed_ptr < strlen($seed))
			{
			  $newname .= $seed[$seed_ptr];
			  $seed_ptr++;
			}
			break;
		  default :
			if (ctype_alnum($c)) $newname .= $c;
			// (else just ignore other characters in pattern)
		}
	  }
	  return $newname;
	}



	// Split up an email address to check for banned domains.
	// Return false if invalid address. Otherwise returns a set of values to check
	function make_email_query($email, $fieldname = 'banlist_ip')
	{
	  global $tp;
	  $tmp = strtolower($tp -> toDB(trim(substr($email, strrpos($email, "@")+1))));	// Pull out the domain name
	  if ($tmp == '') return FALSE;
	  if (strpos($tmp,'.') === FALSE) return FALSE;
	  $em = array_reverse(explode('.',$tmp));
	  $line = '';
	  $out = array();
	  foreach ($em as $e)
	  {
		$line = '.'.$e.$line;
		$out[] = '`'.$fieldname."`='*{$line}'";
	  }
	  return implode(' OR ',$out);
	}



	function makeUserCookie($lode,$autologin = FALSE)
	{
		global $pref;
		$cookieval = $lode['user_id'].".".md5($lode['user_password']);		// (Use extra md5 on cookie value to obscure hashed value for password)
		if ($pref['user_tracking'] == "session")
		{
			$_SESSION[$pref['cookie_name']] = $cookieval;
		}
		else
		{
			if ($autologin == 1)
			{	// Cookie valid for up to 30 days
				cookie($pref['cookie_name'], $cookieval, (time() + 3600 * 24 * 30));
			}
			else
			{
				cookie($pref['cookie_name'], $cookieval);
			}
		}
	}


	// Generate an array of all the basic classes a user belongs to
	// if $asArray TRUE, returns results in an array; else as a comma-separated string
	// If $incInherited is TRUE, includes inherited classes
	function addCommonClasses($userData, $asArray = FALSE, $incInherited = FALSE)
	{
		if ($incInherited)
		{
			$classList = array();
			global $e_userclass;
			if (!isset($e_userclass) && !is_object($e_userclass))
			{
				require_once(e_HANDLER."userclass_class.php");
				$e_userclass = new user_class;
			}
			$classList = $e_userclass->get_all_user_classes($var['user_class']);
		}
		else
		{
			if ($userData['user_class'] != '') $classList = explode(',',$userData['user_class']);
		}
		foreach (array(e_UC_MEMBER, e_UC_READONLY, e_UC_PUBLIC) as $c)
		{
			if (!in_array($c,$classList))
			{
				$classList[] = $c;
			}
		}
		if ((varset($userData['user_admin'],0) == 1) && strlen($userData['user_perms']))
		{
		  $classList[] = e_UC_ADMIN;
		  if (strpos($userData['user_perms'],'0') === 0)
		  {
			$classList[] = e_UC_MAINADMIN;
		  }
		}
		if ($asArray) return $classList;
		return implode(',',$classList);
	}


	// Return an array of descriptive names for each field in the user DB. If $all is false, just returns the modifiable ones. Else returns all
	function getNiceNames($all = FALSE)
	{
//		$ret = array('user_id' => LAN_USER_13);
		foreach ($this->userVettingInfo as $k => $v)
		{
			$ret[$k] = $v['niceName'];
		}
		if ($all)
		{
			$ret = array_merge($ret, $this->otherFields);
		}
		return $ret;
	}
//===================================================
//			User Field validation
//===================================================

/*	$_POST field names:

	DB				signup			usersettings	quick add		function
  ------------------------------------------------------------------------------
  user_id 			-				user_id			-				Unique user ID
  user_name 		name$			username		username		Display name
  user_loginname	loginname		loginname 		loginname		User name (login name)
  user_customtitle 	-				customtitle		-				Custom title
  user_password 	password1		password1		password1		Password (prior to encoding)
					password2		password2		password1		(Check password field)
  user_sess 						*				-				Photo (file on server)
  user_email		email			email			email			Email address
					email_confirm
  user_signature 	signature		signature		-				User signature
  user_image		image			image*			-				Avatar (may be external URL or file on server)
  user_hideemail	hideemail		hideemail		-				Flag to hide user's email address
  user_login		realname		realname		realname		User Real name
  user_xup			xupexist$		user_xup		-				XUP file link
  user_class		class			class			userclass		User class (array on form)

user_loginname may be auto-generated
* avatar (user_image) and photo (user_sess) may be uploaded files
$changed to match the majority vote

Following fields auto-filled in code as required:
  user_join
  user_lastvisit
  user_currentvisit
  user_chats
  user_comments
  user_forums
  user_ip
  user_ban
  user_prefs
  user_viewed
  user_visits
  user_admin
  user_perms
  user_pwchange

*/
	// Function does validation specific to user data. Updates the $targetData array as appropriate.
	// Returns TRUE if nothing updated; FALSE if errors found (only checks data previously passed as good)
	function userValidation(&$targetData)
	{
		global $e107, $pref;
		$u_sql = new db;
		$ret = TRUE;
		if (isset($targetData['data']['user_email']))
		{
			$v = trim($targetData['data']['user_email']);		// Always check email address if its entered
			if ($v == '')
			{
				$errMsg = ERR_MISSING_VALUE;
			}
			elseif (!check_email($v))
			{
				$errMsg = ERR_INVALID_EMAIL;
			}
			elseif ($u_sql->db_Count('user', '(*)', "WHERE `user_email`='".$v."' AND `user_ban`=1 "))
			{
				$errMsg = ERR_BANNED_USER;
			}
			else
			{	// See if email address banned
				$wc = $this->make_email_query($v);		// Generate the query for the ban list
				if ($wc) { $wc = "`banlist_ip`='{$v}' OR ".$wc;  }
				if (($wc === FALSE) || !$e107->check_ban($wc, FALSE, TRUE))
				{
					echo "Email banned<br />";
					$errMsg = ERR_BANNED_EMAIL;
				}
			}
			if ($errMsg)
			{
				unset($targetData['data']['user_email']);			// Remove the valid entry
			}
		}
		else
		{
			if (!isset($targetData['errors']['user_email']) && !varset($pref['disable_emailcheck'],FALSE))
			{	// We may have already picked up an error on the email address - or it may be allowed to be empty
				$errMsg = ERR_MISSING_VALUE;
			}
		}
		if ($errMsg)
		{	// Update the error
			$targetData['errors']['user_email'] = $errMsg;
			$targetData['failed']['user_email'] = $v;
			$ret = FALSE;
		}
		return $ret;
	}

	// Given an array of user data intended to be written to the DB, adds empty strings (or other default value) for any field which doesn't have a default in the SQL definition.
	// (Avoids problems with MySQL in STRICT mode.).
	// Returns TRUE if additions made, FALSE if no change.
	function addNonDefaulted(&$userInfo)
	{
//		$nonDefaulted = array('user_signature' => '', 'user_prefs' => '', 'user_class' => '', 'user_perms' => '');
		$nonDefaulted = array('user_signature' => '', 'user_prefs' => '', 'user_class' => '', 'user_perms' => '', 'user_realm' => '');	// Delete when McFly finished
		$ret = FALSE;
		foreach ($nonDefaulted as $k => $v)
		{
			if (!isset($userInfo[$k]))
			{
				$userInfo[$k] = $v;
				$ret = TRUE;
			}
		}
		return $ret;
	}


	// Delete time-expired partial registrations from the user DB, clean up user_extended table
	function deleteExpired($force = FALSE)
	{
		global $pref, $sql;
		$temp1 = 0;
		if (isset($pref['del_unv']) && $pref['del_unv'] && $pref['user_reg_veri'] != 2)
		{
			$threshold= intval(time() - ($pref['del_unv'] * 60));
			if (($temp1 = $sql->db_Delete('user', 'user_ban = 2 AND user_join < '.$threshold)) > 0) { $force = TRUE; }
		}
		if ($force)
		{	// Remove 'orphaned' extended user field records
			$sql->db_Select_gen("DELETE `#user_extended` FROM `#user_extended` LEFT JOIN `#user` ON `#user_extended`.`user_extended_id`=`#user`.`user_id`
					WHERE `#user`.`user_id` IS NULL");
		}
		return $temp1;
	}


	// Called to update initial user classes, probationary user class etc
	function userClassUpdate(&$user, $event='userveri')
	{
		global $pref, $tp;

		$initClasses = array();
		$doClasses = FALSE;
		$doProbation = FALSE;
		$ret = FALSE;
		switch ($event)
		{
			case 'userall' :
				$doClasses = TRUE;
				$doProbation = TRUE;
				break;
			case 'userfull' :		// A 'fully fledged' user
				if (!$pref['user_reg_veri'] || ($pref['init_class_stage'] == '2'))
				{
					$doClasses = TRUE;
				}
				$doProbation = TRUE;
				break;
			case 'userpartial' :
				if ($pref['init_class_stage'] == '1')
				{	// Set initial classes if to be done on partial signup, or if selected to add them now
					$doClasses = TRUE;
				}
				$doProbation = TRUE;
				break;
		}
		if ($doClasses)
		{
			if (isset($pref['initial_user_classes'])) { $initClasses = explode(',',$pref['initial_user_classes']); }	 // Any initial user classes to be set at some stage
			if ($doProbation && (varset($pref['user_new_period'], 0) > 0))
			{
				$initClasses[] = e_UC_NEWUSER;		// Probationary user class
			}
			if (count($initClasses))
			{	// Update the user classes
				if ($user['user_class'])
				{
					$initClasses = array_unique(array_merge($initClasses, explode(',',$user['user_class'])));
				}
				$user['user_class'] = $tp->toDB(implode(',',$initClasses));
				$ret = TRUE;
			}
		}

	}
}



?>