<?php
/**
 *  This file is part of amfPHP
 *
 * LICENSE
 *
 * This source file is subject to the license that is bundled
 * with this package in the file license.txt.
 * @package Amfphp_Plugins_Authentication
 */

/** 
 * Authentication for Amfphp. 
 * This plugin can be deactivated if the project doesn't need to protect access to its services.
 * 
 * On a service object, the plugin looks for a method called _getMethodRoles. If the method exists, the plugin will look for a role in the session that matches the role.
 * If the roles don't match, an Exception is thrown.
 * The _getMethodRoles takes a parameter $methodName, and must return an array of strings containing acceptable roles for the method. If the return value is null,
 * it is considered that that particular method is not protected.
 * 
 * For example:
 * <code>
 * public function _getMethodRoles($methodName){
 *    if($methodName == 'adminMethod'){
 *        return array('admin');
 *    }else{
 *        return null;
 *    }
 * }
 *
 * </code>
 * 
 * To authenticate a user, the plugin looks for a 'login' method. This method can either be called
 * explicitly, or by setting a header with the name 'Credentials', containing {userid: userid, password: password}, as defined by the AS2
 * NetConnection.setCredentials method. It is considered good practise to have a 'logout' method, though this is optional
 * The login method returns a role in a 'string'. It takes 2 parameters, the user id and the password.
 * The logout method should call AmfphpAuthentication::clearSessionInfo();
 * 
 * See the AuthenticationService class in the test data for an example of an implementation.
 * 
 * @link https://github.com/silexlabs/amfphp-2.0/blob/master/Tests/TestData/Services/AuthenticationService.php
 * @package Amfphp_Plugins_Authentication
 * @author Ariel Sommeria-klein
 */
class AmfphpAuthentication {
    /**
     * the field in the session where the roles array is stored
     */
    const SESSION_FIELD_ROLES = 'amfphp_roles';

    /**
     * the name of the method on the service where the method roles are given
     */
    const METHOD_GET_METHOD_ROLES = '_getMethodRoles';

    /**
     * the name of the login method
     */
    const METHOD_LOGIN = 'login';

    /**
     * the user id passed in the credentials header
     * @var String
     */
    public $headerUserId;

    /**
     * the password passed in the credentials header
     * @var String
     */
    protected $headerPassword;

    /**
     * constructor.
     * @param array $config optional key/value pairs in an associative array. Used to override default configuration values.
     */
    public function  __construct(array $config = null) {
        $filterManager = Amfphp_Core_FilterManager::getInstance();
        $filterManager->addFilter(Amfphp_Core_Common_ServiceRouter::FILTER_SERVICE_OBJECT, $this, 'filterServiceObject');
        $filterManager->addFilter(Amfphp_Core_Amf_Handler::FILTER_AMF_REQUEST_HEADER_HANDLER, $this, 'filterAmfRequestHeaderHandler');
        $this->headerUserId = null;
        $this->headerPassword = null;
    }

    /**
     * @param Object $handler
     * @param Amfphp_Core_Amf_Header $header the request header
     * @return AmfphpAuthentication 
     */
    public function filterAmfRequestHeaderHandler($handler, Amfphp_Core_Amf_Header $header){
        if($header->name == Amfphp_Core_Amf_Constants::CREDENTIALS_HEADER_NAME){
            return $this;
        }

    }


    /**
     * called when the service object is created, just before the method call.
     * Tries to authenticate if a credentials header was sent in the packet.
     * Throws an exception if the roles don't match
     *
     * @param <Object> $serviceObject
     * @param <String> $serviceName
     * @param <String> $methodName
     * @return <array>
     */
    public function filterServiceObject($serviceObject, $serviceName, $methodName){
        if(!method_exists($serviceObject, self::METHOD_GET_METHOD_ROLES)){
            return;
        }
        
        if($methodName == self::METHOD_GET_METHOD_ROLES){
            throw new Exception('_getMethodRoles method access forbidden');
        }

        //the service object has a '_getMethodRoles' method. role checking is necessary if the returned value is not null
        $acceptedRoles = call_user_func(array($serviceObject, self::METHOD_GET_METHOD_ROLES), $methodName);
        if(!$acceptedRoles){
            return;
        }

        //try to authenticate using header info if available
        if($this->headerUserId && $this->headerPassword){
            call_user_func(array($serviceObject, self::METHOD_LOGIN), $this->headerUserId, $this->headerPassword);
        }

        if(session_id () == ''){
            session_start();

        }

        if(!isset ($_SESSION[self::SESSION_FIELD_ROLES])){
            throw new Amfphp_Core_Exception('User not authenticated');
        }

        $userRoles = $_SESSION[self::SESSION_FIELD_ROLES];
        if(!$this->doRolesMatch($userRoles, $acceptedRoles)){
            throw new Amfphp_Core_Exception("roles don't match");
        }
    }
    
    /**
     * looks for a match between the user roles and the accepted roles
     * @param <type> $userRoles
     * @param <type> $acceptedRoles
     * @return <type>
     */
    protected function doRolesMatch($userRoles, $acceptedRoles){
            foreach($userRoles as $userRole){
                foreach($acceptedRoles as $acceptedRole){
                    if($userRole == $acceptedRole){
                        //a match is found
                        return true;

                    }
                }
            }
            return false;
    }


    /**
     * clears the session info set by the plugin. Use to logout
     */
    public static function clearSessionInfo(){
        if(session_id () == ''){
            session_start();

        }
        if(isset ($_SESSION[self::SESSION_FIELD_ROLES])){
            unset ($_SESSION[self::SESSION_FIELD_ROLES]);
        }
    }

    /**
     *
     * @param String $role
     */
    public static function addRole($roleToAdd){
        if(session_id () == ''){
            session_start();
        }
        if(!isset($_SESSION[self::SESSION_FIELD_ROLES])){
            $_SESSION[self::SESSION_FIELD_ROLES] = array();
        }

        //check role isn't already available
        foreach($_SESSION[self::SESSION_FIELD_ROLES] as $userRole){
            if($userRole == $roleToAdd){
                return;
            }
        }
        //role isn't already available. Add it.
        $_SESSION[self::SESSION_FIELD_ROLES][] = $roleToAdd;
    }



    /**
     * looks for a 'Credentials' request header. If there is one, uses it to try to authentify the user.
     * @param Amfphp_Core_Amf_Header $header the request header
     * @return void
     */
    public function handleRequestHeader(Amfphp_Core_Amf_Header $header){
        if($header->name != Amfphp_Core_Amf_Constants::CREDENTIALS_HEADER_NAME){
            throw new Amfphp_Core_Exception('not an authentication amf header. type: ' . $header->name);
        }
        $userIdField = Amfphp_Core_Amf_Constants::CREDENTIALS_FIELD_USERID;
        $passwordField = Amfphp_Core_Amf_Constants::CREDENTIALS_FIELD_PASSWORD;
        $userId = $header->data->$userIdField;
        $password = $header->data->$passwordField;
        if(session_id () == ''){
            session_start();
        }
        $this->headerUserId = $userId;
        $this->headerPassword = $password;

    }
}
?>
