*/ class IXR_Value { /** @var IXR_Value[]|IXR_Date|IXR_Base64|int|bool|double|string */ var $data; /** @var string */ var $type; /** * @param mixed $data * @param bool $type */ 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 IXR_Value objects foreach($this->data as $key => $value) { $this->data[$key] = new IXR_Value($value); } } if($type == 'array') { for($i = 0, $j = count($this->data); $i < $j; $i++) { $this->data[$i] = new IXR_Value($this->data[$i]); } } } /** * @return string */ 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 IXR object types base64 and date if(is_object($this->data) && is_a($this->data, 'IXR_Date')) { return 'date'; } if(is_object($this->data) && is_a($this->data, 'IXR_Base64')) { 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'; } } /** * @return bool|string */ function getXml() { // Return XML for this value switch($this->type) { case 'boolean': return '' . (($this->data) ? '1' : '0') . ''; break; case 'int': return '' . $this->data . ''; break; case 'double': return '' . $this->data . ''; break; case 'string': return '' . htmlspecialchars($this->data) . ''; break; case 'array': $return = '' . "\n"; foreach($this->data as $item) { $return .= ' ' . $item->getXml() . "\n"; } $return .= ''; return $return; break; case 'struct': $return = '' . "\n"; foreach($this->data as $name => $value) { $return .= " $name"; $return .= $value->getXml() . "\n"; } $return .= ''; return $return; break; case 'date': case 'base64': return $this->data->getXml(); break; } return false; } /** * Checks whether or not the supplied array is a struct or not * * @param array $array * @return boolean */ function isStruct($array) { $expected = 0; foreach($array as $key => $value) { if((string) $key != (string) $expected) { return true; } $expected++; } return false; } } /** * IXR_MESSAGE * * @package IXR * @since 1.5 * */ class IXR_Message { var $message; var $messageType; // methodCall / methodResponse / fault var $faultCode; var $faultString; var $methodName; var $params; // Current variable stacks var $_arraystructs = array(); // The stack used to keep track of the current array/struct var $_arraystructstypes = array(); // Stack keeping track of if things are structs or array var $_currentStructName = array(); // A stack as well var $_param; var $_value; var $_currentTag; var $_currentTagContents; var $_lastseen; // The XML parser var $_parser; /** * @param string $message */ function __construct($message) { $this->message =& $message; } /** * @return bool */ function parse() { // first remove the XML declaration // merged from WP #10698 - this method avoids the RAM usage of preg_replace on very large messages $header = preg_replace('/<\?xml.*?\?' . '>/', '', substr($this->message, 0, 100), 1); $this->message = substr_replace($this->message, $header, 0, 100); // workaround for a bug in PHP/libxml2, see http://bugs.php.net/bug.php?id=45996 $this->message = str_replace('<', '<', $this->message); $this->message = str_replace('>', '>', $this->message); $this->message = str_replace('&', '&', $this->message); $this->message = str_replace(''', ''', $this->message); $this->message = str_replace('"', '"', $this->message); $this->message = str_replace("\x0b", ' ', $this->message); //vertical tab if(trim($this->message) == '') { return false; } $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'); $chunk_size = 262144; // 256Kb, parse in chunks to avoid the RAM usage on very large messages $final = false; do { if(strlen($this->message) <= $chunk_size) { $final = true; } $part = substr($this->message, 0, $chunk_size); $this->message = substr($this->message, $chunk_size); if(!xml_parse($this->_parser, $part, $final)) { return false; } if($final) { break; } } while(true); 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; } /** * @param $parser * @param string $tag * @param $attr */ function tag_open($parser, $tag, $attr) { $this->_currentTagContents = ''; $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 purposes more interesting than array $this->_arraystructstypes[] = 'array'; $this->_arraystructs[] = array(); break; case 'struct': $this->_arraystructstypes[] = 'struct'; $this->_arraystructs[] = array(); break; } $this->_lastseen = $tag; } /** * @param $parser * @param string $cdata */ function cdata($parser, $cdata) { $this->_currentTagContents .= $cdata; } /** * @param $parser * @param $tag */ function tag_close($parser, $tag) { $value = null; $valueFlag = false; switch($tag) { case 'int': case 'i4': $value = (int) trim($this->_currentTagContents); $valueFlag = true; break; case 'double': $value = (double) trim($this->_currentTagContents); $valueFlag = true; break; case 'string': $value = (string) $this->_currentTagContents; $valueFlag = true; break; case 'dateTime.iso8601': $value = new IXR_Date(trim($this->_currentTagContents)); $valueFlag = true; break; case 'value': // "If no type is indicated, the type is string." if($this->_lastseen == 'value') { $value = (string) $this->_currentTagContents; $valueFlag = true; } break; case 'boolean': $value = (boolean) trim($this->_currentTagContents); $valueFlag = true; break; case 'base64': $value = base64_decode($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); break; case 'methodName': $this->methodName = trim($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 parameter $this->params[] = $value; } } $this->_currentTagContents = ''; $this->_lastseen = $tag; } } /** * IXR_Server * * @package IXR * @since 1.5 */ class IXR_Server { var $data; /** @var array */ var $callbacks = array(); var $message; /** @var array */ var $capabilities; /** * @param array|bool $callbacks * @param bool $data * @param bool $wait */ function __construct($callbacks = false, $data = false, $wait = false) { $this->setCapabilities(); if($callbacks) { $this->callbacks = $callbacks; } $this->setCallbacks(); if(!$wait) { $this->serve($data); } } /** * @param bool|string $data */ function serve($data = false) { if(!$data) { $postData = trim(http_get_raw_post_data()); if(!$postData) { header('Content-Type: text/plain'); // merged from WP #9093 die('XML-RPC server accepts POST requests only.'); } $data = $postData; } $this->message = new IXR_Message($data); if(!$this->message->parse()) { $this->error(-32700, 'parse error. not well formed'); } if($this->message->messageType != 'methodCall') { $this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall'); } $result = $this->call($this->message->methodName, $this->message->params); // Is the result an error? if(is_a($result, 'IXR_Error')) { $this->error($result); } // Encode the result $r = new IXR_Value($result); $resultxml = $r->getXml(); // Create the XML $xml = << $resultxml EOD; // Send it $this->output($xml); } /** * @param string $methodname * @param array $args * @return IXR_Error|mixed */ function call($methodname, $args) { if(!$this->hasMethod($methodname)) { return new IXR_Error(-32601, 'server error. requested method ' . $methodname . ' does not exist.'); } $method = $this->callbacks[$methodname]; // Perform the callback and send the response # Removed for DokuWiki to have a more consistent interface # if (count($args) == 1) { # // If only one parameter just send that instead of the whole array # $args = $args[0]; # } # Adjusted for DokuWiki to use call_user_func_array // args need to be an array $args = (array) $args; // Are we dealing with a function or a method? if(is_string($method) && substr($method, 0, 5) == 'this:') { // It's a class method - check it exists $method = substr($method, 5); if(!method_exists($this, $method)) { return new IXR_Error(-32601, 'server error. requested class method "' . $method . '" does not exist.'); } // Call the method #$result = $this->$method($args); $result = call_user_func_array(array(&$this, $method), $args); } elseif(substr($method, 0, 7) == 'plugin:') { list($pluginname, $callback) = explode(':', substr($method, 7), 2); if(!plugin_isdisabled($pluginname)) { $plugin = plugin_load('action', $pluginname); return call_user_func_array(array($plugin, $callback), $args); } else { return new IXR_Error(-99999, 'server error'); } } else { // It's a function - does it exist? if(is_array($method)) { if(!is_callable(array($method[0], $method[1]))) { return new IXR_Error(-32601, 'server error. requested object method "' . $method[1] . '" does not exist.'); } } else if(!function_exists($method)) { return new IXR_Error(-32601, 'server error. requested function "' . $method . '" does not exist.'); } // Call the function $result = call_user_func($method, $args); } return $result; } /** * @param int $error * @param string|bool $message */ function error($error, $message = false) { // Accepts either an error object or an error code and message if($message && !is_object($error)) { $error = new IXR_Error($error, $message); } $this->output($error->getXml()); } /** * @param string $xml */ function output($xml) { header('Content-Type: text/xml; charset=utf-8'); echo '', "\n", $xml; exit; } /** * @param string $method * @return bool */ function hasMethod($method) { return in_array($method, array_keys($this->callbacks)); } function setCapabilities() { // Initialises capabilities array $this->capabilities = array( 'xmlrpc' => array( 'specUrl' => 'http://www.xmlrpc.com/spec', 'specVersion' => 1 ), 'faults_interop' => array( 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php', 'specVersion' => 20010516 ), 'system.multicall' => array( 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208', 'specVersion' => 1 ), ); } /** * @return mixed */ function getCapabilities() { return $this->capabilities; } function setCallbacks() { $this->callbacks['system.getCapabilities'] = 'this:getCapabilities'; $this->callbacks['system.listMethods'] = 'this:listMethods'; $this->callbacks['system.multicall'] = 'this:multiCall'; } /** * @return array */ 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)); } /** * @param array $methodcalls * @return array */ function multiCall($methodcalls) { // See http://www.xmlrpc.com/discuss/msgReader$1208 $return = array(); foreach($methodcalls as $call) { $method = $call['methodName']; $params = $call['params']; if($method == 'system.multicall') { $result = new IXR_Error(-32800, 'Recursive calls to system.multicall are forbidden'); } else { $result = $this->call($method, $params); } if(is_a($result, 'IXR_Error')) { $return[] = array( 'faultCode' => $result->code, 'faultString' => $result->message ); } else { $return[] = array($result); } } return $return; } } /** * IXR_Request * * @package IXR * @since 1.5 */ class IXR_Request { /** @var string */ var $method; /** @var array */ var $args; /** @var string */ var $xml; /** * @param string $method * @param array $args */ function __construct($method, $args) { $this->method = $method; $this->args = $args; $this->xml = << {$this->method} EOD; foreach($this->args as $arg) { $this->xml .= ''; $v = new IXR_Value($arg); $this->xml .= $v->getXml(); $this->xml .= "\n"; } $this->xml .= ''; } /** * @return int */ function getLength() { return strlen($this->xml); } /** * @return string */ function getXml() { return $this->xml; } } /** * IXR_Client * * @package IXR * @since 1.5 * * Changed for DokuWiki to use DokuHTTPClient * * This should be compatible to the original class, but uses DokuWiki's * HTTP client library which will respect proxy settings * * Because the XMLRPC client is not used in DokuWiki currently this is completely * untested */ class IXR_Client extends DokuHTTPClient { var $posturl = ''; /** @var IXR_Message|bool */ var $message = false; // Storage place for an error message /** @var IXR_Error|bool */ var $xmlerror = false; /** * @param string $server * @param string|bool $path * @param int $port * @param int $timeout */ function __construct($server, $path = false, $port = 80, $timeout = 15) { parent::__construct(); if(!$path) { // Assume we have been given a URL instead $this->posturl = $server; } else { $this->posturl = 'http://' . $server . ':' . $port . $path; } $this->timeout = $timeout; } /** * parameters: method and arguments * @return bool success or error */ function query() { $args = func_get_args(); $method = array_shift($args); $request = new IXR_Request($method, $args); $xml = $request->getXml(); $this->headers['Content-Type'] = 'text/xml'; if(!$this->sendRequest($this->posturl, $xml, 'POST')) { $this->xmlerror = new IXR_Error(-32300, 'transport error - ' . $this->error); return false; } // Check HTTP Response code if($this->status < 200 || $this->status > 206) { $this->xmlerror = new IXR_Error(-32300, 'transport error - HTTP status ' . $this->status); return false; } // Now parse what we've got back $this->message = new IXR_Message($this->resp_body); if(!$this->message->parse()) { // XML error $this->xmlerror = new IXR_Error(-32700, 'parse error. not well formed'); return false; } // Is the message a fault? if($this->message->messageType == 'fault') { $this->xmlerror = new IXR_Error($this->message->faultCode, $this->message->faultString); return false; } // Message must be OK return true; } /** * @return mixed */ function getResponse() { // methodResponses can only have one param - return that return $this->message->params[0]; } /** * @return bool */ function isError() { return (is_object($this->xmlerror)); } /** * @return int */ function getErrorCode() { return $this->xmlerror->code; } /** * @return string */ function getErrorMessage() { return $this->xmlerror->message; } } /** * IXR_Error * * @package IXR * @since 1.5 */ class IXR_Error { var $code; var $message; /** * @param int $code * @param string $message */ function __construct($code, $message) { $this->code = $code; $this->message = htmlspecialchars($message); } /** * @return string */ function getXml() { $xml = << faultCode {$this->code} faultString {$this->message} EOD; return $xml; } } /** * IXR_Date * * @package IXR * @since 1.5 */ class IXR_Date { const XMLRPC_ISO8601 = "Ymd\TH:i:sO" ; /** @var DateTime */ protected $date; /** * @param int|string $time */ public function __construct($time) { // $time can be a PHP timestamp or an ISO one if(is_numeric($time)) { $this->parseTimestamp($time); } else { $this->parseIso($time); } } /** * Parse unix timestamp * * @param int $timestamp */ protected function parseTimestamp($timestamp) { $this->date = new DateTime('@' . $timestamp); } /** * Parses less or more complete iso dates and much more, if no timezone given assumes UTC * * @param string $iso */ protected function parseIso($iso) { $this->date = new DateTime($iso, new DateTimeZone("UTC")); } /** * Returns date in ISO 8601 format * * @return string */ public function getIso() { return $this->date->format(self::XMLRPC_ISO8601); } /** * Returns date in valid xml * * @return string */ public function getXml() { return '' . $this->getIso() . ''; } /** * Returns Unix timestamp * * @return int */ function getTimestamp() { return $this->date->getTimestamp(); } } /** * IXR_Base64 * * @package IXR * @since 1.5 */ class IXR_Base64 { var $data; /** * @param string $data */ function __construct($data) { $this->data = $data; } /** * @return string */ function getXml() { return '' . base64_encode($this->data) . ''; } } /** * IXR_IntrospectionServer * * @package IXR * @since 1.5 */ class IXR_IntrospectionServer extends IXR_Server { /** @var array[] */ var $signatures; /** @var string[] */ var $help; /** * Constructor */ function __construct() { $this->setCallbacks(); $this->setCapabilities(); $this->capabilities['introspection'] = array( 'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html', 'specVersion' => 1 ); $this->addCallback( 'system.methodSignature', 'this:methodSignature', array('array', 'string'), 'Returns an array describing the return type and required parameters of a method' ); $this->addCallback( 'system.getCapabilities', 'this:getCapabilities', array('struct'), 'Returns a struct describing the XML-RPC specifications supported by this server' ); $this->addCallback( 'system.listMethods', 'this:listMethods', array('array'), 'Returns an array of available methods on this server' ); $this->addCallback( 'system.methodHelp', 'this:methodHelp', array('string', 'string'), 'Returns a documentation string for the specified method' ); } /** * @param string $method * @param string $callback * @param string[] $args * @param string $help */ function addCallback($method, $callback, $args, $help) { $this->callbacks[$method] = $callback; $this->signatures[$method] = $args; $this->help[$method] = $help; } /** * @param string $methodname * @param array $args * @return IXR_Error|mixed */ function call($methodname, $args) { // Make sure it's in an array if($args && !is_array($args)) { $args = array($args); } // Over-rides default call method, adds signature check if(!$this->hasMethod($methodname)) { return new IXR_Error(-32601, 'server error. requested method "' . $this->message->methodName . '" not specified.'); } $method = $this->callbacks[$methodname]; $signature = $this->signatures[$methodname]; $returnType = array_shift($signature); // Check the number of arguments. Check only, if the minimum count of parameters is specified. More parameters are possible. // This is a hack to allow optional parameters... if(count($args) < count($signature)) { // print 'Num of args: '.count($args).' Num in signature: '.count($signature); return new IXR_Error(-32602, 'server error. wrong number of method parameters'); } // Check the argument types $ok = true; $argsbackup = $args; 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)) { $ok = false; } break; case 'base64': case 'string': if(!is_string($arg)) { $ok = false; } break; case 'boolean': if($arg !== false && $arg !== true) { $ok = false; } break; case 'float': case 'double': if(!is_float($arg)) { $ok = false; } break; case 'date': case 'dateTime.iso8601': if(!is_a($arg, 'IXR_Date')) { $ok = false; } break; } if(!$ok) { return new IXR_Error(-32602, 'server error. invalid method parameters'); } } // It passed the test - run the "real" method call return parent::call($methodname, $argsbackup); } /** * @param string $method * @return array|IXR_Error */ function methodSignature($method) { if(!$this->hasMethod($method)) { return new IXR_Error(-32601, 'server error. requested method "' . $method . '" not specified.'); } // We should be returning an array of types $types = $this->signatures[$method]; $return = array(); 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 IXR_Date(time()); break; case 'boolean': $return[] = true; break; case 'base64': $return[] = new IXR_Base64('base64'); break; case 'array': $return[] = array('array'); break; case 'struct': $return[] = array('struct' => 'struct'); break; } } return $return; } /** * @param string $method * @return mixed */ function methodHelp($method) { return $this->help[$method]; } } /** * IXR_ClientMulticall * * @package IXR * @since 1.5 */ class IXR_ClientMulticall extends IXR_Client { /** @var array[] */ var $calls = array(); /** * @param string $server * @param string|bool $path * @param int $port */ function __construct($server, $path = false, $port = 80) { parent::__construct($server, $path, $port); //$this->useragent = 'The Incutio XML-RPC PHP Library (multicall client)'; } /** * Add a call */ function addCall() { $args = func_get_args(); $methodName = array_shift($args); $struct = array( 'methodName' => $methodName, 'params' => $args ); $this->calls[] = $struct; } /** * @return bool */ function query() { // Prepare multicall, then call the parent::query() method return parent::query('system.multicall', $this->calls); } }