[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> IXR_Library.php (source)

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