WordPress 4.1.1-scripts
[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                 header('Content-Type: text/plain'); // merged from WP #9093
406                 die('XML-RPC server accepts POST requests only.');
407             }
408
409             global $HTTP_RAW_POST_DATA;
410             if (empty($HTTP_RAW_POST_DATA)) {
411                 // workaround for a bug in PHP 5.2.2 - http://bugs.php.net/bug.php?id=41293
412                 $data = file_get_contents('php://input');
413             } else {
414                 $data =& $HTTP_RAW_POST_DATA;
415             }
416         }
417         $this->message = new IXR_Message($data);
418         if (!$this->message->parse()) {
419             $this->error(-32700, 'parse error. not well formed');
420         }
421         if ($this->message->messageType != 'methodCall') {
422             $this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall');
423         }
424         $result = $this->call($this->message->methodName, $this->message->params);
425
426         // Is the result an error?
427         if (is_a($result, 'IXR_Error')) {
428             $this->error($result);
429         }
430
431         // Encode the result
432         $r = new IXR_Value($result);
433         $resultxml = $r->getXml();
434
435         // Create the XML
436         $xml = <<<EOD
437 <methodResponse>
438   <params>
439     <param>
440       <value>
441       $resultxml
442       </value>
443     </param>
444   </params>
445 </methodResponse>
446
447 EOD;
448       // Send it
449       $this->output($xml);
450     }
451
452     function call($methodname, $args)
453     {
454         if (!$this->hasMethod($methodname)) {
455             return new IXR_Error(-32601, 'server error. requested method '.$methodname.' does not exist.');
456         }
457         $method = $this->callbacks[$methodname];
458
459         // Perform the callback and send the response
460         if (count($args) == 1) {
461             // If only one parameter just send that instead of the whole array
462             $args = $args[0];
463         }
464
465         // Are we dealing with a function or a method?
466         if (is_string($method) && substr($method, 0, 5) == 'this:') {
467             // It's a class method - check it exists
468             $method = substr($method, 5);
469             if (!method_exists($this, $method)) {
470                 return new IXR_Error(-32601, 'server error. requested class method "'.$method.'" does not exist.');
471             }
472
473             //Call the method
474             $result = $this->$method($args);
475         } else {
476             // It's a function - does it exist?
477             if (is_array($method)) {
478                 if (!is_callable(array($method[0], $method[1]))) {
479                     return new IXR_Error(-32601, 'server error. requested object method "'.$method[1].'" does not exist.');
480                 }
481             } else if (!function_exists($method)) {
482                 return new IXR_Error(-32601, 'server error. requested function "'.$method.'" does not exist.');
483             }
484
485             // Call the function
486             $result = call_user_func($method, $args);
487         }
488         return $result;
489     }
490
491     function error($error, $message = false)
492     {
493         // Accepts either an error object or an error code and message
494         if ($message && !is_object($error)) {
495             $error = new IXR_Error($error, $message);
496         }
497         $this->output($error->getXml());
498     }
499
500     function output($xml)
501     {
502         $charset = function_exists('get_option') ? get_option('blog_charset') : '';
503         if ($charset)
504             $xml = '<?xml version="1.0" encoding="'.$charset.'"?>'."\n".$xml;
505         else
506             $xml = '<?xml version="1.0"?>'."\n".$xml;
507         $length = strlen($xml);
508         header('Connection: close');
509         header('Content-Length: '.$length);
510         if ($charset)
511             header('Content-Type: text/xml; charset='.$charset);
512         else
513             header('Content-Type: text/xml');
514         header('Date: '.date('r'));
515         echo $xml;
516         exit;
517     }
518
519     function hasMethod($method)
520     {
521         return in_array($method, array_keys($this->callbacks));
522     }
523
524     function setCapabilities()
525     {
526         // Initialises capabilities array
527         $this->capabilities = array(
528             'xmlrpc' => array(
529                 'specUrl' => 'http://www.xmlrpc.com/spec',
530                 'specVersion' => 1
531         ),
532             'faults_interop' => array(
533                 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
534                 'specVersion' => 20010516
535         ),
536             'system.multicall' => array(
537                 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
538                 'specVersion' => 1
539         ),
540         );
541     }
542
543     function getCapabilities($args)
544     {
545         return $this->capabilities;
546     }
547
548     function setCallbacks()
549     {
550         $this->callbacks['system.getCapabilities'] = 'this:getCapabilities';
551         $this->callbacks['system.listMethods'] = 'this:listMethods';
552         $this->callbacks['system.multicall'] = 'this:multiCall';
553     }
554
555     function listMethods($args)
556     {
557         // Returns a list of methods - uses array_reverse to ensure user defined
558         // methods are listed before server defined methods
559         return array_reverse(array_keys($this->callbacks));
560     }
561
562     function multiCall($methodcalls)
563     {
564         // See http://www.xmlrpc.com/discuss/msgReader$1208
565         $return = array();
566         foreach ($methodcalls as $call) {
567             $method = $call['methodName'];
568             $params = $call['params'];
569             if ($method == 'system.multicall') {
570                 $result = new IXR_Error(-32600, 'Recursive calls to system.multicall are forbidden');
571             } else {
572                 $result = $this->call($method, $params);
573             }
574             if (is_a($result, 'IXR_Error')) {
575                 $return[] = array(
576                     'faultCode' => $result->code,
577                     'faultString' => $result->message
578                 );
579             } else {
580                 $return[] = array($result);
581             }
582         }
583         return $return;
584     }
585 }
586
587 /**
588  * IXR_Request
589  *
590  * @package IXR
591  * @since 1.5.0
592  */
593 class IXR_Request
594 {
595     var $method;
596     var $args;
597     var $xml;
598
599     function IXR_Request($method, $args)
600     {
601         $this->method = $method;
602         $this->args = $args;
603         $this->xml = <<<EOD
604 <?xml version="1.0"?>
605 <methodCall>
606 <methodName>{$this->method}</methodName>
607 <params>
608
609 EOD;
610         foreach ($this->args as $arg) {
611             $this->xml .= '<param><value>';
612             $v = new IXR_Value($arg);
613             $this->xml .= $v->getXml();
614             $this->xml .= "</value></param>\n";
615         }
616         $this->xml .= '</params></methodCall>';
617     }
618
619     function getLength()
620     {
621         return strlen($this->xml);
622     }
623
624     function getXml()
625     {
626         return $this->xml;
627     }
628 }
629
630 /**
631  * IXR_Client
632  *
633  * @package IXR
634  * @since 1.5.0
635  *
636  */
637 class IXR_Client
638 {
639     var $server;
640     var $port;
641     var $path;
642     var $useragent;
643     var $response;
644     var $message = false;
645     var $debug = false;
646     var $timeout;
647     var $headers = array();
648
649     // Storage place for an error message
650     var $error = false;
651
652     function IXR_Client($server, $path = false, $port = 80, $timeout = 15)
653     {
654         if (!$path) {
655             // Assume we have been given a URL instead
656             $bits = parse_url($server);
657             $this->server = $bits['host'];
658             $this->port = isset($bits['port']) ? $bits['port'] : 80;
659             $this->path = isset($bits['path']) ? $bits['path'] : '/';
660
661             // Make absolutely sure we have a path
662             if (!$this->path) {
663                 $this->path = '/';
664             }
665
666             if ( ! empty( $bits['query'] ) ) {
667                 $this->path .= '?' . $bits['query'];
668             }
669         } else {
670             $this->server = $server;
671             $this->path = $path;
672             $this->port = $port;
673         }
674         $this->useragent = 'The Incutio XML-RPC PHP Library';
675         $this->timeout = $timeout;
676     }
677
678     function query()
679     {
680         $args = func_get_args();
681         $method = array_shift($args);
682         $request = new IXR_Request($method, $args);
683         $length = $request->getLength();
684         $xml = $request->getXml();
685         $r = "\r\n";
686         $request  = "POST {$this->path} HTTP/1.0$r";
687
688         // Merged from WP #8145 - allow custom headers
689         $this->headers['Host']          = $this->server;
690         $this->headers['Content-Type']  = 'text/xml';
691         $this->headers['User-Agent']    = $this->useragent;
692         $this->headers['Content-Length']= $length;
693
694         foreach( $this->headers as $header => $value ) {
695             $request .= "{$header}: {$value}{$r}";
696         }
697         $request .= $r;
698
699         $request .= $xml;
700
701         // Now send the request
702         if ($this->debug) {
703             echo '<pre class="ixr_request">'.htmlspecialchars($request)."\n</pre>\n\n";
704         }
705
706         if ($this->timeout) {
707             $fp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout);
708         } else {
709             $fp = @fsockopen($this->server, $this->port, $errno, $errstr);
710         }
711         if (!$fp) {
712             $this->error = new IXR_Error(-32300, 'transport error - could not open socket');
713             return false;
714         }
715         fputs($fp, $request);
716         $contents = '';
717         $debugContents = '';
718         $gotFirstLine = false;
719         $gettingHeaders = true;
720         while (!feof($fp)) {
721             $line = fgets($fp, 4096);
722             if (!$gotFirstLine) {
723                 // Check line for '200'
724                 if (strstr($line, '200') === false) {
725                     $this->error = new IXR_Error(-32300, 'transport error - HTTP status code was not 200');
726                     return false;
727                 }
728                 $gotFirstLine = true;
729             }
730             if (trim($line) == '') {
731                 $gettingHeaders = false;
732             }
733             if (!$gettingHeaders) {
734                 // merged from WP #12559 - remove trim
735                 $contents .= $line;
736             }
737             if ($this->debug) {
738                 $debugContents .= $line;
739             }
740         }
741         if ($this->debug) {
742             echo '<pre class="ixr_response">'.htmlspecialchars($debugContents)."\n</pre>\n\n";
743         }
744
745         // Now parse what we've got back
746         $this->message = new IXR_Message($contents);
747         if (!$this->message->parse()) {
748             // XML error
749             $this->error = new IXR_Error(-32700, 'parse error. not well formed');
750             return false;
751         }
752
753         // Is the message a fault?
754         if ($this->message->messageType == 'fault') {
755             $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString);
756             return false;
757         }
758
759         // Message must be OK
760         return true;
761     }
762
763     function getResponse()
764     {
765         // methodResponses can only have one param - return that
766         return $this->message->params[0];
767     }
768
769     function isError()
770     {
771         return (is_object($this->error));
772     }
773
774     function getErrorCode()
775     {
776         return $this->error->code;
777     }
778
779     function getErrorMessage()
780     {
781         return $this->error->message;
782     }
783 }
784
785
786 /**
787  * IXR_Error
788  *
789  * @package IXR
790  * @since 1.5.0
791  */
792 class IXR_Error
793 {
794     var $code;
795     var $message;
796
797     function IXR_Error($code, $message)
798     {
799         $this->code = $code;
800         $this->message = htmlspecialchars($message);
801     }
802
803     function getXml()
804     {
805         $xml = <<<EOD
806 <methodResponse>
807   <fault>
808     <value>
809       <struct>
810         <member>
811           <name>faultCode</name>
812           <value><int>{$this->code}</int></value>
813         </member>
814         <member>
815           <name>faultString</name>
816           <value><string>{$this->message}</string></value>
817         </member>
818       </struct>
819     </value>
820   </fault>
821 </methodResponse>
822
823 EOD;
824         return $xml;
825     }
826 }
827
828 /**
829  * IXR_Date
830  *
831  * @package IXR
832  * @since 1.5.0
833  */
834 class IXR_Date {
835     var $year;
836     var $month;
837     var $day;
838     var $hour;
839     var $minute;
840     var $second;
841     var $timezone;
842
843     function IXR_Date($time)
844     {
845         // $time can be a PHP timestamp or an ISO one
846         if (is_numeric($time)) {
847             $this->parseTimestamp($time);
848         } else {
849             $this->parseIso($time);
850         }
851     }
852
853     function parseTimestamp($timestamp)
854     {
855         $this->year = date('Y', $timestamp);
856         $this->month = date('m', $timestamp);
857         $this->day = date('d', $timestamp);
858         $this->hour = date('H', $timestamp);
859         $this->minute = date('i', $timestamp);
860         $this->second = date('s', $timestamp);
861         $this->timezone = '';
862     }
863
864     function parseIso($iso)
865     {
866         $this->year = substr($iso, 0, 4);
867         $this->month = substr($iso, 4, 2);
868         $this->day = substr($iso, 6, 2);
869         $this->hour = substr($iso, 9, 2);
870         $this->minute = substr($iso, 12, 2);
871         $this->second = substr($iso, 15, 2);
872         $this->timezone = substr($iso, 17);
873     }
874
875     function getIso()
876     {
877         return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second.$this->timezone;
878     }
879
880     function getXml()
881     {
882         return '<dateTime.iso8601>'.$this->getIso().'</dateTime.iso8601>';
883     }
884
885     function getTimestamp()
886     {
887         return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year);
888     }
889 }
890
891 /**
892  * IXR_Base64
893  *
894  * @package IXR
895  * @since 1.5.0
896  */
897 class IXR_Base64
898 {
899     var $data;
900
901     function IXR_Base64($data)
902     {
903         $this->data = $data;
904     }
905
906     function getXml()
907     {
908         return '<base64>'.base64_encode($this->data).'</base64>';
909     }
910 }
911
912 /**
913  * IXR_IntrospectionServer
914  *
915  * @package IXR
916  * @since 1.5.0
917  */
918 class IXR_IntrospectionServer extends IXR_Server
919 {
920     var $signatures;
921     var $help;
922
923     function IXR_IntrospectionServer()
924     {
925         $this->setCallbacks();
926         $this->setCapabilities();
927         $this->capabilities['introspection'] = array(
928             'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html',
929             'specVersion' => 1
930         );
931         $this->addCallback(
932             'system.methodSignature',
933             'this:methodSignature',
934             array('array', 'string'),
935             'Returns an array describing the return type and required parameters of a method'
936         );
937         $this->addCallback(
938             'system.getCapabilities',
939             'this:getCapabilities',
940             array('struct'),
941             'Returns a struct describing the XML-RPC specifications supported by this server'
942         );
943         $this->addCallback(
944             'system.listMethods',
945             'this:listMethods',
946             array('array'),
947             'Returns an array of available methods on this server'
948         );
949         $this->addCallback(
950             'system.methodHelp',
951             'this:methodHelp',
952             array('string', 'string'),
953             'Returns a documentation string for the specified method'
954         );
955     }
956
957     function addCallback($method, $callback, $args, $help)
958     {
959         $this->callbacks[$method] = $callback;
960         $this->signatures[$method] = $args;
961         $this->help[$method] = $help;
962     }
963
964     function call($methodname, $args)
965     {
966         // Make sure it's in an array
967         if ($args && !is_array($args)) {
968             $args = array($args);
969         }
970
971         // Over-rides default call method, adds signature check
972         if (!$this->hasMethod($methodname)) {
973             return new IXR_Error(-32601, 'server error. requested method "'.$this->message->methodName.'" not specified.');
974         }
975         $method = $this->callbacks[$methodname];
976         $signature = $this->signatures[$methodname];
977         $returnType = array_shift($signature);
978
979         // Check the number of arguments
980         if (count($args) != count($signature)) {
981             return new IXR_Error(-32602, 'server error. wrong number of method parameters');
982         }
983
984         // Check the argument types
985         $ok = true;
986         $argsbackup = $args;
987         for ($i = 0, $j = count($args); $i < $j; $i++) {
988             $arg = array_shift($args);
989             $type = array_shift($signature);
990             switch ($type) {
991                 case 'int':
992                 case 'i4':
993                     if (is_array($arg) || !is_int($arg)) {
994                         $ok = false;
995                     }
996                     break;
997                 case 'base64':
998                 case 'string':
999                     if (!is_string($arg)) {
1000                         $ok = false;
1001                     }
1002                     break;
1003                 case 'boolean':
1004                     if ($arg !== false && $arg !== true) {
1005                         $ok = false;
1006                     }
1007                     break;
1008                 case 'float':
1009                 case 'double':
1010                     if (!is_float($arg)) {
1011                         $ok = false;
1012                     }
1013                     break;
1014                 case 'date':
1015                 case 'dateTime.iso8601':
1016                     if (!is_a($arg, 'IXR_Date')) {
1017                         $ok = false;
1018                     }
1019                     break;
1020             }
1021             if (!$ok) {
1022                 return new IXR_Error(-32602, 'server error. invalid method parameters');
1023             }
1024         }
1025         // It passed the test - run the "real" method call
1026         return parent::call($methodname, $argsbackup);
1027     }
1028
1029     function methodSignature($method)
1030     {
1031         if (!$this->hasMethod($method)) {
1032             return new IXR_Error(-32601, 'server error. requested method "'.$method.'" not specified.');
1033         }
1034         // We should be returning an array of types
1035         $types = $this->signatures[$method];
1036         $return = array();
1037         foreach ($types as $type) {
1038             switch ($type) {
1039                 case 'string':
1040                     $return[] = 'string';
1041                     break;
1042                 case 'int':
1043                 case 'i4':
1044                     $return[] = 42;
1045                     break;
1046                 case 'double':
1047                     $return[] = 3.1415;
1048                     break;
1049                 case 'dateTime.iso8601':
1050                     $return[] = new IXR_Date(time());
1051                     break;
1052                 case 'boolean':
1053                     $return[] = true;
1054                     break;
1055                 case 'base64':
1056                     $return[] = new IXR_Base64('base64');
1057                     break;
1058                 case 'array':
1059                     $return[] = array('array');
1060                     break;
1061                 case 'struct':
1062                     $return[] = array('struct' => 'struct');
1063                     break;
1064             }
1065         }
1066         return $return;
1067     }
1068
1069     function methodHelp($method)
1070     {
1071         return $this->help[$method];
1072     }
1073 }
1074
1075 /**
1076  * IXR_ClientMulticall
1077  *
1078  * @package IXR
1079  * @since 1.5.0
1080  */
1081 class IXR_ClientMulticall extends IXR_Client
1082 {
1083     var $calls = array();
1084
1085     function IXR_ClientMulticall($server, $path = false, $port = 80)
1086     {
1087         parent::IXR_Client($server, $path, $port);
1088         $this->useragent = 'The Incutio XML-RPC PHP Library (multicall client)';
1089     }
1090
1091     function addCall()
1092     {
1093         $args = func_get_args();
1094         $methodName = array_shift($args);
1095         $struct = array(
1096             'methodName' => $methodName,
1097             'params' => $args
1098         );
1099         $this->calls[] = $struct;
1100     }
1101
1102     function query()
1103     {
1104         // Prepare multicall, then call the parent::query() method
1105         return parent::query('system.multicall', $this->calls);
1106     }
1107 }