From 6f7fdef0f500cd4bb540affd3bc1482243f337c1 Mon Sep 17 00:00:00 2001 From: emkael Date: Wed, 24 Feb 2016 23:18:07 +0100 Subject: * Prado 3.3.0 --- lib/prado/framework/Web/Services/TRpcService.php | 722 +++++++++++++++++++++++ 1 file changed, 722 insertions(+) create mode 100644 lib/prado/framework/Web/Services/TRpcService.php (limited to 'lib/prado/framework/Web/Services/TRpcService.php') diff --git a/lib/prado/framework/Web/Services/TRpcService.php b/lib/prado/framework/Web/Services/TRpcService.php new file mode 100644 index 0000000..cf57944 --- /dev/null +++ b/lib/prado/framework/Web/Services/TRpcService.php @@ -0,0 +1,722 @@ + + * @link https://github.com/pradosoft/prado + * @copyright 2010 Bigpoint GmbH + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @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: + * + * + * + * + * + * + * + * + * + * + * An api can be registered adding a proper 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 + * @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 + * @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 + * @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. + * + * + * public function registerMethods() + * { + * return array( + * 'apiMethodName1' => array('method' => array($this, 'objectMethodName1')), + * 'apiMethodName2' => array('method' => array('ClassName', 'staticMethodName')), + * ); + * } + * + * + * 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 + * @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 + * @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 + * @author Fabio Bas + * @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); + + // 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' => $this->callApiMethod($_request['method'], $parameters), + )); + } 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 + * @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); + } +} -- cgit v1.2.3