<?php
/**
* @package Clearbricks
* @subpackage XML-RPC
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
/**
* @class xmlrpcException
* @brief XML-RPC Exception
*/
class xmlrpcException extends Exception
{
/**
* @param string $message Exception message
* @param integer $code Exception code
*/
public function __construct($message, $code = 0)
{
parent::__construct($message, $code);
}
}
/**
* @class xmlrpcValue
* @brief XML-RPC Value
*/
class xmlrpcValue
{
protected $data; ///< mixed Data value
protected $type; ///< string Data type
/**
* Constructor
*
* @param mixed $data Data value
* @param mixed $type Data type
*/
public function __construct($data, $type = false)
{
$this->data = $data;
if (!$type) {
$type = $this->calculateType();
}
$this->type = $type;
if ($type == 'struct') {
# Turn all the values in the array in to new xmlrpcValue objects
foreach ($this->data as $key => $value) {
$this->data[$key] = new xmlrpcValue($value);
}
}
if ($type == 'array') {
for ($i = 0, $j = count($this->data); $i < $j; $i++) {
$this->data[$i] = new xmlrpcValue($this->data[$i]);
}
}
}
/**
* XML Data
*
* Returns an XML subset of the Value.
*
* @return string
*/
public function getXml()
{
# Return XML for this value
switch ($this->type) {
case 'boolean':
return '<boolean>' . (($this->data) ? '1' : '0') . '</boolean>';
case 'int':
return '<int>' . $this->data . '</int>';
case 'double':
return '<double>' . $this->data . '</double>';
case 'string':
return '<string>' . htmlspecialchars($this->data) . '</string>';
case 'array':
$return = '<array><data>' . "\n";
foreach ($this->data as $item) {
$return .= ' <value>' . $item->getXml() . "</value>\n";
}
$return .= '</data></array>';
return $return;
case 'struct':
$return = '<struct>' . "\n";
foreach ($this->data as $name => $value) {
$return .= " <member><name>$name</name><value>";
$return .= $value->getXml() . "</value></member>\n";
}
$return .= '</struct>';
return $return;
case 'date':
case 'base64':
return $this->data->getXml();
}
return '';
}
/**
* Calculate Type
*
* Returns the type of the value if it was not given in constructor.
*
* @return string
*/
protected function calculateType()
{
if ($this->data === true || $this->data === false) {
return 'boolean';
}
if (is_integer($this->data)) {
return 'int';
}
if (is_double($this->data)) {
return 'double';
}
# Deal with xmlrpc object types base64 and date
if (is_object($this->data) && $this->data instanceof xmlrpcDate) {
return 'date';
}
if (is_object($this->data) && $this->data instanceof xmlrpcBase64) {
return 'base64';
}
# If it is a normal PHP object convert it in to a struct
if (is_object($this->data)) {
$this->data = get_object_vars($this->data);
return 'struct';
}
if (!is_array($this->data)) {
return 'string';
}
# We have an array - is it an array or a struct ?
if ($this->isStruct($this->data)) {
return 'struct';
}
return 'array';
}
/**
* Data is struct
*
* Returns true if <var>$array</var> is a Struct and not only an Array.
*
* @param array $array Array
* @return boolean
*/
protected function isStruct($array)
{
# Nasty function to check if an array is a struct or not
$expected = 0;
foreach ($array as $key => $value) {
if ((string) $key != (string) $expected) {
return true;
}
$expected++;
}
return false;
}
}
/**
* @class xmlrpcMessage
* @brief XML-RPC Message
*/
class xmlrpcMessage
{
protected $brutxml; ///< string Brut XML message
protected $message; ///< string XML message
public $messageType; ///< string Type of message - methodCall / methodResponse / fault
public $faultCode; ///< string Fault code
public $faultString; ///< string Fault string
public $methodName; ///< string Method name
public $params = []; ///< array Method parameters
# Currentstring variable stacks
protected $_arraystructs = []; ///< The stack used to keep track of the current array/struct
protected $_arraystructstypes = []; ///< Stack keeping track of if things are structs or array
protected $_currentStructName = []; ///< A stack as well
protected $_param;
protected $_value;
protected $_currentTag;
protected $_currentTagContents;
protected $_parser; ///< The XML parser
/**
* Constructor
*
* @param string $message XML Message
*/
public function __construct($message)
{
$this->brutxml = $this->message = $message;
}
/**
* Message parser
*/
public function parse()
{
// first remove the XML declaration
$this->message = preg_replace('/<\?xml(.*)?\?' . '>/', '', (string) $this->message);
if (trim($this->message) == '') {
throw new Exception('XML Parser Error. Empty message');
}
// Strip DTD.
$header = preg_replace('/^<!DOCTYPE[^>]*+>/i', '', substr($this->message, 0, 200), 1);
$xml = trim(substr_replace($this->message, $header, 0, 200));
if ($xml == '') {
throw new Exception('XML Parser Error.');
}
// Confirm the XML now starts with a valid root tag. A root tag can end in [> \t\r\n]
$root_tag = substr($xml, 0, strcspn(substr($xml, 0, 20), "> \t\r\n"));
// Reject a second DTD.
if (strtoupper($root_tag) == '<!DOCTYPE') {
throw new Exception('XML Parser Error.');
}
if (!in_array($root_tag, ['<methodCall', '<methodResponse', '<fault'])) {
throw new Exception('XML Parser Error.');
}
try {
$dom = new DOMDocument();
@$dom->loadXML($xml);
if ($dom->getElementsByTagName('*')->length > 30000) {
throw new Exception('XML Parser Error.');
}
} catch (Exception $e) {
throw new Exception('XML Parser Error.');
}
$this->_parser = xml_parser_create();
# Set XML parser to take the case of tags in to account
xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, 0);
# Set XML parser callback functions
xml_set_object($this->_parser, $this);
xml_set_element_handler($this->_parser, [$this, 'tag_open'], [$this, 'tag_close']);
xml_set_character_data_handler($this->_parser, [$this, 'cdata']);
if (!xml_parse($this->_parser, $this->message)) {
$c = xml_get_error_code($this->_parser);
$e = xml_error_string($c);
$e .= ' on line ' . xml_get_current_line_number($this->_parser);
throw new Exception('XML Parser Error. ' . $e, $c);
}
xml_parser_free($this->_parser);
# Grab the error messages, if any
if ($this->messageType == 'fault') {
$this->faultCode = $this->params[0]['faultCode'];
$this->faultString = $this->params[0]['faultString'];
}
return true;
}
protected function tag_open($parser, $tag, $attr)
{
$this->_currentTag = $tag;
switch ($tag) {
case 'methodCall':
case 'methodResponse':
case 'fault':
$this->messageType = $tag;
break;
# Deal with stacks of arrays and structs
case 'data': # data is to all intents and puposes more interesting than array
$this->_arraystructstypes[] = 'array';
$this->_arraystructs[] = [];
break;
case 'struct':
$this->_arraystructstypes[] = 'struct';
$this->_arraystructs[] = [];
break;
}
}
protected function cdata($parser, $cdata)
{
$this->_currentTagContents .= $cdata;
}
protected function tag_close($parser, $tag)
{
$valueFlag = false;
$value = null;
switch ($tag) {
case 'int':
case 'i4':
$value = (int) trim((string) $this->_currentTagContents);
$this->_currentTagContents = '';
$valueFlag = true;
break;
case 'double':
$value = (float) trim((string) $this->_currentTagContents);
$this->_currentTagContents = '';
$valueFlag = true;
break;
case 'string':
$value = (string) trim((string) $this->_currentTagContents);
$this->_currentTagContents = '';
$valueFlag = true;
break;
case 'dateTime.iso8601':
$value = new xmlrpcDate(trim((string) $this->_currentTagContents));
# $value = $iso->getTimestamp();
$this->_currentTagContents = '';
$valueFlag = true;
break;
case 'value':
# "If no type is indicated, the type is string."
if (trim($this->_currentTagContents) != '') {
$value = (string) $this->_currentTagContents;
$this->_currentTagContents = '';
$valueFlag = true;
}
break;
case 'boolean':
$value = (bool) trim((string) $this->_currentTagContents);
$this->_currentTagContents = '';
$valueFlag = true;
break;
case 'base64':
$value = base64_decode($this->_currentTagContents);
$this->_currentTagContents = '';
$valueFlag = true;
break;
# Deal with stacks of arrays and structs
case 'data':
case 'struct':
$value = array_pop($this->_arraystructs);
array_pop($this->_arraystructstypes);
$valueFlag = true;
break;
case 'member':
array_pop($this->_currentStructName);
break;
case 'name':
$this->_currentStructName[] = trim((string) $this->_currentTagContents);
$this->_currentTagContents = '';
break;
case 'methodName':
$this->methodName = trim((string) $this->_currentTagContents);
$this->_currentTagContents = '';
break;
}
if ($valueFlag) {
if (count($this->_arraystructs) > 0) {
# Add value to struct or array
if ($this->_arraystructstypes[count($this->_arraystructstypes) - 1] == 'struct') {
# Add to struct
$this->_arraystructs[count($this->_arraystructs) - 1][$this->_currentStructName[count($this->_currentStructName) - 1]] = $value;
} else {
# Add to array
$this->_arraystructs[count($this->_arraystructs) - 1][] = $value;
}
} else {
# Just add as a paramater
$this->params[] = $value;
}
}
}
}
/**
* @class xmlrpcRequest
* @brief XML-RPC Request
*/
class xmlrpcRequest
{
public $method; ///< string Request method name
public $args; ///< array Request method arguments
public $xml; ///< string Request XML string
/**
* Constructor
*
* @param string $method Method name
* @param array $args Method arguments
*/
public function __construct($method, $args)
{
$this->method = $method;
$this->args = $args;
$this->xml = '<?xml version="1.0"?>' . "\n" .
"<methodCall>\n" .
' <methodName>' . $this->method . "</methodName>\n" .
" <params>\n";
foreach ($this->args as $arg) {
$this->xml .= ' <param><value>';
$v = new xmlrpcValue($arg);
$this->xml .= $v->getXml();
$this->xml .= "</value></param>\n";
}
$this->xml .= ' </params></methodCall>';
}
/**
* Request length
*
* Returns {@link $xml} content length.
*
* @return integer
*/
public function getLength()
{
return strlen($this->xml);
}
/**
* Request XML
*
* Returns request XML version.
*
* @return string
*/
public function getXml()
{
return $this->xml;
}
}
/**
* @class xmlrpcDate
* @brief XML-RPC Date object
*/
class xmlrpcDate
{
protected $year; ///< string
protected $month; ///< string
protected $day; ///< string
protected $hour; ///< string
protected $minute; ///< string
protected $second; ///< string
protected $ts;
/**
* Constructor
*
* Creates a new instance of xmlrpcDate. <var>$time</var> could be a
* timestamp or a litteral date.
*
* @param integer|string $time Timestamp or litteral date.
*/
public function __construct($time)
{
# $time can be a PHP timestamp or an ISO one
if (is_numeric($time)) {
$this->parseTimestamp($time);
} else {
$this->parseTimestamp(strtotime($time));
}
}
/**
* Timestamp parser
*
* @param integer $timestamp Timestamp
*/
protected function parseTimestamp($timestamp)
{
$this->year = date('Y', $timestamp);
$this->month = date('m', $timestamp);
$this->day = date('d', $timestamp);
$this->hour = date('H', $timestamp);
$this->minute = date('i', $timestamp);
$this->second = date('s', $timestamp);
$this->ts = $timestamp;
}
/**
* ISO Date
*
* Returns the date in ISO-8601 format.
*
* @return string
*/
public function getIso()
{
return $this->year . $this->month . $this->day . 'T' . $this->hour . ':' . $this->minute . ':' . $this->second;
}
/**
* XML Date
*
* Returns the XML fragment for XML-RPC message inclusion.
*
* @return string
*/
public function getXml()
{
return '<dateTime.iso8601>' . $this->getIso() . '</dateTime.iso8601>';
}
/**
* Timestamp
*
* Returns the date timestamp.
*
* @return integer
*/
public function getTimestamp()
{
return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year);
}
}
/**
* @class xmlrpcBase64
* @brief XML-RPC Base 64 object
*/
class xmlrpcBase64
{
protected $data; ///< string
/**
* Constructor
*
* Create a new instance of xmlrpcBase64.
*
* @param string $data Data
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* XML Data
*
* Returns the XML fragment for XML-RPC message inclusion.
*
* @return string
*/
public function getXml()
{
return '<base64>' . base64_encode($this->data) . '</base64>';
}
}
/*
* @class xmlrpcClient
* @brief XML-RPC Client
*
* XML-RPC Client
*
* This class library is fully based on Simon Willison's IXR library (http://scripts.incutio.com/xmlrpc/).
*
* Basic XML-RPC Client.
*/
/* @cond ONCE */
if (class_exists('netHttp')) {
/** @endcond */
class xmlrpcClient extends netHttp
{
protected $request; ///< xmlrpcRequest XML-RPC Request object
protected $message; ///< xmlrpcMessage XML-RPC Message object
/**
* Constructor
*
* Creates a new instance. <var>$url</var> is the XML-RPC Server end point.
*
* @param string $url Service URL
*/
public function __construct($url)
{
if (!$this->readUrl($url, $ssl, $host, $port, $path, $user, $pass)) {
return;
}
parent::__construct($host, $port);
$this->useSSL($ssl);
$this->setAuthorization($user, $pass);
$this->path = $path;
$this->user_agent = 'Clearbricks XML/RPC Client';
}
/**
* XML-RPC Query
*
* This method calls the given query (first argument) on XML-RPC Server.
* All other arguments of this method are XML-RPC method arguments.
* This method throws an exception if XML-RPC method returns an error or
* returns the server's response.
*
* Example:
* <code>
* <?php
* $o = new xmlrpcClient('http://example.com/xmlrpc');
* $r = $o->query('method1','hello','world');
* ?>
* </code>
*
* @param string $method
* @param mixed $args
*
* @return mixed
*/
public function query($method, ...$args)
{
$this->request = new xmlrpcRequest($method, $args);
$this->doRequest();
if ($this->status != 200) {
throw new Exception('HTTP Error. ' . $this->status . ' ' . $this->status_string);
}
# Now parse what we've got back
$this->message = new xmlrpcMessage($this->content);
$this->message->parse();
# Is the message a fault?
if ($this->message->messageType == 'fault') {
throw new xmlrpcException($this->message->faultString, $this->message->faultCode);
}
return $this->message->params[0];
}
# Overloading netHttp::buildRequest method, we don't need all the stuff of
# HTTP client.
protected function buildRequest()
{
if ($this->proxy_host) {
$path = $this->getRequestURL();
} else {
$path = $this->path;
}
return [
'POST ' . $path . ' HTTP/1.0',
'Host: ' . $this->host,
'Content-Type: text/xml',
'User-Agent: ' . $this->user_agent,
'Content-Length: ' . $this->request->getLength(),
'',
$this->request->getXML(),
];
}
}
/* @cond ONCE */
}
/* @endcond */
/*
* @class xmlrpcClientMulticall
* @brief Multicall XML-RPC Client
*
* Multicall XML-RPC Client
*
* This class library is fully based on Simon Willison's IXR library (http://scripts.incutio.com/xmlrpc/).
*
* Multicall client using system.multicall method of server.
*/
/* @cond ONCE */
if (class_exists('xmlrpcClient')) {
/** @endcond */
class xmlrpcClientMulticall extends xmlrpcClient
{
protected $calls = []; ///< array
public function __construct($url)
{
parent::__construct($url);
}
/**
* Add call to stack
*
* This method adds a method call for the given query (first argument) to
* calls stack.
* All other arguments of this method are XML-RPC method arguments.
*
* Example:
* <code>
* <?php
* $o = new xmlrpcClient('http://example.com/xmlrpc');
* $o->addCall('method1','hello','world');
* $o->addCall('method2','foo','bar');
* $r = $o->query();
* ?>
* </code>
*
* @param string $method
* @param mixed $args
*
* @return mixed
*/
public function addCall($method, ...$args)
{
$struct = [
'methodName' => $method,
'params' => $args,
];
$this->calls[] = $struct;
}
/**
* XML-RPC Query
*
* This method sends calls stack to XML-RPC system.multicall method.
* See {@link xmlrpcServer::multiCall()} for details and links about it.
*
* @param string $method (not used, use ::addCall() before invoking ::query())
* @param mixed $args (see above)
*
* @return array
*/
public function query($method = null, ...$args)
{
# Prepare multicall, then call the parent::query() method
return parent::query('system.multicall', $this->calls);
}
}
/* @cond ONCE */
}
/** @endcond */
/**
* @class xmlrpcServer
* @brief Basic XML-RPC Server
*
* XML-RPC Server
*
* This class library is fully based on Simon Willison's IXR library (http://scripts.incutio.com/xmlrpc/).
*
* This is the most basic XML-RPC server you can create. Built-in methods are:
*
* - system.getCapabilities
* - system.listMethods
* - system.multicall
*/
class xmlrpcServer
{
protected $callbacks = []; ///< array Server methods
protected $data; ///< string Received data
protected $encoding; ///< string Server encoding
protected $message; ///< xmlrpcMessage Returned message
protected $capabilities; ///< array Server capabilities
public $strict_check = false; ///< boolean Strict XML-RPC checks
/**
* Constructor
*
* @param mixed $callbacks Server callbacks
* @param mixed $data Server data
* @param string $encoding Server encoding
*/
public function __construct($callbacks = false, $data = false, $encoding = 'UTF-8')
{
$this->encoding = $encoding;
$this->setCapabilities();
if ($callbacks) {
$this->callbacks = $callbacks;
}
$this->setCallbacks();
$this->serve($data);
}
/**
* Start XML-RPC Server
*
* This method starts the XML-RPC Server. It could take a data argument
* which should be a valid XML-RPC raw stream. If data is not specified, it
* take values from raw POST data.
*
* @param mixed $data XML-RPC raw stream
*/
public function serve($data = false)
{
$result = null;
if (!$data) {
try {
# Check HTTP Method
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
throw new Exception('XML-RPC server accepts POST requests only.', 405);
}
# Check HTTP_HOST
if (!isset($_SERVER['HTTP_HOST'])) {
throw new Exception('No Host Specified', 400);
}
global $HTTP_RAW_POST_DATA;
if (!$HTTP_RAW_POST_DATA) {
$HTTP_RAW_POST_DATA = @file_get_contents('php://input');
if (!$HTTP_RAW_POST_DATA) {
throw new Exception('No Message', 400);
}
}
if ($this->strict_check) {
# Check USER_AGENT
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
throw new Exception('No User Agent Specified', 400);
}
# Check CONTENT_TYPE
if (!isset($_SERVER['CONTENT_TYPE']) || strpos($_SERVER['CONTENT_TYPE'], 'text/xml') !== 0) {
throw new Exception('Invalid Content-Type', 400);
}
# Check CONTENT_LENGTH
if (!isset($_SERVER['CONTENT_LENGTH']) || $_SERVER['CONTENT_LENGTH'] != strlen($HTTP_RAW_POST_DATA)) {
throw new Exception('Invalid Content-Lenth', 400);
}
}
$data = $HTTP_RAW_POST_DATA;
} catch (Exception $e) {
if ($e->getCode() == 400) {
$this->head(400, 'Bad Request');
} elseif ($e->getCode() == 405) {
$this->head(405, 'Method Not Allowed');
header('Allow: POST');
}
header('Content-Type: text/plain');
echo $e->getMessage();
exit;
}
}
$this->message = new xmlrpcMessage($data);
try {
$this->message->parse();
if ($this->message->messageType != 'methodCall') {
throw new xmlrpcException('Server error. Invalid xml-rpc. not conforming to spec. Request must be a methodCall', -32600);
}
$result = $this->call($this->message->methodName, $this->message->params);
} catch (Exception $e) {
$this->error($e);
}
# Encode the result
$r = new xmlrpcValue($result);
$resultxml = $r->getXml();
# Create the XML
$xml = "<methodResponse>\n" .
"<params>\n" .
"<param>\n" .
" <value>\n" .
' ' . $resultxml . "\n" .
" </value>\n" .
"</param>\n" .
"</params>\n" .
'</methodResponse>';
# Send it
$this->output($xml);
}
/**
* Send HTTP Headers
*
* This method sends a HTTP Header
*
* @param integer $code HTTP Status Code
* @param string $msg Header message
*/
protected function head($code, $msg)
{
$status_mode = preg_match('/cgi/', PHP_SAPI);
if ($status_mode) {
header('Status: ' . $code . ' ' . $msg);
} else {
header($msg, true, $code);
}
}
/**
* Method call
*
* This method calls the given XML-RPC method with arguments.
*
* @param string $methodname Method name
* @param array $args Method arguments
* @return mixed
*/
protected function call($methodname, $args)
{
if (!$this->hasMethod($methodname)) {
throw new xmlrpcException('server error. requested method "' . $methodname . '" does not exist.', -32601);
}
$method = $this->callbacks[$methodname];
# Perform the callback and send the response
if (!is_callable($method)) {
throw new xmlrpcException('server error. internal requested function for "' . $methodname . '" does not exist.', -32601);
}
return call_user_func_array($method, $args);
}
/**
* XML-RPC Error
*
* This method create an XML-RPC error message from a PHP Exception object.
* You should avoid using this in your own method and throw exceptions
* instead.
*
* @param Exception $e Exception object
*/
protected function error($e)
{
$msg = $e->getMessage();
$this->output(
"<methodResponse>\n" .
" <fault>\n" .
" <value>\n" .
" <struct>\n" .
" <member>\n" .
" <name>faultCode</name>\n" .
' <value><int>' . $e->getCode() . "</int></value>\n" .
" </member>\n" .
" <member>\n" .
" <name>faultString</name>\n" .
' <value><string>' . $msg . "</string></value>\n" .
" </member>\n" .
" </struct>\n" .
" </value>\n" .
" </fault>\n" .
"</methodResponse>\n"
);
}
/**
* Output response
*
* This method sends the whole XML-RPC response through HTTP.
*
* @param string $xml XML Content
*/
protected function output($xml)
{
$xml = '<?xml version="1.0" encoding="' . $this->encoding . '"?>' . "\n" . $xml;
$length = strlen($xml);
header('Connection: close');
header('Content-Length: ' . $length);
header('Content-Type: text/xml');
header('Date: ' . date('r'));
echo $xml;
exit;
}
/**
* XML-RPC Server has method?
*
* Returns true if the server has the given method <var>$method</var>
*
* @param string $method Method name
* @return boolean
*/
protected function hasMethod($method)
{
return in_array($method, array_keys($this->callbacks));
}
/**
* Server Capabilities
*
* This method initiates the server capabilities:
* - xmlrpc
* - faults_interop
* - system.multicall
*/
protected function setCapabilities()
{
# Initialises capabilities array
$this->capabilities = [
'xmlrpc' => [
'specUrl' => 'http://www.xmlrpc.com/spec',
'specVersion' => 1,
],
'faults_interop' => [
'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
'specVersion' => 20010516,
],
'system.multicall' => [
'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
'specVersion' => 1,
],
];
}
/**
* Server Methods
*
* This method creates the three main server's methods:
* - system.getCapabilities
* - system.listMethods
* - system.multicall
*
* @see getCapabilities()
* @see listMethods()
* @see multiCall()
*/
protected function setCallbacks()
{
$this->callbacks['system.getCapabilities'] = [$this, 'getCapabilities'];
$this->callbacks['system.listMethods'] = [$this, 'listMethods'];
$this->callbacks['system.multicall'] = [$this, 'multiCall'];
}
/**
* Server Capabilities
*
* Returns server capabilities
*
* @return array
*/
protected function getCapabilities()
{
return $this->capabilities;
}
/**
* Server methods
*
* Returns all server methods
*
* @return array
*/
protected function listMethods()
{
# Returns a list of methods - uses array_reverse to ensure user defined
# methods are listed before server defined methods
return array_reverse(array_keys($this->callbacks));
}
/**
* Multicall
*
* This method handles a multi-methods call
*
* @see http://www.xmlrpc.com/discuss/msgReader$1208
*
* @param array $methodcalls Array of methods
* @return array
*/
protected function multiCall($methodcalls)
{
$return = [];
foreach ($methodcalls as $call) {
$method = $call['methodName'];
$params = $call['params'];
try {
if ($method == 'system.multicall') {
throw new xmlrpcException('Recursive calls to system.multicall are forbidden', -32600);
}
$result = $this->call($method, $params);
$return[] = [$result];
} catch (Exception $e) {
$return[] = [
'faultCode' => $e->getCode(),
'faultString' => $e->getMessage(),
];
}
}
return $return;
}
}
/*
* @class xmlrpcIntrospectionServer
* @brief XML-RPC Introspection Server
*
* This class implements the most used type of XML-RPC Server.
* It allows you to create classes inherited from this one and add methods
* with {@link addCallback() addCallBack method}.
*
* This server class implements the following XML-RPC methods:
* - system.methodSignature
* - system.getCapabilities
* - system.listMethods
* - system.methodHelp
* - system.multicall
*/
/* @cond ONCE */
if (class_exists('xmlrpcServer')) {
/** @endcond */
class xmlrpcIntrospectionServer extends xmlrpcServer
{
protected $signatures;
protected $help;
/**
* Constructor
*
* This method should be inherited to add new callbacks with
* {@link addCallback()}.
*
* @param string $encoding Server encoding
*/
public function __construct($encoding = 'UTF-8')
{
$this->encoding = $encoding;
$this->setCallbacks();
$this->setCapabilities();
$this->capabilities['introspection'] = [
'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html',
'specVersion' => 1,
];
$this->addCallback(
'system.methodSignature',
[$this, 'methodSignature'],
['array', 'string'],
'Returns an array describing the return type and required parameters of a method'
);
$this->addCallback(
'system.getCapabilities',
[$this, 'getCapabilities'],
['struct'],
'Returns a struct describing the XML-RPC specifications supported by this server'
);
$this->addCallback(
'system.listMethods',
[$this, 'listMethods'],
['array'],
'Returns an array of available methods on this server'
);
$this->addCallback(
'system.methodHelp',
[$this, 'methodHelp'],
['string', 'string'],
'Returns a documentation string for the specified method'
);
$this->addCallback(
'system.multicall',
[$this, 'multiCall'],
['struct', 'array'],
'Returns result of multiple methods calls'
);
}
/**
* Add Server Callback
*
* This method creates a new XML-RPC method which references a class
* callback. <var>$callback</var> should be a valid PHP callback.
*
* @param string $method Method name
* @param callable $callback Method callback
* @param array $args Array of arguments type. The first is the returned one.
* @param string $help Method help string
*/
protected function addCallback($method, $callback, $args, $help)
{
$this->callbacks[$method] = $callback;
$this->signatures[$method] = $args;
$this->help[$method] = $help;
}
/**
* Method call
*
* This method calls the callbacks function or method for the given XML-RPC
* method <var>$methodname</var> with arguments in <var>$args</var> array.
*
* @param string $methodname Method name
* @param mixed $args Arguments
* @return mixed
*/
protected function call($methodname, $args)
{
# Make sure it's in an array
if ($args && !is_array($args)) {
$args = [$args];
}
# Over-rides default call method, adds signature check
if (!$this->hasMethod($methodname)) {
throw new xmlrpcException('Server error. Requested method "' . $methodname . '" not specified.', -32601);
}
$method = $this->callbacks[$methodname];
$signature = $this->signatures[$methodname];
if (!is_array($signature)) {
throw new xmlrpcException('Server error. Wrong method signature', -36600);
}
$return_type = array_shift($signature);
# Check the number of arguments
if (count($args) > count($signature)) {
throw new xmlrpcException('Server error. Wrong number of method parameters', -32602);
}
# Check the argument types
if (!$this->checkArgs($args, $signature)) {
throw new xmlrpcException('Server error. Invalid method parameters', -32602);
}
# It passed the test - run the "real" method call
return parent::call($methodname, $args);
}
/**
* Method Arguments Check
*
* This method checks the validity of method arguments.
*
* @param array $args Method given arguments
* @param array $signature Method defined arguments
* @return boolean
*/
protected function checkArgs($args, $signature)
{
for ($i = 0, $j = count($args); $i < $j; $i++) {
$arg = array_shift($args);
$type = array_shift($signature);
switch ($type) {
case 'int':
case 'i4':
if (is_array($arg) || !is_int($arg)) {
return false;
}
break;
case 'base64':
case 'string':
if (!is_string($arg)) {
return false;
}
break;
case 'boolean':
if ($arg !== false && $arg !== true) {
return false;
}
break;
case 'float':
case 'double':
if (!is_float($arg)) {
return false;
}
break;
case 'date':
case 'dateTime.iso8601':
if (!($arg instanceof xmlrpcDate)) {
return false;
}
break;
}
}
return true;
}
/**
* Method Signature
*
* This method return given XML-RPC method signature.
*
* @param string $method Method name
* @return array
*/
protected function methodSignature($method)
{
if (!$this->hasMethod($method)) {
throw new xmlrpcException('Server error. Requested method "' . $method . '" not specified.', -32601);
}
# We should be returning an array of types
$types = $this->signatures[$method];
$return = [];
foreach ($types as $type) {
switch ($type) {
case 'string':
$return[] = 'string';
break;
case 'int':
case 'i4':
$return[] = 42;
break;
case 'double':
$return[] = 3.1415;
break;
case 'dateTime.iso8601':
$return[] = new xmlrpcDate(time());
break;
case 'boolean':
$return[] = true;
break;
case 'base64':
$return[] = new xmlrpcBase64('base64');
break;
case 'array':
$return[] = ['array'];
break;
case 'struct':
$return[] = ['struct' => 'struct'];
break;
}
}
return $return;
}
/**
* Method Help
*
* This method return given XML-RPC method help string.
*
* @param string $method Method name
* @return string
*/
protected function methodHelp($method)
{
return $this->help[$method];
}
}
/* @cond ONCE */
}
/* @endcond */