<?php
/*
 * MvBlog -- An open source no-nonsense blogtool
 *
 * Copyright (C) 2005-2008, Michiel van Baak
 * Logo design (C) 2005-2008, Sofie van Tendeloo
 *
 * Michiel van Baak <mvanbaak@users.sourceforge.net>
 * Sofie van Tendeloo <eldridge@users.sourceforge.net>
 *
 * See http://dev.mvblog.org for more information on MvBlog.
 * That page also provides Bugtrackers, Filereleases etc.
 *
 * This program is free software, distributed under the terms of
 * the GNU General Public License Version 2. See the LICENSE file
 * at the top of the source tree.
 */
/**
 * PHP script to handle incoming XML-RPC requests from clients
 *
 * Based on the IXR - The Incutio XML-RPC Library - (c) Incutio Ltd 2002
 * Version 1.61 - Simon Willison, 11th July 2003 (htmlentities -> htmlspecialchars)
 * Site:   http://scripts.incutio.com/xmlrpc/
 * Manual: http://scripts.incutio.com/xmlrpc/manual.php
 * This version is made available under the terms of GPL v2
 */

/**
 * Simple XML server class
 */
class XML_server {
	/* class variables {{{ */
	/**
	 * @var mixed $data The data we received from the client
	 */
	public $data;
	/**
	 * @var array $callbacks Callback functions we export to clients
	 */
	public $callbacks = array();
	/**
	 * @var mixed $message XML message we return
	 */
	public $message;
	/**
	 * @var array $capabilities XML server capabilities
	 */
	public $capabilities;
	/* }}} */
	/* methods */
	/* __construct {{{ */
	/**
	 * XML server
	 *
	 * @param array $callbacks Callback functions we export to clients
	 * @param mixed $data Data received from the client
	 */
	public function __construct($callbacks = false, $data = false) {
		$this->setCapabilities();
		if ($callbacks)
			$this->callbacks = $callbacks;
		$this->setCallbacks();
		$this->serve($data);
	}
	/* }}} */
	/* setCapabilities {{{ */
	/**
	 * Set our XML Server capabilities
	 */
	public function setCapabilities() {
		$this->capabilities = array(
			"xmlrpc" => array(
				"specUrl"     => "http://www.xmlrpc.com/spec",
				"specVersion" => 1
			),
			"faults_interop" => array(
				"specUrl"     => "http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php",
				"specVersion" => 20010516
			),
			"system.multicall" => array(
				"specUrl"     => "http://www.xmlrpc.com/discuss/msgReader$1208",
				"specVersion" => 1
			),
		);
	}
	/* }}} */
	/* getCapabilities {{{ */
	/**
	 * Get defined capabilities
	 */
	public function getCapabilities() {
		return $this->capabilities;
	}
	/* }}} */
	/* setCallbacks {{{ */
	/**
	 * Populate callbacks variable with some system callbacks to comply with specs
	 */
	public function setCallbacks() {
		$this->callbacks["system.getCapabilities"] = "this:getCapabilities";
		$this->callbacks["system.listMethods"]     = "this:listMethods";
		$this->callbacks["system.multicall"]       = "this:multiCall";
	}
	/* }}} */
	/* listMethods {{{ */
	/**
	 * List registered callback methods
	 *
	 * @return array The registered callbacks
	 */
	public function listMethods($args) {
		// uses array_reverse to ensure user defined methods are listed before server defined methods
		return array_reverse(array_keys($this->callbacks));
	}
	/* }}} */
	/* multiCall {{{ */
	/**
	 * Call multiple callback functions because client requested multiple methods
	 *
	 * @param array $methodcalls The requested methods
	 * @return array The results of the called methods
	 */
	public function multiCall($methodcalls) {
		// See http://www.xmlrpc.com/discuss/msgReader$1208
		$return = array();
		foreach ($methodcalls as $call) {
			$method = $call["methodName"];
			$params = $call["params"];
			if ($method == "system.multicall")
				$result = new XML_Error(-32600, "Recursive calls to system.multicall are forbidden");
			else
				$result = $this->call($method, $params);
			if (is_a($result, "XML_Error")) {
				$return[] = array(
					"faultCode"   => $result->code,
					"faultString" => $result->message
				);
			} else {
				$return[] = array($result);
			}
		}
		return $return;
	}
	/* }}} */
	/* serve {{{ */
	/**
	 * Actually call a callback function and return the result to the client
	 *
	 * @param mixed $data The data received from the client
	 */
	public function serve($data = false) {
		if (!$data) {
			global $HTTP_RAW_POST_DATA;
			if (!$HTTP_RAW_POST_DATA)
				die("XML-RPC server accepts POST requests only.");
			$data = $HTTP_RAW_POST_DATA;
		}
		$this->message = new XML_Message($data);
		if (!$this->message->parse())
			$this->error(-32700, "parse error. not well formed");
		if ($this->message->messageType != "methodCall")
			$this->error(-32600, "server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall");
		$result = $this->call($this->message->methodName, $this->message->params);
		// Is the result an error?
		if (is_a($result, "XML_Error"))
			$this->error($result);
		// Encode the result
		$r = new XML_Value($result);
		$resultxml = $r->getXml();
		// Create the XML
		$xml = <<<EOD
<methodResponse>
	<params>
		<param>
			<value>
				$resultxml
			</value>
		</param>
	</params>
</methodResponse>
EOD;
		// Send it
		$this->output($xml);
	}
	/* }}} */
	/* call {{{ */
	/**
	 * Call a registered callback function
	 *
	 * @param string $methodname The callback function to run
	 * @param mixed $args The callback function parameter data
	 */
	public function call($methodname, $args) {
		if (!$this->hasMethod($methodname))
			return new XML_Error(-32601, sprintf("server error. requested method %s does not exist.", $methodname));
		$method = $this->callbacks[$methodname];
		// Perform the callback and send the response
		if (count($args) == 1) {
			// If only one paramater just send that instead of the whole array
			$args = $args[0];
		}
		// Are we dealing with a function or a method?
		if (substr($method, 0, 5) == "this:") {
			// It's a class method - check it exists
			$method = substr($method, 5);
			if (!method_exists($this, $method))
				return new XML_Error(-32601, sprintf("server error. requested class method \"%s\" does not exist.", $method));
			// Call the method
			$result = $this->$method($args);
		} else {
			// It's a function - does it exist?
			if (!function_exists($method))
				return new XML_Error(-32601, sprintf("server error. requested function \"%s\" does not exist.", $method));
			// Call the function
			$result = $method($args);
		}
		return $result;
	}
	/* }}} */
	/* hasMethod {{{ */
	/**
	 * Check if we have a callbackfunction with specified name
	 *
	 * @param string $method The callbackfunctionname to lookup
	 * @return bool true if the method is there, false if not
	 */
	public function hasMethod($method) {
		return in_array($method, array_keys($this->callbacks));
	}
	/* }}} */
	/* error {{{ */
	/**
	 * Generate xml error document from either XML_error object or errorcode+errormessage
	 *
	 * @param mixed $error Either XML_error object or error code
	 * @param string $message if $error is an errorcode supply the error message
	 */
	public function error($error, $message = false) {
		// Accepts either an error object or an error code and message
		if ($message && !is_object($error))
			$error = new XML_Error($error, $message);
		$this->output($error->getXml());
	}
	/* }}} */
	/* output {{{ */
	/**
	 * Send XML reply to the client
	 *
	 * @param string The XML data to send to the client
	 */
	public function output($xml) {
		$xml = sprintf("<?xml version=\"1.0\"?>\n%s", $xml);
		$length = strlen($xml);
		header("Connection: close");
		header(sprintf("Content-Length: %d", $length));
		header("Content-Type: text/xml");
		header(sprintf("Date: %s", date("r")));
		echo $xml;
		exit;
	}
	/* }}} */
}

/**
 * XML-RPC value
 */
class XML_Value {
	/* variables */
	public $data;
	public $type;
	/* methods */
	/* __construct {{{ */
	/**
	 * Constructor to set class variables
	 *
	 * @param mixed $data Value content
	 * @param string $type The type of $data
	 */
	public function __construct($data, $type = false) {
		$this->data = $data;
		if (!$type)
			$type = $this->calculateType();
		$this->type = $type;
		if ($type == 'struct') {
			/* Turn all the values in the array in to new IXR_Value objects */
			foreach ($this->data as $key => $value)
				$this->data[$key] = new IXR_Value($value);
		}
		if ($type == 'array') {
			for ($i = 0, $j = count($this->data); $i < $j; $i++)
				$this->data[$i] = new IXR_Value($this->data[$i]);
		}
	}
	/* }}} */
	/* calculateType {{{ */
	/**
	 * Guess datatype based on the content of $this->data
	 *
	 * @return string The guessed datatype of $this->data
	 */
	public function calculateType() {
		if ($this->data === true || $this->data === false)
			return "boolean";

		if (is_integer($this->data))
			return "int";

		if (is_double($this->data))
			return "double";

		// Deal with XML object types base64 and date
		if (is_object($this->data) && is_a($this->data, "XML_Date"))
			return "date";

		if (is_object($this->data) && is_a($this->data, "IXR_Base64"))
			return "base64";

		// If it is a normal PHP object convert it in to a struct
		if (is_object($this->data)) {
			$this->data = get_object_vars($this->data);
			return "struct";
		}

		if (!is_array($this->data))
			return "string";

		/* We have an array - is it an array or a struct ? */
		if ($this->isStruct($this->data))
			return "struct";
		else
			return "array";
	}
	/* }}} */
	/* isStruct {{{ */
	/**
	 * ugly hack to find out whether we are an array or an object/struct
	 *
	 * @param mixed $array The data to check
	 * @return bool true on struct, false on array
	 */
	public function isStruct($array) {
		/* Nasty function to check if an array is a struct or not */
		$expected = 0;
		foreach ($array as $key => $value) {
			if ((string)$key != (string)$expected)
				return true;
			$expected++;
		}
		return false;
	}
	/* }}} */
	/* getXML {{{ */
	/**
	 * Return the correct XML for the value
	 */
	public function getXml() {
		/* Return XML for this value */
		switch ($this->type) {
			case "boolean":
				return sprintf("<boolean>%d</boolean>", (($this->data) ? '1' : '0'));
				break;
			case "int":
				return sprintf("<int>%d</int>", $this->data);
				break;
			case "double":
				return sprintf("<double>%s</double>", $this->data);
				break;
			case "string":
				return sprintf("<string>%s</string>", htmlspecialchars($this->data));
				break;
			case "array":
				$return = "<array><data>\n";
				foreach ($this->data as $item) {
					$return .= sprintf("\t<value>%s</value>\n", $item->getXml());
				}
				$return .= "</data></array>";
				return $return;
				break;
			case "struct":
				$return = "<struct>\n";
				foreach ($this->data as $name => $value) {
					$return .= sprintf("\t<member><name>%s</name><value>", $name);
					$return .= sprintf("%s</value></member>\n", $value->getXml());
				}
				$return .= "</struct>";
				return $return;
				break;
			case "date":
			case "base64":
				return $this->data->getXml();
				break;
		}
		return false;
	}
	/* }}} */
}

/**
 * XML-RPC message
 */
class XML_Message {
	/* variables {{{ */
	public $message;
	public $messageType;  // methodCall / methodResponse / fault
	public $faultCode;
	public $faultString;
	public $methodName;
	public $params;
	// Current variable stacks
	private $_arraystructs = array();   // The stack used to keep track of the current array/struct
	private $_arraystructstypes = array(); // Stack keeping track of if things are structs or array
	private $_currentStructName = array();  // A stack as well
	private $_param;
	private $_value;
	private $_currentTag;
	private $_currentTagContents;
	// The XML parser
	private $_parser;
	/* }}} */
	/* methods */
	/* __construct {{{ */
	public function __construct($message) {
		$this->message = $message;
	}
	/* }}} */
	/* parse {{{ */
	/**
	 * Parse the received XML message
	 *
	 * @return bool True on successfull parsing, false otherwise
	 */
	public function parse() {
		// first remove the XML declaration
		$this->message = preg_replace("/<\?xml(.*)?\?".">/", "", $this->message);
		if (trim($this->message) == "")
			return false;
		$this->_parser = xml_parser_create();
		// Set XML parser to take the case of tags in to account
		xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
		// Set XML parser callback functions
		xml_set_object($this->_parser, $this);
		xml_set_element_handler($this->_parser, "tag_open", "tag_close");
		xml_set_character_data_handler($this->_parser, "cdata");
		if (!xml_parse($this->_parser, $this->message))
			return false;
		xml_parser_free($this->_parser);
		// Grab the error messages, if any
		if ($this->messageType == "fault") {
			$this->faultCode = $this->params[0]["faultCode"];
			$this->faultString = $this->params[0]["faultString"];
		}
		return true;
	}
	/* }}} */
	/* tag_open {{{ */
	/**
	 * Callback function for the xml_parser
	 */
	public function tag_open($parser, $tag, $attr) {
		$this->currentTag = $tag;
		switch($tag) {
			case "methodCall":
			case "methodResponse":
			case "fault":
				$this->messageType = $tag;
				break;
			/* Deal with stacks of arrays and structs */
			case "data":    // data is to all intents and puposes more interesting than array
				$this->_arraystructstypes[] = "array";
				$this->_arraystructs[]      = array();
				break;
			case "struct":
				$this->_arraystructstypes[] = "struct";
				$this->_arraystructs[]      = array();
				break;
		}
	}
	/* }}} */
	/* cdata {{{ */
	/**
	 * callback function for the xml_parser
	 */
	public function cdata($parser, $cdata) {
		$this->_currentTagContents .= $cdata;
	}
	/* }}} */
	/* tag_close {{{ */
	/**
	 * callback function for the xml_parser
	 */
	public function tag_close($parser, $tag) {
		$valueFlag = false;
		switch($tag) {
			case "int":
			case "i4":
				$value = (int)trim($this->_currentTagContents);
				$this->_currentTagContents = "";
				$valueFlag = true;
				break;
			case "double":
				$value = (double)trim($this->_currentTagContents);
				$this->_currentTagContents = "";
				$valueFlag = true;
				break;
			case "string":
				$value = (string)trim($this->_currentTagContents);
				$this->_currentTagContents = "";
				$valueFlag = true;
				break;
			case "dateTime.iso8601":
				$value = new XML_Date(trim($this->_currentTagContents));
				$this->_currentTagContents = "";
				$valueFlag = true;
				break;
			case "value":
				// "If no type is indicated, the type is string."
				if (trim($this->_currentTagContents) != "") {
					$value = (string)$this->_currentTagContents;
					$this->_currentTagContents = "";
					$valueFlag = true;
				}
				break;
			case "boolean":
				$value = (boolean)trim($this->_currentTagContents);
				$this->_currentTagContents = "";
				$valueFlag = true;
				break;
			case "base64":
				$value = base64_decode($this->_currentTagContents);
				$this->_currentTagContents = "";
				$valueFlag = true;
				break;
			/* Deal with stacks of arrays and structs */
			case "data":
			case "struct":
				$value = array_pop($this->_arraystructs);
				array_pop($this->_arraystructstypes);
				$valueFlag = true;
				break;
			case "member":
				array_pop($this->_currentStructName);
				break;
			case "name":
				$this->_currentStructName[] = trim($this->_currentTagContents);
				$this->_currentTagContents = "";
				break;
			case "methodName":
				$this->methodName = trim($this->_currentTagContents);
				$this->_currentTagContents = "";
				break;
		}
		if ($valueFlag) {
			if (count($this->_arraystructs) > 0) {
				// Add value to struct or array
				if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == "struct") {
					// Add to struct
					$this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value;
				} else {
					// Add to array
					$this->_arraystructs[count($this->_arraystructs)-1][] = $value;
				}
			} else {
				// Just add as a paramater
				$this->params[] = $value;
			}
		}
	}     
	/* }}} */
}

/**
 * XML-RPC date handling
 */
class XML_Date {
	/* variables */
	public $year;
	public $month;
	public $day;
	public $hour;
	public $minute;
	public $second;
	/* methods */
	/* __construct {{{ */
	public function __construct($time) {
		// $time can be a PHP timestamp or an ISO one
		if (is_numeric($time))
			$this->parseTimestamp($time);
		else
			$this->parseIso($time);
	}
	/* }}} */
	/* parseTimestamp {{{ */
	/**
	 * Split unix timestamp into seperate parts
	 *
	 * @param int $timestamp Unix timestamp
	 */
	public function parseTimestamp($timestamp) {
		$this->year   = date("Y", $timestamp);
		$this->month  = date("m", $timestamp);
		$this->day    = date("d", $timestamp);
		$this->hour   = date("H", $timestamp);
		$this->minute = date("i", $timestamp);
		$this->second = date("s", $timestamp);
	}
	/* }}} */
	/* parseIso {{{ */
	/**
	 * Split iso 8601 date string into seperate parts
	 *
	 * @param string $iso ISO 8601 datetime
	 */
	public function parseIso($iso) {
		$this->year   = substr($iso, 0, 4);
		$this->month  = substr($iso, 4, 2); 
		$this->day    = substr($iso, 6, 2);
		$this->hour   = substr($iso, 9, 2);
		$this->minute = substr($iso, 12, 2);
		$this->second = substr($iso, 15, 2);
	}
	/* }}} */
	/* getIso {{{ */
	/**
	 * Return the ISO 8601 presentation of the stored datetime
	 *
	 * @return string ISO 8601 date/time
	 */
	public function getIso() {
		return sprintf("%d%d%dT%d:%d:%d", $this->year, $this->month, $this->day, $this->hour, $this->minute, $this->second);
	}
	/* }}} */
	/* getXml {{{ */
	/**
	 * Return the XML for the client (ISO 8601 with xml tags around it)
	 *
	 * @return string ISO 8601 date/time with XML tags around it
	 */
	public function getXml() {
		return sprintf("<dateTime.iso8601>%s</dateTime.iso8601>", $this->getIso());
	}
	/* }}} */
	/* getTimestamp {{{ */
	/**
	 * Return Unix timestamp of stored date/time
	 *
	 * return int Unix timestamp
	 */
	public function getTimestamp() {
		return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year);
	}
	/* }}} */
}

/**
 * XML-RPC base64 encoding
 */
class XML_Base64 {
	public $data;
	public function __construct($data) {
		$this->data = $data;
	}
	public function getXml() {
		return sprintf("<base64>%s</base64>", base64_encode($this->data));
	}
}

/**
 * XML-RPC Error
 */
class XML_Error {
	/* variables */
	public $code;
	public $message;
	/* methods */
	/* __construct {{{ */
	/**
	 * Constructor
	 *
	 * @param int $code The error code
	 * @param string $message The error message
	 */
	public function __construct($code, $message) {
		$this->code = $code;
		$this->message = $message;
	}
	/* }}} */
	/* getXml {{{ */
	/**
	 * Return the XML for an error
	 *
	 * @return string XML string with the error
	 */
	public function getXml() {
		$xml = <<<EOD
<methodResponse>
  <fault>
    <value>
      <struct>
        <member>
          <name>faultCode</name>
          <value><int>{$this->code}</int></value>
        </member>
        <member>
          <name>faultString</name>
          <value><string>{$this->message}</string></value>
        </member>
      </struct>
    </value>
  </fault>
</methodResponse> 

EOD;
		return $xml;
	}
	/* }}} */
}
?>
