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