<?php
/**
 * @version 0.1.0
 * @link http://www.xapp-studio.com
 * @author Luis Ramos
 * @author Guenter Baumgart
 * @license : GPL v2. http://www.gnu.org/licenses/gpl-2.0.html
 * @package XApp\xide\Controller
 */

xapp_import("xapp.Commons.Entity");
xapp_import('xapp.xide.Models.User');
xapp_import('xapp.xide.Models.UserRole');
xapp_import('xapp.xide.Models.UserPermission');
xapp_import('xapp.Security.*');

/***
 * Class XApp_UserManager
 */
class XApp_UserManager implements XApp_Security_IAuthenticator {

    function authenticate(array $credentials)
    {

        //prepare user table, from model
        $userList = array();

        foreach( (array) $this->_users as $User)
        {
            $userList[$User->getName()] = $User->getPassword();
        }

        //walk over users and do password match
        list($username, $password) = $credentials;
        foreach ($userList as $name => $pass) {
            if (strcasecmp($name, $username) === 0) {
                if ((string) $pass === (string) $password) {
                    return new XApp_Security_Identity($name);
                } else {
                    throw new XApp_Security_AuthenticationException("Invalid password.", self::INVALID_CREDENTIAL);
                }
            }
        }
        throw new XApp_Security_AuthenticationException("User '$username' not found.", self::IDENTITY_NOT_FOUND);

    }
    /*******************************
     * Hooks
    ********************************/

    /***
     * Hook Data Init: triggers on initWithData
     *
     * Params: XApp_User_set | XApp_UserRole_set | XApp_UserPermission_set
    */
    const HOOK_DATA_INIT = "XAPP_USER_HOOK_DATA_INIT";
    /**
     * Hook Save: triggers on permanent storage save
     *
     * Params: container -> and object with Users, Roles and Permissions
     */
    const HOOK_SAVE = "XAPP_USER_HOOK_SAVE";
    /**
     *  Hook Login: triggers on user login
     *
     * Params: userName, instance
     */
    const HOOK_LOGIN = "XAPP_USER_HOOK_LOGIN";
    /**
     *  Hook Logout: triggers on logout
     *
     *  Params: instance
     */
    const HOOK_LOGOUT = "XAPP_USER_HOOK_LOGOUT";
    /**
     *  Hook Create User: triggers on user creation and copy
     *
     * Params: userName: new user name, instance
     */
    const HOOK_CREATE_USER = "XAPP_USER_HOOK_CREATE_USER";
    /**
     *  Hook Remove User: triggers on user removal
     *
     * Params: userName: removed user name, instance
     */
    const HOOK_REMOVE_USER = "XAPP_USER_HOOK_REMOVE_USER";
    /**
     *  Hook RPC before call: triggers before RPC call
     *
     * Params: instance
     */
    const HOOK_RPC_BEFORE_CALL = "XAPP_USER_HOOK_RPC_BEFORE_CALL";
    /**
     *  Hook RPC after call: triggers after RPC call
     *
     * Params: instance
     */
    const HOOK_RPC_AFTER_CALL = "XAPP_USER_HOOK_RPC_AFTER_CALL";

    /*******************************
     * Options
    ********************************/

    /**
     *  Name of the class to use for creating a user
     */
    const USER_CLASS                = "XAPP_USER_CLASS";

    /**
     *  Name of the class to use for creating a user role
     */
    const USER_ROLE_CLASS           = "XAPP_USER_ROLE_CLASS";

    /**
     *  Name of the class to use for creating a user permission
     */
    const USER_PERMISSION_CLASS     = "XAPP_USER_PERMISSION_CLASS";

    /**
     *  Name of the class for persistence, JSON only at the moment.
     */
    const STORE_CLASS               = "XAPP_USER_STORE_CLASS";

    /**
     *  Options for the store class
     */
    const STORE_CONF                = "XAPP_USER_STORE_CONF";

    /**
     *  User data, will skip creation of the store class as we have the data already
     */
    const STORE_DATA                = "XAPP_USER_STORE_DATA";

    /**
     * options dictionary for this class containing all data type values
     *
     * @var array
     */
    public static $optionsDict = array
    (
        self::STORE_CLASS            => XAPP_TYPE_STRING,
        self::STORE_CONF             => XAPP_TYPE_ARRAY,
        self::STORE_DATA             => XAPP_TYPE_OBJECT,
        self::USER_CLASS             => XAPP_TYPE_STRING,
        self::USER_ROLE_CLASS        => XAPP_TYPE_STRING,
        self::USER_PERMISSION_CLASS  => XAPP_TYPE_STRING
    );

    /**
     * options mandatory map for this class contains all mandatory values
     *
     * @var array
     */
    public static $optionsRule = array
    (
        self::STORE_CLASS            => 0,
        self::STORE_CONF             => 0,
        self::STORE_DATA             => 0,
        self::USER_CLASS             => 1,
        self::USER_ROLE_CLASS        => 1,
        self::USER_PERMISSION_CLASS  => 1
    );
    /**
     * options default value array containing all class option default values
     *
     * @var array
     */
    public $options = array
    (
        self::STORE_CLASS            => 'XApp_Store_JSON',
        self::STORE_CONF             => null,
        self::STORE_DATA             => null,
        self::USER_CLASS             => 'XApp_User',
        self::USER_ROLE_CLASS        => 'XApp_UserRole',
        self::USER_PERMISSION_CLASS  => 'XApp_UserPermission'
    );

    /**
     * @var stdClass | 1:n XApp_User
     */
    protected $_users;
    /***
     * @var stdClass | 1:n XApp_UserRole
     */
    protected $_roles;
    /***
     * @var stdClass | 1:n XApp_UserPermission
     */
    protected $_permissions;

    /***
     * @var stdClass | XApp_Store_JSON
     */
    protected $_store = null;

    protected $sessionStorage = null;

    /***
     * @param null $options
     */
    public function __construct($options = null)
    {
        xapp_set_options($options, $this);
        $this->_users = new stdClass();
        $this->_roles = new stdClass();
        $this->_permissions = new stdClass();
    }

    /**
     * Initialize user data
     *
     * @param $allUserData  :   object with Users, Roles and Permissions
     */
    public function initWithData($allUserData)
    {

        if (isset($allUserData->Users))
        {
            $this->_initSet($this->_users,xo_get(self::USER_CLASS),$allUserData->Users);
        }

        if (isset($allUserData->Roles))
        {
            $this->_initSet($this->_roles,xo_get(self::USER_ROLE_CLASS),$allUserData->Roles);
        }

        if (isset($allUserData->Permissions))
        {
            $this->_initSet($this->_permissions,xo_get(self::USER_PERMISSION_CLASS),$allUserData->Permissions);
        }
    }

    /**
     *
     *
     * @param $set
     * @param $class
     * @param $Data
     */
    private function _initSet(&$set,$class,$Data) {

        foreach($Data as $fObj)
        {
            $Obj =  new $class;
            if (is_array($fObj))
                $Obj->setFields($fObj["Fields"]);
            else
                $Obj->setFields($fObj->Fields);

            $set->{$Obj->getName()} = $Obj;
        }

        // Trigger HOOK_DATA_INIT
        $hook_ret = Xapp_Hook::trigger(self::HOOK_DATA_INIT,array($class.'_set'=>$set));
        $set = $hook_ret[$class.'_set'];
    }

    /***
     * Parse options and init the store
     * @param bool $force does force to re-create the store
     * @return bool
     */
    public function initStore($force=false){

        //skip
        if($force === false && $this->_store){
            return true;
        }

        //Try with store data first
        if(xo_has(self::STORE_DATA) && is_object(xo_get(self::STORE_DATA))){
            $this->initWithData(xo_get(self::STORE_DATA));
            return true;
        }

        //No store data provided from outside, use the store class

        if(xo_has(self::STORE_CLASS)){
            $_storeClz = xo_get(self::STORE_CLASS);

            //check its an instance already :
            if(is_object($_storeClz)){
                $this->_store = $_storeClz;
                return true;
            }//its a class name
            elseif(is_string($_storeClz) && class_exists($_storeClz)){
                $_ctrArgs = xo_has(self::STORE_CONF) ? xo_get(self::STORE_CONF) : array();
                $this->_store = new $_storeClz($_ctrArgs);
                return true;
            }
        }
        return false;
    }

    /***
     * Initializes the store and loads the data
     */
    public function init(){

        if($this->initStore()){
            $this->initWithData( (object) $this->_store->read() );
        }
    }

    /**
     * Saves all users, roles and permissions data into the JSON storage
     */
    public function save() {

        if (isset($this->_store))
        {
            $container = new StdClass();

            $container->Users = $this->_users;
            $container->Roles = $this->_roles;
            $container->Permissions = $this->_permissions;

            // Trigger HOOK_SAVE
            $hook_ret = Xapp_Hook::trigger(self::HOOK_SAVE,array('container'=>$container));
            $container = $hook_ret['container'];

            $this->_store->write($container);
        }

    }

    /**
     * Creates an authenticator for session storage with array( username => password )
     *
     * @return array
     */
    private function getSessionAuthenticator() {
        $returned = array();
        foreach( (array) $this->_users as $User)
        {
            $returned[$User->getName()] = $User->getPassword();
        }
        return $returned;
    }

    /**
     * Init session storage
     *
     */
    public function initSessionStorage() {

        $session = XApp_Http_Session::factory();

        //seems to store user stuff in a session
        $userStorage = new XApp_Http_UserStorage($session);

        //seems to be a 'database'
        $authenticator = new XApp_Security_SimpleAuthenticator(
            $this->getSessionAuthenticator()
        );

        $authorizator = new XApp_Security_SimpleAuthorizator();


        //now construct the user, needs a storage, and an Authenticator(could be from CMS)
        $this->sessionStorage = new XApp_Security_User($userStorage,$this,$authorizator);

    }

    /**
     *  Re-init session storage (if set)
     */
    public function refreshSessionStorage() {
        if ( $this->sessionStorage != null )
        {
            $this->initSessionStorage();
        }
    }

    /**
     * Login an user into the session storage
     *
     * @param $userName
     * @param $password
     * @param $errors   :  array of errors
     * @return bool     :  true if login is successful
     */
    public function login($userName,$password,&$errors=array()) {
        if ($this->sessionStorage == null)
            $this->initSessionStorage();

        $this->sessionStorage->logout();

        try {
            // we try to log the user in
            $this->sessionStorage->login($userName, $password);

            // Trigger HOOK_LOGIN
            Xapp_Hook::trigger(self::HOOK_LOGIN,array(
                'userName'=>$userName,
                'instance'=>$this));
            return true;

        } catch (XApp_Security_AuthenticationException $e) {
            $errors[] =  XAPP_TEXT_FORMATTED('LOGIN_ERROR',array($userName , $e->getMessage() ));
            return false;
        }
    }



    /**
     * Logout user from session storage
     *
     * @return mixed
     */
    public function logout() {
        // Trigger HOOK_LOGOUT
        Xapp_Hook::trigger(self::HOOK_LOGOUT,array('instance'=>$this));

        return $this->sessionStorage->logout();
    }

    /**
     * Check if any user is logged
     * @return bool
     */
    public function isLoggedIn() {
        return $this->sessionStorage->isLoggedIn();
    }

    /**
     * returns logged user name
     *
     * @return bool|string  :   user name if success, false if not
     */
    public function loggedUserName() {
        if ($this->sessionStorage->isLoggedIn())
        {
            return $this->sessionStorage->getId();
        }
        else
        {
            return false;
        }
    }




    /**
     * Adds a new user
     *
     * @param $name
     * @param $password
     * @param array $roles
     * @param array $permissions
     * @param array $fields
     * @return mixed
     */
    public function createUser($name,$password,$roles=Array(),$permissions=Array(),$fields=Array())
    {
        $User = xo_as_instance(self::USER_CLASS,$this,null);// XApp_User();
        $User->setName($name);
        $User->setPassword($password);

        foreach ($roles as $role)
        {
            $User->addRole($role);
        }

        foreach($permissions as $permission)
        {
            $User->addPermission($permission);
        }

        foreach($fields as $field)
        {
            $User->addCustomField($field);
        }

        $this->_users->{$User->getName()} = $User;

        $this->save();

        // Trigger HOOK_CREATE_USER
        Xapp_Hook::trigger(self::HOOK_CREATE_USER,array(
            'user'=>$User,
            'userName'=>$User->getName(),
            'instance'=>$this)
        );

        return $this->_users->{$User->getName()};
    }

    /**
     * Creates a new user coping all roles and permissions from $referenceUserName
     *
     * @param $referenceUserName    :   existing user to be used as reference for roles and permissions
     * @param $newUserName          :   new user name
     * @param $newUserPassword      :   new user password
     * @return bool|object          :   false if error, User object if success
     */
    public function copyUser($referenceUserName,$newUserName,$newUserPassword) {
        if (isset($this->_users->{$referenceUserName}))
        {
            $User = clone $this->_users->{$referenceUserName};
            $User->setName($newUserName);
            $User->setPassword($newUserPassword);
            $this->_users->{$User->getName()} = $User;
            $this->save();

            // re-init session storage if initialized, adding the new user into the authenticator
            $this->refreshSessionStorage();

            // Trigger HOOK_CREATE_USER
            Xapp_Hook::trigger(self::HOOK_CREATE_USER,array(
                'user'=>$this->getUser($newUserName),
                'userName'=>$User->getName(),
                'instance'=>$this));

            return $this->_users->{$User->getName()};
        }
        else
        {
            return false;
        }
    }

    /**
     * Removes the user from memory, permanent storage (if set) and session storage (if set)
     *
     * @param $userName
     */
    public function removeUser($userName) {
        if (isset($this->_users->{$userName})) {

            // Trigger HOOK_REMOVE_USER
            Xapp_Hook::trigger(self::HOOK_REMOVE_USER,array(
                'user'=>$this->getUser($userName),
                'userName'=>$userName,
                'instance'=>$this));

            unset($this->_users->{$userName});


            $this->save();
            // re-init session storage if initialized, removing the user from the authenticator
            $this->refreshSessionStorage();
        }
    }

    /**
     * Creates a new permission
     *
     * @param $name
     * @param null $resourceID
     * @return mixed
     */
    public function createPermission($name,$resourceID = null)
    {
        $Permission = xo_as_instance(self::USER_PERMISSION_CLASS,$this,null);//XApp_UserPermission();
        $Permission->setName( $name );
        $Permission->setResourceID( $resourceID );

        $this->_permissions->{$name} = $Permission;

        $this->save();

        return $this->_permissions->{$name};
    }

    /**
     * Removes a permission from memory and permanent storage (if set)
     *
     * @param $name
     */
    public function removePermission($name) {
        if (isset($this->_permissions->{$name}))
        {
            unset($this->_permissions->{$name});
            $this->save();
        }
    }

    /**
     * Creates a new Role
     *
     * @param $name
     * @param array $permissions
     * @return mixed
     */
    public function createRole($name, $permissions = Array() )
    {
        $Role = xo_as_instance(self::USER_ROLE_CLASS,$this,null);//XApp_UserRole();
        $Role->setName( $name );
        $Role->setPermissions( $permissions );

        $this->_roles->{$name} = $Role;

        $this->save();

        return $this->_roles->{$name};
    }

    /**
     * Removes a role from memory and permanent storage (if set)
     *
     * @param $name
     */
    public function removeRole($name) {
        if (isset($this->_roles->{$name}))
        {
            unset($this->_roles->{$name});
            $this->save();
        }
    }


    public function getUser($userName) {
        if (isset($this->_users->{$userName}))
        {
            return $this->_users->{$userName};
        }
        else
        {
            return false;
        }

    }

    /**
     *
     * Check if an user has a permission
     *
     * @param $userName
     * @param $permissionName
     * @return bool|int
     */
    public function UserIsAllowed($userName,$permissionName)
    {
        $allowed = false;
        if (isset($this->_users->{$userName}))
        {
            // first check User additional permissions
            foreach( $this->_users->{$userName}->getPermissions() as $permission)
            {
                $allowed = $allowed | ( $permission == $permissionName );
            }

            // if not, check roles
            if (!$allowed)
            {
                foreach( $this->_users->{$userName}->getRole() as $role)
                {
                    if (isset($this->_roles->{$role}))
                    {
                        $allowed = $allowed | $this->_roles->{$role}->isAllowed($permissionName);
                    }

                }
            }
        }

        return $allowed;
    }

    /*****************
     *   wraps for User Model functions, with permanent storage save
     */

    /**
     *
     * Adds a role to the user
     *
     * @param $userName
     * @param $role
     */
    public function UserAddRole($userName,$role) {
        if (isset($this->_users->{$userName}))
        {
            $this->_users->{$userName}->addRole($role);
            $this->save();
        }
    }

    /**
     * Removes a role from the user
     *
     * @param $userName
     * @param $role
     */
    public function UserRemoveRole($userName,$role) {
        if (isset($this->_users->{$userName}))
        {
            $this->_users->{$userName}->removeRole($role);
            $this->save();
        }
    }

    /**
     * Check if user has a role
     *
     *
     * @param $userName
     * @param $role
     * @return mixed
     */
    public function UserHasRole($userName,$role) {
        if (isset($this->_users->{$userName}))
        {
            return $this->_users->{$userName}->hasRole($role);
        }
    }

    /**
     * Adds a permission to the user
     *
     * @param $userName
     * @param $permission
     */
    public function UserAddPermission($userName,$permission) {
        if (isset($this->_users->{$userName}))
        {
            $this->_users->{$userName}->addPermission($permission);
            $this->save();
        }
    }

    /**
     * Removes a permission from the user
     *
     * @param $userName
     * @param $permission
     */
    public function UserRemovePermission($userName,$permission) {
        if (isset($this->_users->{$userName}))
        {
            $this->_users->{$userName}->removePermission($permission);
            $this->save();
        }
    }


    /**
     * Adds a new custom field to a User
     *
     * @param string $userName
     * @param string $fieldName
     * @param string $fieldDescription
     * @param string $fieldType
     * @param string $fieldDefaultValue :   (optional)
     */
    public function AddUserCustomField($userName,$fieldName,$fieldDescription,$fieldType,$fieldDefaultValue="") {
        if (isset($this->_users->{$userName}))
        {
            $descriptor = Array(
                XApp_Entity::ENTITY_FIELD_NAME => $fieldName,
                XApp_Entity::ENTITY_FIELD_DESCRIPTION=> $fieldDescription,
                XApp_Entity::ENTITY_FIELD_TYPE => $fieldType,
                XApp_Entity::ENTITY_FIELD_DEFAULT => $fieldDefaultValue
            );
            $this->_users->{$userName}->addCustomField($descriptor);
            $this->save();
        }
    }



    /*****************
     *   wraps for Role Model functions, with permanent storage save
     */


    /**
     *
     * Adds a permission to the role
     *
     * @param $roleName
     * @param $permission
     */
    public function RoleAddPermission($roleName,$permission) {
        if (isset($this->_roles->{$roleName}))
        {
            $this->_roles->{$roleName}->addPermission($permission);
            $this->save();
        }
    }

    /**
     * Removes a permission from the role
     *
     * @param $roleName
     * @param $permission
     */
    public function RoleRemovePermission($roleName,$permission) {
        if (isset($this->_roles->{$roleName}))
        {
            $this->_roles->{$roleName}->removePermission($permission);
            $this->save();
        }
    }

    /**
     * Returns true if the permission is included into the role
     *
     * @param $roleName
     * @param $permission
     * @return mixed
     */
    public function RoleIsAllowed($roleName,$permission) {
        if (isset($this->_roles->{$roleName}))
        {
            return $this->_roles->{$roleName}->isAllowed($permission);
        }
    }

}


?>