<?php /** * @author Robin J. Rogge <rrogge@bigpoint.net> * @link http://www.pradosoft.com/ * @copyright 2010 Bigpoint GmbH * @license http://www.pradosoft.com/license/ * @version $Id$ * @since 3.2 * @package System.Web.Services */ /** * TRpcService class * * The TRpcService class is a generic class that can be extended and used to implement * rpc services using different servers and protocols. * * A server is a {@link TModule} that must subclass {@link TRpcServer}: its role is * to be an intermediate, moving data between the service and the provider. The base * {@link TRpcServer} class should suit the most common needs, but can be sublassed for * logging and debugging purposes, or to filter and modify the request/response on the fly. * * A protocol is a {@link TModule} that must subclass {@link TRpcProtocol}: its role is * to implement the protocol that exposes the rpc api. Prado already implements two * protocols: {@link TXmlRpcProtocol} for Xml-Rpc request and {@link TJsonRpcProtocol} for * JSON-Rpc requests. * * A provider is a {@link TModule} that must subclass {@link TRpcApiProvider}: its role is * to implement the methods that are available through the api. Each defined api must be * a sublass of the abstract class {@link TRpcApiProvider} and implement its methods. * * The flow of requests and reponses is the following: * Request <-> TRpcService <-> TRpcServer <-> TRpcProtocol <-> TRpcApiProvider <-> Response * * To define an rpc service, add the proper application configuration: * * <code> * <service id="rpc" class="System.Web.Services.TRpcService"> * <rpcapi id="customers" class="Application.Api.CustomersApi" /> * <modules> * <!-- register any module needed by the service here --> * </modules> * </service> * </code> * * An api can be registered adding a proper <rpcapi ..> definition inside the service * configuration. Each api definition must contain an id property and a class name * expressed in namespace format. When the service receives a request for that api, * the specified class will be instanciated in order to satisfy the request. * * @author Robin J. Rogge <rrogge@bigpoint.net> * @version $Id$ * @package System.Web.Services * @since 3.2 **/ class TRpcService extends TService { /** * const string base api provider class which every API must extend */ const BASE_API_PROVIDER = 'TRpcApiProvider'; /** * const string base RPC server implementation */ const BASE_RPC_SERVER = 'TRpcServer'; /** * @var array containing mimetype to protocol handler mappings */ protected $protocolHandlers = array( 'application/json' => 'TJsonRpcProtocol', 'text/xml' => 'TXmlRpcProtocol' ); /** * @var array containing API provider and their configured properties */ protected $apiProviders = array(); // methods /** * Creates the API provider instance for the current request * @param TRpcProtocol $protocolHandler instance * @param string $providerId */ public function createApiProvider(TRpcProtocol $protocolHandler, $providerId) { $_properties = $this->apiProviders[$providerId]; if(($_providerClass = $_properties->remove('class')) === null) throw new TConfigurationException('rpcservice_apiprovider_required'); Prado::using($_providerClass); $_providerClassName = ($_pos = strrpos($_providerClass, '.')) !== false ? substr($_providerClass, $_pos + 1) : $_providerClass; if(!is_subclass_of($_providerClassName, self::BASE_API_PROVIDER)) throw new TConfigurationException('rpcservice_apiprovider_invalid'); if(($_rpcServerClass = $_properties->remove('server')) === null) $_rpcServerClass = self::BASE_RPC_SERVER; Prado::using($_rpcServerClass); $_rpcServerClassName = ($_pos = strrpos($_rpcServerClass, '.')) !== false ? substr($_rpcServerClass, $_pos + 1) : $_rpcServerClass; if($_rpcServerClassName!==self::BASE_RPC_SERVER && !is_subclass_of($_rpcServerClassName, self::BASE_RPC_SERVER)) throw new TConfigurationException('rpcservice_rpcserver_invalid'); $_apiProvider = new $_providerClassName(new $_rpcServerClassName($protocolHandler)); $_apiProvider->setId($providerId); foreach($_properties as $_key => $_value) $_apiProvider->setSubProperty($_key, $_value); return $_apiProvider; } /** * Initializes the service * @param TXmlElement $config containing the module configuration */ public function init($config) { $this->loadConfig($config); } /** * Loads the service configuration * @param TXmlElement $xml configuration */ public function loadConfig(TXmlElement $xml) { foreach($xml->getElementsByTagName('rpcapi') as $_apiProviderXml) { $_properties = $_apiProviderXml->getAttributes(); if(($_id = $_properties->remove('id')) === null || $_id == "") throw new TConfigurationException('rpcservice_apiproviderid_required'); if(isset($this->apiProviders[$_id])) throw new TConfigurationException('rpcservice_apiproviderid_duplicated'); $this->apiProviders[$_id] = $_properties; } } /** * Runs the service */ public function run() { $_request = $this->getRequest(); if(($_providerId = $_request->getServiceParameter()) == "") throw new THttpException(400, 'RPC API-Provider id required'); if(($_method = $_request->getRequestType()) != 'POST') throw new THttpException(405, 'Invalid request method "'.$_method.'"!'); // TODO Exception muss "Allow POST" Header setzen if(($_mimeType = $_request->getContentType()) === null) throw new THttpException(406, 'Content-Type is missing!'); // TODO Exception muss gültige Content-Type werte zurück geben if(!in_array($_mimeType, array_keys($this->protocolHandlers))) throw new THttpException(406, 'Unsupported Content-Type!'); // TODO see previous $_protocolHandlerClass = $this->protocolHandlers[$_mimeType]; $_protocolHandler = new $_protocolHandlerClass; if(($_result = $this->createApiProvider($_protocolHandler, $_providerId)->processRequest()) !== null) { $_response = $this->getResponse(); $_protocolHandler->createResponseHeaders($_response); $_response->write($_result); } } } /** * TRpcServer class * * TRpcServer is a class * * TRpcServer is the base class used to creare a server to be used in conjunction with * {@link TRpcService}. * The role of TRpcServer is to be an intermediate, moving data between the service and * the provider. This base class should suit the most common needs, but can be sublassed for * logging and debugging purposes, or to filter and modify the request/response on the fly. * * @author Robin J. Rogge <rrogge@bigpoint.net> * @version $Id$ * @package System.Web.Services * @since 3.2 **/ class TRpcServer extends TModule { /** * @var TRpcProtocol instance */ protected $handler; /** * Constructor * @param TRpcProtocol $protocolHandler instance */ public function __construct(TRpcProtocol $protocolHandler) { $this->handler = $protocolHandler; } /** * Registers the method in the protocol handler * @param string $methodName * @param array $methodDetails */ public function addRpcMethod($methodName, $methodDetails) { $this->handler->addMethod($methodName, $methodDetails); } /** * Retrieves the request payload * @return string request payload */ public function getPayload() { return file_get_contents('php://input'); } /** * Passes the request payload to the protocol handler and returns the result * @return string rpc response */ public function processRequest() { try { return $this->handler->callMethod($this->getPayload()); } catch(TRpcException $e) { return $this->handler->createErrorResponse($e); } } } /** * TRpcException class * * A TRpcException represents a RPC fault i.e. an error that is caused by the input data * sent from the client. * * @author Robin J. Rogge <rrogge@bigpoint.net> * @version $Id$ * @package System.Web.Services * @since 3.2 */ class TRpcException extends TException { public function __construct($message, $errorCode = -1) { $this->setErrorCode($errorCode); parent::__construct($message); } } /** * TRpcApiProvider class * * TRpcApiProvider is an abstract class the can be subclasses in order to implement an * api for a {@link TRpcService}. A subclass of TRpcApiProvider must implement the * {@link registerMethods} method in order to declare the available methods, their * names and the associated callback. * * <code> * public function registerMethods() * { * return array( * 'apiMethodName1' => array('method' => array($this, 'objectMethodName1')), * 'apiMethodName2' => array('method' => array('ClassName', 'staticMethodName')), * ); * } * </code> * * In this example, two api method have been defined. The first refers to an object * method that must be implemented in the same class, the second to a static method * implemented in a 'ClassName' class. * In both cases, the method implementation will receive the request parameters as its * method parameters. Since the number of received parameters depends on * external-supplied data, it's adviced to use php's func_get_args() funtion to * validate them. * * Providers must be registered in the service configuration in order to be available, * as explained in {@link TRpcService}'s documentation. * * @author Robin J. Rogge <rrogge@bigpoint.net> * @version $Id$ * @package System.Web.Services * @since 3.2 */ abstract class TRpcApiProvider extends TModule { /** * @var TRpcServer instance */ protected $rpcServer; /** * Must return an array of the available methods * @abstract */ abstract public function registerMethods(); /** * Constructor: informs the rpc server of the registered methods */ public function __construct(TRpcServer $rpcServer) { $this->rpcServer = $rpcServer; foreach($this->registerMethods() as $_methodName => $_methodDetails) $this->rpcServer->addRpcMethod($_methodName, $_methodDetails); } /** * Processes the request using the server * @return processed request */ public function processRequest() { return $this->rpcServer->processRequest(); } /** * @return rpc server instance */ public function getRpcServer() { return $this->rpcServer; } } /** * TRpcProtocol class * * TRpcProtocol is the base class used to implement a protocol in a {@link TRpcService}. * Prado already implements two protocols: {@link TXmlRpcProtocol} for Xml-Rpc request * and {@link TJsonRpcProtocol} for JSON-Rpc requests. * * @author Robin J. Rogge <rrogge@bigpoint.net> * @version $Id$ * @package System.Web.Services * @since 3.2 **/ abstract class TRpcProtocol { /** * @var array containing the mapping from RPC method names to the actual handlers */ protected $rpcMethods = array(); // abstracts /** * @param string request payload * Processed the request ans returns the response, if any * @return processed response * @abstract */ abstract public function callMethod($requestPayload); /** * @param TRpcException the exception with error details * Creates a proper response for an error condition * @return a response representing the error * @abstract */ abstract public function createErrorResponse(TRpcException $exception); /** * @param response * Sets the needed headers for the response (eg: content-type, charset) * @abstract */ abstract public function createResponseHeaders($response); /** * Encodes the response * @param mixed reponse data * @return string encoded response * @abstract */ abstract public function encode($data); /** * Decodes the request payload * @param string request payload * @return mixed decoded request * @abstract */ abstract public function decode($data); // methods /** * Registers a new RPC method and handler details * @param string $methodName * @param array $handlerDetails containing the callback handler */ public function addMethod($methodName, $handlerDetails) { $this->rpcMethods[$methodName] = $handlerDetails; } /** * Calls the callback handler for the given method * @param string $methodName of the RPC * @param array $parameters for the callback handler as provided by the client * @return mixed whatever the callback handler returns */ public function callApiMethod($methodName, $parameters) { if(!isset($this->rpcMethods[$methodName])) throw new TRpcException('Method "'.$methodName.'" not found'); if($parameters === null) $parameters = array(); if(!is_array($parameters)) $parameters = array($parameters); return call_user_func_array($this->rpcMethods[$methodName]['method'], $parameters); } } /** * TJsonRpcProtocol class * * TJsonRpcProtocol is a class that implements JSON-Rpc protocol in {@link TRpcService}. * Both version 1.0 and 2.0 of the specification are implemented, and the server will try * to answer using the same version of the protocol used by the requesting client. * * @author Robin J. Rogge <rrogge@bigpoint.net> * @author Fabio Bas <ctrlaltca@gmail.com> * @version $Id$ * @package System.Web.Services * @since 3.2 */ class TJsonRpcProtocol extends TRpcProtocol { protected $_id=null; protected $_specificationVersion=1.0; /** * Handles the RPC request * @param string $requestPayload * @return string JSON RPC response */ public function callMethod($requestPayload) { try { $_request = $this->decode($requestPayload); if(isset($_request['jsonrpc'])) { $this->_specificationVersion=$_request['jsonrpc']; if($this->_specificationVersion > 2.0) throw new TRpcException('Unsupported specification version', '-32600'); } if(isset($_request['id'])) $this->_id=$_request['id']; if(!isset($_request['method'])) throw new TRpcException('Missing request method', '-32600'); if(!isset($_request['params'])) $parameters = array(); else $parameters = $_request['params']; if(!is_array($parameters)) $parameters = array($parameters); $ret = $this->callApiMethod($_request['method'], $parameters); // a request without an id is a notification that doesn't need a response if($this->_id !== null) { if($this->_specificationVersion==2.0) { return $this->encode(array( 'jsonrpc' => '2.0', 'id' => $this->_id, 'result' => $ret )); } else { return $this->encode(array( 'id' => $this->_id, 'result' => $this->callApiMethod($_request['method'], $_request['params']), 'error' => null )); } } } catch(TRpcException $e) { return $this->createErrorResponse($e); } catch(THttpException $e) { throw $e; } catch(Exception $e) { return $this->createErrorResponse(new TRpcException('An internal error occured', '-32603')); } } /** * Turns the given exception into an JSON RPC fault * @param TRpcException $exception * @return string JSON RPC fault */ public function createErrorResponse(TRpcException $exception) { if($this->_specificationVersion==2.0) { return $this->encode(array( 'id' => $this->_id, 'result' => null, 'error'=> array( 'code' => $exception->getCode(), 'message'=> $exception->getMessage(), 'data' => null, ) )); } else { return $this->encode(array( 'id' => $this->_id, 'error'=> array( 'code' => $exception->getCode(), 'message'=> $exception->getMessage(), 'data' => null, ) )); } } /** * Sets the correct response headers * @param THttpResponse $response */ public function createResponseHeaders($response) { $response->setContentType('application/json'); $response->setCharset('UTF-8'); } /** * Decodes JSON encoded data into PHP data * @param string $data in JSON format * @return array PHP data */ public function decode($data) { $s = json_decode($data, true); self::checkJsonError(); return $s; } /** * Encodes PHP data into JSON data * @param mixed PHP data * @return string JSON encoded PHP data */ public function encode($data) { $s = json_encode($data); self::checkJsonError(); return $s; } private static function checkJsonError() { $errnum = json_last_error(); if($errnum != JSON_ERROR_NONE) throw new Exception("JSON error: $msg", $err); } /** * Calls the callback handler for the given method * Overrides parent implementation to correctly handle error codes * @param string $methodName of the RPC * @param array $parameters for the callback handler as provided by the client * @return mixed whatever the callback handler returns */ public function callApiMethod($methodName, $parameters) { if(!isset($this->rpcMethods[$methodName])) throw new TRpcException('Method "'.$methodName.'" not found', '-32601'); return call_user_func_array($this->rpcMethods[$methodName]['method'], $parameters); } } /** * TXmlRpcProtocol class * * TXmlRpcProtocol is a class that implements XML-Rpc protocol in {@link TRpcService}. * It's basically a wrapper to the xmlrpc_server_* family of php methods. * * @author Robin J. Rogge <rrogge@bigpoint.net> * @version $Id$ * @package System.Web.Services * @since 3.2 */ class TXmlRpcProtocol extends TRpcProtocol { /** * @var XML RPC server resource */ private $_xmlrpcServer; // magics /** * Constructor */ public function __construct() { $this->_xmlrpcServer = xmlrpc_server_create(); } /** * Destructor */ public function __destruct() { xmlrpc_server_destroy($this->_xmlrpcServer); } // methods /** * Registers a new RPC method and handler details * @param string $methodName * @param array $handlerDetails containing the callback handler */ public function addMethod($methodName, $methodDetails) { parent::addMethod($methodName, $methodDetails); xmlrpc_server_register_method($this->_xmlrpcServer, $methodName, array($this, 'callApiMethod')); } // methods /** * Handles the RPC request * @param string $requestPayload * @return string XML RPC response */ public function callMethod($requestPayload) { try { return xmlrpc_server_call_method($this->_xmlrpcServer, $requestPayload, null); } catch(TRpcException $e) { return $this->createErrorResponse($e); } catch(THttpException $e) { throw $e; } catch(Exception $e) { return $this->createErrorResponse(new TRpcException('An internal error occured')); } } /** * Turns the given exception into an XML RPC fault * @param TRpcException $exception * @return string XML RPC fault */ public function createErrorResponse(TRpcException $exception) { return $this->encode(array( 'faultCode' => $exception->getCode(), 'faultString' => $exception->getMessage() )); } /** * Sets the correct response headers * @param THttpResponse $response */ public function createResponseHeaders($response) { $response->setContentType('text/xml'); $response->setCharset('UTF-8'); } /** * Decodes XML encoded data into PHP data * @param string $data in XML format * @return array PHP data */ public function decode($data) { return xmlrpc_decode($data); } /** * Encodes PHP data into XML data * @param mixed PHP data * @return string XML encoded PHP data */ public function encode($data) { return xmlrpc_encode($data); } }