1362 lines
40 KiB
PHP
1362 lines
40 KiB
PHP
<?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 string $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>';
|
|
break;
|
|
case 'int':
|
|
return '<int>' . $this->data . '</int>';
|
|
break;
|
|
case 'double':
|
|
return '<double>' . $this->data . '</double>';
|
|
break;
|
|
case 'string':
|
|
return '<string>' . htmlspecialchars($this->data) . '</string>';
|
|
break;
|
|
case 'array':
|
|
$return = '<array><data>' . "\n";
|
|
foreach ($this->data as $item) {
|
|
$return .= ' <value>' . $item->getXml() . "</value>\n";
|
|
}
|
|
$return .= '</data></array>';
|
|
return $return;
|
|
break;
|
|
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;
|
|
break;
|
|
case 'date':
|
|
case 'base64':
|
|
return $this->data->getXml();
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
} else {
|
|
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(.*)?\?' . '>/', '', $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, false);
|
|
|
|
# Set XML parser callback functions
|
|
xml_set_object($this->_parser, $this);
|
|
xml_set_element_handler($this->_parser, 'tag_open', 'tag_close');
|
|
xml_set_character_data_handler($this->_parser, '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;
|
|
|
|
switch ($tag) {
|
|
case 'int':
|
|
case 'i4':
|
|
$value = (int) trim($this->_currentTagContents);
|
|
$this->_currentTagContents = '';
|
|
$valueFlag = true;
|
|
break;
|
|
case 'double':
|
|
$value = (double) trim($this->_currentTagContents);
|
|
$this->_currentTagContents = '';
|
|
$valueFlag = true;
|
|
break;
|
|
case 'string':
|
|
$value = (string) trim($this->_currentTagContents);
|
|
$this->_currentTagContents = '';
|
|
$valueFlag = true;
|
|
break;
|
|
case 'dateTime.iso8601':
|
|
$value = new xmlrpcDate(trim($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 = (boolean) trim($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($this->_currentTagContents);
|
|
$this->_currentTagContents = '';
|
|
break;
|
|
case 'methodName':
|
|
$this->methodName = trim($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
|
|
|
|
/**
|
|
* 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 false;
|
|
}
|
|
|
|
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>
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function query()
|
|
{
|
|
$args = func_get_args();
|
|
$method = array_shift($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>
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function addCall()
|
|
{
|
|
$args = func_get_args();
|
|
$methodName = array_shift($args);
|
|
|
|
$struct = [
|
|
'methodName' => $methodName,
|
|
'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.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function query()
|
|
{
|
|
# 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 array $callbacks Server callbacks
|
|
* @param string $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 string $data XML-RPC raw stream
|
|
*/
|
|
public function serve($data = false)
|
|
{
|
|
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 callback $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 array $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 */
|