WordPress 4.2
[autoinstalls/wordpress.git] / wp-includes / class-IXR.php
1 <?php
2 /**
3  * IXR - The Incutio XML-RPC Library
4  *
5  * Copyright (c) 2010, Incutio Ltd.
6  * All rights reserved.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions are met:
10  *
11  *  - Redistributions of source code must retain the above copyright notice,
12  *    this list of conditions and the following disclaimer.
13  *  - Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in the
15  *    documentation and/or other materials provided with the distribution.
16  *  - Neither the name of Incutio Ltd. nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
21  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
22  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
28  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
30  * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  *
32  * @package IXR
33  * @since 1.5.0
34  *
35  * @copyright  Incutio Ltd 2010 (http://www.incutio.com)
36  * @version    1.7.4 7th September 2010
37  * @author     Simon Willison
38  * @link       http://scripts.incutio.com/xmlrpc/ Site/manual
39  * @license    http://www.opensource.org/licenses/bsd-license.php BSD
40  */
41
42 /**
43  * IXR_Value
44  *
45  * @package IXR
46  * @since 1.5.0
47  */
48 class IXR_Value {
49     var $data;
50     var $type;
51
52     function IXR_Value($data, $type = false)
53     {
54         $this->data = $data;
55         if (!$type) {
56             $type = $this->calculateType();
57         }
58         $this->type = $type;
59         if ($type == 'struct') {
60             // Turn all the values in the array in to new IXR_Value objects
61             foreach ($this->data as $key => $value) {
62                 $this->data[$key] = new IXR_Value($value);
63             }
64         }
65         if ($type == 'array') {
66             for ($i = 0, $j = count($this->data); $i < $j; $i++) {
67                 $this->data[$i] = new IXR_Value($this->data[$i]);
68             }
69         }
70     }
71
72     function calculateType()
73     {
74         if ($this->data === true || $this->data === false) {
75             return 'boolean';
76         }
77         if (is_integer($this->data)) {
78             return 'int';
79         }
80         if (is_double($this->data)) {
81             return 'double';
82         }
83
84         // Deal with IXR object types base64 and date
85         if (is_object($this->data) && is_a($this->data, 'IXR_Date')) {
86             return 'date';
87         }
88         if (is_object($this->data) && is_a($this->data, 'IXR_Base64')) {
89             return 'base64';
90         }
91
92         // If it is a normal PHP object convert it in to a struct
93         if (is_object($this->data)) {
94             $this->data = get_object_vars($this->data);
95             return 'struct';
96         }
97         if (!is_array($this->data)) {
98             return 'string';
99         }
100
101         // We have an array - is it an array or a struct?
102         if ($this->isStruct($this->data)) {
103             return 'struct';
104         } else {
105             return 'array';
106         }
107     }
108
109     function getXml()
110     {
111         // Return XML for this value
112         switch ($this->type) {
113             case 'boolean':
114                 return '<boolean>'.(($this->data) ? '1' : '0').'</boolean>';
115                 break;
116             case 'int':
117                 return '<int>'.$this->data.'</int>';
118                 break;
119             case 'double':
120                 return '<double>'.$this->data.'</double>';
121                 break;
122             case 'string':
123                 return '<string>'.htmlspecialchars($this->data).'</string>';
124                 break;
125             case 'array':
126                 $return = '<array><data>'."\n";
127                 foreach ($this->data as $item) {
128                     $return .= '  <value>'.$item->getXml()."</value>\n";
129                 }
130                 $return .= '</data></array>';
131                 return $return;
132                 break;
133             case 'struct':
134                 $return = '<struct>'."\n";
135                 foreach ($this->data as $name => $value) {
136                                         $name = htmlspecialchars($name);
137                     $return .= "  <member><name>$name</name><value>";
138                     $return .= $value->getXml()."</value></member>\n";
139                 }
140                 $return .= '</struct>';
141                 return $return;
142                 break;
143             case 'date':
144             case 'base64':
145                 return $this->data->getXml();
146                 break;
147         }
148         return false;
149     }
150
151     /**
152      * Checks whether or not the supplied array is a struct or not
153      *
154      * @param array $array
155      * @return boolean
156      */
157     function isStruct($array)
158     {
159         $expected = 0;
160         foreach ($array as $key => $value) {
161             if ((string)$key != (string)$expected) {
162                 return true;
163             }
164             $expected++;
165         }
166         return false;
167     }
168 }
169
170 /**
171  * IXR_MESSAGE
172  *
173  * @package IXR
174  * @since 1.5.0
175  *
176  */
177 class IXR_Message
178 {
179     var $message;
180     var $messageType;  // methodCall / methodResponse / fault
181     var $faultCode;
182     var $faultString;
183     var $methodName;
184     var $params;
185
186     // Current variable stacks
187     var $_arraystructs = array();   // The stack used to keep track of the current array/struct
188     var $_arraystructstypes = array(); // Stack keeping track of if things are structs or array
189     var $_currentStructName = array();  // A stack as well
190     var $_param;
191     var $_value;
192     var $_currentTag;
193     var $_currentTagContents;
194     // The XML parser
195     var $_parser;
196
197     function IXR_Message($message)
198     {
199         $this->message =& $message;
200     }
201
202     function parse()
203     {
204         // first remove the XML declaration
205         // merged from WP #10698 - this method avoids the RAM usage of preg_replace on very large messages
206         $header = preg_replace( '/<\?xml.*?\?'.'>/s', '', substr( $this->message, 0, 100 ), 1 );
207         $this->message = trim( substr_replace( $this->message, $header, 0, 100 ) );
208         if ( '' == $this->message ) {
209             return false;
210         }
211
212         // Then remove the DOCTYPE
213         $header = preg_replace( '/^<!DOCTYPE[^>]*+>/i', '', substr( $this->message, 0, 200 ), 1 );
214         $this->message = trim( substr_replace( $this->message, $header, 0, 200 ) );
215         if ( '' == $this->message ) {
216             return false;
217         }
218
219         // Check that the root tag is valid
220         $root_tag = substr( $this->message, 0, strcspn( substr( $this->message, 0, 20 ), "> \t\r\n" ) );
221         if ( '<!DOCTYPE' === strtoupper( $root_tag ) ) {
222             return false;
223         }
224         if ( ! in_array( $root_tag, array( '<methodCall', '<methodResponse', '<fault' ) ) ) {
225             return false;
226         }
227
228         // Bail if there are too many elements to parse
229         $element_limit = 30000;
230         if ( function_exists( 'apply_filters' ) ) {
231             /**
232              * Filter the number of elements to parse in an XML-RPC response.
233              *
234              * @since 4.0.0
235              *
236              * @param int $element_limit Default elements limit.
237              */
238             $element_limit = apply_filters( 'xmlrpc_element_limit', $element_limit );
239         }
240         if ( $element_limit && 2 * $element_limit < substr_count( $this->message, '<' ) ) {
241             return false;
242         }
243
244         $this->_parser = xml_parser_create();
245         // Set XML parser to take the case of tags in to account
246         xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
247         // Set XML parser callback functions
248         xml_set_object($this->_parser, $this);
249         xml_set_element_handler($this->_parser, 'tag_open', 'tag_close');
250         xml_set_character_data_handler($this->_parser, 'cdata');
251         $chunk_size = 262144; // 256Kb, parse in chunks to avoid the RAM usage on very large messages
252         $final = false;
253         do {
254             if (strlen($this->message) <= $chunk_size) {
255                 $final = true;
256             }
257             $part = substr($this->message, 0, $chunk_size);
258             $this->message = substr($this->message, $chunk_size);
259             if (!xml_parse($this->_parser, $part, $final)) {
260                 return false;
261             }
262             if ($final) {
263                 break;
264             }
265         } while (true);
266         xml_parser_free($this->_parser);
267
268         // Grab the error messages, if any
269         if ($this->messageType == 'fault') {
270             $this->faultCode = $this->params[0]['faultCode'];
271             $this->faultString = $this->params[0]['faultString'];
272         }
273         return true;
274     }
275
276     function tag_open($parser, $tag, $attr)
277     {
278         $this->_currentTagContents = '';
279         $this->currentTag = $tag;
280         switch($tag) {
281             case 'methodCall':
282             case 'methodResponse':
283             case 'fault':
284                 $this->messageType = $tag;
285                 break;
286                 /* Deal with stacks of arrays and structs */
287             case 'data':    // data is to all intents and puposes more interesting than array
288                 $this->_arraystructstypes[] = 'array';
289                 $this->_arraystructs[] = array();
290                 break;
291             case 'struct':
292                 $this->_arraystructstypes[] = 'struct';
293                 $this->_arraystructs[] = array();
294                 break;
295         }
296     }
297
298     function cdata($parser, $cdata)
299     {
300         $this->_currentTagContents .= $cdata;
301     }
302
303     function tag_close($parser, $tag)
304     {
305         $valueFlag = false;
306         switch($tag) {
307             case 'int':
308             case 'i4':
309                 $value = (int)trim($this->_currentTagContents);
310                 $valueFlag = true;
311                 break;
312             case 'double':
313                 $value = (double)trim($this->_currentTagContents);
314                 $valueFlag = true;
315                 break;
316             case 'string':
317                 $value = (string)trim($this->_currentTagContents);
318                 $valueFlag = true;
319                 break;
320             case 'dateTime.iso8601':
321                 $value = new IXR_Date(trim($this->_currentTagContents));
322                 $valueFlag = true;
323                 break;
324             case 'value':
325                 // "If no type is indicated, the type is string."
326                 if (trim($this->_currentTagContents) != '') {
327                     $value = (string)$this->_currentTagContents;
328                     $valueFlag = true;
329                 }
330                 break;
331             case 'boolean':
332                 $value = (boolean)trim($this->_currentTagContents);
333                 $valueFlag = true;
334                 break;
335             case 'base64':
336                 $value = base64_decode($this->_currentTagContents);
337                 $valueFlag = true;
338                 break;
339                 /* Deal with stacks of arrays and structs */
340             case 'data':
341             case 'struct':
342                 $value = array_pop($this->_arraystructs);
343                 array_pop($this->_arraystructstypes);
344                 $valueFlag = true;
345                 break;
346             case 'member':
347                 array_pop($this->_currentStructName);
348                 break;
349             case 'name':
350                 $this->_currentStructName[] = trim($this->_currentTagContents);
351                 break;
352             case 'methodName':
353                 $this->methodName = trim($this->_currentTagContents);
354                 break;
355         }
356
357         if ($valueFlag) {
358             if (count($this->_arraystructs) > 0) {
359                 // Add value to struct or array
360                 if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == 'struct') {
361                     // Add to struct
362                     $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value;
363                 } else {
364                     // Add to array
365                     $this->_arraystructs[count($this->_arraystructs)-1][] = $value;
366                 }
367             } else {
368                 // Just add as a parameter
369                 $this->params[] = $value;
370             }
371         }
372         $this->_currentTagContents = '';
373     }
374 }
375
376 /**
377  * IXR_Server
378  *
379  * @package IXR
380  * @since 1.5.0
381  */
382 class IXR_Server
383 {
384     var $data;
385     var $callbacks = array();
386     var $message;
387     var $capabilities;
388
389     function IXR_Server($callbacks = false, $data = false, $wait = false)
390     {
391         $this->setCapabilities();
392         if ($callbacks) {
393             $this->callbacks = $callbacks;
394         }
395         $this->setCallbacks();
396         if (!$wait) {
397             $this->serve($data);
398         }
399     }
400
401     function serve($data = false)
402     {
403         if (!$data) {
404             if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'POST') {
405                 if ( function_exists( 'status_header' ) ) {
406                     status_header( 405 ); // WP #20986
407                     header( 'Allow: POST' );
408                 }
409                 header('Content-Type: text/plain'); // merged from WP #9093
410                 die('XML-RPC server accepts POST requests only.');
411             }
412
413             global $HTTP_RAW_POST_DATA;
414             if (empty($HTTP_RAW_POST_DATA)) {
415                 // workaround for a bug in PHP 5.2.2 - http://bugs.php.net/bug.php?id=41293
416                 $data = file_get_contents('php://input');
417             } else {
418                 $data =& $HTTP_RAW_POST_DATA;
419             }
420         }
421         $this->message = new IXR_Message($data);
422         if (!$this->message->parse()) {
423             $this->error(-32700, 'parse error. not well formed');
424         }
425         if ($this->message->messageType != 'methodCall') {
426             $this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall');
427         }
428         $result = $this->call($this->message->methodName, $this->message->params);
429
430         // Is the result an error?
431         if (is_a($result, 'IXR_Error')) {
432             $this->error($result);
433         }
434
435         // Encode the result
436         $r = new IXR_Value($result);
437         $resultxml = $r->getXml();
438
439         // Create the XML
440         $xml = <<<EOD
441 <methodResponse>
442   <params>
443     <param>
444       <value>
445       $resultxml
446       </value>
447     </param>
448   </params>
449 </methodResponse>
450
451 EOD;
452       // Send it
453       $this->output($xml);
454     }
455
456     function call($methodname, $args)
457     {
458         if (!$this->hasMethod($methodname)) {
459             return new IXR_Error(-32601, 'server error. requested method '.$methodname.' does not exist.');
460         }
461         $method = $this->callbacks[$methodname];
462
463         // Perform the callback and send the response
464         if (count($args) == 1) {
465             // If only one parameter just send that instead of the whole array
466             $args = $args[0];
467         }
468
469         // Are we dealing with a function or a method?
470         if (is_string($method) && substr($method, 0, 5) == 'this:') {
471             // It's a class method - check it exists
472             $method = substr($method, 5);
473             if (!method_exists($this, $method)) {
474                 return new IXR_Error(-32601, 'server error. requested class method "'.$method.'" does not exist.');
475             }
476
477             //Call the method
478             $result = $this->$method($args);
479         } else {
480             // It's a function - does it exist?
481             if (is_array($method)) {
482                 if (!is_callable(array($method[0], $method[1]))) {
483                     return new IXR_Error(-32601, 'server error. requested object method "'.$method[1].'" does not exist.');
484                 }
485             } else if (!function_exists($method)) {
486                 return new IXR_Error(-32601, 'server error. requested function "'.$method.'" does not exist.');
487             }
488
489             // Call the function
490             $result = call_user_func($method, $args);
491         }
492         return $result;
493     }
494
495     function error($error, $message = false)
496     {
497         // Accepts either an error object or an error code and message
498         if ($message && !is_object($error)) {
499             $error = new IXR_Error($error, $message);
500         }
501         $this->output($error->getXml());
502     }
503
504     function output($xml)
505     {
506         $charset = function_exists('get_option') ? get_option('blog_charset') : '';
507         if ($charset)
508             $xml = '<?xml version="1.0" encoding="'.$charset.'"?>'."\n".$xml;
509         else
510             $xml = '<?xml version="1.0"?>'."\n".$xml;
511         $length = strlen($xml);
512         header('Connection: close');
513         header('Content-Length: '.$length);
514         if ($charset)
515             header('Content-Type: text/xml; charset='.$charset);
516         else
517             header('Content-Type: text/xml');
518         header('Date: '.date('r'));
519         echo $xml;
520         exit;
521     }
522
523     function hasMethod($method)
524     {
525         return in_array($method, array_keys($this->callbacks));
526     }
527
528     function setCapabilities()
529     {
530         // Initialises capabilities array
531         $this->capabilities = array(
532             'xmlrpc' => array(
533                 'specUrl' => 'http://www.xmlrpc.com/spec',
534                 'specVersion' => 1
535         ),
536             'faults_interop' => array(
537                 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
538                 'specVersion' => 20010516
539         ),
540             'system.multicall' => array(
541                 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
542                 'specVersion' => 1
543         ),
544         );
545     }
546
547     function getCapabilities($args)
548     {
549         return $this->capabilities;
550     }
551
552     function setCallbacks()
553     {
554         $this->callbacks['system.getCapabilities'] = 'this:getCapabilities';
555         $this->callbacks['system.listMethods'] = 'this:listMethods';
556         $this->callbacks['system.multicall'] = 'this:multiCall';
557     }
558
559     function listMethods($args)
560     {
561         // Returns a list of methods - uses array_reverse to ensure user defined
562         // methods are listed before server defined methods
563         return array_reverse(array_keys($this->callbacks));
564     }
565
566     function multiCall($methodcalls)
567     {
568         // See http://www.xmlrpc.com/discuss/msgReader$1208
569         $return = array();
570         foreach ($methodcalls as $call) {
571             $method = $call['methodName'];
572             $params = $call['params'];
573             if ($method == 'system.multicall') {
574                 $result = new IXR_Error(-32600, 'Recursive calls to system.multicall are forbidden');
575             } else {
576                 $result = $this->call($method, $params);
577             }
578             if (is_a($result, 'IXR_Error')) {
579                 $return[] = array(
580                     'faultCode' => $result->code,
581                     'faultString' => $result->message
582                 );
583             } else {
584                 $return[] = array($result);
585             }
586         }
587         return $return;
588     }
589 }
590
591 /**
592  * IXR_Request
593  *
594  * @package IXR
595  * @since 1.5.0
596  */
597 class IXR_Request
598 {
599     var $method;
600     var $args;
601     var $xml;
602
603     function IXR_Request($method, $args)
604     {
605         $this->method = $method;
606         $this->args = $args;
607         $this->xml = <<<EOD
608 <?xml version="1.0"?>
609 <methodCall>
610 <methodName>{$this->method}</methodName>
611 <params>
612
613 EOD;
614         foreach ($this->args as $arg) {
615             $this->xml .= '<param><value>';
616             $v = new IXR_Value($arg);
617             $this->xml .= $v->getXml();
618             $this->xml .= "</value></param>\n";
619         }
620         $this->xml .= '</params></methodCall>';
621     }
622
623     function getLength()
624     {
625         return strlen($this->xml);
626     }
627
628     function getXml()
629     {
630         return $this->xml;
631     }
632 }
633
634 /**
635  * IXR_Client
636  *
637  * @package IXR
638  * @since 1.5.0
639  *
640  */
641 class IXR_Client
642 {
643     var $server;
644     var $port;
645     var $path;
646     var $useragent;
647     var $response;
648     var $message = false;
649     var $debug = false;
650     var $timeout;
651     var $headers = array();
652
653     // Storage place for an error message
654     var $error = false;
655
656     function IXR_Client($server, $path = false, $port = 80, $timeout = 15)
657     {
658         if (!$path) {
659             // Assume we have been given a URL instead
660             $bits = parse_url($server);
661             $this->server = $bits['host'];
662             $this->port = isset($bits['port']) ? $bits['port'] : 80;
663             $this->path = isset($bits['path']) ? $bits['path'] : '/';
664
665             // Make absolutely sure we have a path
666             if (!$this->path) {
667                 $this->path = '/';
668             }
669
670             if ( ! empty( $bits['query'] ) ) {
671                 $this->path .= '?' . $bits['query'];
672             }
673         } else {
674             $this->server = $server;
675             $this->path = $path;
676             $this->port = $port;
677         }
678         $this->useragent = 'The Incutio XML-RPC PHP Library';
679         $this->timeout = $timeout;
680     }
681
682     function query()
683     {
684         $args = func_get_args();
685         $method = array_shift($args);
686         $request = new IXR_Request($method, $args);
687         $length = $request->getLength();
688         $xml = $request->getXml();
689         $r = "\r\n";
690         $request  = "POST {$this->path} HTTP/1.0$r";
691
692         // Merged from WP #8145 - allow custom headers
693         $this->headers['Host']          = $this->server;
694         $this->headers['Content-Type']  = 'text/xml';
695         $this->headers['User-Agent']    = $this->useragent;
696         $this->headers['Content-Length']= $length;
697
698         foreach( $this->headers as $header => $value ) {
699             $request .= "{$header}: {$value}{$r}";
700         }
701         $request .= $r;
702
703         $request .= $xml;
704
705         // Now send the request
706         if ($this->debug) {
707             echo '<pre class="ixr_request">'.htmlspecialchars($request)."\n</pre>\n\n";
708         }
709
710         if ($this->timeout) {
711             $fp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout);
712         } else {
713             $fp = @fsockopen($this->server, $this->port, $errno, $errstr);
714         }
715         if (!$fp) {
716             $this->error = new IXR_Error(-32300, 'transport error - could not open socket');
717             return false;
718         }
719         fputs($fp, $request);
720         $contents = '';
721         $debugContents = '';
722         $gotFirstLine = false;
723         $gettingHeaders = true;
724         while (!feof($fp)) {
725             $line = fgets($fp, 4096);
726             if (!$gotFirstLine) {
727                 // Check line for '200'
728                 if (strstr($line, '200') === false) {
729                     $this->error = new IXR_Error(-32300, 'transport error - HTTP status code was not 200');
730                     return false;
731                 }
732                 $gotFirstLine = true;
733             }
734             if (trim($line) == '') {
735                 $gettingHeaders = false;
736             }
737             if (!$gettingHeaders) {
738                 // merged from WP #12559 - remove trim
739                 $contents .= $line;
740             }
741             if ($this->debug) {
742                 $debugContents .= $line;
743             }
744         }
745         if ($this->debug) {
746             echo '<pre class="ixr_response">'.htmlspecialchars($debugContents)."\n</pre>\n\n";
747         }
748
749         // Now parse what we've got back
750         $this->message = new IXR_Message($contents);
751         if (!$this->message->parse()) {
752             // XML error
753             $this->error = new IXR_Error(-32700, 'parse error. not well formed');
754             return false;
755         }
756
757         // Is the message a fault?
758         if ($this->message->messageType == 'fault') {
759             $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString);
760             return false;
761         }
762
763         // Message must be OK
764         return true;
765     }
766
767     function getResponse()
768     {
769         // methodResponses can only have one param - return that
770         return $this->message->params[0];
771     }
772
773     function isError()
774     {
775         return (is_object($this->error));
776     }
777
778     function getErrorCode()
779     {
780         return $this->error->code;
781     }
782
783     function getErrorMessage()
784     {
785         return $this->error->message;
786     }
787 }
788
789
790 /**
791  * IXR_Error
792  *
793  * @package IXR
794  * @since 1.5.0
795  */
796 class IXR_Error
797 {
798     var $code;
799     var $message;
800
801     function IXR_Error($code, $message)
802     {
803         $this->code = $code;
804         $this->message = htmlspecialchars($message);
805     }
806
807     function getXml()
808     {
809         $xml = <<<EOD
810 <methodResponse>
811   <fault>
812     <value>
813       <struct>
814         <member>
815           <name>faultCode</name>
816           <value><int>{$this->code}</int></value>
817         </member>
818         <member>
819           <name>faultString</name>
820           <value><string>{$this->message}</string></value>
821         </member>
822       </struct>
823     </value>
824   </fault>
825 </methodResponse>
826
827 EOD;
828         return $xml;
829     }
830 }
831
832 /**
833  * IXR_Date
834  *
835  * @package IXR
836  * @since 1.5.0
837  */
838 class IXR_Date {
839     var $year;
840     var $month;
841     var $day;
842     var $hour;
843     var $minute;
844     var $second;
845     var $timezone;
846
847     function IXR_Date($time)
848     {
849         // $time can be a PHP timestamp or an ISO one
850         if (is_numeric($time)) {
851             $this->parseTimestamp($time);
852         } else {
853             $this->parseIso($time);
854         }
855     }
856
857     function parseTimestamp($timestamp)
858     {
859         $this->year = date('Y', $timestamp);
860         $this->month = date('m', $timestamp);
861         $this->day = date('d', $timestamp);
862         $this->hour = date('H', $timestamp);
863         $this->minute = date('i', $timestamp);
864         $this->second = date('s', $timestamp);
865         $this->timezone = '';
866     }
867
868     function parseIso($iso)
869     {
870         $this->year = substr($iso, 0, 4);
871         $this->month = substr($iso, 4, 2);
872         $this->day = substr($iso, 6, 2);
873         $this->hour = substr($iso, 9, 2);
874         $this->minute = substr($iso, 12, 2);
875         $this->second = substr($iso, 15, 2);
876         $this->timezone = substr($iso, 17);
877     }
878
879     function getIso()
880     {
881         return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second.$this->timezone;
882     }
883
884     function getXml()
885     {
886         return '<dateTime.iso8601>'.$this->getIso().'</dateTime.iso8601>';
887     }
888
889     function getTimestamp()
890     {
891         return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year);
892     }
893 }
894
895 /**
896  * IXR_Base64
897  *
898  * @package IXR
899  * @since 1.5.0
900  */
901 class IXR_Base64
902 {
903     var $data;
904
905     function IXR_Base64($data)
906     {
907         $this->data = $data;
908     }
909
910     function getXml()
911     {
912         return '<base64>'.base64_encode($this->data).'</base64>';
913     }
914 }
915
916 /**
917  * IXR_IntrospectionServer
918  *
919  * @package IXR
920  * @since 1.5.0
921  */
922 class IXR_IntrospectionServer extends IXR_Server
923 {
924     var $signatures;
925     var $help;
926
927     function IXR_IntrospectionServer()
928     {
929         $this->setCallbacks();
930         $this->setCapabilities();
931         $this->capabilities['introspection'] = array(
932             'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html',
933             'specVersion' => 1
934         );
935         $this->addCallback(
936             'system.methodSignature',
937             'this:methodSignature',
938             array('array', 'string'),
939             'Returns an array describing the return type and required parameters of a method'
940         );
941         $this->addCallback(
942             'system.getCapabilities',
943             'this:getCapabilities',
944             array('struct'),
945             'Returns a struct describing the XML-RPC specifications supported by this server'
946         );
947         $this->addCallback(
948             'system.listMethods',
949             'this:listMethods',
950             array('array'),
951             'Returns an array of available methods on this server'
952         );
953         $this->addCallback(
954             'system.methodHelp',
955             'this:methodHelp',
956             array('string', 'string'),
957             'Returns a documentation string for the specified method'
958         );
959     }
960
961     function addCallback($method, $callback, $args, $help)
962     {
963         $this->callbacks[$method] = $callback;
964         $this->signatures[$method] = $args;
965         $this->help[$method] = $help;
966     }
967
968     function call($methodname, $args)
969     {
970         // Make sure it's in an array
971         if ($args && !is_array($args)) {
972             $args = array($args);
973         }
974
975         // Over-rides default call method, adds signature check
976         if (!$this->hasMethod($methodname)) {
977             return new IXR_Error(-32601, 'server error. requested method "'.$this->message->methodName.'" not specified.');
978         }
979         $method = $this->callbacks[$methodname];
980         $signature = $this->signatures[$methodname];
981         $returnType = array_shift($signature);
982
983         // Check the number of arguments
984         if (count($args) != count($signature)) {
985             return new IXR_Error(-32602, 'server error. wrong number of method parameters');
986         }
987
988         // Check the argument types
989         $ok = true;
990         $argsbackup = $args;
991         for ($i = 0, $j = count($args); $i < $j; $i++) {
992             $arg = array_shift($args);
993             $type = array_shift($signature);
994             switch ($type) {
995                 case 'int':
996                 case 'i4':
997                     if (is_array($arg) || !is_int($arg)) {
998                         $ok = false;
999                     }
1000                     break;
1001                 case 'base64':
1002                 case 'string':
1003                     if (!is_string($arg)) {
1004                         $ok = false;
1005                     }
1006                     break;
1007                 case 'boolean':
1008                     if ($arg !== false && $arg !== true) {
1009                         $ok = false;
1010                     }
1011                     break;
1012                 case 'float':
1013                 case 'double':
1014                     if (!is_float($arg)) {
1015                         $ok = false;
1016                     }
1017                     break;
1018                 case 'date':
1019                 case 'dateTime.iso8601':
1020                     if (!is_a($arg, 'IXR_Date')) {
1021                         $ok = false;
1022                     }
1023                     break;
1024             }
1025             if (!$ok) {
1026                 return new IXR_Error(-32602, 'server error. invalid method parameters');
1027             }
1028         }
1029         // It passed the test - run the "real" method call
1030         return parent::call($methodname, $argsbackup);
1031     }
1032
1033     function methodSignature($method)
1034     {
1035         if (!$this->hasMethod($method)) {
1036             return new IXR_Error(-32601, 'server error. requested method "'.$method.'" not specified.');
1037         }
1038         // We should be returning an array of types
1039         $types = $this->signatures[$method];
1040         $return = array();
1041         foreach ($types as $type) {
1042             switch ($type) {
1043                 case 'string':
1044                     $return[] = 'string';
1045                     break;
1046                 case 'int':
1047                 case 'i4':
1048                     $return[] = 42;
1049                     break;
1050                 case 'double':
1051                     $return[] = 3.1415;
1052                     break;
1053                 case 'dateTime.iso8601':
1054                     $return[] = new IXR_Date(time());
1055                     break;
1056                 case 'boolean':
1057                     $return[] = true;
1058                     break;
1059                 case 'base64':
1060                     $return[] = new IXR_Base64('base64');
1061                     break;
1062                 case 'array':
1063                     $return[] = array('array');
1064                     break;
1065                 case 'struct':
1066                     $return[] = array('struct' => 'struct');
1067                     break;
1068             }
1069         }
1070         return $return;
1071     }
1072
1073     function methodHelp($method)
1074     {
1075         return $this->help[$method];
1076     }
1077 }
1078
1079 /**
1080  * IXR_ClientMulticall
1081  *
1082  * @package IXR
1083  * @since 1.5.0
1084  */
1085 class IXR_ClientMulticall extends IXR_Client
1086 {
1087     var $calls = array();
1088
1089     function IXR_ClientMulticall($server, $path = false, $port = 80)
1090     {
1091         parent::IXR_Client($server, $path, $port);
1092         $this->useragent = 'The Incutio XML-RPC PHP Library (multicall client)';
1093     }
1094
1095     function addCall()
1096     {
1097         $args = func_get_args();
1098         $methodName = array_shift($args);
1099         $struct = array(
1100             'methodName' => $methodName,
1101             'params' => $args
1102         );
1103         $this->calls[] = $struct;
1104     }
1105
1106     function query()
1107     {
1108         // Prepare multicall, then call the parent::query() method
1109         return parent::query('system.multicall', $this->calls);
1110     }
1111 }