<?php
/**
 * TUserManager class
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2011 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Security
 */

/**
 * Using TUser class
 */
Prado::using('System.Security.TUser');

/**
 * TUserManager class
 *
 * TUserManager manages a static list of users {@link TUser}.
 * The user information is specified via module configuration using the following XML syntax,
 * <code>
 * <module id="users" class="System.Security.TUserManager" PasswordMode="Clear">
 *   <user name="Joe" password="demo" />
 *   <user name="John" password="demo" />
 *   <role name="Administrator" users="John" />
 *   <role name="Writer" users="Joe,John" />
 * </module>
 * </code>
 *
 * PHP configuration style:
 * <code>
 * array(
 *   'users' => array(
 *      'class' => 'System.Security.TUserManager',
 *      'properties' => array(
 *         'PasswordMode' => 'Clear',
 *       ),
 *       'users' => array(
 *          array('name'=>'Joe','password'=>'demo'),
 *          array('name'=>'John','password'=>'demo'),
 *       ),
 *       'roles' => array(
 *          array('name'=>'Administrator','users'=>'John'),
 *          array('name'=>'Writer','users'=>'Joe,John'),
 *       ),
 *    ),
 * )
 * </code>
 *
 * In addition, user information can also be loaded from an external file
 * specified by {@link setUserFile UserFile} property. Note, the property
 * only accepts a file path in namespace format. The user file format is
 * similar to the above sample.
 *
 * The user passwords may be specified as clear text, SH1 or MD5 hashed by setting
 * {@link setPasswordMode PasswordMode} as <b>Clear</b>, <b>SHA1</b> or <b>MD5</b>.
 * The default name for a guest user is <b>Guest</b>. It may be changed
 * by setting {@link setGuestName GuestName} property.
 *
 * TUserManager may be used together with {@link TAuthManager} which manages
 * how users are authenticated and authorized in a Prado application.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Carl Mathisen <carl@kamikazemedia.no>
 * @version $Id$
 * @package System.Security
 * @since 3.0
 */
class TUserManager extends TModule implements IUserManager
{
	/**
	 * extension name to the user file
	 */
	const USER_FILE_EXT='.xml';

	/**
	 * @var array list of users managed by this module
	 */
	private $_users=array();
	/**
	 * @var array list of roles managed by this module
	 */
	private $_roles=array();
	/**
	 * @var string guest name
	 */
	private $_guestName='Guest';
	/**
	 * @var TUserManagerPasswordMode password mode
	 */
	private $_passwordMode=TUserManagerPasswordMode::MD5;
	/**
	 * @var boolean whether the module has been initialized
	 */
	private $_initialized=false;
	/**
	 * @var string user/role information file
	 */
	private $_userFile=null;

	/**
	 * Initializes the module.
	 * This method is required by IModule and is invoked by application.
	 * It loads user/role information from the module configuration.
	 * @param mixed module configuration
	 */
	public function init($config)
	{
		$this->loadUserData($config);	
		if($this->_userFile!==null)
		{
			if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP)
			{
				$userFile = include $this->_userFile;
				$this->loadUserDataFromPhp($userFile);
			}
			else
			{
				$dom=new TXmlDocument;
				$dom->loadFromFile($this->_userFile);
				$this->loadUserDataFromXml($dom);
			}
		}
		$this->_initialized=true;
	}
	
	/*
	 * Loads user/role information
	 * @param mixed the variable containing the user information
	 */
	private function loadUserData($config)
	{
		if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP)
			$this->loadUserDataFromPhp($config);
		else
			$this->loadUserDataFromXml($config);
	}

	/**
	 * Loads user/role information from an php array.
	 * @param array the array containing the user information
	 */
	private function loadUserDataFromPhp($config)
	{
		if(isset($config['users']) && is_array($config['users']))
		{
			foreach($config['users'] as $user)
			{
				$name = trim(strtolower(isset($user['name'])?$user['name']:''));
				$password = isset($user['password'])?$user['password']:'';
				$this->_users[$name] = $password;
				$roles = isset($user['roles'])?$user['roles']:'';
				if($roles!=='')
				{
					foreach(explode(',',$roles) as $role)
					{
						if(($role=trim($role))!=='')
							$this->_roles[$name][]=$role;
					}
				}
			}
		}
		if(isset($config['roles']) && is_array($config['roles']))
		{
			foreach($config['roles'] as $role)
			{
				$name = isset($role['name'])?$role['name']:'';
				$users = isset($role['users'])?$role['users']:'';
				foreach(explode(',',$users) as $user)
				{
					if(($user=trim($user))!=='')
						$this->_roles[strtolower($user)][]=$name;
				}
			}
		}
	}

	/**
	 * Loads user/role information from an XML node.
	 * @param TXmlElement the XML node containing the user information
	 */
	private function loadUserDataFromXml($xmlNode)
	{
		foreach($xmlNode->getElementsByTagName('user') as $node)
		{
			$name=trim(strtolower($node->getAttribute('name')));
			$this->_users[$name]=$node->getAttribute('password');
			if(($roles=trim($node->getAttribute('roles')))!=='')
			{
				foreach(explode(',',$roles) as $role)
				{
					if(($role=trim($role))!=='')
						$this->_roles[$name][]=$role;
				}
			}
		}
		foreach($xmlNode->getElementsByTagName('role') as $node)
		{
			foreach(explode(',',$node->getAttribute('users')) as $user)
			{
				if(($user=trim($user))!=='')
					$this->_roles[strtolower($user)][]=$node->getAttribute('name');
			}
		}
	}

	/**
	 * Returns an array of all users.
	 * Each array element represents a single user.
	 * The array key is the username in lower case, and the array value is the
	 * corresponding user password.
	 * @return array list of users
	 */
	public function getUsers()
	{
		return $this->_users;
	}

	/**
	 * Returns an array of user role information.
	 * Each array element represents the roles for a single user.
	 * The array key is the username in lower case, and the array value is
	 * the roles (represented as an array) that the user is in.
	 * @return array list of user role information
	 */
	public function getRoles()
	{
		return $this->_roles;
	}

	/**
	 * @return string the full path to the file storing user/role information
	 */
	public function getUserFile()
	{
		return $this->_userFile;
	}

	/**
	 * @param string user/role data file path (in namespace form). The file format is XML
	 * whose content is similar to that user/role block in application configuration.
	 * @throws TInvalidOperationException if the module is already initialized
	 * @throws TConfigurationException if the file is not in proper namespace format
	 */
	public function setUserFile($value)
	{
		if($this->_initialized)
			throw new TInvalidOperationException('usermanager_userfile_unchangeable');
		else if(($this->_userFile=Prado::getPathOfNamespace($value,self::USER_FILE_EXT))===null || !is_file($this->_userFile))
			throw new TConfigurationException('usermanager_userfile_invalid',$value);
	}

	/**
	 * @return string guest name, defaults to 'Guest'
	 */
	public function getGuestName()
	{
		return $this->_guestName;
	}

	/**
	 * @param string name to be used for guest users.
	 */
	public function setGuestName($value)
	{
		$this->_guestName=$value;
	}

	/**
	 * @return TUserManagerPasswordMode how password is stored, clear text, or MD5 or SHA1 hashed. Default to TUserManagerPasswordMode::MD5.
	 */
	public function getPasswordMode()
	{
		return $this->_passwordMode;
	}

	/**
	 * @param TUserManagerPasswordMode how password is stored, clear text, or MD5 or SHA1 hashed.
	 */
	public function setPasswordMode($value)
	{
		$this->_passwordMode=TPropertyValue::ensureEnum($value,'TUserManagerPasswordMode');
	}

	/**
	 * Validates if the username and password are correct.
	 * @param string user name
	 * @param string password
	 * @return boolean true if validation is successful, false otherwise.
	 */
	public function validateUser($username,$password)
	{
		if($this->_passwordMode===TUserManagerPasswordMode::MD5)
			$password=md5($password);
		else if($this->_passwordMode===TUserManagerPasswordMode::SHA1)
			$password=sha1($password);
		$username=strtolower($username);
		return (isset($this->_users[$username]) && $this->_users[$username]===$password);
	}

	/**
	 * Returns a user instance given the user name.
	 * @param string user name, null if it is a guest.
	 * @return TUser the user instance, null if the specified username is not in the user database.
	 */
	public function getUser($username=null)
	{
		if($username===null)
		{
			$user=new TUser($this);
			$user->setIsGuest(true);
			return $user;
		}
		else
		{
			$username=strtolower($username);
			if(isset($this->_users[$username]))
			{
				$user=new TUser($this);
				$user->setName($username);
				$user->setIsGuest(false);
				if(isset($this->_roles[$username]))
					$user->setRoles($this->_roles[$username]);
				return $user;
			}
			else
				return null;
		}
	}

	/**
	 * Returns a user instance according to auth data stored in a cookie.
	 * @param THttpCookie the cookie storing user authentication information
	 * @return TUser the user instance generated based on the cookie auth data, null if the cookie does not have valid auth data.
	 * @since 3.1.1
	 */
	public function getUserFromCookie($cookie)
	{
		if(($data=$cookie->getValue())!=='')
		{
			$data=unserialize($data);
			if(is_array($data) && count($data)===2)
			{
				list($username,$token)=$data;
				if(isset($this->_users[$username]) && $token===md5($username.$this->_users[$username]))
					return $this->getUser($username);
			}
		}
		return null;
	}

	/**
	 * Saves user auth data into a cookie.
	 * @param THttpCookie the cookie to receive the user auth data.
	 * @since 3.1.1
	 */
	public function saveUserToCookie($cookie)
	{
		$user=$this->getApplication()->getUser();
		$username=strtolower($user->getName());
		if(isset($this->_users[$username]))
		{
			$data=array($username,md5($username.$this->_users[$username]));
			$cookie->setValue(serialize($data));
		}
	}

	/**
	 * Sets a user as a guest.
	 * User name is changed as guest name, and roles are emptied.
	 * @param TUser the user to be changed to a guest.
	 */
	public function switchToGuest($user)
	{
		$user->setIsGuest(true);
	}
}

/**
 * TUserManagerPasswordMode class.
 * TUserManagerPasswordMode defines the enumerable type for the possible modes
 * that user passwords can be specified for a {@link TUserManager}.
 *
 * The following enumerable values are defined:
 * - Clear: the password is in plain text
 * - MD5: the password is recorded as the MD5 hash value of the original password
 * - SHA1: the password is recorded as the SHA1 hash value of the original password
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System.Security
 * @since 3.0.4
 */
class TUserManagerPasswordMode extends TEnumerable
{
	const Clear='Clear';
	const MD5='MD5';
	const SHA1='SHA1';
}