<?php
/**
* $Id: $
*
* A PHP Client/Server implementation of JSON-RPC.
*
*
* LICENSE:
* 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.
*
* @author Jason Hines <hide@address.com>
* @copyright 2005 Jason Hines
* @license http://www.opensource.org/licenses/bsd-license.php
*/
include("JSON.php");
/**
* Client
*/
class JSON_RPC_Client {
var $host;
var $path;
var $port;
var $response = null;
var $debug = false;
var $error = false;
var $username = null;
var $password = null;
function JSON_RPC_Client($url=null,$port=80) {
$parts = parse_url($url);
$this->host = $parts['host'];
$this->path = $parts['path'];
$this->port = $port;
}
function setBasicAuth($username,$password) {
$this->username = $username;
$this->password = $password;
}
/**
* Send the client request and return response
*
* @param string method name
* @param mixed anything ...
* @return object JSON_RPC_Response
*/
function query() {
$params = func_get_args();
$method = array_shift($params);
$rpc_request = new JSON_RPC_Request(time());
$rpc_request->setMethod($method);
$rpc_request->setParams($params);
$json = new Services_JSON;
$encoded_request = $json->encode($rpc_request);
if ($this->debug) {
echo "Encoded Request: " . $encoded_request . "\n";
}
$socket = fsockopen($this->host, $this->port, $errno, $errstr, 30);
if (!$socket) {
$this->error = new JSON_RPC_Error(-32300, "HTTP transport error - Could not open socket. [{$errno}] {$errstr}");
return false;
}
fwrite($socket, "POST {$this->path} HTTP/1.0\r\n");
fwrite($socket, "Host: {$this->host}\r\n");
fwrite($socket, "Content-type: application/x-javascript\r\n");
fwrite($socket, "Content-length: " . strlen($encoded_request) . "\r\n");
fwrite($socket, "User-Agent: JSON_RPC_Client\r\n");
fwrite($socket, "Accept: */*\r\n");
if (!is_null($this->username) && !is_null($this->password)) {
fwrite($socket,"Authorization: Basic ".base64_encode($this->username.':'.$this->password)."\r\n");
}
fwrite($socket, "\r\n");
fwrite($socket, $encoded_request."\r\n");
fwrite($socket, "\r\n");
$contents = "";
$gotFirstLine = false;
$gettingHeaders = true;
while (!feof($socket)) {
$line = fgets($socket, 4096);
if (!$gotFirstLine) {
// Check line for '200'
if (strstr($line, '200') === false) {
$this->error = new JSON_RPC_Error(-32300, "HTTP transport error [Server responded: ".trim($line)."]");
return false;
}
$gotFirstLine = true;
}
if (trim($line) == '') {
$gettingHeaders = false;
}
if (!$gettingHeaders) {
$contents .= trim($line)."\n";
}
}
fclose($socket);
$contents = trim($contents);
if ($this->debug) {
echo "Encoded Response: " . $contents . "\n";
}
// Got returned response, decode it back
$object = $json->decode($contents);
$response = new JSON_RPC_Response($object->id);
$response->setResult($object->result);
$response->setError($object->error);
if ($this->debug) {
echo "Decoded Response: " . var_export($response,true) . "\n";
}
return $response;
}
/**
*
* @return booleon TRUE if we have an error
*/
function isError() {
return (is_object($this->error));
}
/**
*
* @return mixed Error string, if any
*/
function getError() {
if ($this->isError()) {
return $this->error->toString();
}
}
}
/**
*
*/
class JSON_RPC_Error {
var $code;
var $message;
function JSON_RPC_Error($code, $message) {
$this->code = $code;
$this->message = $message;
}
function toString() {
return "JSON_RPC_Error: [{$this->code}] $this->message";
}
}
/**
*
*/
class JSON_RPC_Request {
var $id = null;
var $method = null;
var $params = null;
function JSON_RPC_Request($id) {
$this->id = $id;
}
function setMethod($method) {
$this->method = $method;
}
function setParams($params=array()) {
if (!is_array($params)) $params = array($params);
$this->params = $params;
}
}
/**
*
*/
class JSON_RPC_Response {
var $id = null;
var $result = null;
var $error = null;
function JSON_RPC_Response($id) {
$this->id = $id;
}
function setResult($result) {
$this->result = $result;
}
function setError($error) {
$this->error = $error;
}
function getResult() {
return $this->result;
}
function getError() {
if ($this->isError()) {
return $this->error->message;
}
}
function isError() {
return !is_null($this->error);
}
}
/**
*
*/
class JSON_RPC_Server {
var $callbacks = array();
var $signatures;
var $help;
function JSON_RPC_Server() {
$this->addCallback(
'system.listMethods',
'this:listMethods',
array('array'),
'Returns an array of available methods on this server'
);
}
/**
*
*/
function addCallback($method, $callback, $signature, $help) {
$this->callbacks[$method] = $callback;
$this->signatures[$method] = $signature;
$this->help[$method] = $help;
}
function call($methodname, $params) {
// Over-rides default call method, adds signature check
if (!$this->hasMethod($methodname)) {
return new JSON_RPC_Error(-32601, 'Requested method "'.$methodname.'" not specified.');
}
$method = $this->callbacks[$methodname];
// TODO: check the params against the signature
if (empty($params)) $params = array($params);
// Do we have the expected param value types?
$signatures = $this->signatures[$methodname];
$return_type = array_shift($signatures);
$expected_types = $signatures;
for ($i=0;$i<count($expected_types);$i++) {
$type = gettype($params[$i]);
if (gettype($params[$i]) != $expected_types[$i]) {
return new JSON_RPC_Error(-32601, "Argument type mismatch. Param {$i} is a {$type}, expected {$expected_types[$i]}.");
}
}
// Are we dealing with a function or a method?
if (substr($method, 0, 5) == 'this:') {
// It's a class method - check it exists
$method = substr($method, 5);
if (!method_exists($this, $method)) {
return new JSON_RPC_Error(-32601, "Requested class method '{$method}' does not exist.");
}
// Call the method
$result = call_user_func_array(array($this,$method),$params);
} else {
// It's a function - does it exist?
if (!function_exists($method)) {
return new JSON_RPC_Error(-32601, "Requested function '{$method}' does not exist.");
}
// Call the function
$result = call_user_func_array($method,$params);
}
if (gettype($result) != $return_type) {
return new JSON_RPC_Error(-32612, "Return value type mismatch! Value is type ".gettype($result).", expected {$return_type}.");
}
return $result;
}
/**
*
*/
function serve() {
if (!isset($GLOBALS['HTTP_RAW_POST_DATA']) || $_SERVER['REQUEST_METHOD']!='POST') {
die('JSON_RPC server accepts POST requests only.');
}
$json = new Services_JSON;
$request = $json->decode($GLOBALS['HTTP_RAW_POST_DATA']);
$response = new JSON_RPC_Response($request->id);
$result = $this->call($request->method, $request->params);
if (is_a($result, 'JSON_RPC_Error')) {
$response->setError($result);
} else {
$response->setResult($result);
}
echo $json->encode($response);
exit;
}
/**
*
*/
function listMethods() {
$hints = array();
foreach ($this->callbacks as $method=>$callback) {
$return_sig = array_shift($this->signatures[$method]);
$hints[] = array(
'method' => $method,
'signature' => $return_sig . " " . $method . "(".join(",",$this->signatures[$method]).")",
'help' => $this->help[$method]
);
}
return $hints;
}
function hasMethod($method) {
return in_array($method, array_keys($this->callbacks));
}
}
?>