579 lines
17 KiB
PHP
579 lines
17 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* Ripcord is an easy to use XML-RPC library for PHP.
|
||
|
* @package Ripcord
|
||
|
* @author Auke van Slooten <auke@muze.nl>
|
||
|
* @copyright Copyright (C) 2010, Muze <www.muze.nl>
|
||
|
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
|
||
|
* @version Ripcord 0.9 - PHP 5
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Includes the static ripcord factory class and exceptions
|
||
|
*/
|
||
|
require_once(dirname(__FILE__).'/ripcord.php');
|
||
|
|
||
|
/**
|
||
|
* This class implements a simple RPC client, for XML-RPC, (simplified) SOAP 1.1 or Simple RPC. The client abstracts
|
||
|
* the entire RPC process behind native PHP methods. Any method defined by the rpc server can be called as if it was
|
||
|
* a native method of the rpc client.
|
||
|
*
|
||
|
* E.g.
|
||
|
* <code>
|
||
|
* <?php
|
||
|
* $client = ripcord::client( 'http://www.moviemeter.nl/ws' );
|
||
|
* $score = $client->film->getScore( 'e3dee9d19a8c3af7c92f9067d2945b59', 500 );
|
||
|
* ?>
|
||
|
* </code>
|
||
|
*
|
||
|
* The client has a simple interface for the system.multiCall method:
|
||
|
* <code>
|
||
|
* <?php
|
||
|
* $client = ripcord::client( 'http://ripcord.muze.nl/ripcord.php' );
|
||
|
* $client->system->multiCall()->start();
|
||
|
* ripcord::bind( $methods, $client->system->listMethods() );
|
||
|
* ripcord::bind( $foo, $client->getFoo() );
|
||
|
* $client->system->multiCall()->execute();
|
||
|
* ?>
|
||
|
* </code>
|
||
|
*
|
||
|
* The soap client can only handle the basic php types and doesn't understand xml namespaces. Use PHP's SoapClient
|
||
|
* for complex soap calls. This client cannot parse wsdl.
|
||
|
* If you want to skip the ripcord::client factory method, you _must_ provide a transport object explicitly.
|
||
|
*
|
||
|
* @link http://wiki.moviemeter.nl/index.php/API Moviemeter API documentation
|
||
|
* @package Ripcord
|
||
|
*/
|
||
|
class Ripcord_Client
|
||
|
{
|
||
|
/**
|
||
|
* The url of the rpc server
|
||
|
*/
|
||
|
private $_url = '';
|
||
|
|
||
|
/**
|
||
|
* The transport object, used to post requests.
|
||
|
*/
|
||
|
private $_transport = null;
|
||
|
|
||
|
/**
|
||
|
* A list of output options, used with the xmlrpc_encode_request method.
|
||
|
* @see Ripcord_Server::setOutputOption()
|
||
|
*/
|
||
|
private $_outputOptions = array(
|
||
|
"output_type" => "xml",
|
||
|
"verbosity" => "pretty",
|
||
|
"escaping" => array("markup"),
|
||
|
"version" => "xmlrpc",
|
||
|
"encoding" => "utf-8"
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* The namespace to use when calling a method.
|
||
|
*/
|
||
|
private $_namespace = null;
|
||
|
|
||
|
/**
|
||
|
* A reference to the root client object. This is so when you use namespaced sub clients, you can always
|
||
|
* find the _response and _request data in the root client.
|
||
|
*/
|
||
|
private $_rootClient = null;
|
||
|
|
||
|
/**
|
||
|
* A flag to indicate whether or not to preemptively clone objects passed as arguments to methods, see
|
||
|
* php bug #50282. Only correctly set in the rootClient.
|
||
|
*/
|
||
|
private $_cloneObjects = false;
|
||
|
|
||
|
/**
|
||
|
* A flag to indicate if we are in a multiCall block. Start this with $client->system->multiCall()->start()
|
||
|
*/
|
||
|
protected $_multiCall = false;
|
||
|
|
||
|
/**
|
||
|
* A list of deferred encoded calls.
|
||
|
*/
|
||
|
protected $_multiCallArgs = array();
|
||
|
|
||
|
/**
|
||
|
* The exact response from the rpc server. For debugging purposes.
|
||
|
*/
|
||
|
public $_response = '';
|
||
|
|
||
|
/**
|
||
|
* The exact request from the client. For debugging purposes.
|
||
|
*/
|
||
|
public $_request = '';
|
||
|
|
||
|
/**
|
||
|
* Whether or not to throw exceptions when an xml-rpc fault is returned by the server. Default is false.
|
||
|
*/
|
||
|
public $_throwExceptions = false;
|
||
|
|
||
|
/**
|
||
|
* Whether or not to decode the XML-RPC datetime and base64 types to unix timestamp and binary string
|
||
|
* respectively.
|
||
|
*/
|
||
|
public $_autoDecode = true;
|
||
|
|
||
|
/**
|
||
|
* The constructor for the RPC client.
|
||
|
* @param string $url The url of the rpc server
|
||
|
* @param array $options Optional. A list of outputOptions. See {@link Ripcord_Server::setOutputOption()}
|
||
|
* @param object $rootClient Optional. Used internally when using namespaces.
|
||
|
* @throws Ripcord_ConfigurationException (ripcord::xmlrpcNotInstalled) when the xmlrpc extension is not available.
|
||
|
*/
|
||
|
public function __construct( $url, array $options = null, $transport = null, $rootClient = null )
|
||
|
{
|
||
|
if ( !isset($rootClient) ) {
|
||
|
$rootClient = $this;
|
||
|
if ( !function_exists( 'xmlrpc_encode_request' ) )
|
||
|
{
|
||
|
throw new Ripcord_ConfigurationException('PHP XMLRPC library is not installed',
|
||
|
ripcord::xmlrpcNotInstalled);
|
||
|
}
|
||
|
$version = explode('.', phpversion() );
|
||
|
if ( (0 + $version[0]) == 5) {
|
||
|
if ( ( 0 + $version[1]) < 2 ) {
|
||
|
$this->_cloneObjects = true; // workaround for bug #50282
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
$this->_rootClient = $rootClient;
|
||
|
$this->_url = $url;
|
||
|
if ( isset($options) )
|
||
|
{
|
||
|
if ( isset($options['namespace']) )
|
||
|
{
|
||
|
$this->_namespace = $options['namespace'];
|
||
|
unset( $options['namespace'] );
|
||
|
}
|
||
|
$this->_outputOptions = $options;
|
||
|
}
|
||
|
if ( isset($transport) ) {
|
||
|
$this->_transport = $transport;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method catches any native method called on the client and calls it on the rpc server instead. It automatically
|
||
|
* parses the resulting xml and returns native php type results.
|
||
|
* @throws Ripcord_InvalidArgumentException (ripcord::notRipcordCall) when handling a multiCall and the
|
||
|
* arguments passed do not have the correct method call information
|
||
|
* @throws Ripcord_RemoteException when _throwExceptions is true and the server returns an XML-RPC Fault.
|
||
|
*/
|
||
|
public function __call($name, $args)
|
||
|
{
|
||
|
if ( isset($this->_namespace) )
|
||
|
{
|
||
|
$name = $this->_namespace . '.' . $name;
|
||
|
}
|
||
|
|
||
|
if ( $name === 'system.multiCall' || $name == 'system.multicall' )
|
||
|
{
|
||
|
if ( !$args || ( is_array($args) && count($args)==0 ) )
|
||
|
{
|
||
|
// multiCall is called without arguments, so return the fetch interface object
|
||
|
return new Ripcord_Client_MultiCall( $this->_rootClient, $name );
|
||
|
} else if ( is_array( $args ) && (count( $args ) == 1) &&
|
||
|
is_array( $args[0] ) && !isset( $args[0]['methodName'] ) )
|
||
|
{
|
||
|
// multicall is called with a simple array of calls.
|
||
|
$args = $args[0];
|
||
|
}
|
||
|
$this->_rootClient->_multiCall = false;
|
||
|
$params = array();
|
||
|
$bound = array();
|
||
|
foreach ( $args as $key => $arg )
|
||
|
{
|
||
|
if ( !is_a( $arg, 'Ripcord_Client_Call' ) &&
|
||
|
(!is_array($arg) || !isset($arg['methodName']) ) )
|
||
|
{
|
||
|
throw new Ripcord_InvalidArgumentException(
|
||
|
'Argument '.$key.' is not a valid Ripcord call',
|
||
|
ripcord::notRipcordCall);
|
||
|
}
|
||
|
if ( is_a( $arg, 'Ripcord_Client_Call' ) )
|
||
|
{
|
||
|
$arg->index = count( $params );
|
||
|
$params[] = $arg->encode();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$arg['index'] = count( $params );
|
||
|
$params[] = array(
|
||
|
'methodName' => $arg['methodName'],
|
||
|
'params' => isset($arg['params']) ?
|
||
|
(array) $arg['params'] : array()
|
||
|
);
|
||
|
}
|
||
|
$bound[$key] = $arg;
|
||
|
}
|
||
|
$args = array( $params );
|
||
|
$this->_rootClient->_multiCallArgs = array();
|
||
|
}
|
||
|
if ( $this->_rootClient->_multiCall ) {
|
||
|
$call = new Ripcord_Client_Call( $name, $args );
|
||
|
$this->_rootClient->_multiCallArgs[] = $call;
|
||
|
return $call;
|
||
|
}
|
||
|
if ($this->_rootClient->_cloneObjects) { //workaround for php bug 50282
|
||
|
foreach( $args as $key => $arg) {
|
||
|
if (is_object($arg)) {
|
||
|
$args[$key] = clone $arg;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
$request = xmlrpc_encode_request( $name, $args, $this->_outputOptions );
|
||
|
$response = $this->_transport->post( $this->_url, $request );
|
||
|
$result = xmlrpc_decode( $response, $this->_outputOptions['encoding'] );
|
||
|
$this->_rootClient->_request = $request;
|
||
|
$this->_rootClient->_response = $response;
|
||
|
if ( ripcord::isFault( $result ) && $this->_throwExceptions )
|
||
|
{
|
||
|
throw new Ripcord_RemoteException($result['faultString'], $result['faultCode']);
|
||
|
}
|
||
|
if ( isset($bound) && is_array( $bound ) )
|
||
|
{
|
||
|
foreach ( $bound as $key => $callObject )
|
||
|
{
|
||
|
if ( is_a( $callObject, 'Ripcord_Client_Call' ) )
|
||
|
{
|
||
|
$returnValue = $result[$callObject->index];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$returnValue = $result[$callObject['index']];
|
||
|
}
|
||
|
if ( is_array( $returnValue ) && count( $returnValue ) == 1 )
|
||
|
{
|
||
|
// XML-RPC specification says that non-fault results must be in a single item array
|
||
|
$returnValue = current($returnValue);
|
||
|
}
|
||
|
if ($this->_autoDecode)
|
||
|
{
|
||
|
$type = xmlrpc_get_type($returnValue);
|
||
|
switch ($type)
|
||
|
{
|
||
|
case 'base64' :
|
||
|
$returnValue = ripcord::binary($returnValue);
|
||
|
break;
|
||
|
case 'datetime' :
|
||
|
$returnValue = ripcord::timestamp($returnValue);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if ( is_a( $callObject, 'Ripcord_Client_Call' ) ) {
|
||
|
$callObject->bound = $returnValue;
|
||
|
}
|
||
|
$bound[$key] = $returnValue;
|
||
|
}
|
||
|
$result = $bound;
|
||
|
}
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method catches any reference to properties of the client and uses them as a namespace. The
|
||
|
* property is automatically created as a new instance of the rpc client, with the name of the property
|
||
|
* as a namespace.
|
||
|
* @param string $name The name of the namespace
|
||
|
* @return object A Ripcord Client with the given namespace set.
|
||
|
*/
|
||
|
public function __get($name)
|
||
|
{
|
||
|
$result = null;
|
||
|
if ( !isset($this->{$name}) )
|
||
|
{
|
||
|
$result = new Ripcord_Client(
|
||
|
$this->_url,
|
||
|
array_merge($this->_outputOptions, array(
|
||
|
'namespace' => $this->_namespace ?
|
||
|
$this->_namespace . '.' . $name : $name
|
||
|
) ),
|
||
|
$this->_transport,
|
||
|
$this->_rootClient
|
||
|
);
|
||
|
$this->{$name} = $result;
|
||
|
}
|
||
|
return $result;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This class provides the fetch interface for system.multiCall. It is returned
|
||
|
* when calling $client->system->multiCall() with no arguments. Upon construction
|
||
|
* it puts the originating client into multiCall deferred mode. The client will
|
||
|
* gather the requested method calls instead of executing them immediately. It
|
||
|
* will them execute all of them, in order, when calling
|
||
|
* $client->system->multiCall()->fetch().
|
||
|
* This class extends Ripcord_Client only so it has access to its protected _multiCall
|
||
|
* property.
|
||
|
*/
|
||
|
class Ripcord_Client_MultiCall extends Ripcord_Client
|
||
|
{
|
||
|
|
||
|
/*
|
||
|
* The reference to the originating client to put into multiCall mode.
|
||
|
*/
|
||
|
private $client = null;
|
||
|
|
||
|
/*
|
||
|
* This method creates a new multiCall fetch api object.
|
||
|
*/
|
||
|
public function __construct( $client, $methodName = 'system.multiCall' )
|
||
|
{
|
||
|
$this->client = $client;
|
||
|
$this->methodName = $methodName;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* This method puts the client into multiCall mode. While in this mode all
|
||
|
* method calls are collected as deferred calls (Ripcord_Client_Call).
|
||
|
*/
|
||
|
public function start()
|
||
|
{
|
||
|
$this->client->_multiCall = true;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* This method finally calls the clients multiCall method with all deferred
|
||
|
* method calls since multiCall mode was enabled.
|
||
|
*/
|
||
|
public function execute()
|
||
|
{
|
||
|
if ($this->methodName=='system.multiCall') {
|
||
|
return $this->client->system->multiCall( $this->client->_multiCallArgs );
|
||
|
} else { // system.multicall
|
||
|
return $this->client->system->multicall( $this->client->_multiCallArgs );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This class is used with the Ripcord_Client when calling system.multiCall. Instead of immediately calling the method on the rpc server,
|
||
|
* a Ripcord_Client_Call object is created with all the information needed to call the method using the multicall parameters. The call object is
|
||
|
* returned immediately and is used as input parameter for the multiCall call. The result of the call can be bound to a php variable. This
|
||
|
* variable will be filled with the result of the call when it is available.
|
||
|
* @package Ripcord
|
||
|
*/
|
||
|
class Ripcord_Client_Call
|
||
|
{
|
||
|
/**
|
||
|
* The method to call on the rpc server
|
||
|
*/
|
||
|
public $method = null;
|
||
|
|
||
|
/**
|
||
|
* The arguments to pass on to the method.
|
||
|
*/
|
||
|
public $params = array();
|
||
|
|
||
|
/**
|
||
|
* The index in the multicall request array, if any.
|
||
|
*/
|
||
|
public $index = null;
|
||
|
|
||
|
/**
|
||
|
* A reference to the php variable to fill with the result of the call, if any.
|
||
|
*/
|
||
|
public $bound = null;
|
||
|
|
||
|
/**
|
||
|
* The constructor for the Ripcord_Client_Call class.
|
||
|
* @param string $method The name of the rpc method to call
|
||
|
* @param array $params The parameters for the rpc method.
|
||
|
*/
|
||
|
public function __construct($method, $params)
|
||
|
{
|
||
|
$this->method = $method;
|
||
|
$this->params = $params;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method allows you to bind a php variable to the result of this method call.
|
||
|
* When the method call's result is available, the php variable will be filled with
|
||
|
* this result.
|
||
|
* @param mixed $bound The variable to bind the result from this call to.
|
||
|
* @return object Returns this object for chaining.
|
||
|
*/
|
||
|
public function bind(&$bound)
|
||
|
{
|
||
|
$this->bound =& $bound;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method returns the correct format for a multiCall argument.
|
||
|
* @return array An array with the methodName and params
|
||
|
*/
|
||
|
public function encode() {
|
||
|
return array(
|
||
|
'methodName' => $this->method,
|
||
|
'params' => (array) $this->params
|
||
|
);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This interface describes the minimum interface needed for the transport object used by the
|
||
|
* Ripcord_Client
|
||
|
* @package Ripcord
|
||
|
*/
|
||
|
interface Ripcord_Transport
|
||
|
{
|
||
|
/**
|
||
|
* This method must post the request to the given url and return the results.
|
||
|
* @param string $url The url to post to.
|
||
|
* @param string $request The request to post.
|
||
|
* @return string The server response
|
||
|
*/
|
||
|
public function post( $url, $request );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This class implements the Ripcord_Transport interface using PHP streams.
|
||
|
* @package Ripcord
|
||
|
*/
|
||
|
class Ripcord_Transport_Stream implements Ripcord_Transport
|
||
|
{
|
||
|
/**
|
||
|
* A list of stream context options.
|
||
|
*/
|
||
|
private $options = array();
|
||
|
|
||
|
/**
|
||
|
* Contains the headers sent by the server.
|
||
|
*/
|
||
|
public $responseHeaders = null;
|
||
|
|
||
|
/**
|
||
|
* This is the constructor for the Ripcord_Transport_Stream class.
|
||
|
* @param array $contextOptions Optional. An array with stream context options.
|
||
|
*/
|
||
|
public function __construct( $contextOptions = null )
|
||
|
{
|
||
|
if ( isset($contextOptions) )
|
||
|
{
|
||
|
$this->options = $contextOptions;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method posts the request to the given url.
|
||
|
* @param string $url The url to post to.
|
||
|
* @param string $request The request to post.
|
||
|
* @return string The server response
|
||
|
* @throws Ripcord_TransportException (ripcord::cannotAccessURL) when the given URL cannot be accessed for any reason.
|
||
|
*/
|
||
|
public function post( $url, $request )
|
||
|
{
|
||
|
$options = array_merge(
|
||
|
$this->options,
|
||
|
array(
|
||
|
'http' => array(
|
||
|
'method' => "POST",
|
||
|
'header' => "Content-Type: text/xml",
|
||
|
'content' => $request
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
$context = stream_context_create( $options );
|
||
|
$result = @file_get_contents( $url, false, $context );
|
||
|
$this->responseHeaders = $http_response_header;
|
||
|
if ( !$result )
|
||
|
{
|
||
|
throw new Ripcord_TransportException( 'Could not access ' . $url,
|
||
|
ripcord::cannotAccessURL );
|
||
|
}
|
||
|
return $result;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This class implements the Ripcord_Transport interface using CURL.
|
||
|
* @package Ripcord
|
||
|
*/
|
||
|
class Ripcord_Transport_CURL implements Ripcord_Transport
|
||
|
{
|
||
|
/**
|
||
|
* A list of CURL options.
|
||
|
*/
|
||
|
private $options = array();
|
||
|
|
||
|
/**
|
||
|
* A flag that indicates whether or not we can safely pass the previous exception to a new exception.
|
||
|
*/
|
||
|
private $skipPreviousException = false;
|
||
|
|
||
|
/**
|
||
|
* Contains the headers sent by the server.
|
||
|
*/
|
||
|
public $responseHeaders = null;
|
||
|
|
||
|
/**
|
||
|
* This is the constructor for the Ripcord_Transport_CURL class.
|
||
|
* @param array $curlOptions A list of CURL options.
|
||
|
*/
|
||
|
public function __construct( $curlOptions = null )
|
||
|
{
|
||
|
if ( isset($curlOptions) )
|
||
|
{
|
||
|
$this->options = $curlOptions;
|
||
|
}
|
||
|
$version = explode('.', phpversion() );
|
||
|
if ( ( (0 + $version[0]) == 5) && ( 0 + $version[1]) < 3 ) { // previousException supported in php >= 5.3
|
||
|
$this->_skipPreviousException = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method posts the request to the given url
|
||
|
* @param string $url The url to post to.
|
||
|
* @param string $request The request to post.
|
||
|
* @throws Ripcord_TransportException (ripcord::cannotAccessURL) when the given URL cannot be accessed for any reason.
|
||
|
* @return string The server response
|
||
|
*/
|
||
|
public function post( $url, $request)
|
||
|
{
|
||
|
$curl = curl_init();
|
||
|
$options = (array) $this->options + array(
|
||
|
CURLOPT_RETURNTRANSFER => 1,
|
||
|
CURLOPT_URL => $url,
|
||
|
CURLOPT_POST => true,
|
||
|
CURLOPT_POSTFIELDS => $request,
|
||
|
CURLOPT_HEADER => true
|
||
|
);
|
||
|
curl_setopt_array( $curl, $options );
|
||
|
$contents = curl_exec( $curl );
|
||
|
$headerSize = curl_getinfo( $curl, CURLINFO_HEADER_SIZE );
|
||
|
$this->responseHeaders = substr( $contents, 0, $headerSize );
|
||
|
$contents = substr( $contents, $headerSize );
|
||
|
|
||
|
if ( curl_errno( $curl ) )
|
||
|
{
|
||
|
$errorNumber = curl_errno( $curl );
|
||
|
$errorMessage = curl_error( $curl );
|
||
|
curl_close( $curl );
|
||
|
$version = explode('.', phpversion() );
|
||
|
if (!$this->_skipPreviousException) { // previousException supported in php >= 5.3
|
||
|
$exception = new Ripcord_TransportException( 'Could not access ' . $url
|
||
|
, ripcord::cannotAccessURL
|
||
|
, new Exception( $errorMessage, $errorNumber )
|
||
|
);
|
||
|
} else {
|
||
|
$exception = new Ripcord_TransportException( 'Could not access ' . $url
|
||
|
. ' ( original CURL error: ' . $errorMessage . ' ) ',
|
||
|
ripcord::cannotAccessURL
|
||
|
);
|
||
|
}
|
||
|
throw $exception;
|
||
|
}
|
||
|
curl_close($curl);
|
||
|
return $contents;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
?>
|