root/trunk/xmlrpc.php

Revision 776, 18.7 kB (checked in by michiel, 6 months ago)

update copyright year.

Closes #180

Line 
1<?php
2/*
3 * MvBlog -- An open source no-nonsense blogtool
4 *
5 * Copyright (C) 2005-2008, Michiel van Baak
6 * Logo design (C) 2005-2008, Sofie van Tendeloo
7 *
8 * Michiel van Baak <mvanbaak@users.sourceforge.net>
9 * Sofie van Tendeloo <eldridge@users.sourceforge.net>
10 *
11 * See http://dev.mvblog.org for more information on MvBlog.
12 * That page also provides Bugtrackers, Filereleases etc.
13 *
14 * This program is free software, distributed under the terms of
15 * the GNU General Public License Version 2. See the LICENSE file
16 * at the top of the source tree.
17 */
18/**
19 * PHP script to handle incoming XML-RPC requests from clients
20 *
21 * Based on the IXR - The Incutio XML-RPC Library - (c) Incutio Ltd 2002
22 * Version 1.61 - Simon Willison, 11th July 2003 (htmlentities -> htmlspecialchars)
23 * Site:   http://scripts.incutio.com/xmlrpc/
24 * Manual: http://scripts.incutio.com/xmlrpc/manual.php
25 * This version is made available under the terms of GPL v2
26 */
27
28/**
29 * Simple XML server class
30 */
31class XML_server {
32    /* class variables {{{ */
33    /**
34     * @var mixed $data The data we received from the client
35     */
36    public $data;
37    /**
38     * @var array $callbacks Callback functions we export to clients
39     */
40    public $callbacks = array();
41    /**
42     * @var mixed $message XML message we return
43     */
44    public $message;
45    /**
46     * @var array $capabilities XML server capabilities
47     */
48    public $capabilities;
49    /* }}} */
50    /* methods */
51    /* __construct {{{ */
52    /**
53     * XML server
54     *
55     * @param array $callbacks Callback functions we export to clients
56     * @param mixed $data Data received from the client
57     */
58    public function __construct($callbacks = false, $data = false) {
59        $this->setCapabilities();
60        if ($callbacks)
61            $this->callbacks = $callbacks;
62        $this->setCallbacks();
63        $this->serve($data);
64    }
65    /* }}} */
66    /* setCapabilities {{{ */
67    /**
68     * Set our XML Server capabilities
69     */
70    public function setCapabilities() {
71        $this->capabilities = array(
72            "xmlrpc" => array(
73                "specUrl"     => "http://www.xmlrpc.com/spec",
74                "specVersion" => 1
75            ),
76            "faults_interop" => array(
77                "specUrl"     => "http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php",
78                "specVersion" => 20010516
79            ),
80            "system.multicall" => array(
81                "specUrl"     => "http://www.xmlrpc.com/discuss/msgReader$1208",
82                "specVersion" => 1
83            ),
84        );
85    }
86    /* }}} */
87    /* getCapabilities {{{ */
88    /**
89     * Get defined capabilities
90     */
91    public function getCapabilities() {
92        return $this->capabilities;
93    }
94    /* }}} */
95    /* setCallbacks {{{ */
96    /**
97     * Populate callbacks variable with some system callbacks to comply with specs
98     */
99    public function setCallbacks() {
100        $this->callbacks["system.getCapabilities"] = "this:getCapabilities";
101        $this->callbacks["system.listMethods"]     = "this:listMethods";
102        $this->callbacks["system.multicall"]       = "this:multiCall";
103    }
104    /* }}} */
105    /* listMethods {{{ */
106    /**
107     * List registered callback methods
108     *
109     * @return array The registered callbacks
110     */
111    public function listMethods($args) {
112        // uses array_reverse to ensure user defined methods are listed before server defined methods
113        return array_reverse(array_keys($this->callbacks));
114    }
115    /* }}} */
116    /* multiCall {{{ */
117    /**
118     * Call multiple callback functions because client requested multiple methods
119     *
120     * @param array $methodcalls The requested methods
121     * @return array The results of the called methods
122     */
123    public function multiCall($methodcalls) {
124        // See http://www.xmlrpc.com/discuss/msgReader$1208
125        $return = array();
126        foreach ($methodcalls as $call) {
127            $method = $call["methodName"];
128            $params = $call["params"];
129            if ($method == "system.multicall")
130                $result = new XML_Error(-32600, "Recursive calls to system.multicall are forbidden");
131            else
132                $result = $this->call($method, $params);
133            if (is_a($result, "XML_Error")) {
134                $return[] = array(
135                    "faultCode"   => $result->code,
136                    "faultString" => $result->message
137                );
138            } else {
139                $return[] = array($result);
140            }
141        }
142        return $return;
143    }
144    /* }}} */
145    /* serve {{{ */
146    /**
147     * Actually call a callback function and return the result to the client
148     *
149     * @param mixed $data The data received from the client
150     */
151    public function serve($data = false) {
152        if (!$data) {
153            global $HTTP_RAW_POST_DATA;
154            if (!$HTTP_RAW_POST_DATA)
155                die("XML-RPC server accepts POST requests only.");
156            $data = $HTTP_RAW_POST_DATA;
157        }
158        $this->message = new XML_Message($data);
159        if (!$this->message->parse())
160            $this->error(-32700, "parse error. not well formed");
161        if ($this->message->messageType != "methodCall")
162            $this->error(-32600, "server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall");
163        $result = $this->call($this->message->methodName, $this->message->params);
164        // Is the result an error?
165        if (is_a($result, "XML_Error"))
166            $this->error($result);
167        // Encode the result
168        $r = new XML_Value($result);
169        $resultxml = $r->getXml();
170        // Create the XML
171        $xml = <<<EOD
172<methodResponse>
173    <params>
174        <param>
175            <value>
176                $resultxml
177            </value>
178        </param>
179    </params>
180</methodResponse>
181EOD;
182        // Send it
183        $this->output($xml);
184    }
185    /* }}} */
186    /* call {{{ */
187    /**
188     * Call a registered callback function
189     *
190     * @param string $methodname The callback function to run
191     * @param mixed $args The callback function parameter data
192     */
193    public function call($methodname, $args) {
194        if (!$this->hasMethod($methodname))
195            return new XML_Error(-32601, sprintf("server error. requested method %s does not exist.", $methodname));
196        $method = $this->callbacks[$methodname];
197        // Perform the callback and send the response
198        if (count($args) == 1) {
199            // If only one paramater just send that instead of the whole array
200            $args = $args[0];
201        }
202        // Are we dealing with a function or a method?
203        if (substr($method, 0, 5) == "this:") {
204            // It's a class method - check it exists
205            $method = substr($method, 5);
206            if (!method_exists($this, $method))
207                return new XML_Error(-32601, sprintf("server error. requested class method \"%s\" does not exist.", $method));
208            // Call the method
209            $result = $this->$method($args);
210        } else {
211            // It's a function - does it exist?
212            if (!function_exists($method))
213                return new XML_Error(-32601, sprintf("server error. requested function \"%s\" does not exist.", $method));
214            // Call the function
215            $result = $method($args);
216        }
217        return $result;
218    }
219    /* }}} */
220    /* hasMethod {{{ */
221    /**
222     * Check if we have a callbackfunction with specified name
223     *
224     * @param string $method The callbackfunctionname to lookup
225     * @return bool true if the method is there, false if not
226     */
227    public function hasMethod($method) {
228        return in_array($method, array_keys($this->callbacks));
229    }
230    /* }}} */
231    /* error {{{ */
232    /**
233     * Generate xml error document from either XML_error object or errorcode+errormessage
234     *
235     * @param mixed $error Either XML_error object or error code
236     * @param string $message if $error is an errorcode supply the error message
237     */
238    public function error($error, $message = false) {
239        // Accepts either an error object or an error code and message
240        if ($message && !is_object($error))
241            $error = new XML_Error($error, $message);
242        $this->output($error->getXml());
243    }
244    /* }}} */
245    /* output {{{ */
246    /**
247     * Send XML reply to the client
248     *
249     * @param string The XML data to send to the client
250     */
251    public function output($xml) {
252        $xml = sprintf("<?xml version=\"1.0\"?>\n%s", $xml);
253        $length = strlen($xml);
254        header("Connection: close");
255        header(sprintf("Content-Length: %d", $length));
256        header("Content-Type: text/xml");
257        header(sprintf("Date: %s", date("r")));
258        echo $xml;
259        exit;
260    }
261    /* }}} */
262}
263
264/**
265 * XML-RPC value
266 */
267class XML_Value {
268    /* variables */
269    public $data;
270    public $type;
271    /* methods */
272    /* __construct {{{ */
273    /**
274     * Constructor to set class variables
275     *
276     * @param mixed $data Value content
277     * @param string $type The type of $data
278     */
279    public function __construct($data, $type = false) {
280        $this->data = $data;
281        if (!$type)
282            $type = $this->calculateType();
283        $this->type = $type;
284        if ($type == 'struct') {
285            /* Turn all the values in the array in to new IXR_Value objects */
286            foreach ($this->data as $key => $value)
287                $this->data[$key] = new IXR_Value($value);
288        }
289        if ($type == 'array') {
290            for ($i = 0, $j = count($this->data); $i < $j; $i++)
291                $this->data[$i] = new IXR_Value($this->data[$i]);
292        }
293    }
294    /* }}} */
295    /* calculateType {{{ */
296    /**
297     * Guess datatype based on the content of $this->data
298     *
299     * @return string The guessed datatype of $this->data
300     */
301    public function calculateType() {
302        if ($this->data === true || $this->data === false)
303            return "boolean";
304
305        if (is_integer($this->data))
306            return "int";
307
308        if (is_double($this->data))
309            return "double";
310
311        // Deal with XML object types base64 and date
312        if (is_object($this->data) && is_a($this->data, "XML_Date"))
313            return "date";
314
315        if (is_object($this->data) && is_a($this->data, "IXR_Base64"))
316            return "base64";
317
318        // If it is a normal PHP object convert it in to a struct
319        if (is_object($this->data)) {
320            $this->data = get_object_vars($this->data);
321            return "struct";
322        }
323
324        if (!is_array($this->data))
325            return "string";
326
327        /* We have an array - is it an array or a struct ? */
328        if ($this->isStruct($this->data))
329            return "struct";
330        else
331            return "array";
332    }
333    /* }}} */
334    /* isStruct {{{ */
335    /**
336     * ugly hack to find out whether we are an array or an object/struct
337     *
338     * @param mixed $array The data to check
339     * @return bool true on struct, false on array
340     */
341    public function isStruct($array) {
342        /* Nasty function to check if an array is a struct or not */
343        $expected = 0;
344        foreach ($array as $key => $value) {
345            if ((string)$key != (string)$expected)
346                return true;
347            $expected++;
348        }
349        return false;
350    }
351    /* }}} */
352    /* getXML {{{ */
353    /**
354     * Return the correct XML for the value
355     */
356    public function getXml() {
357        /* Return XML for this value */
358        switch ($this->type) {
359            case "boolean":
360                return sprintf("<boolean>%d</boolean>", (($this->data) ? '1' : '0'));
361                break;
362            case "int":
363                return sprintf("<int>%d</int>", $this->data);
364                break;
365            case "double":
366                return sprintf("<double>%s</double>", $this->data);
367                break;
368            case "string":
369                return sprintf("<string>%s</string>", htmlspecialchars($this->data));
370                break;
371            case "array":
372                $return = "<array><data>\n";
373                foreach ($this->data as $item) {
374                    $return .= sprintf("\t<value>%s</value>\n", $item->getXml());
375                }
376                $return .= "</data></array>";
377                return $return;
378                break;
379            case "struct":
380                $return = "<struct>\n";
381                foreach ($this->data as $name => $value) {
382                    $return .= sprintf("\t<member><name>%s</name><value>", $name);
383                    $return .= sprintf("%s</value></member>\n", $value->getXml());
384                }
385                $return .= "</struct>";
386                return $return;
387                break;
388            case "date":
389            case "base64":
390                return $this->data->getXml();
391                break;
392        }
393        return false;
394    }
395    /* }}} */
396}
397
398/**
399 * XML-RPC message
400 */
401class XML_Message {
402    /* variables {{{ */
403    public $message;
404    public $messageType// methodCall / methodResponse / fault
405    public $faultCode;
406    public $faultString;
407    public $methodName;
408    public $params;
409    // Current variable stacks
410    private $_arraystructs = array();   // The stack used to keep track of the current array/struct
411    private $_arraystructstypes = array(); // Stack keeping track of if things are structs or array
412    private $_currentStructName = array();  // A stack as well
413    private $_param;
414    private $_value;
415    private $_currentTag;
416    private $_currentTagContents;
417    // The XML parser
418    private $_parser;
419    /* }}} */
420    /* methods */
421    /* __construct {{{ */
422    public function __construct($message) {
423        $this->message = $message;
424    }
425    /* }}} */
426    /* parse {{{ */
427    /**
428     * Parse the received XML message
429     *
430     * @return bool True on successfull parsing, false otherwise
431     */
432    public function parse() {
433        // first remove the XML declaration
434        $this->message = preg_replace("/<\?xml(.*)?\?".">/", "", $this->message);
435        if (trim($this->message) == "")
436            return false;
437        $this->_parser = xml_parser_create();
438        // Set XML parser to take the case of tags in to account
439        xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
440        // Set XML parser callback functions
441        xml_set_object($this->_parser, $this);
442        xml_set_element_handler($this->_parser, "tag_open", "tag_close");
443        xml_set_character_data_handler($this->_parser, "cdata");
444        if (!xml_parse($this->_parser, $this->message))
445            return false;
446        xml_parser_free($this->_parser);
447        // Grab the error messages, if any
448        if ($this->messageType == "fault") {
449            $this->faultCode = $this->params[0]["faultCode"];
450            $this->faultString = $this->params[0]["faultString"];
451        }
452        return true;
453    }
454    /* }}} */
455    /* tag_open {{{ */
456    /**
457     * Callback function for the xml_parser
458     */
459    public function tag_open($parser, $tag, $attr) {
460        $this->currentTag = $tag;
461        switch($tag) {
462            case "methodCall":
463            case "methodResponse":
464            case "fault":
465                $this->messageType = $tag;
466                break;
467            /* Deal with stacks of arrays and structs */
468            case "data":    // data is to all intents and puposes more interesting than array
469                $this->_arraystructstypes[] = "array";
470                $this->_arraystructs[]      = array();
471                break;
472            case "struct":
473                $this->_arraystructstypes[] = "struct";
474                $this->_arraystructs[]      = array();
475                break;
476        }
477    }
478    /* }}} */
479    /* cdata {{{ */
480    /**
481     * callback function for the xml_parser
482     */
483    public function cdata($parser, $cdata) {
484        $this->_currentTagContents .= $cdata;
485    }
486    /* }}} */
487    /* tag_close {{{ */
488    /**
489     * callback function for the xml_parser
490     */
491    public function tag_close($parser, $tag) {
492        $valueFlag = false;
493        switch($tag) {
494            case "int":
495            case "i4":
496                $value = (int)trim($this->_currentTagContents);
497                $this->_currentTagContents = "";
498                $valueFlag = true;
499                break;
500            case "double":
501                $value = (double)trim($this->_currentTagContents);
502                $this->_currentTagContents = "";
503                $valueFlag = true;
504                break;
505            case "string":
506                $value = (string)trim($this->_currentTagContents);
507                $this->_currentTagContents = "";
508                $valueFlag = true;
509                break;
510            case "dateTime.iso8601":
511                $value = new XML_Date(trim($this->_currentTagContents));
512                $this->_currentTagContents = "";
513                $valueFlag = true;
514                break;
515            case "value":
516                // "If no type is indicated, the type is string."
517                if (trim($this->_currentTagContents) != "") {
518                    $value = (string)$this->_currentTagContents;
519                    $this->_currentTagContents = "";
520                    $valueFlag = true;
521                }
522                break;
523            case "boolean":
524                $value = (boolean)trim($this->_currentTagContents);
525                $this->_currentTagContents = "";
526                $valueFlag = true;
527                break;
528            case "base64":
529                $value = base64_decode($this->_currentTagContents);
530                $this->_currentTagContents = "";
531                $valueFlag = true;
532                break;
533            /* Deal with stacks of arrays and structs */
534            case "data":
535            case "struct":
536                $value = array_pop($this->_arraystructs);
537                array_pop($this->_arraystructstypes);
538                $valueFlag = true;
539                break;
540            case "member":
541                array_pop($this->_currentStructName);
542                break;
543            case "name":
544                $this->_currentStructName[] = trim($this->_currentTagContents);
545                $this->_currentTagContents = "";
546                break;
547            case "methodName":
548                $this->methodName = trim($this->_currentTagContents);
549                $this->_currentTagContents = "";
550                break;
551        }
552        if ($valueFlag) {
553            if (count($this->_arraystructs) > 0) {
554                // Add value to struct or array
555                if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == "struct") {
556                    // Add to struct
557                    $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value;
558                } else {
559                    // Add to array
560                    $this->_arraystructs[count($this->_arraystructs)-1][] = $value;
561                }
562            } else {
563                // Just add as a paramater
564                $this->params[] = $value;
565            }
566        }
567    }     
568    /* }}} */
569}
570
571/**
572 * XML-RPC date handling
573 */