summaryrefslogtreecommitdiff
path: root/libs/jsonrpc
diff options
context:
space:
mode:
authorFrédéric Guillot <fred@kanboard.net>2018-06-21 14:13:41 -0700
committerFrédéric Guillot <fred@kanboard.net>2018-06-21 14:13:41 -0700
commita491348d442ab8e6cd2fa403d4365cdad78e52ce (patch)
treea00f575d82afb2c9051bad95398b4250f4a3d44d /libs/jsonrpc
parentc73ac5f1f818b6b21083f6785b4b2f6d778a6496 (diff)
Vendoring deprecated composer libs
Diffstat (limited to 'libs/jsonrpc')
-rw-r--r--libs/jsonrpc/LICENSE21
-rw-r--r--libs/jsonrpc/README.markdown412
-rw-r--r--libs/jsonrpc/src/JsonRPC/Client.php198
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/AccessDeniedException.php13
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/AuthenticationFailureException.php13
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/ConnectionFailureException.php13
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonFormatException.php13
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php13
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php13
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/ResponseException.php62
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/RpcCallFailedException.php15
-rw-r--r--libs/jsonrpc/src/JsonRPC/Exception/ServerErrorException.php13
-rw-r--r--libs/jsonrpc/src/JsonRPC/HttpClient.php449
-rw-r--r--libs/jsonrpc/src/JsonRPC/MiddlewareHandler.php114
-rw-r--r--libs/jsonrpc/src/JsonRPC/MiddlewareInterface.php27
-rw-r--r--libs/jsonrpc/src/JsonRPC/ProcedureHandler.php296
-rw-r--r--libs/jsonrpc/src/JsonRPC/Request/BatchRequestParser.php55
-rw-r--r--libs/jsonrpc/src/JsonRPC/Request/RequestBuilder.php129
-rw-r--r--libs/jsonrpc/src/JsonRPC/Request/RequestParser.php200
-rw-r--r--libs/jsonrpc/src/JsonRPC/Response/ResponseBuilder.php336
-rw-r--r--libs/jsonrpc/src/JsonRPC/Response/ResponseParser.php154
-rw-r--r--libs/jsonrpc/src/JsonRPC/Server.php386
-rw-r--r--libs/jsonrpc/src/JsonRPC/Validator/HostValidator.php73
-rw-r--r--libs/jsonrpc/src/JsonRPC/Validator/JsonEncodingValidator.php44
-rw-r--r--libs/jsonrpc/src/JsonRPC/Validator/JsonFormatValidator.php30
-rw-r--r--libs/jsonrpc/src/JsonRPC/Validator/RpcFormatValidator.php35
-rw-r--r--libs/jsonrpc/src/JsonRPC/Validator/UserValidator.php21
-rw-r--r--libs/jsonrpc/tests/ClientTest.php103
-rw-r--r--libs/jsonrpc/tests/HttpClientTest.php220
-rw-r--r--libs/jsonrpc/tests/MiddlewareHandlerTest.php40
-rw-r--r--libs/jsonrpc/tests/ProcedureHandlerTest.php153
-rw-r--r--libs/jsonrpc/tests/Request/RequestBuilderTest.php53
-rw-r--r--libs/jsonrpc/tests/Response/HeaderMockTest.php25
-rw-r--r--libs/jsonrpc/tests/Response/ResponseBuilderTest.php151
-rw-r--r--libs/jsonrpc/tests/Response/ResponseParserTest.php100
-rw-r--r--libs/jsonrpc/tests/ServerProtocolTest.php237
-rw-r--r--libs/jsonrpc/tests/ServerTest.php258
-rw-r--r--libs/jsonrpc/tests/Validator/HostValidatorTest.php32
-rw-r--r--libs/jsonrpc/tests/Validator/JsonEncodingValidatorTest.php22
-rw-r--r--libs/jsonrpc/tests/Validator/JsonFormatValidatorTest.php19
-rw-r--r--libs/jsonrpc/tests/Validator/RpcFormatValidatorTest.php48
-rw-r--r--libs/jsonrpc/tests/Validator/UserValidatorTest.php24
42 files changed, 4633 insertions, 0 deletions
diff --git a/libs/jsonrpc/LICENSE b/libs/jsonrpc/LICENSE
new file mode 100644
index 00000000..6a362bc1
--- /dev/null
+++ b/libs/jsonrpc/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Frederic Guillot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/jsonrpc/README.markdown b/libs/jsonrpc/README.markdown
new file mode 100644
index 00000000..91891a21
--- /dev/null
+++ b/libs/jsonrpc/README.markdown
@@ -0,0 +1,412 @@
+JsonRPC PHP Client and Server
+=============================
+
+A simple Json-RPC client/server that just works.
+
+Features
+--------
+
+- JSON-RPC 2.0 only
+- The server support batch requests and notifications
+- Authentication and IP based client restrictions
+- Custom Middleware
+- Fully unit tested
+- Requirements: PHP >= 5.3.4
+- License: MIT
+
+Author
+------
+
+Frédéric Guillot
+
+Installation with Composer
+--------------------------
+
+```bash
+composer require fguillot/json-rpc @stable
+```
+
+Examples
+--------
+
+### Server
+
+Callback binding:
+
+```php
+<?php
+
+use JsonRPC\Server;
+
+$server = new Server();
+$server->getProcedureHandler()
+ ->withCallback('addition', function ($a, $b) {
+ return $a + $b;
+ })
+ ->withCallback('random', function ($start, $end) {
+ return mt_rand($start, $end);
+ })
+;
+
+echo $server->execute();
+```
+
+Callback binding from array:
+
+```php
+<?php
+
+use JsonRPC\Server;
+
+$callbacks = array(
+ 'getA' => function() { return 'A'; },
+ 'getB' => function() { return 'B'; },
+ 'getC' => function() { return 'C'; }
+);
+
+$server = new Server();
+$server->getProcedureHandler()->withCallbackArray($callbacks);
+
+echo $server->execute();
+```
+
+Class/Method binding:
+
+```php
+<?php
+
+use JsonRPC\Server;
+
+class Api
+{
+ public function doSomething($arg1, $arg2 = 3)
+ {
+ return $arg1 + $arg2;
+ }
+}
+
+$server = new Server();
+$procedureHandler = $server->getProcedureHandler();
+
+// Bind the method Api::doSomething() to the procedure myProcedure
+$procedureHandler->withClassAndMethod('myProcedure', 'Api', 'doSomething');
+
+// Use a class instance instead of the class name
+$procedureHandler->withClassAndMethod('mySecondProcedure', new Api, 'doSomething');
+
+// The procedure and the method are the same
+$procedureHandler->withClassAndMethod('doSomething', 'Api');
+
+// Attach the class, the client will be able to call directly Api::doSomething()
+$procedureHandler->withObject(new Api());
+
+echo $server->execute();
+```
+
+Class/Method binding from array:
+
+```php
+<?php
+
+use JsonRPC\Server;
+
+class MathApi
+{
+ public function addition($arg1, $arg2)
+ {
+ return $arg1 + $arg2;
+ }
+
+ public function subtraction($arg1, $arg2)
+ {
+ return $arg1 - $arg2;
+ }
+
+ public function multiplication($arg1, $arg2)
+ {
+ return $arg1 * $arg2;
+ }
+
+ public function division($arg1, $arg2)
+ {
+ return $arg1 / $arg2;
+ }
+}
+
+$callbacks = array(
+ 'addition' => array( 'MathApi', addition ),
+ 'subtraction' => array( 'MathApi', subtraction ),
+ 'multiplication' => array( 'MathApi', multiplication ),
+ 'division' => array( 'MathApi', division )
+);
+
+$server = new Server();
+$server->getProcedureHandler()->withClassAndMethodArray($callbacks);
+
+echo $server->execute();
+```
+
+Server Middleware:
+
+Middleware might be used to authenticate and authorize the client.
+They are executed before each procedure.
+
+```php
+<?php
+
+use JsonRPC\Server;
+use JsonRPC\MiddlewareInterface;
+use JsonRPC\Exception\AuthenticationFailureException;
+
+class Api
+{
+ public function doSomething($arg1, $arg2 = 3)
+ {
+ return $arg1 + $arg2;
+ }
+}
+
+class MyMiddleware implements MiddlewareInterface
+{
+ public function execute($username, $password, $procedureName)
+ {
+ if ($username !== 'foobar') {
+ throw new AuthenticationFailureException('Wrong credentials!');
+ }
+ }
+}
+
+$server = new Server();
+$server->getMiddlewareHandler()->withMiddleware(new MyMiddleware());
+$server->getProcedureHandler()->withObject(new Api());
+echo $server->execute();
+```
+
+You can raise a `AuthenticationFailureException` when the API credentials are wrong or a `AccessDeniedException` when the user is not allowed to access to the procedure.
+
+### Client
+
+Example with positional parameters:
+
+```php
+<?php
+
+use JsonRPC\Client;
+
+$client = new Client('http://localhost/server.php');
+$result = $client->execute('addition', [3, 5]);
+```
+
+Example with named arguments:
+
+```php
+<?php
+
+use JsonRPC\Client;
+
+$client = new Client('http://localhost/server.php');
+$result = $client->execute('random', ['end' => 10, 'start' => 1]);
+```
+
+Arguments are called in the right order.
+
+Examples with the magic method `__call()`:
+
+```php
+<?php
+
+use JsonRPC\Client;
+
+$client = new Client('http://localhost/server.php');
+$result = $client->random(50, 100);
+```
+
+The example above use positional arguments for the request and this one use named arguments:
+
+```php
+$result = $client->random(['end' => 10, 'start' => 1]);
+```
+
+### Client batch requests
+
+Call several procedures in a single HTTP request:
+
+```php
+<?php
+
+use JsonRPC\Client;
+
+$client = new Client('http://localhost/server.php');
+
+$results = $client->batch()
+ ->foo(['arg1' => 'bar'])
+ ->random(1, 100)
+ ->add(4, 3)
+ ->execute('add', [2, 5])
+ ->send();
+
+print_r($results);
+```
+
+All results are stored at the same position of the call.
+
+### Client exceptions
+
+Client exceptions are normally thrown when an error is returned by the server. You can change this behaviour by
+using the `$returnException` argument which causes exceptions to be returned. This can be extremely useful when
+executing the batch request.
+
+- `BadFunctionCallException`: Procedure not found on the server
+- `InvalidArgumentException`: Wrong procedure arguments
+- `JsonRPC\Exception\AccessDeniedException`: Access denied
+- `JsonRPC\Exception\ConnectionFailureException`: Connection failure
+- `JsonRPC\Exception\ServerErrorException`: Internal server error
+
+### Enable client debugging
+
+You can enable the debug mode to see the JSON request and response:
+
+```php
+<?php
+
+use JsonRPC\Client;
+
+$client = new Client('http://localhost/server.php');
+$client->getHttpClient()->withDebug();
+```
+
+The debug output is sent to the PHP system logger.
+You can configure the log destination in your `php.ini`.
+
+Output example:
+
+```json
+==> Request:
+{
+ "jsonrpc": "2.0",
+ "method": "removeCategory",
+ "id": 486782327,
+ "params": [
+ 1
+ ]
+}
+==> Response:
+{
+ "jsonrpc": "2.0",
+ "id": 486782327,
+ "result": true
+}
+```
+
+### IP based client restrictions
+
+The server can allow only some IP addresses:
+
+```php
+<?php
+
+use JsonRPC\Server;
+
+$server = new Server;
+
+// IP client restrictions
+$server->allowHosts(['192.168.0.1', '127.0.0.1']);
+
+[...]
+
+// Return the response to the client
+echo $server->execute();
+```
+
+If the client is blocked, you got a 403 Forbidden HTTP response.
+
+### HTTP Basic Authentication
+
+If you use HTTPS, you can allow client by using a username/password.
+
+```php
+<?php
+
+use JsonRPC\Server;
+
+$server = new Server;
+
+// List of users to allow
+$server->authentication(['user1' => 'password1', 'user2' => 'password2']);
+
+[...]
+
+// Return the response to the client
+echo $server->execute();
+```
+
+On the client, set credentials like that:
+
+```php
+<?php
+
+use JsonRPC\Client;
+
+$client = new Client('http://localhost/server.php');
+$client->getHttpClient()
+ ->withUsername('Foo')
+ ->withPassword('Bar');
+```
+
+If the authentication failed, the client throw a RuntimeException.
+
+Using an alternative authentication header:
+
+```php
+
+use JsonRPC\Server;
+
+$server = new Server();
+$server->setAuthenticationHeader('X-Authentication');
+$server->authentication(['myusername' => 'mypassword']);
+```
+
+The example above will use the HTTP header `X-Authentication` instead of the standard `Authorization: Basic [BASE64_CREDENTIALS]`.
+The username/password values need be encoded in base64: `base64_encode('username:password')`.
+
+### Local Exceptions
+
+By default, the server will relay all exceptions to the client.
+If you would like to relay only some of them, use the method `Server::withLocalException($exception)`:
+
+```php
+<?php
+
+use JsonRPC\Server;
+class MyException1 extends Exception {};
+class MyException2 extends Exception {};
+
+$server = new Server();
+
+// Exceptions that should NOT be relayed to the client, if they occurs
+$server
+ ->withLocalException('MyException1')
+ ->withLocalException('MyException2')
+;
+
+[...]
+
+echo $server->execute();
+```
+
+### Callback before client request
+
+You can use a callback to change the HTTP headers or the URL before to make the request to the server.
+
+Example:
+
+```php
+<?php
+
+$client = new Client();
+$client->getHttpClient()->withBeforeRequestCallback(function(HttpClient $client, $payload) {
+ $client->withHeaders(array('Content-Length: '.strlen($payload)));
+});
+
+$client->myProcedure(123);
+```
diff --git a/libs/jsonrpc/src/JsonRPC/Client.php b/libs/jsonrpc/src/JsonRPC/Client.php
new file mode 100644
index 00000000..73ab1342
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Client.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace JsonRPC;
+
+use Exception;
+use JsonRPC\Request\RequestBuilder;
+use JsonRPC\Response\ResponseParser;
+
+/**
+ * JsonRPC client class
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+class Client
+{
+ /**
+ * If the only argument passed to a function is an array
+ * assume it contains named arguments
+ *
+ * @access private
+ * @var boolean
+ */
+ private $isNamedArguments = true;
+
+ /**
+ * Do not immediately throw an exception on error. Return it instead.
+ *
+ * @access public
+ * @var boolean
+ */
+ private $returnException = false;
+
+ /**
+ * True for a batch request
+ *
+ * @access private
+ * @var boolean
+ */
+ private $isBatch = false;
+
+ /**
+ * Batch payload
+ *
+ * @access private
+ * @var array
+ */
+ private $batch = array();
+
+ /**
+ * Http Client
+ *
+ * @access private
+ * @var HttpClient
+ */
+ private $httpClient;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $url Server URL
+ * @param bool $returnException Return exceptions
+ * @param HttpClient $httpClient HTTP client object
+ */
+ public function __construct($url = '', $returnException = false, HttpClient $httpClient = null)
+ {
+ $this->httpClient = $httpClient ?: new HttpClient($url);
+ $this->returnException = $returnException;
+ }
+
+ /**
+ * Arguments passed are always positional
+ *
+ * @access public
+ * @return $this
+ */
+ public function withPositionalArguments()
+ {
+ $this->isNamedArguments = false;
+ return $this;
+ }
+
+ /**
+ * Get HTTP Client
+ *
+ * @access public
+ * @return HttpClient
+ */
+ public function getHttpClient()
+ {
+ return $this->httpClient;
+ }
+
+ /**
+ * Set username and password
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @return $this
+ */
+ public function authentication($username, $password)
+ {
+ $this->httpClient
+ ->withUsername($username)
+ ->withPassword($password);
+
+ return $this;
+ }
+
+ /**
+ * Automatic mapping of procedures
+ *
+ * @access public
+ * @param string $method Procedure name
+ * @param array $params Procedure arguments
+ * @return mixed
+ */
+ public function __call($method, array $params)
+ {
+ if ($this->isNamedArguments && count($params) === 1 && is_array($params[0])) {
+ $params = $params[0];
+ }
+
+ return $this->execute($method, $params);
+ }
+
+ /**
+ * Start a batch request
+ *
+ * @access public
+ * @return Client
+ */
+ public function batch()
+ {
+ $this->isBatch = true;
+ $this->batch = array();
+ return $this;
+ }
+
+ /**
+ * Send a batch request
+ *
+ * @access public
+ * @return array
+ */
+ public function send()
+ {
+ $this->isBatch = false;
+ return $this->sendPayload('['.implode(', ', $this->batch).']');
+ }
+
+ /**
+ * Execute a procedure
+ *
+ * @access public
+ * @param string $procedure Procedure name
+ * @param array $params Procedure arguments
+ * @param array $reqattrs
+ * @param string|null $requestId Request Id
+ * @param string[] $headers Headers for this request
+ * @return mixed
+ */
+ public function execute($procedure, array $params = array(), array $reqattrs = array(), $requestId = null, array $headers = array())
+ {
+ $payload = RequestBuilder::create()
+ ->withProcedure($procedure)
+ ->withParams($params)
+ ->withRequestAttributes($reqattrs)
+ ->withId($requestId)
+ ->build();
+
+ if ($this->isBatch) {
+ $this->batch[] = $payload;
+ return $this;
+ }
+
+ return $this->sendPayload($payload, $headers);
+ }
+
+ /**
+ * Send payload
+ *
+ * @access private
+ * @throws Exception
+ * @param string $payload
+ * @param string[] $headers
+ * @return Exception|Client
+ */
+ private function sendPayload($payload, array $headers = array())
+ {
+ return ResponseParser::create()
+ ->withReturnException($this->returnException)
+ ->withPayload($this->httpClient->execute($payload, $headers))
+ ->parse();
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/AccessDeniedException.php b/libs/jsonrpc/src/JsonRPC/Exception/AccessDeniedException.php
new file mode 100644
index 00000000..d1aabfbe
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/AccessDeniedException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+/**
+ * Class AccessDeniedException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class AccessDeniedException extends RpcCallFailedException
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/AuthenticationFailureException.php b/libs/jsonrpc/src/JsonRPC/Exception/AuthenticationFailureException.php
new file mode 100644
index 00000000..770edeeb
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/AuthenticationFailureException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+/**
+ * Class AuthenticationFailureException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class AuthenticationFailureException extends RpcCallFailedException
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ConnectionFailureException.php b/libs/jsonrpc/src/JsonRPC/Exception/ConnectionFailureException.php
new file mode 100644
index 00000000..195f8910
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/ConnectionFailureException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+/**
+ * Class ConnectionFailureException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class ConnectionFailureException extends RpcCallFailedException
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonFormatException.php b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonFormatException.php
new file mode 100644
index 00000000..294bc74c
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonFormatException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+/**
+ * Class InvalidJsonFormatException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class InvalidJsonFormatException extends RpcCallFailedException
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php
new file mode 100644
index 00000000..2e3ff05b
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+/**
+ * Class InvalidJsonRpcFormatException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class InvalidJsonRpcFormatException extends RpcCallFailedException
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php b/libs/jsonrpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php
new file mode 100644
index 00000000..16f75910
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+/**
+ * Class ResponseEncodingFailureException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class ResponseEncodingFailureException extends RpcCallFailedException
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ResponseException.php b/libs/jsonrpc/src/JsonRPC/Exception/ResponseException.php
new file mode 100644
index 00000000..e97b4e6b
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/ResponseException.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+use Exception;
+
+/**
+ * Class ResponseException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class ResponseException extends RpcCallFailedException
+{
+ /**
+ * A value that contains additional information about the error.
+ *
+ * @access protected
+ * @link http://www.jsonrpc.org/specification#error_object
+ * @var mixed
+ */
+ protected $data;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $message [optional] The Exception message to throw.
+ * @param int $code [optional] The Exception code.
+ * @param Exception $previous [optional] The previous exception used for the exception chaining. Since 5.3.0
+ * @param mixed $data [optional] A value that contains additional information about the error.
+ */
+ public function __construct($message = '', $code = 0, Exception $previous = null, $data = null)
+ {
+ parent::__construct($message, $code, $previous);
+ $this->setData($data);
+ }
+
+ /**
+ * Attach additional information
+ *
+ * @access public
+ * @param mixed $data [optional] A value that contains additional information about the error.
+ * @return \JsonRPC\Exception\ResponseException
+ */
+ public function setData($data = null)
+ {
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Get additional information
+ *
+ * @access public
+ * @return mixed|null
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/RpcCallFailedException.php b/libs/jsonrpc/src/JsonRPC/Exception/RpcCallFailedException.php
new file mode 100644
index 00000000..b3fcd84e
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/RpcCallFailedException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+use Exception;
+
+/**
+ * Class RpcCallFailedException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class RpcCallFailedException extends Exception
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ServerErrorException.php b/libs/jsonrpc/src/JsonRPC/Exception/ServerErrorException.php
new file mode 100644
index 00000000..29031604
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Exception/ServerErrorException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JsonRPC\Exception;
+
+/**
+ * Class ServerErrorException
+ *
+ * @package JsonRPC\Exception
+ * @author Frederic Guillot
+ */
+class ServerErrorException extends RpcCallFailedException
+{
+}
diff --git a/libs/jsonrpc/src/JsonRPC/HttpClient.php b/libs/jsonrpc/src/JsonRPC/HttpClient.php
new file mode 100644
index 00000000..01d50445
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/HttpClient.php
@@ -0,0 +1,449 @@
+<?php
+
+namespace JsonRPC;
+
+use Closure;
+use JsonRPC\Exception\AccessDeniedException;
+use JsonRPC\Exception\ConnectionFailureException;
+use JsonRPC\Exception\ServerErrorException;
+
+/**
+ * Class HttpClient
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+class HttpClient
+{
+ /**
+ * URL of the server
+ *
+ * @access protected
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * HTTP client timeout
+ *
+ * @access protected
+ * @var integer
+ */
+ protected $timeout = 5;
+
+ /**
+ * Default HTTP headers to send to the server
+ *
+ * @access protected
+ * @var array
+ */
+ protected $headers = array(
+ 'User-Agent: JSON-RPC PHP Client <https://github.com/fguillot/JsonRPC>',
+ 'Content-Type: application/json',
+ 'Accept: application/json',
+ 'Connection: close',
+ );
+
+ /**
+ * Username for authentication
+ *
+ * @access protected
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * Password for authentication
+ *
+ * @access protected
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * Enable debug output to the php error log
+ *
+ * @access protected
+ * @var boolean
+ */
+ protected $debug = false;
+
+ /**
+ * Cookies
+ *
+ * @access protected
+ * @var array
+ */
+ protected $cookies = array();
+
+ /**
+ * SSL certificates verification
+ *
+ * @access protected
+ * @var boolean
+ */
+ protected $verifySslCertificate = true;
+
+ /**
+ * SSL client certificate
+ *
+ * @access protected
+ * @var string
+ */
+ protected $sslLocalCert;
+
+ /**
+ * Callback called before the doing the request
+ *
+ * @access protected
+ * @var Closure
+ */
+ protected $beforeRequest;
+
+ /**
+ * HttpClient constructor
+ *
+ * @access public
+ * @param string $url
+ */
+ public function __construct($url = '')
+ {
+ $this->url = $url;
+ }
+
+ /**
+ * Set URL
+ *
+ * @access public
+ * @param string $url
+ * @return $this
+ */
+ public function withUrl($url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ * @return $this
+ */
+ public function withUsername($username)
+ {
+ $this->username = $username;
+ return $this;
+ }
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ * @return $this
+ */
+ public function withPassword($password)
+ {
+ $this->password = $password;
+ return $this;
+ }
+
+ /**
+ * Set timeout
+ *
+ * @access public
+ * @param integer $timeout
+ * @return $this
+ */
+ public function withTimeout($timeout)
+ {
+ $this->timeout = $timeout;
+ return $this;
+ }
+
+ /**
+ * Set headers
+ *
+ * @access public
+ * @param array $headers
+ * @return $this
+ */
+ public function withHeaders(array $headers)
+ {
+ $this->headers = array_merge($this->headers, $headers);
+ return $this;
+ }
+
+ /**
+ * Set cookies
+ *
+ * @access public
+ * @param array $cookies
+ * @param boolean $replace
+ */
+ public function withCookies(array $cookies, $replace = false)
+ {
+ if ($replace) {
+ $this->cookies = $cookies;
+ } else {
+ $this->cookies = array_merge($this->cookies, $cookies);
+ }
+ }
+
+ /**
+ * Enable debug mode
+ *
+ * @access public
+ * @return $this
+ */
+ public function withDebug()
+ {
+ $this->debug = true;
+ return $this;
+ }
+
+ /**
+ * Disable SSL verification
+ *
+ * @access public
+ * @return $this
+ */
+ public function withoutSslVerification()
+ {
+ $this->verifySslCertificate = false;
+ return $this;
+ }
+
+ /**
+ * Assign a certificate to use TLS
+ *
+ * @access public
+ * @return $this
+ */
+ public function withSslLocalCert($path)
+ {
+ $this->sslLocalCert = $path;
+ return $this;
+ }
+
+ /**
+ * Assign a callback before the request
+ *
+ * @access public
+ * @param Closure $closure
+ * @return $this
+ */
+ public function withBeforeRequestCallback(Closure $closure)
+ {
+ $this->beforeRequest = $closure;
+ return $this;
+ }
+
+ /**
+ * Get cookies
+ *
+ * @access public
+ * @return array
+ */
+ public function getCookies()
+ {
+ return $this->cookies;
+ }
+
+ /**
+ * Do the HTTP request
+ *
+ * @access public
+ * @throws ConnectionFailureException
+ * @param string $payload
+ * @param string[] $headers Headers for this request
+ * @return array
+ */
+ public function execute($payload, array $headers = array())
+ {
+ if (is_callable($this->beforeRequest)) {
+ call_user_func_array($this->beforeRequest, array($this, $payload, $headers));
+ }
+
+ if ($this->isCurlLoaded()) {
+ $ch = curl_init();
+ $requestHeaders = $this->buildHeaders($headers);
+ $headers = array();
+ curl_setopt_array($ch, array(
+ CURLOPT_URL => trim($this->url),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => $this->timeout,
+ CURLOPT_MAXREDIRS => 2,
+ CURLOPT_SSL_VERIFYPEER => $this->verifySslCertificate,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $payload,
+ CURLOPT_HTTPHEADER => $requestHeaders,
+ CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$headers) {
+ $headers[] = $header;
+ return strlen($header);
+ }
+ ));
+ if ($this->sslLocalCert !== null) {
+ curl_setopt($ch, CURLOPT_CAINFO, $this->sslLocalCert);
+ }
+ $response = curl_exec($ch);
+ curl_close($ch);
+ if ($response !== false) {
+ $response = json_decode($response, true);
+ } else {
+ throw new ConnectionFailureException('Unable to establish a connection');
+ }
+ } else {
+ $stream = fopen(trim($this->url), 'r', false, $this->buildContext($payload, $headers));
+
+ if (! is_resource($stream)) {
+ throw new ConnectionFailureException('Unable to establish a connection');
+ }
+
+ $metadata = stream_get_meta_data($stream);
+ $headers = $metadata['wrapper_data'];
+ $response = json_decode(stream_get_contents($stream), true);
+
+ fclose($stream);
+ }
+
+ if ($this->debug) {
+ error_log('==> Request: '.PHP_EOL.(is_string($payload) ? $payload : json_encode($payload, JSON_PRETTY_PRINT)));
+ error_log('==> Headers: '.PHP_EOL.var_export($headers, true));
+ error_log('==> Response: '.PHP_EOL.json_encode($response, JSON_PRETTY_PRINT));
+ }
+
+ $this->handleExceptions($headers);
+ $this->parseCookies($headers);
+
+ return $response;
+ }
+
+ /**
+ * Prepare stream context
+ *
+ * @access protected
+ * @param string $payload
+ * @param string[] $headers
+ * @return resource
+ */
+ protected function buildContext($payload, array $headers = array())
+ {
+ $headers = $this->buildHeaders($headers);
+
+ $options = array(
+ 'http' => array(
+ 'method' => 'POST',
+ 'protocol_version' => 1.1,
+ 'timeout' => $this->timeout,
+ 'max_redirects' => 2,
+ 'header' => implode("\r\n", $headers),
+ 'content' => $payload,
+ 'ignore_errors' => true,
+ ),
+ 'ssl' => array(
+ 'verify_peer' => $this->verifySslCertificate,
+ 'verify_peer_name' => $this->verifySslCertificate,
+ )
+ );
+
+ if ($this->sslLocalCert !== null) {
+ $options['ssl']['local_cert'] = $this->sslLocalCert;
+ }
+
+ return stream_context_create($options);
+ }
+
+ /**
+ * Parse cookies from response
+ *
+ * @access protected
+ * @param array $headers
+ */
+ protected function parseCookies(array $headers)
+ {
+ foreach ($headers as $header) {
+ $pos = stripos($header, 'Set-Cookie:');
+
+ if ($pos !== false) {
+ $cookies = explode(';', substr($header, $pos + 11));
+
+ foreach ($cookies as $cookie) {
+ $item = explode('=', $cookie);
+
+ if (count($item) === 2) {
+ $name = trim($item[0]);
+ $value = $item[1];
+ $this->cookies[$name] = $value;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Throw an exception according the HTTP response
+ *
+ * @access public
+ * @param array $headers
+ * @throws AccessDeniedException
+ * @throws ServerErrorException
+ */
+ public function handleExceptions(array $headers)
+ {
+ $exceptions = array(
+ '401' => '\JsonRPC\Exception\AccessDeniedException',
+ '403' => '\JsonRPC\Exception\AccessDeniedException',
+ '404' => '\JsonRPC\Exception\ConnectionFailureException',
+ '500' => '\JsonRPC\Exception\ServerErrorException',
+ );
+
+ foreach ($headers as $header) {
+ foreach ($exceptions as $code => $exception) {
+ if (strpos($header, 'HTTP/1.0 '.$code) !== false || strpos($header, 'HTTP/1.1 '.$code) !== false) {
+ throw new $exception('Response: '.$header);
+ }
+ }
+ }
+ }
+
+ /**
+ * Tests if the curl extension is loaded
+ *
+ * @access protected
+ * @return bool
+ */
+ protected function isCurlLoaded()
+ {
+ return extension_loaded('curl');
+ }
+
+ /**
+ * Prepare Headers
+ *
+ * @access protected
+ * @param array $headers
+ * @return array
+ */
+ protected function buildHeaders(array $headers)
+ {
+ $headers = array_merge($this->headers, $headers);
+
+ if (!empty($this->username) && !empty($this->password)) {
+ $headers[] = 'Authorization: Basic ' . base64_encode($this->username . ':' . $this->password);
+ }
+
+ if (!empty($this->cookies)) {
+ $cookies = array();
+
+ foreach ($this->cookies as $key => $value) {
+ $cookies[] = $key . '=' . $value;
+ }
+
+ $headers[] = 'Cookie: ' . implode('; ', $cookies);
+ }
+ return $headers;
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/MiddlewareHandler.php b/libs/jsonrpc/src/JsonRPC/MiddlewareHandler.php
new file mode 100644
index 00000000..61d5a2d2
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/MiddlewareHandler.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace JsonRPC;
+
+/**
+ * Class MiddlewareHandler
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+class MiddlewareHandler
+{
+ /**
+ * Procedure Name
+ *
+ * @access protected
+ * @var string
+ */
+ protected $procedureName = '';
+
+ /**
+ * Username
+ *
+ * @access protected
+ * @var string
+ */
+ protected $username = '';
+
+ /**
+ * Password
+ *
+ * @access protected
+ * @var string
+ */
+ protected $password = '';
+
+ /**
+ * List of middleware to execute before to call the method
+ *
+ * @access protected
+ * @var MiddlewareInterface[]
+ */
+ protected $middleware = array();
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ * @return $this
+ */
+ public function withUsername($username)
+ {
+ if (! empty($username)) {
+ $this->username = $username;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ * @return $this
+ */
+ public function withPassword($password)
+ {
+ if (! empty($password)) {
+ $this->password = $password;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set procedure name
+ *
+ * @access public
+ * @param string $procedureName
+ * @return $this
+ */
+ public function withProcedure($procedureName)
+ {
+ $this->procedureName = $procedureName;
+ return $this;
+ }
+
+ /**
+ * Add a new middleware
+ *
+ * @access public
+ * @param MiddlewareInterface $middleware
+ * @return MiddlewareHandler
+ */
+ public function withMiddleware(MiddlewareInterface $middleware)
+ {
+ $this->middleware[] = $middleware;
+ return $this;
+ }
+
+ /**
+ * Execute all middleware
+ *
+ * @access public
+ */
+ public function execute()
+ {
+ foreach ($this->middleware as $middleware) {
+ $middleware->execute($this->username, $this->password, $this->procedureName);
+ }
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/MiddlewareInterface.php b/libs/jsonrpc/src/JsonRPC/MiddlewareInterface.php
new file mode 100644
index 00000000..ab55261d
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/MiddlewareInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace JsonRPC;
+
+use JsonRPC\Exception\AccessDeniedException;
+use JsonRPC\Exception\AuthenticationFailureException;
+
+/**
+ * Interface MiddlewareInterface
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+interface MiddlewareInterface
+{
+ /**
+ * Execute Middleware
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @param string $procedureName
+ * @throws AccessDeniedException
+ * @throws AuthenticationFailureException
+ */
+ public function execute($username, $password, $procedureName);
+}
diff --git a/libs/jsonrpc/src/JsonRPC/ProcedureHandler.php b/libs/jsonrpc/src/JsonRPC/ProcedureHandler.php
new file mode 100644
index 00000000..fe33f6b1
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/ProcedureHandler.php
@@ -0,0 +1,296 @@
+<?php
+
+namespace JsonRPC;
+
+use BadFunctionCallException;
+use Closure;
+use InvalidArgumentException;
+use ReflectionFunction;
+use ReflectionMethod;
+
+/**
+ * Class ProcedureHandler
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+class ProcedureHandler
+{
+ /**
+ * List of procedures
+ *
+ * @access protected
+ * @var array
+ */
+ protected $callbacks = array();
+
+ /**
+ * List of classes
+ *
+ * @access protected
+ * @var array
+ */
+ protected $classes = array();
+
+ /**
+ * List of instances
+ *
+ * @access protected
+ * @var array
+ */
+ protected $instances = array();
+
+ /**
+ * Before method name to call
+ *
+ * @access protected
+ * @var string
+ */
+ protected $beforeMethodName = '';
+
+ /**
+ * Register a new procedure
+ *
+ * @access public
+ * @param string $procedure Procedure name
+ * @param closure $callback Callback
+ * @return $this
+ */
+ public function withCallback($procedure, Closure $callback)
+ {
+ $this->callbacks[$procedure] = $callback;
+ return $this;
+ }
+
+ /**
+ * Bind a procedure to a class
+ *
+ * @access public
+ * @param string $procedure Procedure name
+ * @param mixed $class Class name or instance
+ * @param string $method Procedure name
+ * @return $this
+ */
+ public function withClassAndMethod($procedure, $class, $method = '')
+ {
+ if ($method === '') {
+ $method = $procedure;
+ }
+
+ $this->classes[$procedure] = array($class, $method);
+ return $this;
+ }
+
+ /**
+ * Bind a class instance
+ *
+ * @access public
+ * @param mixed $instance
+ * @return $this
+ */
+ public function withObject($instance)
+ {
+ $this->instances[] = $instance;
+ return $this;
+ }
+
+ /**
+ * Set a before method to call
+ *
+ * @access public
+ * @param string $methodName
+ * @return $this
+ */
+ public function withBeforeMethod($methodName)
+ {
+ $this->beforeMethodName = $methodName;
+ return $this;
+ }
+
+ /**
+ * Register multiple procedures from array
+ *
+ * @access public
+ * @param array $callbacks Array with procedure names (array keys) and callbacks (array values)
+ * @return $this
+ */
+ public function withCallbackArray($callbacks)
+ {
+ foreach ($callbacks as $procedure => $callback) {
+ $this->withCallback($procedure, $callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Bind multiple procedures to classes from array
+ *
+ * @access public
+ * @param array $callbacks Array with procedure names (array keys) and class and method names (array values)
+ * @return $this
+ */
+ public function withClassAndMethodArray($callbacks)
+ {
+ foreach ($callbacks as $procedure => $callback) {
+ $this->withClassAndMethod($procedure, $callback[0], $callback[1]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Execute the procedure
+ *
+ * @access public
+ * @param string $procedure Procedure name
+ * @param array $params Procedure params
+ * @return mixed
+ */
+ public function executeProcedure($procedure, array $params = array())
+ {
+ if (isset($this->callbacks[$procedure])) {
+ return $this->executeCallback($this->callbacks[$procedure], $params);
+ } elseif (isset($this->classes[$procedure]) && method_exists($this->classes[$procedure][0], $this->classes[$procedure][1])) {
+ return $this->executeMethod($this->classes[$procedure][0], $this->classes[$procedure][1], $params);
+ }
+
+ foreach ($this->instances as $instance) {
+ if (method_exists($instance, $procedure)) {
+ return $this->executeMethod($instance, $procedure, $params);
+ }
+ }
+
+ throw new BadFunctionCallException('Unable to find the procedure');
+ }
+
+ /**
+ * Execute a callback
+ *
+ * @access public
+ * @param Closure $callback Callback
+ * @param array $params Procedure params
+ * @return mixed
+ */
+ public function executeCallback(Closure $callback, $params)
+ {
+ $reflection = new ReflectionFunction($callback);
+
+ $arguments = $this->getArguments(
+ $params,
+ $reflection->getParameters(),
+ $reflection->getNumberOfRequiredParameters(),
+ $reflection->getNumberOfParameters()
+ );
+
+ return $reflection->invokeArgs($arguments);
+ }
+
+ /**
+ * Execute a method
+ *
+ * @access public
+ * @param mixed $class Class name or instance
+ * @param string $method Method name
+ * @param array $params Procedure params
+ * @return mixed
+ */
+ public function executeMethod($class, $method, $params)
+ {
+ $instance = is_string($class) ? new $class : $class;
+ $reflection = new ReflectionMethod($class, $method);
+
+ $this->executeBeforeMethod($instance, $method);
+
+ $arguments = $this->getArguments(
+ $params,
+ $reflection->getParameters(),
+ $reflection->getNumberOfRequiredParameters(),
+ $reflection->getNumberOfParameters()
+ );
+
+ return $reflection->invokeArgs($instance, $arguments);
+ }
+
+ /**
+ * Execute before method if defined
+ *
+ * @access public
+ * @param mixed $object
+ * @param string $method
+ */
+ public function executeBeforeMethod($object, $method)
+ {
+ if ($this->beforeMethodName !== '' && method_exists($object, $this->beforeMethodName)) {
+ call_user_func_array(array($object, $this->beforeMethodName), array($method));
+ }
+ }
+
+ /**
+ * Get procedure arguments
+ *
+ * @access public
+ * @param array $requestParams Incoming arguments
+ * @param array $methodParams Procedure arguments
+ * @param integer $nbRequiredParams Number of required parameters
+ * @param integer $nbMaxParams Maximum number of parameters
+ * @return array
+ */
+ public function getArguments(array $requestParams, array $methodParams, $nbRequiredParams, $nbMaxParams)
+ {
+ $nbParams = count($requestParams);
+
+ if ($nbParams < $nbRequiredParams) {
+ throw new InvalidArgumentException('Wrong number of arguments');
+ }
+
+ if ($nbParams > $nbMaxParams) {
+ throw new InvalidArgumentException('Too many arguments');
+ }
+
+ if ($this->isPositionalArguments($requestParams)) {
+ return $requestParams;
+ }
+
+ return $this->getNamedArguments($requestParams, $methodParams);
+ }
+
+ /**
+ * Return true if we have positional parameters
+ *
+ * @access public
+ * @param array $request_params Incoming arguments
+ * @return bool
+ */
+ public function isPositionalArguments(array $request_params)
+ {
+ return array_keys($request_params) === range(0, count($request_params) - 1);
+ }
+
+ /**
+ * Get named arguments
+ *
+ * @access public
+ * @param array $requestParams Incoming arguments
+ * @param array $methodParams Procedure arguments
+ * @return array
+ */
+ public function getNamedArguments(array $requestParams, array $methodParams)
+ {
+ $params = array();
+
+ foreach ($methodParams as $p) {
+ $name = $p->getName();
+
+ if (array_key_exists($name, $requestParams)) {
+ $params[$name] = $requestParams[$name];
+ } elseif ($p->isDefaultValueAvailable()) {
+ $params[$name] = $p->getDefaultValue();
+ } else {
+ throw new InvalidArgumentException('Missing argument: '.$name);
+ }
+ }
+
+ return $params;
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Request/BatchRequestParser.php b/libs/jsonrpc/src/JsonRPC/Request/BatchRequestParser.php
new file mode 100644
index 00000000..c0fc776e
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Request/BatchRequestParser.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace JsonRPC\Request;
+
+/**
+ * Class BatchRequestParser
+ *
+ * @package JsonRPC\Request
+ * @author Frederic Guillot
+ */
+class BatchRequestParser extends RequestParser
+{
+ /**
+ * Parse incoming request
+ *
+ * @access public
+ * @return string
+ */
+ public function parse()
+ {
+ $responses = array();
+
+ foreach ($this->payload as $payload) {
+ $responses[] = RequestParser::create()
+ ->withPayload($payload)
+ ->withProcedureHandler($this->procedureHandler)
+ ->withMiddlewareHandler($this->middlewareHandler)
+ ->withLocalException($this->localExceptions)
+ ->parse();
+ }
+
+ $responses = array_filter($responses);
+ return empty($responses) ? '' : '['.implode(',', $responses).']';
+ }
+
+ /**
+ * Return true if we have a batch request
+ *
+ * ex : [
+ * 0 => '...',
+ * 1 => '...',
+ * 2 => '...',
+ * 3 => '...',
+ * ]
+ *
+ * @static
+ * @access public
+ * @param array $payload
+ * @return bool
+ */
+ public static function isBatchRequest(array $payload)
+ {
+ return array_keys($payload) === range(0, count($payload) - 1);
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Request/RequestBuilder.php b/libs/jsonrpc/src/JsonRPC/Request/RequestBuilder.php
new file mode 100644
index 00000000..145d21c1
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Request/RequestBuilder.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace JsonRPC\Request;
+
+/**
+ * Class RequestBuilder
+ *
+ * @package JsonRPC\Request
+ * @author Frederic Guillot
+ */
+class RequestBuilder
+{
+ /**
+ * Request ID
+ *
+ * @access private
+ * @var mixed
+ */
+ private $id = null;
+
+ /**
+ * Method name
+ *
+ * @access private
+ * @var string
+ */
+ private $procedure = '';
+
+ /**
+ * Method arguments
+ *
+ * @access private
+ * @var array
+ */
+ private $params = array();
+
+ /**
+ * Additional request attributes
+ *
+ * @access private
+ * @var array
+ */
+ private $reqattrs = array();
+
+ /**
+ * Get new object instance
+ *
+ * @static
+ * @access public
+ * @return RequestBuilder
+ */
+ public static function create()
+ {
+ return new static();
+ }
+
+ /**
+ * Set id
+ *
+ * @access public
+ * @param null $id
+ * @return RequestBuilder
+ */
+ public function withId($id)
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * Set method
+ *
+ * @access public
+ * @param string $procedure
+ * @return RequestBuilder
+ */
+ public function withProcedure($procedure)
+ {
+ $this->procedure = $procedure;
+ return $this;
+ }
+
+ /**
+ * Set parameters
+ *
+ * @access public
+ * @param array $params
+ * @return RequestBuilder
+ */
+ public function withParams(array $params)
+ {
+ $this->params = $params;
+ return $this;
+ }
+
+ /**
+ * Set additional request attributes
+ *
+ * @access public
+ * @param array $reqattrs
+ * @return RequestBuilder
+ */
+ public function withRequestAttributes(array $reqattrs)
+ {
+ $this->reqattrs = $reqattrs;
+ return $this;
+ }
+
+ /**
+ * Build the payload
+ *
+ * @access public
+ * @return string
+ */
+ public function build()
+ {
+ $payload = array_merge_recursive($this->reqattrs, array(
+ 'jsonrpc' => '2.0',
+ 'method' => $this->procedure,
+ 'id' => $this->id ?: mt_rand(),
+ ));
+
+ if (! empty($this->params)) {
+ $payload['params'] = $this->params;
+ }
+
+ return json_encode($payload);
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Request/RequestParser.php b/libs/jsonrpc/src/JsonRPC/Request/RequestParser.php
new file mode 100644
index 00000000..ea1b7d43
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Request/RequestParser.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace JsonRPC\Request;
+
+use Exception;
+use JsonRPC\Exception\AccessDeniedException;
+use JsonRPC\Exception\AuthenticationFailureException;
+use JsonRPC\Exception\InvalidJsonRpcFormatException;
+use JsonRPC\MiddlewareHandler;
+use JsonRPC\ProcedureHandler;
+use JsonRPC\Response\ResponseBuilder;
+use JsonRPC\Validator\JsonFormatValidator;
+use JsonRPC\Validator\RpcFormatValidator;
+
+/**
+ * Class RequestParser
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+class RequestParser
+{
+ /**
+ * Request payload
+ *
+ * @access protected
+ * @var mixed
+ */
+ protected $payload;
+
+ /**
+ * List of exceptions that should not be relayed to the client
+ *
+ * @access protected
+ * @var array()
+ */
+ protected $localExceptions = array(
+ 'JsonRPC\Exception\AuthenticationFailureException',
+ 'JsonRPC\Exception\AccessDeniedException',
+ );
+
+ /**
+ * ProcedureHandler
+ *
+ * @access protected
+ * @var ProcedureHandler
+ */
+ protected $procedureHandler;
+
+ /**
+ * MiddlewareHandler
+ *
+ * @access protected
+ * @var MiddlewareHandler
+ */
+ protected $middlewareHandler;
+
+ /**
+ * Get new object instance
+ *
+ * @static
+ * @access public
+ * @return RequestParser
+ */
+ public static function create()
+ {
+ return new static();
+ }
+
+ /**
+ * Set payload
+ *
+ * @access public
+ * @param mixed $payload
+ * @return $this
+ */
+ public function withPayload($payload)
+ {
+ $this->payload = $payload;
+ return $this;
+ }
+
+ /**
+ * Exception classes that should not be relayed to the client
+ *
+ * @access public
+ * @param mixed $exception
+ * @return $this
+ */
+ public function withLocalException($exception)
+ {
+ if (is_array($exception)) {
+ $this->localExceptions = array_merge($this->localExceptions, $exception);
+ } else {
+ $this->localExceptions[] = $exception;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set procedure handler
+ *
+ * @access public
+ * @param ProcedureHandler $procedureHandler
+ * @return $this
+ */
+ public function withProcedureHandler(ProcedureHandler $procedureHandler)
+ {
+ $this->procedureHandler = $procedureHandler;
+ return $this;
+ }
+
+ /**
+ * Set middleware handler
+ *
+ * @access public
+ * @param MiddlewareHandler $middlewareHandler
+ * @return $this
+ */
+ public function withMiddlewareHandler(MiddlewareHandler $middlewareHandler)
+ {
+ $this->middlewareHandler = $middlewareHandler;
+ return $this;
+ }
+
+ /**
+ * Parse incoming request
+ *
+ * @access public
+ * @return string
+ * @throws AccessDeniedException
+ * @throws AuthenticationFailureException
+ */
+ public function parse()
+ {
+ try {
+
+ JsonFormatValidator::validate($this->payload);
+ RpcFormatValidator::validate($this->payload);
+
+ $this->middlewareHandler
+ ->withProcedure($this->payload['method'])
+ ->execute();
+
+ $result = $this->procedureHandler->executeProcedure(
+ $this->payload['method'],
+ empty($this->payload['params']) ? array() : $this->payload['params']
+ );
+
+ if (! $this->isNotification()) {
+ return ResponseBuilder::create()
+ ->withId($this->payload['id'])
+ ->withResult($result)
+ ->build();
+ }
+ } catch (Exception $e) {
+ return $this->handleExceptions($e);
+ }
+
+ return '';
+ }
+
+ /**
+ * Handle exceptions
+ *
+ * @access protected
+ * @param Exception $e
+ * @return string
+ * @throws Exception
+ */
+ protected function handleExceptions(Exception $e)
+ {
+ foreach ($this->localExceptions as $exception) {
+ if ($e instanceof $exception) {
+ throw $e;
+ }
+ }
+
+ if ($e instanceof InvalidJsonRpcFormatException || ! $this->isNotification()) {
+ return ResponseBuilder::create()
+ ->withId(isset($this->payload['id']) ? $this->payload['id'] : null)
+ ->withException($e)
+ ->build();
+ }
+
+ return '';
+ }
+
+ /**
+ * Return true if the message is a notification
+ *
+ * @access protected
+ * @return bool
+ */
+ protected function isNotification()
+ {
+ return is_array($this->payload) && !isset($this->payload['id']);
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Response/ResponseBuilder.php b/libs/jsonrpc/src/JsonRPC/Response/ResponseBuilder.php
new file mode 100644
index 00000000..a0348ce3
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Response/ResponseBuilder.php
@@ -0,0 +1,336 @@
+<?php
+
+namespace JsonRPC\Response;
+
+use BadFunctionCallException;
+use Exception;
+use InvalidArgumentException;
+use JsonRPC\Exception\AccessDeniedException;
+use JsonRPC\Exception\AuthenticationFailureException;
+use JsonRPC\Exception\InvalidJsonFormatException;
+use JsonRPC\Exception\InvalidJsonRpcFormatException;
+use JsonRPC\Exception\ResponseEncodingFailureException;
+use JsonRPC\Exception\ResponseException;
+use JsonRPC\Validator\JsonEncodingValidator;
+
+/**
+ * Class ResponseBuilder
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+class ResponseBuilder
+{
+ /**
+ * Payload ID
+ *
+ * @access protected
+ * @var mixed
+ */
+ protected $id;
+
+ /**
+ * Payload ID
+ *
+ * @access protected
+ * @var mixed
+ */
+ protected $result;
+
+ /**
+ * Payload error code
+ *
+ * @access protected
+ * @var integer
+ */
+ protected $errorCode;
+
+ /**
+ * Payload error message
+ *
+ * @access private
+ * @var string
+ */
+ protected $errorMessage;
+
+ /**
+ * Payload error data
+ *
+ * @access protected
+ * @var mixed
+ */
+ protected $errorData;
+
+ /**
+ * HTTP Headers
+ *
+ * @access protected
+ * @var array
+ */
+ protected $headers = array(
+ 'Content-Type' => 'application/json',
+ );
+
+ /**
+ * HTTP status
+ *
+ * @access protected
+ * @var string
+ */
+ protected $status;
+
+ /**
+ * Exception
+ *
+ * @access protected
+ * @var ResponseException
+ */
+ protected $exception;
+
+ /**
+ * Get new object instance
+ *
+ * @static
+ * @access public
+ * @return ResponseBuilder
+ */
+ public static function create()
+ {
+ return new static();
+ }
+
+ /**
+ * Set id
+ *
+ * @access public
+ * @param mixed $id
+ * @return $this
+ */
+ public function withId($id)
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * Set result
+ *
+ * @access public
+ * @param mixed $result
+ * @return $this
+ */
+ public function withResult($result)
+ {
+ $this->result = $result;
+ return $this;
+ }
+
+ /**
+ * Set error
+ *
+ * @access public
+ * @param integer $code
+ * @param string $message
+ * @param string $data
+ * @return $this
+ */
+ public function withError($code, $message, $data = '')
+ {
+ $this->errorCode = $code;
+ $this->errorMessage = $message;
+ $this->errorData = $data;
+ return $this;
+ }
+
+ /**
+ * Set exception
+ *
+ * @access public
+ * @param Exception $exception
+ * @return $this
+ */
+ public function withException(Exception $exception)
+ {
+ $this->exception = $exception;
+ return $this;
+ }
+
+ /**
+ * Add HTTP header
+ *
+ * @access public
+ * @param string $name
+ * @param string $value
+ * @return $this
+ */
+ public function withHeader($name, $value)
+ {
+ $this->headers[$name] = $value;
+ return $this;
+ }
+
+ /**
+ * Add HTTP Status
+ *
+ * @access public
+ * @param string $status
+ * @return $this
+ */
+ public function withStatus($status)
+ {
+ $this->status = $status;
+ return $this;
+ }
+
+ /**
+ * Get status
+ *
+ * @access public
+ * @return string
+ */
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ /**
+ * Get headers
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+
+ /**
+ * Build response
+ *
+ * @access public
+ * @return string
+ */
+ public function build()
+ {
+ $options = 0;
+ if (defined('JSON_UNESCAPED_SLASHES')) {
+ $options |= JSON_UNESCAPED_SLASHES;
+ }
+ if (defined('JSON_UNESCAPED_UNICODE')) {
+ $options |= JSON_UNESCAPED_UNICODE;
+ }
+ $encodedResponse = json_encode($this->buildResponse(), $options);
+ JsonEncodingValidator::validate();
+
+ return $encodedResponse;
+ }
+
+ /**
+ * Send HTTP headers
+ *
+ * @access public
+ * @return $this
+ */
+ public function sendHeaders()
+ {
+ if (! empty($this->status)) {
+ header($this->status);
+ }
+
+ foreach ($this->headers as $name => $value) {
+ header($name.': '.$value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Build response payload
+ *
+ * @access protected
+ * @return array
+ */
+ protected function buildResponse()
+ {
+ $response = array('jsonrpc' => '2.0');
+ $this->handleExceptions();
+
+ if (! empty($this->errorMessage)) {
+ $response['error'] = $this->buildErrorResponse();
+ } else {
+ $response['result'] = $this->result;
+ }
+
+ $response['id'] = $this->id;
+ return $response;
+ }
+
+ /**
+ * Build response error payload
+ *
+ * @access protected
+ * @return array
+ */
+ protected function buildErrorResponse()
+ {
+ $response = array(
+ 'code' => $this->errorCode,
+ 'message' => $this->errorMessage,
+ );
+
+ if (! empty($this->errorData)) {
+ $response['data'] = $this->errorData;
+ }
+
+ return $response;
+ }
+
+ /**
+ * Transform exceptions to JSON-RPC errors
+ *
+ * @access protected
+ */
+ protected function handleExceptions()
+ {
+ try {
+ if ($this->exception instanceof Exception) {
+ throw $this->exception;
+ }
+ } catch (InvalidJsonFormatException $e) {
+ $this->errorCode = -32700;
+ $this->errorMessage = 'Parse error';
+ $this->id = null;
+ } catch (InvalidJsonRpcFormatException $e) {
+ $this->errorCode = -32600;
+ $this->errorMessage = 'Invalid Request';
+ $this->id = null;
+ } catch (BadFunctionCallException $e) {
+ $this->errorCode = -32601;
+ $this->errorMessage = 'Method not found';
+ } catch (InvalidArgumentException $e) {
+ $this->errorCode = -32602;
+ $this->errorMessage = 'Invalid params';
+ $this->errorData = $this->exception->getMessage();
+ } catch (ResponseEncodingFailureException $e) {
+ $this->errorCode = -32603;
+ $this->errorMessage = 'Internal error';
+ $this->errorData = $this->exception->getMessage();
+ } catch (AuthenticationFailureException $e) {
+ $this->errorCode = 401;
+ $this->errorMessage = 'Unauthorized';
+ $this->status = 'HTTP/1.0 401 Unauthorized';
+ $this->withHeader('WWW-Authenticate', 'Basic realm="JsonRPC"');
+ } catch (AccessDeniedException $e) {
+ $this->errorCode = 403;
+ $this->errorMessage = 'Forbidden';
+ $this->status = 'HTTP/1.0 403 Forbidden';
+ } catch (ResponseException $e) {
+ $this->errorCode = $this->exception->getCode();
+ $this->errorMessage = $this->exception->getMessage();
+ $this->errorData = $this->exception->getData();
+ } catch (Exception $e) {
+ $this->errorCode = $this->exception->getCode();
+ $this->errorMessage = $this->exception->getMessage();
+ }
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Response/ResponseParser.php b/libs/jsonrpc/src/JsonRPC/Response/ResponseParser.php
new file mode 100644
index 00000000..02d449ba
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Response/ResponseParser.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace JsonRPC\Response;
+
+use BadFunctionCallException;
+use InvalidArgumentException;
+use Exception;
+use JsonRPC\Exception\InvalidJsonFormatException;
+use JsonRPC\Exception\InvalidJsonRpcFormatException;
+use JsonRPC\Exception\ResponseException;
+use JsonRPC\Validator\JsonFormatValidator;
+
+/**
+ * Class ResponseParser
+ *
+ * @package JsonRPC\Request
+ * @author Frederic Guillot
+ */
+class ResponseParser
+{
+ /**
+ * Payload
+ *
+ * @access private
+ * @var mixed
+ */
+ private $payload;
+
+ /**
+ * Do not immediately throw an exception on error. Return it instead.
+ *
+ * @var bool
+ */
+ private $returnException = false;
+
+ /**
+ * Get new object instance
+ *
+ * @static
+ * @access public
+ * @return ResponseParser
+ */
+ public static function create()
+ {
+ return new static();
+ }
+
+ /**
+ * Set Return Exception Or Throw It
+ *
+ * @param $returnException
+ * @return ResponseParser
+ */
+ public function withReturnException($returnException)
+ {
+ $this->returnException = $returnException;
+ return $this;
+ }
+
+ /**
+ * Set payload
+ *
+ * @access public
+ * @param mixed $payload
+ * @return $this
+ */
+ public function withPayload($payload)
+ {
+ $this->payload = $payload;
+ return $this;
+ }
+
+ /**
+ * Parse response
+ *
+ * @return array|Exception|null
+ * @throws InvalidJsonFormatException
+ * @throws BadFunctionCallException
+ * @throws InvalidJsonRpcFormatException
+ * @throws InvalidArgumentException
+ * @throws Exception
+ * @throws ResponseException
+ */
+ public function parse()
+ {
+ JsonFormatValidator::validate($this->payload);
+
+ if ($this->isBatchResponse()) {
+ $results = array();
+
+ foreach ($this->payload as $response) {
+ $results[] = self::create()
+ ->withReturnException($this->returnException)
+ ->withPayload($response)
+ ->parse();
+ }
+
+ return $results;
+ }
+
+ if (isset($this->payload['error']['code'])) {
+ try {
+ $this->handleExceptions();
+ } catch (Exception $e) {
+ if ($this->returnException) {
+ return $e;
+ }
+ throw $e;
+ }
+ }
+
+ return isset($this->payload['result']) ? $this->payload['result'] : null;
+ }
+
+ /**
+ * Handle exceptions
+ *
+ * @access private
+ * @throws InvalidJsonFormatException
+ * @throws InvalidJsonRpcFormatException
+ * @throws ResponseException
+ */
+ private function handleExceptions()
+ {
+ switch ($this->payload['error']['code']) {
+ case -32700:
+ throw new InvalidJsonFormatException('Parse error: '.$this->payload['error']['message']);
+ case -32600:
+ throw new InvalidJsonRpcFormatException('Invalid Request: '.$this->payload['error']['message']);
+ case -32601:
+ throw new BadFunctionCallException('Procedure not found: '.$this->payload['error']['message']);
+ case -32602:
+ throw new InvalidArgumentException('Invalid arguments: '.$this->payload['error']['message']);
+ default:
+ throw new ResponseException(
+ $this->payload['error']['message'],
+ $this->payload['error']['code'],
+ null,
+ isset($this->payload['error']['data']) ? $this->payload['error']['data'] : null
+ );
+ }
+ }
+
+ /**
+ * Return true if we have a batch response
+ *
+ * @access private
+ * @return boolean
+ */
+ private function isBatchResponse()
+ {
+ return array_keys($this->payload) === range(0, count($this->payload) - 1);
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Server.php b/libs/jsonrpc/src/JsonRPC/Server.php
new file mode 100644
index 00000000..1ed075a4
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Server.php
@@ -0,0 +1,386 @@
+<?php
+
+namespace JsonRPC;
+
+use Closure;
+use Exception;
+use JsonRPC\Request\BatchRequestParser;
+use JsonRPC\Request\RequestParser;
+use JsonRPC\Response\ResponseBuilder;
+use JsonRPC\Validator\HostValidator;
+use JsonRPC\Validator\JsonFormatValidator;
+use JsonRPC\Validator\UserValidator;
+
+/**
+ * JsonRPC server class
+ *
+ * @package JsonRPC
+ * @author Frederic Guillot
+ */
+class Server
+{
+ /**
+ * Allowed hosts
+ *
+ * @access protected
+ * @var array
+ */
+ protected $hosts = array();
+
+ /**
+ * Data received from the client
+ *
+ * @access protected
+ * @var array
+ */
+ protected $payload = array();
+
+ /**
+ * List of exceptions that should not be relayed to the client
+ *
+ * @access protected
+ * @var array()
+ */
+ protected $localExceptions = array();
+
+ /**
+ * Username
+ *
+ * @access protected
+ * @var string
+ */
+ protected $username = '';
+
+ /**
+ * Password
+ *
+ * @access protected
+ * @var string
+ */
+ protected $password = '';
+
+ /**
+ * Allowed users
+ *
+ * @access protected
+ * @var array
+ */
+ protected $users = array();
+
+ /**
+ * $_SERVER
+ *
+ * @access protected
+ * @var array
+ */
+ protected $serverVariable;
+
+ /**
+ * ProcedureHandler object
+ *
+ * @access protected
+ * @var ProcedureHandler
+ */
+ protected $procedureHandler;
+
+ /**
+ * MiddlewareHandler object
+ *
+ * @access protected
+ * @var MiddlewareHandler
+ */
+ protected $middlewareHandler;
+
+ /**
+ * Response builder
+ *
+ * @access protected
+ * @var ResponseBuilder
+ */
+ protected $responseBuilder;
+
+ /**
+ * Response builder
+ *
+ * @access protected
+ * @var RequestParser
+ */
+ protected $requestParser;
+
+ /**
+ *
+ * Batch request parser
+ *
+ * @access protected
+ * @var BatchRequestParser
+ */
+ protected $batchRequestParser;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $request
+ * @param array $server
+ * @param ResponseBuilder $responseBuilder
+ * @param RequestParser $requestParser
+ * @param BatchRequestParser $batchRequestParser
+ * @param ProcedureHandler $procedureHandler
+ * @param MiddlewareHandler $middlewareHandler
+ */
+ public function __construct(
+ $request = '',
+ array $server = array(),
+ ResponseBuilder $responseBuilder = null,
+ RequestParser $requestParser = null,
+ BatchRequestParser $batchRequestParser = null,
+ ProcedureHandler $procedureHandler = null,
+ MiddlewareHandler $middlewareHandler = null
+ ) {
+ if ($request !== '') {
+ $this->payload = json_decode($request, true);
+ } else {
+ $this->payload = json_decode(file_get_contents('php://input'), true);
+ }
+
+ $this->serverVariable = $server ?: $_SERVER;
+ $this->responseBuilder = $responseBuilder ?: ResponseBuilder::create();
+ $this->requestParser = $requestParser ?: RequestParser::create();
+ $this->batchRequestParser = $batchRequestParser ?: BatchRequestParser::create();
+ $this->procedureHandler = $procedureHandler ?: new ProcedureHandler();
+ $this->middlewareHandler = $middlewareHandler ?: new MiddlewareHandler();
+ }
+
+ /**
+ * Define alternative authentication header
+ *
+ * @access public
+ * @param string $header Header name
+ * @return $this
+ */
+ public function setAuthenticationHeader($header)
+ {
+ if (! empty($header)) {
+ $header = 'HTTP_'.str_replace('-', '_', strtoupper($header));
+ $value = $this->getServerVariable($header);
+
+ if (! empty($value)) {
+ list($this->username, $this->password) = explode(':', base64_decode($value));
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get ProcedureHandler
+ *
+ * @access public
+ * @return ProcedureHandler
+ */
+ public function getProcedureHandler()
+ {
+ return $this->procedureHandler;
+ }
+
+ /**
+ * Get MiddlewareHandler
+ *
+ * @access public
+ * @return MiddlewareHandler
+ */
+ public function getMiddlewareHandler()
+ {
+ return $this->middlewareHandler;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username ?: $this->getServerVariable('PHP_AUTH_USER');
+ }
+
+ /**
+ * Get password
+ *
+ * @access public
+ * @return string
+ */
+ public function getPassword()
+ {
+ return $this->password ?: $this->getServerVariable('PHP_AUTH_PW');
+ }
+
+ /**
+ * IP based client restrictions
+ *
+ * @access public
+ * @param array $hosts List of hosts
+ * @return $this
+ */
+ public function allowHosts(array $hosts)
+ {
+ $this->hosts = $hosts;
+ return $this;
+ }
+
+ /**
+ * HTTP Basic authentication
+ *
+ * @access public
+ * @param array $users Dictionary of username/password
+ * @return $this
+ */
+ public function authentication(array $users)
+ {
+ $this->users = $users;
+ return $this;
+ }
+
+ /**
+ * Register a new procedure
+ *
+ * @access public
+ * @deprecated Use $server->getProcedureHandler()->withCallback($procedure, $callback)
+ * @param string $procedure Procedure name
+ * @param closure $callback Callback
+ * @return $this
+ */
+ public function register($procedure, Closure $callback)
+ {
+ $this->procedureHandler->withCallback($procedure, $callback);
+ return $this;
+ }
+
+ /**
+ * Bind a procedure to a class
+ *
+ * @access public
+ * @deprecated Use $server->getProcedureHandler()->withClassAndMethod($procedure, $class, $method);
+ * @param string $procedure Procedure name
+ * @param mixed $class Class name or instance
+ * @param string $method Procedure name
+ * @return $this
+ */
+ public function bind($procedure, $class, $method = '')
+ {
+ $this->procedureHandler->withClassAndMethod($procedure, $class, $method);
+ return $this;
+ }
+
+ /**
+ * Bind a class instance
+ *
+ * @access public
+ * @deprecated Use $server->getProcedureHandler()->withObject($instance);
+ * @param mixed $instance Instance name
+ * @return $this
+ */
+ public function attach($instance)
+ {
+ $this->procedureHandler->withObject($instance);
+ return $this;
+ }
+
+ /**
+ * Exception classes that should not be relayed to the client
+ *
+ * @access public
+ * @param Exception|string $exception
+ * @return $this
+ */
+ public function withLocalException($exception)
+ {
+ $this->localExceptions[] = $exception;
+ return $this;
+ }
+
+ /**
+ * Parse incoming requests
+ *
+ * @access public
+ * @return string
+ */
+ public function execute()
+ {
+ try {
+ JsonFormatValidator::validate($this->payload);
+ HostValidator::validate($this->hosts, $this->getServerVariable('REMOTE_ADDR'));
+ UserValidator::validate($this->users, $this->getUsername(), $this->getPassword());
+
+ $this->middlewareHandler
+ ->withUsername($this->getUsername())
+ ->withPassword($this->getPassword())
+ ;
+
+ $response = $this->parseRequest();
+
+ } catch (Exception $e) {
+ $response = $this->handleExceptions($e);
+ }
+
+ $this->responseBuilder->sendHeaders();
+ return $response;
+ }
+
+ /**
+ * Handle exceptions
+ *
+ * @access protected
+ * @param Exception $e
+ * @return string
+ * @throws Exception
+ */
+ protected function handleExceptions(Exception $e)
+ {
+ foreach ($this->localExceptions as $exception) {
+ if ($e instanceof $exception) {
+ throw $e;
+ }
+ }
+
+ return $this->responseBuilder->withException($e)->build();
+ }
+
+ /**
+ * Parse incoming request
+ *
+ * @access protected
+ * @return string
+ */
+ protected function parseRequest()
+ {
+ if (BatchRequestParser::isBatchRequest($this->payload)) {
+ return $this->batchRequestParser
+ ->withPayload($this->payload)
+ ->withProcedureHandler($this->procedureHandler)
+ ->withMiddlewareHandler($this->middlewareHandler)
+ ->withLocalException($this->localExceptions)
+ ->parse();
+ }
+
+ return $this->requestParser
+ ->withPayload($this->payload)
+ ->withProcedureHandler($this->procedureHandler)
+ ->withMiddlewareHandler($this->middlewareHandler)
+ ->withLocalException($this->localExceptions)
+ ->parse();
+ }
+
+ /**
+ * Check existence and get value of server variable
+ *
+ * @access protected
+ * @param string $variable
+ * @return string|null
+ */
+ protected function getServerVariable($variable)
+ {
+ return isset($this->serverVariable[$variable]) ? $this->serverVariable[$variable] : null;
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Validator/HostValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/HostValidator.php
new file mode 100644
index 00000000..3f9d6989
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Validator/HostValidator.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace JsonRPC\Validator;
+
+use JsonRPC\Exception\AccessDeniedException;
+
+/**
+ * Class HostValidator
+ *
+ * @package JsonRPC\Validator
+ * @author Frederic Guillot
+ */
+class HostValidator
+{
+ /**
+ * Validate
+ *
+ * @static
+ * @access public
+ * @param array $hosts
+ * @param string $remoteAddress
+ * @throws AccessDeniedException
+ */
+ public static function validate(array $hosts, $remoteAddress)
+ {
+ if (!empty($hosts)) {
+ foreach ($hosts as $host) {
+ if (self::ipMatch($remoteAddress, $host)) {
+ return;
+ }
+ }
+ throw new AccessDeniedException('Access Forbidden');
+ }
+ }
+
+ /**
+ * Validate remoteAddress match host
+ * @param $remoteAddress
+ * @param $host
+ * @return bool
+ */
+ public static function ipMatch($remoteAddress, $host)
+ {
+ $host = trim($host);
+ if (strpos($host, '/') !== false) {
+ list($network, $mask) = explode('/', $host);
+ if (self::netMatch($remoteAddress, $network, $mask)) {
+ return true;
+ }
+ }
+
+ if ($host === $remoteAddress) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * validate the ipAddress in network
+ * 192.168.1.1/24
+ * @param $clientIp
+ * @param $networkIp
+ * @param $mask
+ *
+ * @return bool
+ */
+ public static function netMatch($clientIp, $networkIp, $mask)
+ {
+ $mask1 = 32 - $mask;
+ return ((ip2long($clientIp) >> $mask1) == (ip2long($networkIp) >> $mask1));
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Validator/JsonEncodingValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/JsonEncodingValidator.php
new file mode 100644
index 00000000..0bbc4abd
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Validator/JsonEncodingValidator.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace JsonRPC\Validator;
+
+use JsonRPC\Exception\ResponseEncodingFailureException;
+
+/**
+ * Class JsonEncodingValidator
+ *
+ * @package JsonRPC\Validator
+ * @author Frederic Guillot
+ */
+class JsonEncodingValidator
+{
+ public static function validate()
+ {
+ $jsonError = json_last_error();
+
+ if ($jsonError !== JSON_ERROR_NONE) {
+ switch ($jsonError) {
+ case JSON_ERROR_DEPTH:
+ $errorMessage = 'Maximum stack depth exceeded';
+ break;
+ case JSON_ERROR_STATE_MISMATCH:
+ $errorMessage = 'Underflow or the modes mismatch';
+ break;
+ case JSON_ERROR_CTRL_CHAR:
+ $errorMessage = 'Unexpected control character found';
+ break;
+ case JSON_ERROR_SYNTAX:
+ $errorMessage = 'Syntax error, malformed JSON';
+ break;
+ case JSON_ERROR_UTF8:
+ $errorMessage = 'Malformed UTF-8 characters, possibly incorrectly encoded';
+ break;
+ default:
+ $errorMessage = 'Unknown error';
+ break;
+ }
+
+ throw new ResponseEncodingFailureException($errorMessage, $jsonError);
+ }
+ }
+}
diff --git a/libs/jsonrpc/src/JsonRPC/Validator/JsonFormatValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/JsonFormatValidator.php
new file mode 100644
index 00000000..ca8e7a69
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Validator/JsonFormatValidator.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace JsonRPC\Validator;
+
+use JsonRPC\Exception\InvalidJsonFormatException;
+
+/**
+ * Class JsonFormatValidator
+ *
+ * @package JsonRPC\Validator
+ * @author Frederic Guillot
+ */
+class JsonFormatValidator
+{
+ /**
+ * Validate
+ *
+ * @static
+ * @access public
+ * @param mixed $payload
+ * @throws InvalidJsonFormatException
+ */
+ public static function validate($payload)
+ {
+ if (! is_array($payload)) {
+ throw new InvalidJsonFormatException('Malformed payload');
+ }
+ }
+}
+
diff --git a/libs/jsonrpc/src/JsonRPC/Validator/RpcFormatValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/RpcFormatValidator.php
new file mode 100644
index 00000000..f253a5a1
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Validator/RpcFormatValidator.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace JsonRPC\Validator;
+
+use JsonRPC\Exception\InvalidJsonRpcFormatException;
+
+/**
+ * Class RpcFormatValidator
+ *
+ * @package JsonRPC\Validator
+ * @author Frederic Guillot
+ */
+class RpcFormatValidator
+{
+ /**
+ * Validate
+ *
+ * @static
+ * @access public
+ * @param array $payload
+ * @throws InvalidJsonRpcFormatException
+ */
+ public static function validate(array $payload)
+ {
+ if (! isset($payload['jsonrpc']) ||
+ ! isset($payload['method']) ||
+ ! is_string($payload['method']) ||
+ $payload['jsonrpc'] !== '2.0' ||
+ (isset($payload['params']) && ! is_array($payload['params']))) {
+
+ throw new InvalidJsonRpcFormatException('Invalid JSON RPC payload');
+ }
+ }
+}
+
diff --git a/libs/jsonrpc/src/JsonRPC/Validator/UserValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/UserValidator.php
new file mode 100644
index 00000000..4f889719
--- /dev/null
+++ b/libs/jsonrpc/src/JsonRPC/Validator/UserValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace JsonRPC\Validator;
+
+use JsonRPC\Exception\AuthenticationFailureException;
+
+/**
+ * Class UserValidator
+ *
+ * @package JsonRPC\Validator
+ * @author Frederic Guillot
+ */
+class UserValidator
+{
+ public static function validate(array $users, $username, $password)
+ {
+ if (! empty($users) && (! isset($users[$username]) || $users[$username] !== $password)) {
+ throw new AuthenticationFailureException('Access not allowed');
+ }
+ }
+}
diff --git a/libs/jsonrpc/tests/ClientTest.php b/libs/jsonrpc/tests/ClientTest.php
new file mode 100644
index 00000000..d1f83877
--- /dev/null
+++ b/libs/jsonrpc/tests/ClientTest.php
@@ -0,0 +1,103 @@
+<?php
+
+use JsonRPC\Client;
+
+require_once __DIR__.'/../../../vendor/autoload.php';
+
+class ClientTest extends PHPUnit_Framework_TestCase
+{
+ private $httpClient;
+
+ public function setUp()
+ {
+ $this->httpClient = $this
+ ->getMockBuilder('\JsonRPC\HttpClient')
+ ->setMethods(array('execute'))
+ ->getMock();
+ }
+
+ public function testSendBatch()
+ {
+ $client = new Client('', false, $this->httpClient);
+ $response = array(
+ array(
+ 'jsonrpc' => '2.0',
+ 'result' => 'c',
+ 'id' => 1,
+ ),
+ array(
+ 'jsonrpc' => '2.0',
+ 'result' => 'd',
+ 'id' => 2,
+ )
+ );
+
+ $this->httpClient
+ ->expects($this->once())
+ ->method('execute')
+ ->with($this->stringContains('[{"jsonrpc":"2.0","method":"methodA","id":'))
+ ->will($this->returnValue($response));
+
+
+ $result = $client->batch()
+ ->execute('methodA', array('a' => 'b'))
+ ->execute('methodB', array('a' => 'b'))
+ ->send();
+
+ $this->assertEquals(array('c', 'd'), $result);
+ }
+
+ public function testSendRequest()
+ {
+ $client = new Client('', false, $this->httpClient);
+
+ $this->httpClient
+ ->expects($this->once())
+ ->method('execute')
+ ->with($this->stringContains('{"jsonrpc":"2.0","method":"methodA","id":'))
+ ->will($this->returnValue(array('jsonrpc' => '2.0', 'result' => 'foobar', 'id' => 1)));
+
+ $result = $client->execute('methodA', array('a' => 'b'));
+ $this->assertEquals($result, 'foobar');
+ }
+
+ public function testSendRequestWithError()
+ {
+ $client = new Client('', false, $this->httpClient);
+
+ $this->httpClient
+ ->expects($this->once())
+ ->method('execute')
+ ->with($this->stringContains('{"jsonrpc":"2.0","method":"methodA","id":'))
+ ->will($this->returnValue(array(
+ 'jsonrpc' => '2.0',
+ 'error' => array(
+ 'code' => -32601,
+ 'message' => 'Method not found',
+ ),
+ )));
+
+ $this->setExpectedException('BadFunctionCallException');
+ $client->execute('methodA', array('a' => 'b'));
+ }
+
+ public function testSendRequestWithErrorAndReturnExceptionEnabled()
+ {
+ $client = new Client('', true, $this->httpClient);
+
+ $this->httpClient
+ ->expects($this->once())
+ ->method('execute')
+ ->with($this->stringContains('{"jsonrpc":"2.0","method":"methodA","id":'))
+ ->will($this->returnValue(array(
+ 'jsonrpc' => '2.0',
+ 'error' => array(
+ 'code' => -32601,
+ 'message' => 'Method not found',
+ ),
+ )));
+
+ $result = $client->execute('methodA', array('a' => 'b'));
+ $this->assertInstanceOf('BadFunctionCallException', $result);
+ }
+}
diff --git a/libs/jsonrpc/tests/HttpClientTest.php b/libs/jsonrpc/tests/HttpClientTest.php
new file mode 100644
index 00000000..71e6c8d0
--- /dev/null
+++ b/libs/jsonrpc/tests/HttpClientTest.php
@@ -0,0 +1,220 @@
+<?php
+
+namespace JsonRPC;
+
+require_once __DIR__.'/../../../vendor/autoload.php';
+
+defined('CURLOPT_URL') or define('CURLOPT_URL', 10002);
+defined('CURLOPT_RETURNTRANSFER') or define('CURLOPT_RETURNTRANSFER', 19913);
+defined('CURLOPT_CONNECTTIMEOUT') or define('CURLOPT_CONNECTTIMEOUT', 78);
+defined('CURLOPT_MAXREDIRS') or define('CURLOPT_MAXREDIRS', 68);
+defined('CURLOPT_SSL_VERIFYPEER') or define('CURLOPT_SSL_VERIFYPEER', 64);
+defined('CURLOPT_POST') or define('CURLOPT_POST', 47);
+defined('CURLOPT_POSTFIELDS') or define('CURLOPT_POSTFIELDS', 10015);
+defined('CURLOPT_HTTPHEADER') or define('CURLOPT_HTTPHEADER', 10023);
+defined('CURLOPT_HEADERFUNCTION') or define('CURLOPT_HEADERFUNCTION', 20079);
+defined('CURLOPT_CAINFO') or define('CURLOPT_CAINFO', 10065);
+
+function extension_loaded($extension) {
+ return HttpClientTest::$functions->extension_loaded($extension);
+}
+
+function fopen($url, $mode, $use_include_path, $context)
+{
+ return HttpClientTest::$functions->fopen($url, $mode, $use_include_path, $context);
+}
+
+function stream_context_create(array $params)
+{
+ return HttpClientTest::$functions->stream_context_create($params);
+}
+
+function curl_init() {
+ return HttpClientTest::$functions->curl_init();
+}
+
+function curl_setopt_array($ch, array $params) {
+ HttpClientTest::$functions->curl_setopt_array($ch, $params);
+}
+
+function curl_setopt($ch, $option, $value) {
+ HttpClientTest::$functions->curl_setopt($ch, $option, $value);
+}
+
+function curl_exec($ch) {
+ return HttpClientTest::$functions->curl_exec($ch);
+}
+
+function curl_close($ch) {
+ HttpClientTest::$functions->curl_close($ch);
+}
+
+class HttpClientTest extends \PHPUnit_Framework_TestCase
+{
+ public static $functions;
+
+ public function setUp()
+ {
+ self::$functions = $this
+ ->getMockBuilder('stdClass')
+ ->setMethods(array('extension_loaded', 'fopen', 'stream_context_create',
+ 'curl_init', 'curl_setopt_array', 'curl_setopt', 'curl_exec', 'curl_close'))
+ ->getMock();
+ }
+
+ public function testWithServerError()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\ServerErrorException');
+
+ $httpClient = new HttpClient();
+ $httpClient->handleExceptions(array(
+ 'HTTP/1.0 301 Moved Permanently',
+ 'Connection: close',
+ 'HTTP/1.1 500 Internal Server Error',
+ ));
+ }
+
+ public function testWithConnectionFailure()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\ConnectionFailureException');
+
+ $httpClient = new HttpClient();
+ $httpClient->handleExceptions(array(
+ 'HTTP/1.1 404 Not Found',
+ ));
+ }
+
+ public function testWithAccessForbidden()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\AccessDeniedException');
+
+ $httpClient = new HttpClient();
+ $httpClient->handleExceptions(array(
+ 'HTTP/1.1 403 Forbidden',
+ ));
+ }
+
+ public function testWithAccessNotAllowed()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\AccessDeniedException');
+
+ $httpClient = new HttpClient();
+ $httpClient->handleExceptions(array(
+ 'HTTP/1.0 401 Unauthorized',
+ ));
+ }
+
+ public function testWithCallback()
+ {
+ self::$functions
+ ->expects($this->at(0))
+ ->method('extension_loaded')
+ ->with('curl')
+ ->will($this->returnValue(false));
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('stream_context_create')
+ ->with(array(
+ 'http' => array(
+ 'method' => 'POST',
+ 'protocol_version' => 1.1,
+ 'timeout' => 5,
+ 'max_redirects' => 2,
+ 'header' => implode("\r\n", array(
+ 'User-Agent: JSON-RPC PHP Client <https://github.com/fguillot/JsonRPC>',
+ 'Content-Type: application/json',
+ 'Accept: application/json',
+ 'Connection: close',
+ 'Content-Length: 4',
+ )),
+ 'content' => 'test',
+ 'ignore_errors' => true,
+ ),
+ 'ssl' => array(
+ 'verify_peer' => true,
+ 'verify_peer_name' => true,
+ )
+ ))
+ ->will($this->returnValue('context'));
+
+ self::$functions
+ ->expects($this->at(2))
+ ->method('fopen')
+ ->with('url', 'r', false, 'context')
+ ->will($this->returnValue(false));
+
+ $httpClient = new HttpClient('url');
+ $httpClient->withBeforeRequestCallback(function(HttpClient $client, $payload) {
+ $client->withHeaders(array('Content-Length: '.strlen($payload)));
+ });
+
+ $this->setExpectedException('\JsonRPC\Exception\ConnectionFailureException');
+ $httpClient->execute('test');
+ }
+
+ public function testWithCurl()
+ {
+ self::$functions
+ ->expects($this->at(0))
+ ->method('extension_loaded')
+ ->with('curl')
+ ->will($this->returnValue(true));
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('curl_init')
+ ->will($this->returnValue('curl'));
+
+ self::$functions
+ ->expects($this->at(2))
+ ->method('curl_setopt_array')
+ ->with('curl', array(
+ CURLOPT_URL => 'url',
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => 5,
+ CURLOPT_MAXREDIRS => 2,
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => 'test',
+ CURLOPT_HTTPHEADER => array(
+ 'User-Agent: JSON-RPC PHP Client <https://github.com/fguillot/JsonRPC>',
+ 'Content-Type: application/json',
+ 'Accept: application/json',
+ 'Connection: close',
+ 'Content-Length: 4',
+ ),
+ CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$headers) {
+ $headers[] = $header;
+ return strlen($header);
+ }
+ ));
+
+ self::$functions
+ ->expects($this->at(3))
+ ->method('curl_setopt')
+ ->with('curl', CURLOPT_CAINFO, 'test.crt');
+
+ self::$functions
+ ->expects($this->at(4))
+ ->method('curl_exec')
+ ->with('curl')
+ ->will($this->returnValue(false));
+
+ self::$functions
+ ->expects($this->at(5))
+ ->method('curl_close')
+ ->with('curl');
+
+ $httpClient = new HttpClient('url');
+ $httpClient
+ ->withSslLocalCert('test.crt')
+ ->withBeforeRequestCallback(function(HttpClient $client, $payload) {
+ $client->withHeaders(array('Content-Length: '.strlen($payload)));
+ });
+
+
+ $this->setExpectedException('\JsonRPC\Exception\ConnectionFailureException');
+ $httpClient->execute('test');
+ }
+}
diff --git a/libs/jsonrpc/tests/MiddlewareHandlerTest.php b/libs/jsonrpc/tests/MiddlewareHandlerTest.php
new file mode 100644
index 00000000..be70cbf7
--- /dev/null
+++ b/libs/jsonrpc/tests/MiddlewareHandlerTest.php
@@ -0,0 +1,40 @@
+<?php
+
+use JsonRPC\Exception\AuthenticationFailureException;
+use JsonRPC\MiddlewareHandler;
+use JsonRPC\MiddlewareInterface;
+
+require_once __DIR__.'/../../../vendor/autoload.php';
+
+class FirstMiddleware implements MiddlewareInterface
+{
+ public function execute($username, $password, $procedureName)
+ {
+ }
+}
+
+class SecondMiddleware implements MiddlewareInterface
+{
+ public function execute($username, $password, $procedureName)
+ {
+ if ($username === 'myUsername' && $password === 'myPassword' && $procedureName === 'myProcedure') {
+ throw new AuthenticationFailureException('Bad user');
+ }
+ }
+}
+
+class MiddlewareHandlerTest extends PHPUnit_Framework_TestCase
+{
+ public function testMiddlewareCanRaiseException()
+ {
+ $this->setExpectedException('JsonRpc\Exception\AuthenticationFailureException');
+
+ $middlewareHandler = new MiddlewareHandler();
+ $middlewareHandler->withUsername('myUsername');
+ $middlewareHandler->withPassword('myPassword');
+ $middlewareHandler->withProcedure('myProcedure');
+ $middlewareHandler->withMiddleware(new FirstMiddleware());
+ $middlewareHandler->withMiddleware(new SecondMiddleware());
+ $middlewareHandler->execute();
+ }
+}
diff --git a/libs/jsonrpc/tests/ProcedureHandlerTest.php b/libs/jsonrpc/tests/ProcedureHandlerTest.php
new file mode 100644
index 00000000..983016c5
--- /dev/null
+++ b/libs/jsonrpc/tests/ProcedureHandlerTest.php
@@ -0,0 +1,153 @@
+<?php
+
+use JsonRPC\ProcedureHandler;
+
+require_once __DIR__.'/../../../vendor/autoload.php';
+
+class A
+{
+ public function getAll($p1, $p2, $p3 = 4)
+ {
+ return $p1 + $p2 + $p3;
+ }
+}
+
+class B
+{
+ public function getAll($p1)
+ {
+ return $p1 + 2;
+ }
+}
+
+class ClassWithBeforeMethod
+{
+ private $foobar = '';
+
+ public function before($procedure)
+ {
+ $this->foobar = $procedure;
+ }
+
+ public function myProcedure()
+ {
+ return $this->foobar;
+ }
+}
+
+class ProcedureHandlerTest extends PHPUnit_Framework_TestCase
+{
+ public function testProcedureNotFound()
+ {
+ $this->setExpectedException('BadFunctionCallException');
+ $handler = new ProcedureHandler;
+ $handler->executeProcedure('a');
+ }
+
+ public function testCallbackNotFound()
+ {
+ $this->setExpectedException('BadFunctionCallException');
+ $handler = new ProcedureHandler;
+ $handler->withCallback('b', function() {});
+ $handler->executeProcedure('a');
+ }
+
+ public function testClassNotFound()
+ {
+ $this->setExpectedException('BadFunctionCallException');
+ $handler = new ProcedureHandler;
+ $handler->withClassAndMethod('getAllTasks', 'c', 'getAll');
+ $handler->executeProcedure('getAllTasks');
+ }
+
+ public function testMethodNotFound()
+ {
+ $this->setExpectedException('BadFunctionCallException');
+ $handler = new ProcedureHandler;
+ $handler->withClassAndMethod('getAllTasks', 'A', 'getNothing');
+ $handler->executeProcedure('getAllTasks');
+ }
+
+ public function testIsPositionalArguments()
+ {
+ $handler = new ProcedureHandler;
+ $this->assertFalse($handler->isPositionalArguments(
+ array('a' => 'b', 'c' => 'd')
+ ));
+
+ $handler = new ProcedureHandler;
+ $this->assertTrue($handler->isPositionalArguments(
+ array('a', 'b', 'c')
+ ));
+ }
+
+ public function testBindNamedArguments()
+ {
+ $handler = new ProcedureHandler;
+ $handler->withClassAndMethod('getAllA', 'A', 'getAll');
+ $handler->withClassAndMethod('getAllB', 'B', 'getAll');
+ $handler->withClassAndMethod('getAllC', new B, 'getAll');
+ $this->assertEquals(6, $handler->executeProcedure('getAllA', array('p2' => 4, 'p1' => -2)));
+ $this->assertEquals(10, $handler->executeProcedure('getAllA', array('p2' => 4, 'p3' => 8, 'p1' => -2)));
+ $this->assertEquals(6, $handler->executeProcedure('getAllB', array('p1' => 4)));
+ $this->assertEquals(5, $handler->executeProcedure('getAllC', array('p1' => 3)));
+ }
+
+ public function testBindPositionalArguments()
+ {
+ $handler = new ProcedureHandler;
+ $handler->withClassAndMethod('getAllA', 'A', 'getAll');
+ $handler->withClassAndMethod('getAllB', 'B', 'getAll');
+ $this->assertEquals(6, $handler->executeProcedure('getAllA', array(4, -2)));
+ $this->assertEquals(2, $handler->executeProcedure('getAllA', array(4, 0, -2)));
+ $this->assertEquals(4, $handler->executeProcedure('getAllB', array(2)));
+ }
+
+ public function testRegisterNamedArguments()
+ {
+ $handler = new ProcedureHandler;
+ $handler->withCallback('getAllA', function($p1, $p2, $p3 = 4) {
+ return $p1 + $p2 + $p3;
+ });
+
+ $this->assertEquals(6, $handler->executeProcedure('getAllA', array('p2' => 4, 'p1' => -2)));
+ $this->assertEquals(10, $handler->executeProcedure('getAllA', array('p2' => 4, 'p3' => 8, 'p1' => -2)));
+ }
+
+ public function testRegisterPositionalArguments()
+ {
+ $handler = new ProcedureHandler;
+ $handler->withCallback('getAllA', function($p1, $p2, $p3 = 4) {
+ return $p1 + $p2 + $p3;
+ });
+
+ $this->assertEquals(6, $handler->executeProcedure('getAllA', array(4, -2)));
+ $this->assertEquals(2, $handler->executeProcedure('getAllA', array(4, 0, -2)));
+ }
+
+ public function testTooManyArguments()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+
+ $handler = new ProcedureHandler;
+ $handler->withClassAndMethod('getAllC', new B, 'getAll');
+ $handler->executeProcedure('getAllC', array('p1' => 3, 'p2' => 5));
+ }
+
+ public function testNotEnoughArguments()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+
+ $handler = new ProcedureHandler;
+ $handler->withClassAndMethod('getAllC', new B, 'getAll');
+ $handler->executeProcedure('getAllC');
+ }
+
+ public function testBeforeMethod()
+ {
+ $handler = new ProcedureHandler;
+ $handler->withObject(new ClassWithBeforeMethod);
+ $handler->withBeforeMethod('before');
+ $this->assertEquals('myProcedure', $handler->executeProcedure('myProcedure'));
+ }
+}
diff --git a/libs/jsonrpc/tests/Request/RequestBuilderTest.php b/libs/jsonrpc/tests/Request/RequestBuilderTest.php
new file mode 100644
index 00000000..ce9cf674
--- /dev/null
+++ b/libs/jsonrpc/tests/Request/RequestBuilderTest.php
@@ -0,0 +1,53 @@
+<?php
+
+use JsonRPC\Request\RequestBuilder;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class RequestBuilderTest extends PHPUnit_Framework_TestCase
+{
+ public function testBuilder()
+ {
+ $payload = RequestBuilder::create()
+ ->withId(123)
+ ->withProcedure('foobar')
+ ->withParams(array(1, 2, 3))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","method":"foobar","id":123,"params":[1,2,3]}', $payload);
+ }
+
+ public function testBuilderWithoutParams()
+ {
+ $payload = RequestBuilder::create()
+ ->withId(123)
+ ->withProcedure('foobar')
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","method":"foobar","id":123}', $payload);
+ }
+
+ public function testBuilderWithoutId()
+ {
+ $payload = RequestBuilder::create()
+ ->withProcedure('foobar')
+ ->withParams(array(1, 2, 3))
+ ->build();
+
+ $result = json_decode($payload, true);
+ $this->assertNotNull($result['id']);
+ }
+
+ public function testBuilderWithAdditionalRequestAttributes()
+ {
+ $payload = RequestBuilder::create()
+ ->withProcedure('foobar')
+ ->withParams(array(1, 2, 3))
+ ->withRequestAttributes(array("some-attr" => 42))
+ ->build();
+
+ $result = json_decode($payload, true);
+ $this->assertNotNull($result['some-attr']);
+ }
+
+}
diff --git a/libs/jsonrpc/tests/Response/HeaderMockTest.php b/libs/jsonrpc/tests/Response/HeaderMockTest.php
new file mode 100644
index 00000000..cbeb7388
--- /dev/null
+++ b/libs/jsonrpc/tests/Response/HeaderMockTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace JsonRPC\Response;
+
+use PHPUnit_Framework_TestCase;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+function header($value)
+{
+ HeaderMockTest::$functions->header($value);
+}
+
+abstract class HeaderMockTest extends PHPUnit_Framework_TestCase
+{
+ public static $functions;
+
+ public function setUp()
+ {
+ self::$functions = $this
+ ->getMockBuilder('stdClass')
+ ->setMethods(array('header'))
+ ->getMock();
+ }
+}
diff --git a/libs/jsonrpc/tests/Response/ResponseBuilderTest.php b/libs/jsonrpc/tests/Response/ResponseBuilderTest.php
new file mode 100644
index 00000000..e2dcb2a0
--- /dev/null
+++ b/libs/jsonrpc/tests/Response/ResponseBuilderTest.php
@@ -0,0 +1,151 @@
+<?php
+
+use JsonRPC\Exception\AccessDeniedException;
+use JsonRPC\Exception\AuthenticationFailureException;
+use JsonRPC\Exception\InvalidJsonFormatException;
+use JsonRPC\Exception\InvalidJsonRpcFormatException;
+use JsonRPC\Exception\ResponseEncodingFailureException;
+use JsonRPC\Exception\ResponseException;
+use JsonRPC\Response\ResponseBuilder;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class ResponseBuilderTest extends PHPUnit_Framework_TestCase
+{
+ public function testBuildResponse()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","result":"test","id":123}', $response);
+ }
+
+ public function testBuildResponseWithError()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withError(42, 'Test', 'More info')
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":42,"message":"Test","data":"More info"},"id":123}', $response);
+ }
+
+ public function testBuildResponseWithException()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new Exception('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":0,"message":"Test"},"id":123}', $response);
+ }
+
+ public function testBuildResponseWithResponseException()
+ {
+ $exception = new ResponseException('Error', 42);
+ $exception->setData('Data');
+
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withException($exception)
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":42,"message":"Error","data":"Data"},"id":123}', $response);
+ }
+
+ public function testBuildResponseWithAccessDeniedException()
+ {
+ $responseBuilder = ResponseBuilder::create();
+ $response = $responseBuilder
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new AccessDeniedException('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":403,"message":"Forbidden"},"id":123}', $response);
+ $this->assertEquals('HTTP/1.0 403 Forbidden', $responseBuilder->getStatus());
+
+ $this->assertEquals(
+ array('Content-Type' => 'application/json'),
+ $responseBuilder->getHeaders()
+ );
+ }
+
+ public function testBuildResponseWithAuthenticationFailureException()
+ {
+ $responseBuilder = ResponseBuilder::create();
+ $response = $responseBuilder
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new AuthenticationFailureException('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":401,"message":"Unauthorized"},"id":123}', $response);
+ $this->assertEquals('HTTP/1.0 401 Unauthorized', $responseBuilder->getStatus());
+
+ $this->assertEquals(
+ array('Content-Type' => 'application/json', 'WWW-Authenticate' => 'Basic realm="JsonRPC"'),
+ $responseBuilder->getHeaders()
+ );
+ }
+
+ public function testBuildResponseWithResponseEncodingFailureException()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new ResponseEncodingFailureException('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error","data":"Test"},"id":123}', $response);
+ }
+
+ public function testBuildResponseWithInvalidArgumentException()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new InvalidArgumentException('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"Test"},"id":123}', $response);
+ }
+
+ public function testBuildResponseWithBadFunctionCallException()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new BadFunctionCallException('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":123}', $response);
+ }
+
+ public function testBuildResponseWithInvalidJsonRpcFormatException()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new InvalidJsonRpcFormatException('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}', $response);
+ }
+
+ public function testBuildResponseWithInvalidJsonFormatException()
+ {
+ $response = ResponseBuilder::create()
+ ->withId(123)
+ ->withResult('test')
+ ->withException(new InvalidJsonFormatException('Test'))
+ ->build();
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}', $response);
+ }
+}
diff --git a/libs/jsonrpc/tests/Response/ResponseParserTest.php b/libs/jsonrpc/tests/Response/ResponseParserTest.php
new file mode 100644
index 00000000..f195014f
--- /dev/null
+++ b/libs/jsonrpc/tests/Response/ResponseParserTest.php
@@ -0,0 +1,100 @@
+<?php
+
+use JsonRPC\Response\ResponseParser;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class ResponseParserTest extends PHPUnit_Framework_TestCase
+{
+ public function testSingleRequest()
+ {
+ $result = ResponseParser::create()
+ ->withPayload(json_decode('{"jsonrpc": "2.0", "result": "foobar", "id": "1"}', true))
+ ->parse();
+
+ $this->assertEquals('foobar', $result);
+ }
+
+ public function testWithBadJsonFormat()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonFormatException');
+
+ ResponseParser::create()
+ ->withPayload('foobar')
+ ->parse();
+ }
+
+ public function testWithBadProcedure()
+ {
+ $this->setExpectedException('BadFunctionCallException');
+
+ ResponseParser::create()
+ ->withPayload(json_decode('{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}', true))
+ ->parse();
+ }
+
+ public function testWithInvalidArgs()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+
+ ResponseParser::create()
+ ->withPayload(json_decode('{"jsonrpc": "2.0", "error": {"code": -32602, "message": "Invalid params"}, "id": "1"}', true))
+ ->parse();
+ }
+
+ public function testWithInvalidRequest()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonRpcFormatException');
+
+ ResponseParser::create()
+ ->withPayload(json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true))
+ ->parse();
+ }
+
+ public function testWithParseError()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonFormatException');
+
+ ResponseParser::create()
+ ->withPayload(json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true))
+ ->parse();
+ }
+
+ public function testWithOtherError()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\ResponseException');
+
+ ResponseParser::create()
+ ->withPayload(json_decode('{"jsonrpc": "2.0", "error": {"code": 42, "message": "Something", "data": "foobar"}, "id": null}', true))
+ ->parse();
+ }
+
+ public function testBatch()
+ {
+ $payload = '[
+ {"jsonrpc": "2.0", "result": 7, "id": "1"},
+ {"jsonrpc": "2.0", "result": 19, "id": "2"}
+ ]';
+
+ $result = ResponseParser::create()
+ ->withPayload(json_decode($payload, true))
+ ->parse();
+
+ $this->assertEquals(array(7, 19), $result);
+ }
+
+ public function testBatchWithError()
+ {
+ $payload = '[
+ {"jsonrpc": "2.0", "result": 7, "id": "1"},
+ {"jsonrpc": "2.0", "result": 19, "id": "2"},
+ {"jsonrpc": "2.0", "error": {"code": -32602, "message": "Invalid params"}, "id": "1"}
+ ]';
+
+ $this->setExpectedException('InvalidArgumentException');
+
+ ResponseParser::create()
+ ->withPayload(json_decode($payload, true))
+ ->parse();
+ }
+}
diff --git a/libs/jsonrpc/tests/ServerProtocolTest.php b/libs/jsonrpc/tests/ServerProtocolTest.php
new file mode 100644
index 00000000..a488015b
--- /dev/null
+++ b/libs/jsonrpc/tests/ServerProtocolTest.php
@@ -0,0 +1,237 @@
+<?php
+
+use JsonRPC\Server;
+
+require_once __DIR__.'/../../../vendor/autoload.php';
+require_once __DIR__.'/Response/HeaderMockTest.php';
+
+class C
+{
+ public function doSomething()
+ {
+ return 'something';
+ }
+}
+
+class ServerProtocolTest extends \JsonRPC\Response\HeaderMockTest
+{
+ public function testPositionalParameters()
+ {
+ $subtract = function ($minuend, $subtrahend) {
+ return $minuend - $subtrahend;
+ };
+
+ $server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}');
+ $server->register('subtract', $subtract);
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "result": 19, "id": 1}', true),
+ json_decode($server->execute(), true)
+ );
+
+ $server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 1}');
+ $server->register('subtract', $subtract);
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "result": -19, "id": 1}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testNamedParameters()
+ {
+ $subtract = function ($minuend, $subtrahend) {
+ return $minuend - $subtrahend;
+ };
+
+ $server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}');
+ $server->register('subtract', $subtract);
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "result": 19, "id": 3}', true),
+ json_decode($server->execute(), true)
+ );
+
+ $server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}');
+ $server->register('subtract', $subtract);
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "result": 19, "id": 4}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testNotification()
+ {
+ $update = function($p1, $p2, $p3, $p4, $p5) {};
+ $foobar = function() {};
+
+ $server = new Server('{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}');
+ $server->register('update', $update);
+ $server->register('foobar', $foobar);
+
+ $this->assertEquals('', $server->execute());
+
+ $server = new Server('{"jsonrpc": "2.0", "method": "foobar"}');
+ $server->register('update', $update);
+ $server->register('foobar', $foobar);
+
+ $this->assertEquals('', $server->execute());
+ }
+
+ public function testNoMethod()
+ {
+ $server = new Server('{"jsonrpc": "2.0", "method": "foobar", "id": "1"}');
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testInvalidJson()
+ {
+ $server = new Server('{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]');
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testInvalidRequest()
+ {
+ $server = new Server('{"jsonrpc": "2.0", "method": 1, "params": "bar", "id": 1}');
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testInvalidResponse_MalformedCharacters()
+ {
+ $server = new Server('{"jsonrpc": "2.0", "method": "invalidresponse","id": 1}');
+
+ $invalidresponse = function() {
+ return pack("H*" ,'c32e');
+ };
+
+ $server->register('invalidresponse', $invalidresponse);
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0","id": 1, "error": {"code": -32603, "message": "Internal error","data": "Malformed UTF-8 characters, possibly incorrectly encoded"}}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testBatchInvalidJson()
+ {
+ $server = new Server('[
+ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
+ {"jsonrpc": "2.0", "method"
+ ]');
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testBatchEmptyArray()
+ {
+ $server = new Server('[]');
+
+ $this->assertEquals(
+ json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testBatchNotEmptyButInvalid()
+ {
+ $server = new Server('[1]');
+
+ $this->assertEquals(
+ json_decode('[{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}]', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testBatchInvalid()
+ {
+ $server = new Server('[1,2,3]');
+
+ $this->assertEquals(
+ json_decode('[
+ {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null},
+ {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null},
+ {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}
+ ]', true),
+ json_decode($server->execute(), true)
+ );
+ }
+
+ public function testBatchOk()
+ {
+ $server = new Server('[
+ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
+ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
+ {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
+ {"foo": "boo"},
+ {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
+ {"jsonrpc": "2.0", "method": "get_data", "id": "9"},
+ {"jsonrpc": "2.0", "method": "doSomething", "id": 10},
+ {"jsonrpc": "2.0", "method": "doStuff", "id": 15}
+ ]');
+
+ $server->register('sum', function($a, $b, $c) {
+ return $a + $b + $c;
+ });
+
+ $server->register('subtract', function($minuend, $subtrahend) {
+ return $minuend - $subtrahend;
+ });
+
+ $server->register('get_data', function() {
+ return array('hello', 5);
+ });
+
+ $server->attach(new C);
+
+ $server->bind('doStuff', 'C', 'doSomething');
+
+ $response = $server->execute();
+
+ $this->assertEquals(
+ json_decode('[
+ {"jsonrpc": "2.0", "result": 7, "id": "1"},
+ {"jsonrpc": "2.0", "result": 19, "id": "2"},
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
+ {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
+ {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"},
+ {"jsonrpc": "2.0", "result": "something", "id": "10"},
+ {"jsonrpc": "2.0", "result": "something", "id": "15"}
+ ]', true),
+ json_decode($response, true)
+ );
+ }
+
+ public function testBatchNotifications()
+ {
+ $server = new Server('[
+ {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
+ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}
+ ]');
+
+ $server->register('notify_sum', function($a, $b, $c) {
+
+ });
+
+ $server->register('notify_hello', function($id) {
+
+ });
+
+ $this->assertEquals('', $server->execute());
+ }
+}
diff --git a/libs/jsonrpc/tests/ServerTest.php b/libs/jsonrpc/tests/ServerTest.php
new file mode 100644
index 00000000..87f37c2d
--- /dev/null
+++ b/libs/jsonrpc/tests/ServerTest.php
@@ -0,0 +1,258 @@
+<?php
+
+use JsonRPC\Exception\AccessDeniedException;
+use JsonRPC\Exception\AuthenticationFailureException;
+use JsonRPC\Exception\ResponseException;
+use JsonRPC\MiddlewareInterface;
+use JsonRPC\Response\HeaderMockTest;
+use JsonRPC\Server;
+
+require_once __DIR__.'/../../../vendor/autoload.php';
+require_once __DIR__.'/Response/HeaderMockTest.php';
+
+class MyException extends Exception
+{
+
+}
+
+class DummyMiddleware implements MiddlewareInterface
+{
+ public function execute($username, $password, $procedureName)
+ {
+ throw new AuthenticationFailureException('Bad user');
+ }
+}
+
+class ServerTest extends HeaderMockTest
+{
+ private $payload = '{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}';
+
+ public function testCustomAuthenticationHeader()
+ {
+ $env = array(
+ 'HTTP_X_AUTH' => base64_encode('myuser:mypassword'),
+ );
+
+ $server = new Server($this->payload, $env);
+ $server->setAuthenticationHeader('X-Auth');
+ $this->assertEquals('myuser', $server->getUsername());
+ $this->assertEquals('mypassword', $server->getPassword());
+ }
+
+ public function testCustomAuthenticationHeaderWithEmptyValue()
+ {
+ $server = new Server($this->payload);
+ $server->setAuthenticationHeader('X-Auth');
+ $this->assertNull($server->getUsername());
+ $this->assertNull($server->getPassword());
+ }
+
+ public function testGetUsername()
+ {
+ $server = new Server($this->payload);
+ $this->assertNull($server->getUsername());
+
+ $server = new Server($this->payload, array('PHP_AUTH_USER' => 'username'));
+ $this->assertEquals('username', $server->getUsername());
+ }
+
+ public function testGetPassword()
+ {
+ $server = new Server($this->payload);
+ $this->assertNull($server->getPassword());
+
+ $server = new Server($this->payload, array('PHP_AUTH_PW' => 'password'));
+ $this->assertEquals('password', $server->getPassword());
+ }
+
+ public function testExecute()
+ {
+ $server = new Server($this->payload);
+ $server->getProcedureHandler()->withCallback('sum', function($a, $b, $c) {
+ return $a + $b + $c;
+ });
+
+ self::$functions
+ ->expects($this->once())
+ ->method('header')
+ ->with('Content-Type: application/json');
+
+ $this->assertEquals('{"jsonrpc":"2.0","result":7,"id":"1"}', $server->execute());
+ }
+
+ public function testExecuteRequestParserOverride()
+ {
+ $requestParser = $this->getMockBuilder('JsonRPC\Request\RequestParser')
+ ->getMock();
+
+ $requestParser->method('withPayload')->willReturn($requestParser);
+ $requestParser->method('withProcedureHandler')->willReturn($requestParser);
+ $requestParser->method('withMiddlewareHandler')->willReturn($requestParser);
+ $requestParser->method('withLocalException')->willReturn($requestParser);
+
+ $server = new Server($this->payload, array(), null, $requestParser);
+
+ $requestParser->expects($this->once())
+ ->method('parse');
+
+ $server->execute();
+ }
+
+ public function testExecuteBatchRequestParserOverride()
+ {
+ $batchRequestParser = $this->getMockBuilder('JsonRPC\Request\BatchRequestParser')
+ ->getMock();
+
+ $batchRequestParser->method('withPayload')->willReturn($batchRequestParser);
+ $batchRequestParser->method('withProcedureHandler')->willReturn($batchRequestParser);
+ $batchRequestParser->method('withMiddlewareHandler')->willReturn($batchRequestParser);
+ $batchRequestParser->method('withLocalException')->willReturn($batchRequestParser);
+
+ $server = new Server('["...", "..."]', array(), null, null, $batchRequestParser);
+
+ $batchRequestParser->expects($this->once())
+ ->method('parse');
+
+ $server->execute();
+ }
+
+ public function testExecuteResponseBuilderOverride()
+ {
+ $responseBuilder = $this->getMockBuilder('JsonRPC\Response\ResponseBuilder')
+ ->getMock();
+
+ $responseBuilder->expects($this->once())
+ ->method('sendHeaders');
+
+ $server = new Server($this->payload, array(), $responseBuilder);
+ $server->execute();
+ }
+
+ public function testExecuteProcedureHandlerOverride()
+ {
+ $batchRequestParser = $this->getMockBuilder('JsonRPC\Request\BatchRequestParser')
+ ->getMock();
+
+ $procedureHandler = $this->getMockBuilder('JsonRPC\ProcedureHandler')
+ ->getMock();
+
+ $batchRequestParser->method('withPayload')->willReturn($batchRequestParser);
+ $batchRequestParser->method('withProcedureHandler')->willReturn($batchRequestParser);
+ $batchRequestParser->method('withMiddlewareHandler')->willReturn($batchRequestParser);
+ $batchRequestParser->method('withLocalException')->willReturn($batchRequestParser);
+
+ $server = new Server('["...", "..."]', array(), null, null, $batchRequestParser, $procedureHandler);
+
+ $batchRequestParser->expects($this->once())
+ ->method('parse');
+
+ $batchRequestParser->expects($this->once())
+ ->method('withProcedureHandler')
+ ->with($this->identicalTo($procedureHandler));
+
+ $server->execute();
+ }
+
+ public function testWhenCallbackRaiseForbiddenException()
+ {
+ $server = new Server($this->payload);
+ $server->getProcedureHandler()->withCallback('sum', function($a, $b, $c) {
+ throw new AccessDeniedException();
+ });
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('header')
+ ->with('HTTP/1.0 403 Forbidden');
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('header')
+ ->with('Content-Type: application/json');
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":403,"message":"Forbidden"},"id":null}', $server->execute());
+ }
+
+ public function testWhenCallbackRaiseUnauthorizedException()
+ {
+ $server = new Server($this->payload);
+ $server->getProcedureHandler()->withCallback('sum', function($a, $b, $c) {
+ throw new AuthenticationFailureException();
+ });
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('header')
+ ->with('HTTP/1.0 401 Unauthorized');
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('header')
+ ->with('Content-Type: application/json');
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":401,"message":"Unauthorized"},"id":null}', $server->execute());
+ }
+
+ public function testWhenMiddlewareRaiseUnauthorizedException()
+ {
+ $server = new Server($this->payload);
+ $server->getMiddlewareHandler()->withMiddleware(new DummyMiddleware());
+ $server->getProcedureHandler()->withCallback('sum', function($a, $b) {
+ return $a + $b;
+ });
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('header')
+ ->with('HTTP/1.0 401 Unauthorized');
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('header')
+ ->with('Content-Type: application/json');
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":401,"message":"Unauthorized"},"id":null}', $server->execute());
+ }
+
+ public function testFilterRelayExceptions()
+ {
+ $server = new Server($this->payload);
+ $server->withLocalException('MyException');
+ $server->getProcedureHandler()->withCallback('sum', function($a, $b, $c) {
+ throw new MyException('test');
+ });
+
+ $this->setExpectedException('MyException');
+ $server->execute();
+ }
+
+ public function testCustomExceptionAreRelayedToClient()
+ {
+ $server = new Server($this->payload);
+ $server->getProcedureHandler()->withCallback('sum', function($a, $b, $c) {
+ throw new MyException('test');
+ });
+
+ self::$functions
+ ->expects($this->once())
+ ->method('header')
+ ->with('Content-Type: application/json');
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":0,"message":"test"},"id":"1"}', $server->execute());
+ }
+
+ public function testCustomResponseException()
+ {
+ $server = new Server($this->payload);
+ $server->getProcedureHandler()->withCallback('sum', function($a, $b, $c) {
+ throw new ResponseException('test', 123, null, 'more info');
+ });
+
+ self::$functions
+ ->expects($this->once())
+ ->method('header')
+ ->with('Content-Type: application/json');
+
+ $this->assertEquals('{"jsonrpc":"2.0","error":{"code":123,"message":"test","data":"more info"},"id":"1"}', $server->execute());
+ }
+}
diff --git a/libs/jsonrpc/tests/Validator/HostValidatorTest.php b/libs/jsonrpc/tests/Validator/HostValidatorTest.php
new file mode 100644
index 00000000..a5fed7e0
--- /dev/null
+++ b/libs/jsonrpc/tests/Validator/HostValidatorTest.php
@@ -0,0 +1,32 @@
+<?php
+
+use JsonRPC\Validator\HostValidator;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class HostValidatorTest extends PHPUnit_Framework_TestCase
+{
+ public function testWithEmptyHosts()
+ {
+ $this->assertNull(HostValidator::validate(array(), '127.0.0.1', '127.0.0.1'));
+ }
+
+ public function testWithValidHosts()
+ {
+ $this->assertNull(HostValidator::validate(array('127.0.0.1'), '127.0.0.1', '127.0.0.1'));
+ }
+
+ public function testWithValidNetwork()
+ {
+ $this->assertNull(HostValidator::validate(array('192.168.10.1/24'), '192.168.10.1'),'test ip match');
+ $this->assertNull(HostValidator::validate(array('192.168.10.1/24'), '192.168.10.250'),'test ip match');
+ $this->setExpectedException('\JsonRPC\Exception\AccessDeniedException');
+ HostValidator::validate(array('192.168.10.1/24'), '192.168.11.1');
+ }
+
+ public function testWithNotAuthorizedHosts()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\AccessDeniedException');
+ HostValidator::validate(array('192.168.1.1'), '127.0.0.1', '127.0.0.1');
+ }
+}
diff --git a/libs/jsonrpc/tests/Validator/JsonEncodingValidatorTest.php b/libs/jsonrpc/tests/Validator/JsonEncodingValidatorTest.php
new file mode 100644
index 00000000..a1b2b80e
--- /dev/null
+++ b/libs/jsonrpc/tests/Validator/JsonEncodingValidatorTest.php
@@ -0,0 +1,22 @@
+<?php
+
+use JsonRPC\Validator\JsonEncodingValidator;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class JsonEncodingValidatorTest extends PHPUnit_Framework_TestCase
+{
+ public function testWithValidJson()
+ {
+ json_encode('{"foo": "bar"}');
+ $this->assertNull(JsonEncodingValidator::validate());
+ }
+
+ public function testWithJsonError()
+ {
+ json_encode("\xB1\x31");
+
+ $this->setExpectedException('\JsonRPC\Exception\ResponseEncodingFailureException');
+ JsonEncodingValidator::validate();
+ }
+}
diff --git a/libs/jsonrpc/tests/Validator/JsonFormatValidatorTest.php b/libs/jsonrpc/tests/Validator/JsonFormatValidatorTest.php
new file mode 100644
index 00000000..a838ada9
--- /dev/null
+++ b/libs/jsonrpc/tests/Validator/JsonFormatValidatorTest.php
@@ -0,0 +1,19 @@
+<?php
+
+use JsonRPC\Validator\JsonFormatValidator;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class JsonFormatValidatorTest extends PHPUnit_Framework_TestCase
+{
+ public function testJsonParsedCorrectly()
+ {
+ $this->assertNull(JsonFormatValidator::validate(array('foobar')));
+ }
+
+ public function testJsonNotParsedCorrectly()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonFormatException');
+ JsonFormatValidator::validate('');
+ }
+}
diff --git a/libs/jsonrpc/tests/Validator/RpcFormatValidatorTest.php b/libs/jsonrpc/tests/Validator/RpcFormatValidatorTest.php
new file mode 100644
index 00000000..3e6ba8bc
--- /dev/null
+++ b/libs/jsonrpc/tests/Validator/RpcFormatValidatorTest.php
@@ -0,0 +1,48 @@
+<?php
+
+use JsonRPC\Validator\RpcFormatValidator;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class RpcFormatValidatorTest extends PHPUnit_Framework_TestCase
+{
+ public function testWithMinimumRequirement()
+ {
+ $this->assertNull(RpcFormatValidator::validate(array('jsonrpc' => '2.0', 'method' => 'foobar')));
+ }
+
+ public function testWithNoVersion()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonRpcFormatException');
+ RpcFormatValidator::validate(array('method' => 'foobar'));
+ }
+
+ public function testWithNoMethod()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonRpcFormatException');
+ RpcFormatValidator::validate(array('jsonrpc' => '2.0'));
+ }
+
+ public function testWithMethodNotString()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonRpcFormatException');
+ RpcFormatValidator::validate(array('jsonrpc' => '2.0', 'method' => array()));
+ }
+
+ public function testWithBadVersion()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonRpcFormatException');
+ RpcFormatValidator::validate(array('jsonrpc' => '1.0', 'method' => 'abc'));
+ }
+
+ public function testWithBadParams()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\InvalidJsonRpcFormatException');
+ RpcFormatValidator::validate(array('jsonrpc' => '2.0', 'method' => 'abc', 'params' => 'foobar'));
+ }
+
+ public function testWithParams()
+ {
+ $this->assertNull(RpcFormatValidator::validate(array('jsonrpc' => '2.0', 'method' => 'abc', 'params' => array(1, 2))));
+ }
+}
diff --git a/libs/jsonrpc/tests/Validator/UserValidatorTest.php b/libs/jsonrpc/tests/Validator/UserValidatorTest.php
new file mode 100644
index 00000000..e514c105
--- /dev/null
+++ b/libs/jsonrpc/tests/Validator/UserValidatorTest.php
@@ -0,0 +1,24 @@
+<?php
+
+use JsonRPC\Validator\UserValidator;
+
+require_once __DIR__.'/../../../../vendor/autoload.php';
+
+class UserValidatorTest extends PHPUnit_Framework_TestCase
+{
+ public function testWithEmptyHosts()
+ {
+ $this->assertNull(UserValidator::validate(array(), 'user', 'pass'));
+ }
+
+ public function testWithValidHosts()
+ {
+ $this->assertNull(UserValidator::validate(array('user' => 'pass'), 'user', 'pass'));
+ }
+
+ public function testWithNotAuthorizedHosts()
+ {
+ $this->setExpectedException('\JsonRPC\Exception\AuthenticationFailureException');
+ UserValidator::validate(array('user' => 'pass'), 'user', 'wrong password');
+ }
+}