<?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 <jason@greenhell.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$errstr30);
        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($socket4096);
            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($method05) == 'this:') {
            
// It's a class method - check it exists
            
$method substr($method5);
            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($methodarray_keys($this->callbacks));
    }
}

?>