3 * IXR - The Inutio XML-RPC Library
8 * @copyright Incutio Ltd 2002-2005
9 * @version 1.7 (beta) 23rd May 2005
10 * @author Simon Willison
11 * @link http://scripts.incutio.com/xmlrpc/ Site
12 * @link http://scripts.incutio.com/xmlrpc/manual.php Manual
13 * @license BSD License http://www.opensource.org/licenses/bsd-license.php
19 function IXR_Value ($data, $type = false) {
22 $type = $this->calculateType();
25 if ($type == 'struct') {
26 /* Turn all the values in the array in to new IXR_Value objects */
27 foreach ($this->data as $key => $value) {
28 $this->data[$key] = new IXR_Value($value);
31 if ($type == 'array') {
32 for ($i = 0, $j = count($this->data); $i < $j; $i++) {
33 $this->data[$i] = new IXR_Value($this->data[$i]);
37 function calculateType() {
38 if ($this->data === true || $this->data === false) {
41 if (is_integer($this->data)) {
44 if (is_double($this->data)) {
47 // Deal with IXR object types base64 and date
48 if (is_object($this->data) && is_a($this->data, 'IXR_Date')) {
51 if (is_object($this->data) && is_a($this->data, 'IXR_Base64')) {
54 // If it is a normal PHP object convert it in to a struct
55 if (is_object($this->data)) {
57 $this->data = get_object_vars($this->data);
60 if (!is_array($this->data)) {
63 /* We have an array - is it an array or a struct ? */
64 if ($this->isStruct($this->data)) {
71 /* Return XML for this value */
72 switch ($this->type) {
74 return '<boolean>'.(($this->data) ? '1' : '0').'</boolean>';
77 return '<int>'.$this->data.'</int>';
80 return '<double>'.$this->data.'</double>';
83 return '<string>'.htmlspecialchars($this->data).'</string>';
86 $return = '<array><data>'."\n";
87 foreach ($this->data as $item) {
88 $return .= ' <value>'.$item->getXml()."</value>\n";
90 $return .= '</data></array>';
94 $return = '<struct>'."\n";
95 foreach ($this->data as $name => $value) {
96 $name = htmlspecialchars($name);
97 $return .= " <member><name>$name</name><value>";
98 $return .= $value->getXml()."</value></member>\n";
100 $return .= '</struct>';
105 return $this->data->getXml();
110 function isStruct($array) {
111 /* Nasty function to check if an array is a struct or not */
113 foreach ($array as $key => $value) {
114 if ((string)$key != (string)$expected) {
126 var $messageType; // methodCall / methodResponse / fault
131 // Current variable stacks
132 var $_arraystructs = array(); // The stack used to keep track of the current array/struct
133 var $_arraystructstypes = array(); // Stack keeping track of if things are structs or array
134 var $_currentStructName = array(); // A stack as well
138 var $_currentTagContents;
141 function IXR_Message ($message) {
142 $this->message = $message;
145 // first remove the XML declaration
146 $this->message = preg_replace('/<\?xml(.*)?\?'.'>/', '', $this->message);
147 if (trim($this->message) == '') {
150 $this->_parser = xml_parser_create();
151 // Set XML parser to take the case of tags in to account
152 xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
153 // Set XML parser callback functions
154 xml_set_object($this->_parser, $this);
155 xml_set_element_handler($this->_parser, 'tag_open', 'tag_close');
156 xml_set_character_data_handler($this->_parser, 'cdata');
157 if (!xml_parse($this->_parser, $this->message)) {
158 /* die(sprintf('XML error: %s at line %d',
159 xml_error_string(xml_get_error_code($this->_parser)),
160 xml_get_current_line_number($this->_parser))); */
163 xml_parser_free($this->_parser);
164 // Grab the error messages, if any
165 if ($this->messageType == 'fault') {
166 $this->faultCode = $this->params[0]['faultCode'];
167 $this->faultString = $this->params[0]['faultString'];
171 function tag_open($parser, $tag, $attr) {
172 $this->_currentTagContents = '';
173 $this->currentTag = $tag;
176 case 'methodResponse':
178 $this->messageType = $tag;
180 /* Deal with stacks of arrays and structs */
181 case 'data': // data is to all intents and puposes more interesting than array
182 $this->_arraystructstypes[] = 'array';
183 $this->_arraystructs[] = array();
186 $this->_arraystructstypes[] = 'struct';
187 $this->_arraystructs[] = array();
191 function cdata($parser, $cdata) {
192 $this->_currentTagContents .= $cdata;
194 function tag_close($parser, $tag) {
199 $value = (int) trim($this->_currentTagContents);
203 $value = (double) trim($this->_currentTagContents);
207 $value = $this->_currentTagContents;
210 case 'dateTime.iso8601':
211 $value = new IXR_Date(trim($this->_currentTagContents));
212 // $value = $iso->getTimestamp();
216 // "If no type is indicated, the type is string."
217 if (trim($this->_currentTagContents) != '') {
218 $value = (string)$this->_currentTagContents;
223 $value = (boolean) trim($this->_currentTagContents);
227 $value = base64_decode( trim( $this->_currentTagContents ) );
230 /* Deal with stacks of arrays and structs */
233 $value = array_pop($this->_arraystructs);
234 array_pop($this->_arraystructstypes);
238 array_pop($this->_currentStructName);
241 $this->_currentStructName[] = trim($this->_currentTagContents);
244 $this->methodName = trim($this->_currentTagContents);
248 if (count($this->_arraystructs) > 0) {
249 // Add value to struct or array
250 if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == 'struct') {
252 $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value;
255 $this->_arraystructs[count($this->_arraystructs)-1][] = $value;
258 // Just add as a paramater
259 $this->params[] = $value;
262 $this->_currentTagContents = '';
269 var $callbacks = array();
272 function IXR_Server($callbacks = false, $data = false) {
273 $this->setCapabilities();
275 $this->callbacks = $callbacks;
277 $this->setCallbacks();
280 function serve($data = false) {
282 global $HTTP_RAW_POST_DATA;
283 if (!$HTTP_RAW_POST_DATA) {
284 die('XML-RPC server accepts POST requests only.');
286 $data = $HTTP_RAW_POST_DATA;
288 $this->message = new IXR_Message($data);
289 if (!$this->message->parse()) {
290 $this->error(-32700, 'parse error. not well formed');
292 if ($this->message->messageType != 'methodCall') {
293 $this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall');
295 $result = $this->call($this->message->methodName, $this->message->params);
296 // Is the result an error?
297 if (is_a($result, 'IXR_Error')) {
298 $this->error($result);
301 $r = new IXR_Value($result);
302 $resultxml = $r->getXml();
319 function call($methodname, $args) {
320 if (!$this->hasMethod($methodname)) {
321 return new IXR_Error(-32601, 'server error. requested method '.$methodname.' does not exist.');
323 $method = $this->callbacks[$methodname];
324 // Perform the callback and send the response
325 if (count($args) == 1) {
326 // If only one paramater just send that instead of the whole array
329 // Are we dealing with a function or a method?
330 if (substr($method, 0, 5) == 'this:') {
331 // It's a class method - check it exists
332 $method = substr($method, 5);
333 if (!method_exists($this, $method)) {
334 return new IXR_Error(-32601, 'server error. requested class method "'.$method.'" does not exist.');
337 $result = $this->$method($args);
339 // It's a function - does it exist?
340 if (is_array($method)) {
341 if (!method_exists($method[0], $method[1])) {
342 return new IXR_Error(-32601, 'server error. requested object method "'.$method[1].'" does not exist.');
344 } else if (!function_exists($method)) {
345 return new IXR_Error(-32601, 'server error. requested function "'.$method.'" does not exist.');
348 $result = call_user_func($method, $args);
353 function error($error, $message = false) {
354 // Accepts either an error object or an error code and message
355 if ($message && !is_object($error)) {
356 $error = new IXR_Error($error, $message);
358 $this->output($error->getXml());
360 function output($xml) {
361 $xml = '<?xml version="1.0"?>'."\n".$xml;
362 $length = strlen($xml);
363 header('Connection: close');
364 header('Content-Length: '.$length);
365 header('Content-Type: text/xml');
366 header('Date: '.date('r'));
370 function hasMethod($method) {
371 return in_array($method, array_keys($this->callbacks));
373 function setCapabilities() {
374 // Initialises capabilities array
375 $this->capabilities = array(
377 'specUrl' => 'http://www.xmlrpc.com/spec',
380 'faults_interop' => array(
381 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
382 'specVersion' => 20010516
384 'system.multicall' => array(
385 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
390 function getCapabilities($args) {
391 return $this->capabilities;
393 function setCallbacks() {
394 $this->callbacks['system.getCapabilities'] = 'this:getCapabilities';
395 $this->callbacks['system.listMethods'] = 'this:listMethods';
396 $this->callbacks['system.multicall'] = 'this:multiCall';
398 function listMethods($args) {
399 // Returns a list of methods - uses array_reverse to ensure user defined
400 // methods are listed before server defined methods
401 return array_reverse(array_keys($this->callbacks));
403 function multiCall($methodcalls) {
404 // See http://www.xmlrpc.com/discuss/msgReader$1208
406 foreach ($methodcalls as $call) {
407 $method = $call['methodName'];
408 $params = $call['params'];
409 if ($method == 'system.multicall') {
410 $result = new IXR_Error(-32600, 'Recursive calls to system.multicall are forbidden');
412 $result = $this->call($method, $params);
414 if (is_a($result, 'IXR_Error')) {
416 'faultCode' => $result->code,
417 'faultString' => $result->message
420 $return[] = array($result);
431 function IXR_Request($method, $args) {
432 $this->method = $method;
435 <?xml version="1.0"?>
437 <methodName>{$this->method}</methodName>
441 foreach ($this->args as $arg) {
442 $this->xml .= '<param><value>';
443 $v = new IXR_Value($arg);
444 $this->xml .= $v->getXml();
445 $this->xml .= "</value></param>\n";
447 $this->xml .= '</params></methodCall>';
449 function getLength() {
450 return strlen($this->xml);
464 var $message = false;
467 // Storage place for an error message
469 function IXR_Client($server, $path = false, $port = 80, $timeout = false) {
471 // Assume we have been given a URL instead
472 $bits = parse_url($server);
473 $this->server = $bits['host'];
474 $this->port = isset($bits['port']) ? $bits['port'] : 80;
475 $this->path = isset($bits['path']) ? $bits['path'] : '/';
476 // Make absolutely sure we have a path
481 $this->server = $server;
485 $this->useragent = 'Incutio XML-RPC';
486 $this->timeout = $timeout;
489 $args = func_get_args();
490 $method = array_shift($args);
491 $request = new IXR_Request($method, $args);
492 $length = $request->getLength();
493 $xml = $request->getXml();
495 $request = "POST {$this->path} HTTP/1.0$r";
496 $request .= "Host: {$this->server}$r";
497 $request .= "Content-Type: text/xml$r";
498 $request .= "User-Agent: {$this->useragent}$r";
499 $request .= "Content-length: {$length}$r$r";
501 // Now send the request
503 echo '<pre>'.htmlspecialchars($request)."\n</pre>\n\n";
505 if ($this->timeout) {
506 $fp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout);
508 $fp = @fsockopen($this->server, $this->port, $errno, $errstr);
511 $this->error = new IXR_Error(-32300, "transport error - could not open socket: $errno $errstr");
514 fputs($fp, $request);
516 $gotFirstLine = false;
517 $gettingHeaders = true;
519 $line = fgets($fp, 4096);
520 if (!$gotFirstLine) {
521 // Check line for '200'
522 if (strstr($line, '200') === false) {
523 $this->error = new IXR_Error(-32300, 'transport error - HTTP status code was not 200');
526 $gotFirstLine = true;
528 if (trim($line) == '') {
529 $gettingHeaders = false;
531 if (!$gettingHeaders) {
532 $contents .= trim($line)."\n";
536 echo '<pre>'.htmlspecialchars($contents)."\n</pre>\n\n";
538 // Now parse what we've got back
539 $this->message = new IXR_Message($contents);
540 if (!$this->message->parse()) {
542 $this->error = new IXR_Error(-32700, 'parse error. not well formed');
545 // Is the message a fault?
546 if ($this->message->messageType == 'fault') {
547 $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString);
550 // Message must be OK
553 function getResponse() {
554 // methodResponses can only have one param - return that
555 return $this->message->params[0];
558 return (is_object($this->error));
560 function getErrorCode() {
561 return $this->error->code;
563 function getErrorMessage() {
564 return $this->error->message;
572 function IXR_Error($code, $message) {
574 $this->message = htmlspecialchars($message);
583 <name>faultCode</name>
584 <value><int>{$this->code}</int></value>
587 <name>faultString</name>
588 <value><string>{$this->message}</string></value>
608 function IXR_Date($time) {
609 // $time can be a PHP timestamp or an ISO one
610 if (is_numeric($time)) {
611 $this->parseTimestamp($time);
613 $this->parseIso($time);
616 function parseTimestamp($timestamp) {
617 $this->year = date('Y', $timestamp);
618 $this->month = date('m', $timestamp);
619 $this->day = date('d', $timestamp);
620 $this->hour = date('H', $timestamp);
621 $this->minute = date('i', $timestamp);
622 $this->second = date('s', $timestamp);
624 function parseIso($iso) {
625 $this->year = substr($iso, 0, 4);
626 $this->month = substr($iso, 4, 2);
627 $this->day = substr($iso, 6, 2);
628 $this->hour = substr($iso, 9, 2);
629 $this->minute = substr($iso, 12, 2);
630 $this->second = substr($iso, 15, 2);
631 $this->timezone = substr($iso, 17);
634 return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second.$this->timezone;
637 return '<dateTime.iso8601>'.$this->getIso().'</dateTime.iso8601>';
639 function getTimestamp() {
640 return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year);
647 function IXR_Base64($data) {
651 return '<base64>'.base64_encode($this->data).'</base64>';
656 class IXR_IntrospectionServer extends IXR_Server {
659 function IXR_IntrospectionServer() {
660 $this->setCallbacks();
661 $this->setCapabilities();
662 $this->capabilities['introspection'] = array(
663 'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html',
667 'system.methodSignature',
668 'this:methodSignature',
669 array('array', 'string'),
670 'Returns an array describing the return type and required parameters of a method'
673 'system.getCapabilities',
674 'this:getCapabilities',
676 'Returns a struct describing the XML-RPC specifications supported by this server'
679 'system.listMethods',
682 'Returns an array of available methods on this server'
687 array('string', 'string'),
688 'Returns a documentation string for the specified method'
691 function addCallback($method, $callback, $args, $help) {
692 $this->callbacks[$method] = $callback;
693 $this->signatures[$method] = $args;
694 $this->help[$method] = $help;
696 function call($methodname, $args) {
697 // Make sure it's in an array
698 if ($args && !is_array($args)) {
699 $args = array($args);
701 // Over-rides default call method, adds signature check
702 if (!$this->hasMethod($methodname)) {
703 return new IXR_Error(-32601, 'server error. requested method "'.$this->message->methodName.'" not specified.');
705 $method = $this->callbacks[$methodname];
706 $signature = $this->signatures[$methodname];
707 $returnType = array_shift($signature);
708 // Check the number of arguments
709 if (count($args) != count($signature)) {
710 return new IXR_Error(-32602, 'server error. wrong number of method parameters');
712 // Check the argument types
715 for ($i = 0, $j = count($args); $i < $j; $i++) {
716 $arg = array_shift($args);
717 $type = array_shift($signature);
721 if (is_array($arg) || !is_int($arg)) {
727 if (!is_string($arg)) {
732 if ($arg !== false && $arg !== true) {
738 if (!is_float($arg)) {
743 case 'dateTime.iso8601':
744 if (!is_a($arg, 'IXR_Date')) {
750 return new IXR_Error(-32602, 'server error. invalid method parameters');
753 // It passed the test - run the "real" method call
754 return parent::call($methodname, $argsbackup);
756 function methodSignature($method) {
757 if (!$this->hasMethod($method)) {
758 return new IXR_Error(-32601, 'server error. requested method "'.$method.'" not specified.');
760 // We should be returning an array of types
761 $types = $this->signatures[$method];
763 foreach ($types as $type) {
766 $return[] = 'string';
775 case 'dateTime.iso8601':
776 $return[] = new IXR_Date(time());
782 $return[] = new IXR_Base64('base64');
785 $return[] = array('array');
788 $return[] = array('struct' => 'struct');
794 function methodHelp($method) {
795 return $this->help[$method];
800 class IXR_ClientMulticall extends IXR_Client {
801 var $calls = array();
802 function IXR_ClientMulticall($server, $path = false, $port = 80) {
803 parent::IXR_Client($server, $path, $port);
804 $this->useragent = 'The Incutio XML-RPC PHP Library (multicall client)';
807 $args = func_get_args();
808 $methodName = array_shift($args);
810 'methodName' => $methodName,
813 $this->calls[] = $struct;
816 // Prepare multicall, then call the parent::query() method
817 return parent::query('system.multicall', $this->calls);