Update JsonRPC library

This commit is contained in:
Frederic Guillot 2016-05-08 15:53:45 -04:00
parent ded8552b8d
commit d964c97f90
27 changed files with 1925 additions and 808 deletions

View File

@ -13,7 +13,7 @@
}, },
"require": { "require": {
"fguillot/simple-validator": "v1.0.0", "fguillot/simple-validator": "v1.0.0",
"fguillot/json-rpc": "v1.0.2", "fguillot/json-rpc": "v1.1.0",
"fguillot/picodb": "v1.0.2", "fguillot/picodb": "v1.0.2",
"fguillot/picofeed": "v0.1.23", "fguillot/picofeed": "v0.1.23",
"pda/pheanstalk": "v3.1.0" "pda/pheanstalk": "v3.1.0"

View File

@ -3,27 +3,26 @@
require __DIR__.'/common.php'; require __DIR__.'/common.php';
use JsonRPC\Server; use JsonRPC\Server;
use PicoFeed\PicoFeedException;
use Model\Config; use Model\Config;
$server = new Server; $server = new Server();
$server->authentication(array( $server->authentication(array(
Config\get('username') => Config\get('api_token') Config\get('username') => Config\get('api_token')
)); ));
// Get version // Get version
$server->register('app.version', function () { $server->register('app.version', function () {
return array('version' => APP_VERSION); return array('version' => APP_VERSION);
}); });
// Get all feeds // Get all feeds
$server->register('feed.list', function () { $server->register('feed.list', function () {
$feeds = Model\Feed\get_all(); $feeds = Model\Feed\get_all();
if (!$feeds) { if (empty($feeds)) {
return $feeds; return array();
} }
$groups = Model\Group\get_feeds_map(); $groups = Model\Group\get_feeds_map();
foreach ($feeds as &$feed) { foreach ($feeds as &$feed) {
$feed_id = $feed['id']; $feed_id = $feed['id'];
@ -38,23 +37,16 @@ $server->register('feed.list', function () {
// Get one feed // Get one feed
$server->register('feed.info', function ($feed_id) { $server->register('feed.info', function ($feed_id) {
$result = Model\Feed\get($feed_id); $result = Model\Feed\get($feed_id);
$result['feed_group_ids'] = Model\Group\get_feed_group_ids($feed_id); $result['feed_group_ids'] = Model\Group\get_feed_group_ids($feed_id);
return $result; return $result;
}); });
// Add a new feed // Add a new feed
$server->register('feed.create', function($url) { $server->register('feed.create', function($url) {
try { try {
$result = Model\Feed\create($url); $result = Model\Feed\create($url);
} } catch (Exception $e) {
catch (PicoFeedException $e) {
$result = false;
}
catch (UnexpectedValueException $e) {
$result = false; $result = false;
} }
@ -65,157 +57,131 @@ $server->register('feed.create', function($url) {
// Delete a feed // Delete a feed
$server->register('feed.delete', function($feed_id) { $server->register('feed.delete', function($feed_id) {
return Model\Feed\remove($feed_id); return Model\Feed\remove($feed_id);
}); });
// Delete all feeds // Delete all feeds
$server->register('feed.delete_all', function() { $server->register('feed.delete_all', function() {
return Model\Feed\remove_all(); return Model\Feed\remove_all();
}); });
// Enable a feed // Enable a feed
$server->register('feed.enable', function($feed_id) { $server->register('feed.enable', function($feed_id) {
return Model\Feed\enable($feed_id); return Model\Feed\enable($feed_id);
}); });
// Disable a feed // Disable a feed
$server->register('feed.disable', function($feed_id) { $server->register('feed.disable', function($feed_id) {
return Model\Feed\disable($feed_id); return Model\Feed\disable($feed_id);
}); });
// Update a feed // Update a feed
$server->register('feed.update', function($feed_id) { $server->register('feed.update', function($feed_id) {
return Model\Feed\refresh($feed_id); return Model\Feed\refresh($feed_id);
}); });
// Get all groups // Get all groups
$server->register('group.list', function () { $server->register('group.list', function () {
return Model\Group\get_all(); return Model\Group\get_all();
}); });
// Get all items for a specific feed // Get all items for a specific feed
$server->register('item.feed.list', function ($feed_id, $offset = null, $limit = null) { $server->register('item.feed.list', function ($feed_id, $offset = null, $limit = null) {
return Model\Item\get_all_by_feed($feed_id, $offset, $limit); return Model\Item\get_all_by_feed($feed_id, $offset, $limit);
}); });
// Count all feed items // Count all feed items
$server->register('item.feed.count', function ($feed_id) { $server->register('item.feed.count', function ($feed_id) {
return Model\Item\count_by_feed($feed_id); return Model\Item\count_by_feed($feed_id);
}); });
// Get all bookmark items // Get all bookmark items
$server->register('item.bookmark.list', function ($offset = null, $limit = null) { $server->register('item.bookmark.list', function ($offset = null, $limit = null) {
return Model\Item\get_bookmarks($offset, $limit); return Model\Item\get_bookmarks($offset, $limit);
}); });
// Count bookmarks // Count bookmarks
$server->register('item.bookmark.count', function () { $server->register('item.bookmark.count', function () {
return Model\Item\count_bookmarks(); return Model\Item\count_bookmarks();
}); });
// Add a bookmark // Add a bookmark
$server->register('item.bookmark.create', function ($item_id) { $server->register('item.bookmark.create', function ($item_id) {
return Model\Item\set_bookmark_value($item_id, 1); return Model\Item\set_bookmark_value($item_id, 1);
}); });
// Remove a bookmark // Remove a bookmark
$server->register('item.bookmark.delete', function ($item_id) { $server->register('item.bookmark.delete', function ($item_id) {
return Model\Item\set_bookmark_value($item_id, 0); return Model\Item\set_bookmark_value($item_id, 0);
}); });
// Get all unread items // Get all unread items
$server->register('item.list_unread', function ($offset = null, $limit = null) { $server->register('item.list_unread', function ($offset = null, $limit = null) {
return Model\Item\get_all_by_status('unread', array(), $offset, $limit); return Model\Item\get_all_by_status('unread', array(), $offset, $limit);
}); });
// Count all unread items // Count all unread items
$server->register('item.count_unread', function () { $server->register('item.count_unread', function () {
return Model\Item\count_by_status('unread'); return Model\Item\count_by_status('unread');
}); });
// Get all read items // Get all read items
$server->register('item.list_read', function ($offset = null, $limit = null) { $server->register('item.list_read', function ($offset = null, $limit = null) {
return Model\Item\get_all_by_status('read', array(), $offset, $limit); return Model\Item\get_all_by_status('read', array(), $offset, $limit);
}); });
// Count all read items // Count all read items
$server->register('item.count_read', function () { $server->register('item.count_read', function () {
return Model\Item\count_by_status('read'); return Model\Item\count_by_status('read');
}); });
// Get one item // Get one item
$server->register('item.info', function ($item_id) { $server->register('item.info', function ($item_id) {
return Model\Item\get($item_id); return Model\Item\get($item_id);
}); });
// Delete an item // Delete an item
$server->register('item.delete', function($item_id) { $server->register('item.delete', function($item_id) {
return Model\Item\set_removed($item_id); return Model\Item\set_removed($item_id);
}); });
// Mark item as read // Mark item as read
$server->register('item.mark_as_read', function($item_id) { $server->register('item.mark_as_read', function($item_id) {
return Model\Item\set_read($item_id); return Model\Item\set_read($item_id);
}); });
// Mark item as unread // Mark item as unread
$server->register('item.mark_as_unread', function($item_id) { $server->register('item.mark_as_unread', function($item_id) {
return Model\Item\set_unread($item_id); return Model\Item\set_unread($item_id);
}); });
// Change the status of list of items // Change the status of list of items
$server->register('item.set_list_status', function($status, array $items) { $server->register('item.set_list_status', function($status, array $items) {
return Model\Item\set_status($status, $items); return Model\Item\set_status($status, $items);
}); });
// Flush all read items // Flush all read items
$server->register('item.flush', function() { $server->register('item.flush', function() {
return Model\Item\mark_all_as_removed(); return Model\Item\mark_all_as_removed();
}); });
// Mark all unread items as read // Mark all unread items as read
$server->register('item.mark_all_as_read', function() { $server->register('item.mark_all_as_read', function() {
return Model\Item\mark_all_as_read(); return Model\Item\mark_all_as_read();
}); });
// Get all items with the content // Get all items with the content
$server->register('item.get_all', function() { $server->register('item.get_all', function() {
return Model\Item\get_all(); return Model\Item\get_all();
}); });
// Get all items since a date // Get all items since a date
$server->register('item.get_all_since', function($timestamp) { $server->register('item.get_all_since', function($timestamp) {
return Model\Item\get_all_since($timestamp); return Model\Item\get_all_since($timestamp);
}); });
// Get all items id and status // Get all items id and status
$server->register('item.get_all_status', function() { $server->register('item.get_all_status', function() {
return Model\Item\get_all_status(); return Model\Item\get_all_status();
}); });

View File

@ -6,15 +6,28 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'JsonRPC\\AccessDeniedException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/AccessDeniedException.php',
'JsonRPC\\AuthenticationFailure' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php',
'JsonRPC\\Client' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Client.php', 'JsonRPC\\Client' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Client.php',
'JsonRPC\\ConnectionFailureException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Client.php', 'JsonRPC\\Exception\\AccessDeniedException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/AccessDeniedException.php',
'JsonRPC\\InvalidJsonFormat' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php', 'JsonRPC\\Exception\\AuthenticationFailureException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/AuthenticationFailureException.php',
'JsonRPC\\InvalidJsonRpcFormat' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php', 'JsonRPC\\Exception\\ConnectionFailureException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/ConnectionFailureException.php',
'JsonRPC\\ResponseException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/ResponseException.php', 'JsonRPC\\Exception\\InvalidJsonFormatException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/InvalidJsonFormatException.php',
'JsonRPC\\Exception\\InvalidJsonRpcFormatException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php',
'JsonRPC\\Exception\\ResponseEncodingFailureException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php',
'JsonRPC\\Exception\\ResponseException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/ResponseException.php',
'JsonRPC\\Exception\\ServerErrorException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Exception/ServerErrorException.php',
'JsonRPC\\HttpClient' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/HttpClient.php',
'JsonRPC\\ProcedureHandler' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/ProcedureHandler.php',
'JsonRPC\\Request\\BatchRequestParser' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Request/BatchRequestParser.php',
'JsonRPC\\Request\\RequestBuilder' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Request/RequestBuilder.php',
'JsonRPC\\Request\\RequestParser' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Request/RequestParser.php',
'JsonRPC\\Response\\ResponseBuilder' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Response/ResponseBuilder.php',
'JsonRPC\\Response\\ResponseParser' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Response/ResponseParser.php',
'JsonRPC\\Server' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php', 'JsonRPC\\Server' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php',
'JsonRPC\\ServerErrorException' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Client.php', 'JsonRPC\\Validator\\HostValidator' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Validator/HostValidator.php',
'JsonRPC\\Validator\\JsonEncodingValidator' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Validator/JsonEncodingValidator.php',
'JsonRPC\\Validator\\JsonFormatValidator' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Validator/JsonFormatValidator.php',
'JsonRPC\\Validator\\RpcFormatValidator' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Validator/RpcFormatValidator.php',
'JsonRPC\\Validator\\UserValidator' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Validator/UserValidator.php',
'Pheanstalk\\Command' => $vendorDir . '/pda/pheanstalk/src/Command.php', 'Pheanstalk\\Command' => $vendorDir . '/pda/pheanstalk/src/Command.php',
'Pheanstalk\\Command\\AbstractCommand' => $vendorDir . '/pda/pheanstalk/src/Command/AbstractCommand.php', 'Pheanstalk\\Command\\AbstractCommand' => $vendorDir . '/pda/pheanstalk/src/Command/AbstractCommand.php',
'Pheanstalk\\Command\\BuryCommand' => $vendorDir . '/pda/pheanstalk/src/Command/BuryCommand.php', 'Pheanstalk\\Command\\BuryCommand' => $vendorDir . '/pda/pheanstalk/src/Command/BuryCommand.php',

View File

@ -37,44 +37,6 @@
"description": "Simple validator library", "description": "Simple validator library",
"homepage": "https://github.com/fguillot/simpleValidator" "homepage": "https://github.com/fguillot/simpleValidator"
}, },
{
"name": "fguillot/json-rpc",
"version": "v1.0.2",
"version_normalized": "1.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/fguillot/JsonRPC.git",
"reference": "265cf039c2823f684349de78c0c03a597992bea9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/265cf039c2823f684349de78c0c03a597992bea9",
"reference": "265cf039c2823f684349de78c0c03a597992bea9",
"shasum": ""
},
"require": {
"php": ">=5.3.4"
},
"time": "2015-09-12 16:27:13",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"JsonRPC": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frédéric Guillot"
}
],
"description": "Simple Json-RPC client/server library that just works",
"homepage": "https://github.com/fguillot/JsonRPC"
},
{ {
"name": "fguillot/picodb", "name": "fguillot/picodb",
"version": "v1.0.2", "version": "v1.0.2",
@ -262,5 +224,43 @@
"keywords": [ "keywords": [
"beanstalkd" "beanstalkd"
] ]
},
{
"name": "fguillot/json-rpc",
"version": "v1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/fguillot/JsonRPC.git",
"reference": "e915dab71940e7ac251955c785570048f460d332"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/e915dab71940e7ac251955c785570048f460d332",
"reference": "e915dab71940e7ac251955c785570048f460d332",
"shasum": ""
},
"require": {
"php": ">=5.3.4"
},
"time": "2016-04-27 02:48:10",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"JsonRPC": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frédéric Guillot"
}
],
"description": "Simple Json-RPC client/server library that just works",
"homepage": "https://github.com/fguillot/JsonRPC"
} }
] ]

View File

@ -1,7 +0,0 @@
<?php
namespace JsonRPC;
class AccessDeniedException extends \Exception
{
}

View File

@ -3,12 +3,8 @@
namespace JsonRPC; namespace JsonRPC;
use Exception; use Exception;
use RuntimeException; use JsonRPC\Request\RequestBuilder;
use BadFunctionCallException; use JsonRPC\Response\ResponseParser;
use InvalidArgumentException;
class ConnectionFailureException extends Exception {};
class ServerErrorException extends Exception {};
/** /**
* JsonRPC client class * JsonRPC client class
@ -18,105 +14,99 @@ class ServerErrorException extends Exception {};
*/ */
class Client class Client
{ {
/**
* URL of the server
*
* @access private
* @var string
*/
private $url;
/** /**
* If the only argument passed to a function is an array * If the only argument passed to a function is an array
* assume it contains named arguments * 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 * @access public
* @var boolean * @var boolean
*/ */
public $named_arguments = true; private $returnException = false;
/**
* HTTP client timeout
*
* @access private
* @var integer
*/
private $timeout;
/**
* Username for authentication
*
* @access private
* @var string
*/
private $username;
/**
* Password for authentication
*
* @access private
* @var string
*/
private $password;
/** /**
* True for a batch request * True for a batch request
* *
* @access public * @access private
* @var boolean * @var boolean
*/ */
public $is_batch = false; private $isBatch = false;
/** /**
* Batch payload * Batch payload
* *
* @access public
* @var array
*/
public $batch = array();
/**
* Enable debug output to the php error log
*
* @access public
* @var boolean
*/
public $debug = false;
/**
* Default HTTP headers to send to the server
*
* @access private * @access private
* @var array * @var array
*/ */
private $headers = array( private $batch = array();
'User-Agent: JSON-RPC PHP Client <https://github.com/fguillot/JsonRPC>',
'Content-Type: application/json',
'Accept: application/json',
'Connection: close',
);
/** /**
* SSL certificates verification * Http Client
* *
* @access public * @access private
* @var boolean * @var HttpClient
*/ */
public $ssl_verify_peer = true; private $httpClient;
/** /**
* Constructor * Constructor
* *
* @access public * @access public
* @param string $url Server URL * @param string $url Server URL
* @param integer $timeout HTTP timeout * @param bool $returnException Return exceptions
* @param array $headers Custom HTTP headers * @param HttpClient $httpClient HTTP client object
*/ */
public function __construct($url, $timeout = 3, $headers = array()) public function __construct($url = '', $returnException = false, HttpClient $httpClient = null)
{ {
$this->url = $url; $this->httpClient = $httpClient ?: new HttpClient($url);
$this->timeout = $timeout; $this->returnException = $returnException;
$this->headers = array_merge($this->headers, $headers); }
/**
* 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;
} }
/** /**
@ -129,30 +119,13 @@ class Client
*/ */
public function __call($method, array $params) public function __call($method, array $params)
{ {
// Allow to pass an array and use named arguments if ($this->isNamedArguments && count($params) === 1 && is_array($params[0])) {
if ($this->named_arguments && count($params) === 1 && is_array($params[0])) {
$params = $params[0]; $params = $params[0];
} }
return $this->execute($method, $params); return $this->execute($method, $params);
} }
/**
* Set authentication parameters
*
* @access public
* @param string $username Username
* @param string $password Password
* @return Client
*/
public function authentication($username, $password)
{
$this->username = $username;
$this->password = $password;
return $this;
}
/** /**
* Start a batch request * Start a batch request
* *
@ -161,9 +134,8 @@ class Client
*/ */
public function batch() public function batch()
{ {
$this->is_batch = true; $this->isBatch = true;
$this->batch = array(); $this->batch = array();
return $this; return $this;
} }
@ -175,11 +147,8 @@ class Client
*/ */
public function send() public function send()
{ {
$this->is_batch = false; $this->isBatch = false;
return $this->sendPayload('['.implode(', ', $this->batch).']');
return $this->parseResponse(
$this->doRequest($this->batch)
);
} }
/** /**
@ -192,198 +161,41 @@ class Client
*/ */
public function execute($procedure, array $params = array()) public function execute($procedure, array $params = array())
{ {
if ($this->is_batch) { $payload = RequestBuilder::create()
$this->batch[] = $this->prepareRequest($procedure, $params); ->withProcedure($procedure)
->withParams($params)
->build();
if ($this->isBatch) {
$this->batch[] = $payload;
return $this; return $this;
} }
return $this->parseResponse( return $this->sendPayload($payload);
$this->doRequest($this->prepareRequest($procedure, $params))
);
} }
/** /**
* Prepare the payload * Send payload
*
* @access public
* @param string $procedure Procedure name
* @param array $params Procedure arguments
* @return array
*/
public function prepareRequest($procedure, array $params = array())
{
$payload = array(
'jsonrpc' => '2.0',
'method' => $procedure,
'id' => mt_rand()
);
if (! empty($params)) {
$payload['params'] = $params;
}
return $payload;
}
/**
* Parse the response and return the procedure result
*
* @access public
* @param array $payload
* @return mixed
*/
public function parseResponse(array $payload)
{
if ($this->isBatchResponse($payload)) {
$results = array();
foreach ($payload as $response) {
$results[] = $this->getResult($response);
}
return $results;
}
return $this->getResult($payload);
}
/**
* Throw an exception according the RPC error
*
* @access public
* @param array $error
* @throws BadFunctionCallException
* @throws InvalidArgumentException
* @throws RuntimeException
*/
public function handleRpcErrors(array $error)
{
switch ($error['code']) {
case -32601:
throw new BadFunctionCallException('Procedure not found: '. $error['message']);
case -32602:
throw new ResponseException(
'Invalid arguments: '. $error['message'],
$error['code'],
null,
isset($error['data']) ? $error['data'] : null
);
default:
throw new RuntimeException('Invalid request/response: '. $error['message'], $error['code']);
}
}
/**
* Throw an exception according the HTTP response
*
* @access public
* @param array $headers
* @throws AccessDeniedException
* @throws ServerErrorException
*/
public function handleHttpErrors(array $headers)
{
$exceptions = array(
'401' => 'JsonRPC\AccessDeniedException',
'403' => 'JsonRPC\AccessDeniedException',
'404' => 'JsonRPC\ConnectionFailureException',
'500' => 'JsonRPC\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);
}
}
}
}
/**
* Do the HTTP request
* *
* @access private * @access private
* @param array $payload * @throws Exception
* @return array * @param string $payload
* @return Exception|Client
*/ */
private function doRequest(array $payload) private function sendPayload($payload)
{ {
$stream = @fopen(trim($this->url), 'r', false, $this->getContext($payload)); try {
if (! is_resource($stream)) { return ResponseParser::create()
throw new ConnectionFailureException('Unable to establish a connection'); ->withPayload($this->httpClient->execute($payload))
->parse();
} catch (Exception $e) {
if ($this->returnException) {
return $e;
} }
$metadata = stream_get_meta_data($stream); throw $e;
$this->handleHttpErrors($metadata['wrapper_data']); }
$response = json_decode(stream_get_contents($stream), true);
if ($this->debug) {
error_log('==> Request: '.PHP_EOL.json_encode($payload, JSON_PRETTY_PRINT));
error_log('==> Response: '.PHP_EOL.json_encode($response, JSON_PRETTY_PRINT));
}
return is_array($response) ? $response : array();
}
/**
* Prepare stream context
*
* @access private
* @param array $payload
* @return resource
*/
private function getContext(array $payload)
{
$headers = $this->headers;
if (! empty($this->username) && ! empty($this->password)) {
$headers[] = 'Authorization: Basic '.base64_encode($this->username.':'.$this->password);
}
return stream_context_create(array(
'http' => array(
'method' => 'POST',
'protocol_version' => 1.1,
'timeout' => $this->timeout,
'max_redirects' => 2,
'header' => implode("\r\n", $headers),
'content' => json_encode($payload),
'ignore_errors' => true,
),
"ssl" => array(
"verify_peer" => $this->ssl_verify_peer,
"verify_peer_name" => $this->ssl_verify_peer,
)
));
}
/**
* Return true if we have a batch response
*
* @access public
* @param array $payload
* @return boolean
*/
private function isBatchResponse(array $payload)
{
return array_keys($payload) === range(0, count($payload) - 1);
}
/**
* Get a RPC call result
*
* @access private
* @param array $payload
* @return mixed
*/
private function getResult(array $payload)
{
if (isset($payload['error']['code'])) {
$this->handleRpcErrors($payload['error']);
}
return isset($payload['result']) ? $payload['result'] : null;
} }
} }

View File

@ -0,0 +1,15 @@
<?php
namespace JsonRPC\Exception;
use Exception;
/**
* Class AccessDeniedException
*
* @package JsonRPC\Exception
* @author Frederic Guillot
*/
class AccessDeniedException extends Exception
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace JsonRPC\Exception;
use Exception;
/**
* Class AuthenticationFailureException
*
* @package JsonRPC\Exception
* @author Frederic Guillot
*/
class AuthenticationFailureException extends Exception
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace JsonRPC\Exception;
use Exception;
/**
* Class ConnectionFailureException
*
* @package JsonRPC\Exception
* @author Frederic Guillot
*/
class ConnectionFailureException extends Exception
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace JsonRPC\Exception;
use Exception;
/**
* Class InvalidJsonFormatException
*
* @package JsonRPC\Exception
* @author Frederic Guillot
*/
class InvalidJsonFormatException extends Exception
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace JsonRPC\Exception;
use Exception;
/**
* Class InvalidJsonRpcFormatException
*
* @package JsonRPC\Exception
* @author Frederic Guillot
*/
class InvalidJsonRpcFormatException extends Exception
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace JsonRPC\Exception;
use Exception;
/**
* Class ResponseEncodingFailureException
*
* @package JsonRPC\Exception
* @author Frederic Guillot
*/
class ResponseEncodingFailureException extends Exception
{
}

View File

@ -1,13 +1,14 @@
<?php <?php
namespace JsonRPC; namespace JsonRPC\Exception;
use Exception; use Exception;
/** /**
* Class ResponseException * Class ResponseException
* *
* @package JsonRPC * @package JsonRPC\Exception
* @author Frederic Guillot
*/ */
class ResponseException extends Exception class ResponseException extends Exception
{ {
@ -40,7 +41,7 @@ class ResponseException extends Exception
* *
* @access public * @access public
* @param mixed $data [optional] A value that contains additional information about the error. * @param mixed $data [optional] A value that contains additional information about the error.
* @return \JsonRPC\ResponseException * @return \JsonRPC\Exception\ResponseException
*/ */
public function setData($data = null) public function setData($data = null)
{ {

View File

@ -0,0 +1,15 @@
<?php
namespace JsonRPC\Exception;
use Exception;
/**
* Class ServerErrorException
*
* @package JsonRPC\Exception
* @author Frederic Guillot
*/
class ServerErrorException extends Exception
{
}

View File

@ -0,0 +1,365 @@
<?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 private
* @var string
*/
private $url;
/**
* HTTP client timeout
*
* @access private
* @var integer
*/
private $timeout = 5;
/**
* Default HTTP headers to send to the server
*
* @access private
* @var array
*/
private $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 private
* @var string
*/
private $username;
/**
* Password for authentication
*
* @access private
* @var string
*/
private $password;
/**
* Enable debug output to the php error log
*
* @access private
* @var boolean
*/
private $debug = false;
/**
* Cookies
*
* @access private
* @var array
*/
private $cookies = array();
/**
* SSL certificates verification
*
* @access private
* @var boolean
*/
private $verifySslCertificate = true;
/**
* Callback called before the doing the request
*
* @access private
* @var Closure
*/
private $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 timeout
*
* @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 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
* @return array
*/
public function execute($payload)
{
if (is_callable($this->beforeRequest)) {
call_user_func_array($this->beforeRequest, array($this, $payload));
}
$stream = fopen(trim($this->url), 'r', false, $this->buildContext($payload));
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);
if ($this->debug) {
error_log('==> Request: '.PHP_EOL.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 private
* @param string $payload
* @return resource
*/
private function buildContext($payload)
{
$headers = $this->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 stream_context_create(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,
)
));
}
/**
* Parse cookies from response
*
* @access private
* @param array $headers
*/
private 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);
}
}
}
}
}

View File

@ -0,0 +1,304 @@
<?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 private
* @var array
*/
private $callbacks = array();
/**
* List of classes
*
* @access private
* @var array
*/
private $classes = array();
/**
* List of instances
*
* @access private
* @var array
*/
private $instances = array();
/**
* Method name to execute before the procedure
*
* @access private
* @var string
*/
private $before = '';
/**
* Username
*
* @access private
* @var string
*/
private $username;
/**
* Password
*
* @access private
* @var string
*/
private $password;
/**
* 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;
}
/**
* Register a new procedure
*
* @access public
* @param string $procedure Procedure name
* @param closure $callback Callback
* @return Server
*/
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 Server
*/
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 Server
*/
public function withObject($instance)
{
$this->instances[] = $instance;
return $this;
}
/**
* Attach a method that will be called before the procedure
*
* @access public
* @param string $before
* @return Server
*/
public function withBeforeMethod($before)
{
$this->before = $before;
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);
}
else if (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;
// Execute before action
if (! empty($this->before)) {
if (is_callable($this->before)) {
call_user_func_array($this->before, array($this->username, $this->password, get_class($class), $method));
}
else if (method_exists($instance, $this->before)) {
$instance->{$this->before}($this->username, $this->password, get_class($class), $method);
}
}
$reflection = new ReflectionMethod($class, $method);
$arguments = $this->getArguments(
$params,
$reflection->getParameters(),
$reflection->getNumberOfRequiredParameters(),
$reflection->getNumberOfParameters()
);
return $reflection->invokeArgs($instance, $arguments);
}
/**
* Get procedure arguments
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @param integer $nb_required_params Number of required parameters
* @param integer $nb_max_params Maximum number of parameters
* @return array
*/
public function getArguments(array $request_params, array $method_params, $nb_required_params, $nb_max_params)
{
$nb_params = count($request_params);
if ($nb_params < $nb_required_params) {
throw new InvalidArgumentException('Wrong number of arguments');
}
if ($nb_params > $nb_max_params) {
throw new InvalidArgumentException('Too many arguments');
}
if ($this->isPositionalArguments($request_params)) {
return $request_params;
}
return $this->getNamedArguments($request_params, $method_params);
}
/**
* 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 $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @return array
*/
public function getNamedArguments(array $request_params, array $method_params)
{
$params = array();
foreach ($method_params as $p) {
$name = $p->getName();
if (isset($request_params[$name])) {
$params[$name] = $request_params[$name];
}
else if ($p->isDefaultValueAvailable()) {
$params[$name] = $p->getDefaultValue();
}
else {
throw new InvalidArgumentException('Missing argument: '.$name);
}
}
return $params;
}
}

View File

@ -0,0 +1,46 @@
<?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)
->parse();
}
$responses = array_filter($responses);
return empty($responses) ? '' : '['.implode(',', $responses).']';
}
/**
* Return true if we have a batch request
*
* @static
* @access public
* @param array $payload
* @return bool
*/
public static function isBatchRequest(array $payload)
{
return is_array($payload) && array_keys($payload) === range(0, count($payload) - 1);
}
}

View File

@ -0,0 +1,108 @@
<?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();
/**
* 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;
}
/**
* Build the payload
*
* @access public
* @return string
*/
public function build()
{
$payload = array(
'jsonrpc' => '2.0',
'method' => $this->procedure,
'id' => $this->id ?: mt_rand(),
);
if (! empty($this->params)) {
$payload['params'] = $this->params;
}
return json_encode($payload);
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace JsonRPC\Request;
use Exception;
use JsonRPC\Exception\AccessDeniedException;
use JsonRPC\Exception\AuthenticationFailureException;
use JsonRPC\Exception\InvalidJsonRpcFormatException;
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;
/**
* ProcedureHandler
*
* @access protected
* @var ProcedureHandler
*/
protected $procedureHandler;
/**
* 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;
}
/**
* Set procedure handler
*
* @access public
* @param ProcedureHandler $procedureHandler
* @return $this
*/
public function withProcedureHandler(ProcedureHandler $procedureHandler)
{
$this->procedureHandler = $procedureHandler;
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);
$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) {
if ($e instanceof AccessDeniedException || $e instanceof AuthenticationFailureException) {
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 private
* @return bool
*/
private function isNotification()
{
return is_array($this->payload) && !isset($this->payload['id']);
}
}

View File

@ -0,0 +1,324 @@
<?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 private
* @var mixed
*/
private $id;
/**
* Payload ID
*
* @access private
* @var mixed
*/
private $result;
/**
* Payload error code
*
* @access private
* @var integer
*/
private $errorCode;
/**
* Payload error message
*
* @access private
* @var string
*/
private $errorMessage;
/**
* Payload error data
*
* @access private
* @var mixed
*/
private $errorData;
/**
* HTTP Headers
*
* @access private
* @var array
*/
private $headers = array(
'Content-Type' => 'application/json',
);
/**
* HTTP status
*
* @access private
* @var string
*/
private $status;
/**
* Exception
*
* @access private
* @var ResponseException
*/
private $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()
{
$encodedResponse = json_encode($this->buildResponse());
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 private
* @return array
*/
private 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 private
* @return array
*/
private 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 private
*/
private function handleExceptions()
{
if ($this->exception instanceof InvalidJsonFormatException) {
$this->errorCode = -32700;
$this->errorMessage = 'Parse error';
$this->id = null;
} elseif ($this->exception instanceof InvalidJsonRpcFormatException) {
$this->errorCode = -32600;
$this->errorMessage = 'Invalid Request';
$this->id = null;
} elseif ($this->exception instanceof BadFunctionCallException) {
$this->errorCode = -32601;
$this->errorMessage = 'Method not found';
} elseif ($this->exception instanceof InvalidArgumentException) {
$this->errorCode = -32602;
$this->errorMessage = 'Invalid params';
} elseif ($this->exception instanceof ResponseEncodingFailureException) {
$this->errorCode = -32603;
$this->errorMessage = 'Internal error';
$this->errorData = $this->exception->getMessage();
} elseif ($this->exception instanceof AuthenticationFailureException) {
$this->errorCode = 401;
$this->errorMessage = 'Unauthorized';
$this->status = 'HTTP/1.0 401 Unauthorized';
$this->withHeader('WWW-Authenticate', 'Basic realm="JsonRPC"');
} elseif ($this->exception instanceof AccessDeniedException) {
$this->errorCode = 403;
$this->errorMessage = 'Forbidden';
$this->status = 'HTTP/1.0 403 Forbidden';
} elseif ($this->exception instanceof ResponseException) {
$this->errorCode = $this->exception->getCode();
$this->errorMessage = $this->exception->getMessage();
$this->errorData = $this->exception->getData();
} elseif ($this->exception instanceof Exception) {
$this->errorCode = $this->exception->getCode();
$this->errorMessage = $this->exception->getMessage();
}
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace JsonRPC\Response;
use BadFunctionCallException;
use InvalidArgumentException;
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;
/**
* Get new object instance
*
* @static
* @access public
* @return ResponseParser
*/
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;
}
/**
* Parse response
*
* @access public
* @throws InvalidJsonFormatException
* @throws InvalidJsonRpcFormatException
* @throws ResponseException
* @return mixed
*/
public function parse()
{
JsonFormatValidator::validate($this->payload);
if ($this->isBatchResponse()) {
$results = array();
foreach ($this->payload as $response) {
$results[] = self::create()
->withPayload($response)
->parse();
}
return $results;
}
if (isset($this->payload['error']['code'])) {
$this->handleExceptions();
}
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);
}
}

View File

@ -3,16 +3,13 @@
namespace JsonRPC; namespace JsonRPC;
use Closure; use Closure;
use BadFunctionCallException;
use Exception; use Exception;
use InvalidArgumentException; use JsonRPC\Request\BatchRequestParser;
use LogicException; use JsonRPC\Request\RequestParser;
use ReflectionFunction; use JsonRPC\Response\ResponseBuilder;
use ReflectionMethod; use JsonRPC\Validator\HostValidator;
use JsonRPC\Validator\JsonFormatValidator;
class InvalidJsonRpcFormat extends Exception {}; use JsonRPC\Validator\UserValidator;
class InvalidJsonFormat extends Exception {};
class AuthenticationFailure extends Exception {};
/** /**
* JsonRPC server class * JsonRPC server class
@ -22,6 +19,14 @@ class AuthenticationFailure extends Exception {};
*/ */
class Server class Server
{ {
/**
* Allowed hosts
*
* @access private
* @var array
*/
private $hosts = array();
/** /**
* Data received from the client * Data received from the client
* *
@ -30,30 +35,6 @@ class Server
*/ */
private $payload = array(); private $payload = array();
/**
* List of procedures
*
* @access private
* @var array
*/
private $callbacks = array();
/**
* List of classes
*
* @access private
* @var array
*/
private $classes = array();
/**
* List of instances
*
* @access private
* @var array
*/
private $instances = array();
/** /**
* List of exception classes that should be relayed to client * List of exception classes that should be relayed to client
* *
@ -62,14 +43,6 @@ class Server
*/ */
private $exceptions = array(); private $exceptions = array();
/**
* Method name to execute before the procedure
*
* @access private
* @var string
*/
private $before = '';
/** /**
* Username * Username
* *
@ -86,32 +59,47 @@ class Server
*/ */
private $password = ''; private $password = '';
/**
* Allowed users
*
* @access private
* @var array
*/
private $users = array();
/**
* $_SERVER
*
* @access private
* @var array
*/
private $serverVariable;
/**
* ProcedureHandler object
*
* @access private
* @var ProcedureHandler
*/
private $procedureHandler;
/** /**
* Constructor * Constructor
* *
* @access public * @access public
* @param string $request * @param string $request
* @param array $server
*/ */
public function __construct($request = '') public function __construct($request = '', array $server = array())
{ {
if ($request !== '') { if ($request !== '') {
$this->payload = json_decode($request, true); $this->payload = json_decode($request, true);
} } else {
else {
$this->payload = json_decode(file_get_contents('php://input'), true); $this->payload = json_decode(file_get_contents('php://input'), true);
} }
}
/** $this->serverVariable = $server ?: $_SERVER;
* Set a payload $this->procedureHandler = new ProcedureHandler();
*
* @access public
* @param array $payload
* @return Server
*/
public function setPayload(array $payload)
{
$this->payload = $payload;
} }
/** /**
@ -124,17 +112,28 @@ class Server
public function setAuthenticationHeader($header) public function setAuthenticationHeader($header)
{ {
if (! empty($header)) { if (! empty($header)) {
$header = 'HTTP_'.str_replace('-', '_', strtoupper($header)); $header = 'HTTP_'.str_replace('-', '_', strtoupper($header));
$value = $this->getServerVariable($header);
if (isset($_SERVER[$header])) { if (! empty($value)) {
list($this->username, $this->password) = explode(':', @base64_decode($_SERVER[$header])); list($this->username, $this->password) = explode(':', base64_decode($value));
} }
} }
return $this; return $this;
} }
/**
* Get ProcedureHandler
*
* @access public
* @return ProcedureHandler
*/
public function getProcedureHandler()
{
return $this->procedureHandler;
}
/** /**
* Get username * Get username
* *
@ -143,7 +142,7 @@ class Server
*/ */
public function getUsername() public function getUsername()
{ {
return $this->username ?: @$_SERVER['PHP_AUTH_USER']; return $this->username ?: $this->getServerVariable('PHP_AUTH_USER');
} }
/** /**
@ -154,66 +153,32 @@ class Server
*/ */
public function getPassword() public function getPassword()
{ {
return $this->password ?: @$_SERVER['PHP_AUTH_PW']; return $this->password ?: $this->getServerVariable('PHP_AUTH_PW');
}
/**
* Send authentication failure response
*
* @access public
*/
public function sendAuthenticationFailureResponse()
{
header('WWW-Authenticate: Basic realm="JsonRPC"');
header('Content-Type: application/json');
header('HTTP/1.0 401 Unauthorized');
echo '{"error": "Authentication failed"}';
exit;
}
/**
* Send forbidden response
*
* @access public
*/
public function sendForbiddenResponse()
{
header('Content-Type: application/json');
header('HTTP/1.0 403 Forbidden');
echo '{"error": "Access Forbidden"}';
exit;
} }
/** /**
* IP based client restrictions * IP based client restrictions
* *
* Return an HTTP error 403 if the client is not allowed
*
* @access public * @access public
* @param array $hosts List of hosts * @param array $hosts List of hosts
* @return Server
*/ */
public function allowHosts(array $hosts) public function allowHosts(array $hosts)
{ {
if (! in_array($_SERVER['REMOTE_ADDR'], $hosts)) { $this->hosts = $hosts;
$this->sendForbiddenResponse(); return $this;
}
} }
/** /**
* HTTP Basic authentication * HTTP Basic authentication
* *
* Return an HTTP error 401 if the client is not allowed
*
* @access public * @access public
* @param array $users Map of username/password * @param array $users Dictionary of username/password
* @return Server * @return Server
*/ */
public function authentication(array $users) public function authentication(array $users)
{ {
if (! isset($users[$this->getUsername()]) || $users[$this->getUsername()] !== $this->getPassword()) { $this->users = $users;
$this->sendAuthenticationFailureResponse();
}
return $this; return $this;
} }
@ -227,7 +192,7 @@ class Server
*/ */
public function register($procedure, Closure $callback) public function register($procedure, Closure $callback)
{ {
$this->callbacks[$procedure] = $callback; $this->procedureHandler->withCallback($procedure, $callback);
return $this; return $this;
} }
@ -242,11 +207,7 @@ class Server
*/ */
public function bind($procedure, $class, $method = '') public function bind($procedure, $class, $method = '')
{ {
if ($method === '') { $this->procedureHandler->withClassAndMethod($procedure, $class, $method);
$method = $procedure;
}
$this->classes[$procedure] = array($class, $method);
return $this; return $this;
} }
@ -259,7 +220,7 @@ class Server
*/ */
public function attach($instance) public function attach($instance)
{ {
$this->instances[] = $instance; $this->procedureHandler->withObject($instance);
return $this; return $this;
} }
@ -286,112 +247,10 @@ class Server
*/ */
public function before($before) public function before($before)
{ {
$this->before = $before; $this->procedureHandler->withBeforeMethod($before);
return $this; return $this;
} }
/**
* Return the response to the client
*
* @access public
* @param array $data Data to send to the client
* @param array $payload Incoming data
* @return string
*/
public function getResponse(array $data, array $payload = array())
{
if (! array_key_exists('id', $payload)) {
return '';
}
$response = array(
'jsonrpc' => '2.0',
'id' => $payload['id']
);
$response = array_merge($response, $data);
@header('Content-Type: application/json');
return json_encode($response);
}
/**
* Parse the payload and test if the parsed JSON is ok
*
* @access private
*/
private function checkJsonFormat()
{
if (! is_array($this->payload)) {
throw new InvalidJsonFormat('Malformed payload');
}
}
/**
* Test if all required JSON-RPC parameters are here
*
* @access private
*/
private function checkRpcFormat()
{
if (! isset($this->payload['jsonrpc']) ||
! isset($this->payload['method']) ||
! is_string($this->payload['method']) ||
$this->payload['jsonrpc'] !== '2.0' ||
(isset($this->payload['params']) && ! is_array($this->payload['params']))) {
throw new InvalidJsonRpcFormat('Invalid JSON RPC payload');
}
}
/**
* Return true if we have a batch request
*
* @access public
* @return boolean
*/
private function isBatchRequest()
{
return array_keys($this->payload) === range(0, count($this->payload) - 1);
}
/**
* Handle batch request
*
* @access private
* @return string
*/
private function handleBatchRequest()
{
$responses = array();
foreach ($this->payload as $payload) {
if (! is_array($payload)) {
$responses[] = $this->getResponse(array(
'error' => array(
'code' => -32600,
'message' => 'Invalid Request'
)),
array('id' => null)
);
}
else {
$server = clone($this);
$server->setPayload($payload);
$response = $server->execute();
if (! empty($response)) {
$responses[] = $response;
}
}
}
return empty($responses) ? '' : '['.implode(',', $responses).']';
}
/** /**
* Parse incoming requests * Parse incoming requests
* *
@ -400,249 +259,57 @@ class Server
*/ */
public function execute() public function execute()
{ {
$responseBuilder = ResponseBuilder::create();
try { try {
$this->procedureHandler
->withUsername($this->getUsername())
->withPassword($this->getPassword());
$this->checkJsonFormat(); JsonFormatValidator::validate($this->payload);
HostValidator::validate($this->hosts, $this->getServerVariable('REMOTE_ADDR'));
UserValidator::validate($this->users, $this->getUsername(), $this->getPassword());
if ($this->isBatchRequest()){ $response = $this->parseRequest();
return $this->handleBatchRequest();
} catch (Exception $e) {
$response = $responseBuilder->withException($e)->build();
} }
$this->checkRpcFormat(); $responseBuilder->sendHeaders();
return $response;
$result = $this->executeProcedure(
$this->payload['method'],
empty($this->payload['params']) ? array() : $this->payload['params']
);
return $this->getResponse(array('result' => $result), $this->payload);
}
catch (InvalidJsonFormat $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32700,
'message' => 'Parse error'
)),
array('id' => null)
);
}
catch (InvalidJsonRpcFormat $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32600,
'message' => 'Invalid Request'
)),
array('id' => null)
);
}
catch (BadFunctionCallException $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32601,
'message' => 'Method not found'
)),
$this->payload
);
}
catch (InvalidArgumentException $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32602,
'message' => 'Invalid params'
)),
$this->payload
);
}
catch (AuthenticationFailure $e) {
$this->sendAuthenticationFailureResponse();
}
catch (AccessDeniedException $e) {
$this->sendForbiddenResponse();
}
catch (ResponseException $e) {
return $this->getResponse(array(
'error' => array(
'code' => $e->getCode(),
'message' => $e->getMessage(),
'data' => $e->getData(),
)),
$this->payload
);
}
catch (Exception $e) {
foreach ($this->exceptions as $class) {
if ($e instanceof $class) {
return $this->getResponse(array(
'error' => array(
'code' => $e->getCode(),
'message' => $e->getMessage()
)),
$this->payload
);
}
}
throw $e;
}
} }
/** /**
* Execute the procedure * Parse incoming request
* *
* @access public * @access private
* @param string $procedure Procedure name * @return string
* @param array $params Procedure params
* @return mixed
*/ */
public function executeProcedure($procedure, array $params = array()) private function parseRequest()
{ {
if (isset($this->callbacks[$procedure])) { if (BatchRequestParser::isBatchRequest($this->payload)) {
return $this->executeCallback($this->callbacks[$procedure], $params); return BatchRequestParser::create()
} ->withPayload($this->payload)
else if (isset($this->classes[$procedure]) && method_exists($this->classes[$procedure][0], $this->classes[$procedure][1])) { ->withProcedureHandler($this->procedureHandler)
return $this->executeMethod($this->classes[$procedure][0], $this->classes[$procedure][1], $params); ->parse();
} }
foreach ($this->instances as $instance) { return RequestParser::create()
if (method_exists($instance, $procedure)) { ->withPayload($this->payload)
return $this->executeMethod($instance, $procedure, $params); ->withProcedureHandler($this->procedureHandler)
} ->parse();
}
throw new BadFunctionCallException('Unable to find the procedure');
} }
/** /**
* Execute a callback * Check existence and get value of server variable
* *
* @access public * @access private
* @param Closure $callback Callback * @param string $variable
* @param array $params Procedure params * @return string|null
* @return mixed
*/ */
public function executeCallback(Closure $callback, $params) private function getServerVariable($variable)
{ {
$reflection = new ReflectionFunction($callback); return isset($this->serverVariable[$variable]) ? $this->serverVariable[$variable] : null;
$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;
// Execute before action
if (! empty($this->before)) {
if (is_callable($this->before)) {
call_user_func_array($this->before, array($this->getUsername(), $this->getPassword(), get_class($class), $method));
}
else if (method_exists($instance, $this->before)) {
$instance->{$this->before}($this->getUsername(), $this->getPassword(), get_class($class), $method);
}
}
$reflection = new ReflectionMethod($class, $method);
$arguments = $this->getArguments(
$params,
$reflection->getParameters(),
$reflection->getNumberOfRequiredParameters(),
$reflection->getNumberOfParameters()
);
return $reflection->invokeArgs($instance, $arguments);
}
/**
* Get procedure arguments
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @param integer $nb_required_params Number of required parameters
* @param integer $nb_max_params Maximum number of parameters
* @return array
*/
public function getArguments(array $request_params, array $method_params, $nb_required_params, $nb_max_params)
{
$nb_params = count($request_params);
if ($nb_params < $nb_required_params) {
throw new InvalidArgumentException('Wrong number of arguments');
}
if ($nb_params > $nb_max_params) {
throw new InvalidArgumentException('Too many arguments');
}
if ($this->isPositionalArguments($request_params, $method_params)) {
return $request_params;
}
return $this->getNamedArguments($request_params, $method_params);
}
/**
* Return true if we have positional parametes
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @return bool
*/
public function isPositionalArguments(array $request_params, array $method_params)
{
return array_keys($request_params) === range(0, count($request_params) - 1);
}
/**
* Get named arguments
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @return array
*/
public function getNamedArguments(array $request_params, array $method_params)
{
$params = array();
foreach ($method_params as $p) {
$name = $p->getName();
if (isset($request_params[$name])) {
$params[$name] = $request_params[$name];
}
else if ($p->isDefaultValueAvailable()) {
$params[$name] = $p->getDefaultValue();
}
else {
throw new InvalidArgumentException('Missing argument: '.$name);
}
}
return $params;
} }
} }

View File

@ -0,0 +1,30 @@
<?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) && ! in_array($remoteAddress, $hosts)) {
throw new AccessDeniedException('Access Forbidden');
}
}
}

View File

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

View File

@ -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');
}
}
}

View File

@ -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');
}
}
}

View File

@ -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');
}
}
}