<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\XmlRpc;

use ReflectionClass;
use Zend\Server\AbstractServer;
use Zend\Server\Definition;
use Zend\Server\Reflection;

/**
 * An XML-RPC server implementation
 *
 * Example:
 * <code>
 * use Zend\XmlRpc;
 *
 * // Instantiate server
 * $server = new XmlRpc\Server();
 *
 * // Allow some exceptions to report as fault responses:
 * XmlRpc\Server\Fault::attachFaultException('My\\Exception');
 * XmlRpc\Server\Fault::attachObserver('My\\Fault\\Observer');
 *
 * // Get or build dispatch table:
 * if (!XmlRpc\Server\Cache::get($filename, $server)) {
 *
 *     // Attach Some_Service_Class in 'some' namespace
 *     $server->setClass('Some\\Service\\Class', 'some');
 *
 *     // Attach Another_Service_Class in 'another' namespace
 *     $server->setClass('Another\\Service\\Class', 'another');
 *
 *     // Create dispatch table cache file
 *     XmlRpc\Server\Cache::save($filename, $server);
 * }
 *
 * $response = $server->handle();
 * echo $response;
 * </code>
 */
class Server extends AbstractServer
{
    /**
     * Character encoding
     * @var string
     */
    protected $encoding = 'UTF-8';

    /**
     * Request processed
     * @var null|Request
     */
    protected $request = null;

    /**
     * Class to use for responses; defaults to {@link Response\Http}
     * @var string
     */
    protected $responseClass = 'Zend\XmlRpc\Response\Http';

    /**
     * Dispatch table of name => method pairs
     * @var Definition
     */
    protected $table;

    /**
     * PHP types => XML-RPC types
     * @var array
     */
    protected $typeMap = array(
        'i4'                         => 'i4',
        'int'                        => 'int',
        'integer'                    => 'int',
        'i8'                         => 'i8',
        'ex:i8'                      => 'i8',
        'double'                     => 'double',
        'float'                      => 'double',
        'real'                       => 'double',
        'boolean'                    => 'boolean',
        'bool'                       => 'boolean',
        'true'                       => 'boolean',
        'false'                      => 'boolean',
        'string'                     => 'string',
        'str'                        => 'string',
        'base64'                     => 'base64',
        'dateTime.iso8601'           => 'dateTime.iso8601',
        'date'                       => 'dateTime.iso8601',
        'time'                       => 'dateTime.iso8601',
        'DateTime'                   => 'dateTime.iso8601',
        'array'                      => 'array',
        'struct'                     => 'struct',
        'null'                       => 'nil',
        'nil'                        => 'nil',
        'ex:nil'                     => 'nil',
        'void'                       => 'void',
        'mixed'                      => 'struct',
    );

    /**
     * Send arguments to all methods or just constructor?
     *
     * @var bool
     */
    protected $sendArgumentsToAllMethods = true;

    /**
     * Flag: whether or not {@link handle()} should return a response instead
     * of automatically emitting it.
     * @var bool
     */
    protected $returnResponse = false;

    /**
     * Last response results.
     * @var Response
     */
    protected $response;

    /**
     * Constructor
     *
     * Creates system.* methods.
     *
     */
    public function __construct()
    {
        $this->table = new Definition();
        $this->registerSystemMethods();
    }

    /**
     * Proxy calls to system object
     *
     * @param  string $method
     * @param  array $params
     * @return mixed
     * @throws Server\Exception\BadMethodCallException
     */
    public function __call($method, $params)
    {
        $system = $this->getSystem();
        if (!method_exists($system, $method)) {
            throw new Server\Exception\BadMethodCallException('Unknown instance method called on server: ' . $method);
        }
        return call_user_func_array(array($system, $method), $params);
    }

    /**
     * Attach a callback as an XMLRPC method
     *
     * Attaches a callback as an XMLRPC method, prefixing the XMLRPC method name
     * with $namespace, if provided. Reflection is done on the callback's
     * docblock to create the methodHelp for the XMLRPC method.
     *
     * Additional arguments to pass to the function at dispatch may be passed;
     * any arguments following the namespace will be aggregated and passed at
     * dispatch time.
     *
     * @param string|array|callable $function  Valid callback
     * @param string                $namespace Optional namespace prefix
     * @throws Server\Exception\InvalidArgumentException
     * @return void
     */
    public function addFunction($function, $namespace = '')
    {
        if (!is_string($function) && !is_array($function)) {
            throw new Server\Exception\InvalidArgumentException('Unable to attach function; invalid', 611);
        }

        $argv = null;
        if (2 < func_num_args()) {
            $argv = func_get_args();
            $argv = array_slice($argv, 2);
        }

        $function = (array) $function;
        foreach ($function as $func) {
            if (!is_string($func) || !function_exists($func)) {
                throw new Server\Exception\InvalidArgumentException('Unable to attach function; invalid', 611);
            }
            $reflection = Reflection::reflectFunction($func, $argv, $namespace);
            $this->_buildSignature($reflection);
        }
    }

    /**
     * Attach class methods as XMLRPC method handlers
     *
     * $class may be either a class name or an object. Reflection is done on the
     * class or object to determine the available public methods, and each is
     * attached to the server as an available method; if a $namespace has been
     * provided, that namespace is used to prefix the XMLRPC method names.
     *
     * Any additional arguments beyond $namespace will be passed to a method at
     * invocation.
     *
     * @param string|object $class
     * @param string $namespace Optional
     * @param mixed $argv Optional arguments to pass to methods
     * @return void
     * @throws Server\Exception\InvalidArgumentException on invalid input
     */
    public function setClass($class, $namespace = '', $argv = null)
    {
        if (is_string($class) && !class_exists($class)) {
            throw new Server\Exception\InvalidArgumentException('Invalid method class', 610);
        }

        if (2 < func_num_args()) {
            $argv = func_get_args();
            $argv = array_slice($argv, 2);
        }

        $dispatchable = Reflection::reflectClass($class, $argv, $namespace);
        foreach ($dispatchable->getMethods() as $reflection) {
            $this->_buildSignature($reflection, $class);
        }
    }

    /**
     * Raise an xmlrpc server fault
     *
     * @param string|\Exception $fault
     * @param int $code
     * @return Server\Fault
     */
    public function fault($fault = null, $code = 404)
    {
        if (!$fault instanceof \Exception) {
            $fault = (string) $fault;
            if (empty($fault)) {
                $fault = 'Unknown Error';
            }
            $fault = new Server\Exception\RuntimeException($fault, $code);
        }

        return Server\Fault::getInstance($fault);
    }

    /**
     * Set return response flag
     *
     * If true, {@link handle()} will return the response instead of
     * automatically sending it back to the requesting client.
     *
     * The response is always available via {@link getResponse()}.
     *
     * @param  bool $flag
     * @return Server
     */
    public function setReturnResponse($flag = true)
    {
        $this->returnResponse = ($flag) ? true : false;
        return $this;
    }

    /**
     * Retrieve return response flag
     *
     * @return bool
     */
    public function getReturnResponse()
    {
        return $this->returnResponse;
    }

    /**
     * Handle an xmlrpc call
     *
     * @param  Request $request Optional
     * @return Response|Fault
     */
    public function handle($request = false)
    {
        // Get request
        if ((!$request || !$request instanceof Request)
            && (null === ($request = $this->getRequest()))
        ) {
            $request = new Request\Http();
            $request->setEncoding($this->getEncoding());
        }

        $this->setRequest($request);

        if ($request->isFault()) {
            $response = $request->getFault();
        } else {
            try {
                $response = $this->handleRequest($request);
            } catch (\Exception $e) {
                $response = $this->fault($e);
            }
        }

        // Set output encoding
        $response->setEncoding($this->getEncoding());
        $this->response = $response;

        if (!$this->returnResponse) {
            echo $response;
            return;
        }

        return $response;
    }

    /**
     * Load methods as returned from {@link getFunctions}
     *
     * Typically, you will not use this method; it will be called using the
     * results pulled from {@link Zend\XmlRpc\Server\Cache::get()}.
     *
     * @param  array|Definition $definition
     * @return void
     * @throws Server\Exception\InvalidArgumentException on invalid input
     */
    public function loadFunctions($definition)
    {
        if (!is_array($definition) && (!$definition instanceof Definition)) {
            if (is_object($definition)) {
                $type = get_class($definition);
            } else {
                $type = gettype($definition);
            }
            throw new Server\Exception\InvalidArgumentException('Unable to load server definition; must be an array or Zend\Server\Definition, received ' . $type, 612);
        }

        $this->table->clearMethods();
        $this->registerSystemMethods();

        if ($definition instanceof Definition) {
            $definition = $definition->getMethods();
        }

        foreach ($definition as $key => $method) {
            if ('system.' == substr($key, 0, 7)) {
                continue;
            }
            $this->table->addMethod($method, $key);
        }
    }

    /**
     * Set encoding
     *
     * @param  string $encoding
     * @return Server
     */
    public function setEncoding($encoding)
    {
        $this->encoding = $encoding;
        AbstractValue::setEncoding($encoding);
        return $this;
    }

    /**
     * Retrieve current encoding
     *
     * @return string
     */
    public function getEncoding()
    {
        return $this->encoding;
    }

    /**
     * Do nothing; persistence is handled via {@link Zend\XmlRpc\Server\Cache}
     *
     * @param  mixed $mode
     * @return void
     */
    public function setPersistence($mode)
    {
    }

    /**
     * Set the request object
     *
     * @param  string|Request $request
     * @return Server
     * @throws Server\Exception\InvalidArgumentException on invalid request class or object
     */
    public function setRequest($request)
    {
        if (is_string($request) && class_exists($request)) {
            $request = new $request();
            if (!$request instanceof Request) {
                throw new Server\Exception\InvalidArgumentException('Invalid request class');
            }
            $request->setEncoding($this->getEncoding());
        } elseif (!$request instanceof Request) {
            throw new Server\Exception\InvalidArgumentException('Invalid request object');
        }

        $this->request = $request;
        return $this;
    }

    /**
     * Return currently registered request object
     *
     * @return null|Request
     */
    public function getRequest()
    {
        return $this->request;
    }

    /**
     * Last response.
     *
     * @return Response
     */
    public function getResponse()
    {
        return $this->response;
    }

    /**
     * Set the class to use for the response
     *
     * @param  string $class
     * @throws Server\Exception\InvalidArgumentException if invalid response class
     * @return bool True if class was set, false if not
     */
    public function setResponseClass($class)
    {
        if (!class_exists($class) || !static::isSubclassOf($class, 'Zend\XmlRpc\Response')) {
            throw new Server\Exception\InvalidArgumentException('Invalid response class');

        }
        $this->responseClass = $class;
        return true;
    }

    /**
     * Retrieve current response class
     *
     * @return string
     */
    public function getResponseClass()
    {
        return $this->responseClass;
    }

    /**
     * Retrieve dispatch table
     *
     * @return array
     */
    public function getDispatchTable()
    {
        return $this->table;
    }

    /**
     * Returns a list of registered methods
     *
     * Returns an array of dispatchables (Zend\Server\Reflection\ReflectionFunction,
     * ReflectionMethod, and ReflectionClass items).
     *
     * @return array
     */
    public function getFunctions()
    {
        return $this->table->toArray();
    }

    /**
     * Retrieve system object
     *
     * @return Server\System
     */
    public function getSystem()
    {
        return $this->system;
    }

    /**
     * Send arguments to all methods?
     *
     * If setClass() is used to add classes to the server, this flag defined
     * how to handle arguments. If set to true, all methods including constructor
     * will receive the arguments. If set to false, only constructor will receive the
     * arguments
     */
    public function sendArgumentsToAllMethods($flag = null)
    {
        if ($flag === null) {
            return $this->sendArgumentsToAllMethods;
        }

        $this->sendArgumentsToAllMethods = (bool) $flag;
        return $this;
    }

    /**
     * Map PHP type to XML-RPC type
     *
     * @param  string $type
     * @return string
     */
    protected function _fixType($type)
    {
        if (isset($this->typeMap[$type])) {
            return $this->typeMap[$type];
        }
        return 'void';
    }

    /**
     * Handle an xmlrpc call (actual work)
     *
     * @param  Request $request
     * @return Response
     * @throws Server\Exception\RuntimeException
     * Zend\XmlRpc\Server\Exceptions are thrown for internal errors; otherwise,
     * any other exception may be thrown by the callback
     */
    protected function handleRequest(Request $request)
    {
        $method = $request->getMethod();

        // Check for valid method
        if (!$this->table->hasMethod($method)) {
            throw new Server\Exception\RuntimeException('Method "' . $method . '" does not exist', 620);
        }

        $info     = $this->table->getMethod($method);
        $params   = $request->getParams();
        $argv     = $info->getInvokeArguments();
        if (0 < count($argv) and $this->sendArgumentsToAllMethods()) {
            $params = array_merge($params, $argv);
        }

        // Check calling parameters against signatures
        $matched    = false;
        $sigCalled  = $request->getTypes();

        $sigLength  = count($sigCalled);
        $paramsLen  = count($params);
        if ($sigLength < $paramsLen) {
            for ($i = $sigLength; $i < $paramsLen; ++$i) {
                $xmlRpcValue = AbstractValue::getXmlRpcValue($params[$i]);
                $sigCalled[] = $xmlRpcValue->getType();
            }
        }

        $signatures = $info->getPrototypes();
        foreach ($signatures as $signature) {
            $sigParams = $signature->getParameters();
            if ($sigCalled === $sigParams) {
                $matched = true;
                break;
            }
        }
        if (!$matched) {
            throw new Server\Exception\RuntimeException('Calling parameters do not match signature', 623);
        }

        $return        = $this->_dispatch($info, $params);
        $responseClass = $this->getResponseClass();
        return new $responseClass($return);
    }

    /**
     * Register system methods with the server
     *
     * @return void
     */
    protected function registerSystemMethods()
    {
        $system = new Server\System($this);
        $this->system = $system;
        $this->setClass($system, 'system');
    }

    /**
     * Checks if the object has this class as one of its parents
     *
     * @see https://bugs.php.net/bug.php?id=53727
     * @see https://github.com/zendframework/zf2/pull/1807
     *
     * @param string $className
     * @param string $type
     * @return bool
     */
    protected static function isSubclassOf($className, $type)
    {
        if (is_subclass_of($className, $type)) {
            return true;
        }
        if (version_compare(PHP_VERSION, '5.3.7', '>=')) {
            return false;
        }
        if (!interface_exists($type)) {
            return false;
        }
        $r = new ReflectionClass($className);
        return $r->implementsInterface($type);
    }
}