<?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); } }