<?php
/**
 * dom_xmlrpc_server provides basic XML-RPC server functionality
 * @package   dom-xmlrpc
 * @copyright (C) 2004 John Heinstein. All rights reserved
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License
 * @author    John Heinstein <johnkarl@nbnet.nb.ca>
 * @link      http://www.engageinteractive.com/dom_xmlrpc/ DOM XML-RPC Home Page
 *            DOM XML-RPC is Free Software
 **/

if (!defined('DOM_XMLRPC_INCLUDE_PATH'))
{
	define('DOM_XMLRPC_INCLUDE_PATH', (dirname(__FILE__) . "/"));
}

//XML-RPC Exceptions
//From Specification for Fault Code Interoperability
/** @var int XML not well formed error */
define('DOM_XMLRPC_PARSE_ERROR_NOT_WELL_FORMED', -32700);
/** @var int unsupported encoding error */
define('DOM_XMLRPC_PARSE_ERROR_UNSUPPORTED_ENCODING', -32701);
/** @var int invalid encoding error */
define('DOM_XMLRPC_PARSE_ERROR_INVALID_CHARACTER_ENCODING', -32702);
/** @var int nonconformat XML-RPC error */
define('DOM_XMLRPC_SERVER_ERROR_INVALID_XMLRPC_NONCONFORMANT', -32600);
/** @var int requested method not found error */
define('DOM_XMLRPC_SERVER_ERROR_REQUESTED_METHOD_NOT_FOUND', -32601);
/** @var int invalid method parameters error */
define('DOM_XMLRPC_SERVER_ERROR_INVALID_METHOD_PARAMETERS', -32602);
/** @var int internal server error */
define('DOM_XMLRPC_SERVER_ERROR_INTERNAL_XMLRPC', -32603);
/** @var int application error */
define('DOM_XMLRPC_APPLICATION_ERROR', -32500);
/** @var int system error */
define('DOM_XMLRPC_SYSTEM_ERROR', -32400);
/** @var int http transport error */
define('DOM_XMLRPC_TRANSPORT_ERROR', -32300);

require_once(DOM_XMLRPC_INCLUDE_PATH . 'php_http_server_generic.php');
require_once(DOM_XMLRPC_INCLUDE_PATH . 'dom_xmlrpc_constants.php');

/**
 * Provides basic XML-RPC server functionality
 *
 * @package dom-xmlrpc
 * @author  John Heinstein <johnkarl@nbnet.nb.ca>
 */
class dom_xmlrpc_server extends php_http_server_generic
{
	/** @var object A reference to a method mapping class */
	var $methodmapper;
	/** @var boolean True if params array is to be tokenized */
	var $tokenizeParamsArray = false;
	/** @var int Server error code */
	var $serverError = -1;
	/** @var string Server error string */
	var $serverErrorString = '';
	/** @var object A reference to the handler for missing methods */
	var $methodNotFoundHandler = null;
	/** @var boolean True if object awareness is enabled */
	var $objectAwareness = false;
	/** @var object A reference to the handler for PHP object values */
	var $objectDefinitionHandler = null;
	/** @var array Repository for multiple method responses */
	var $multiresponse = array();

	/**
	 * Constructor
	 *
	 * @param object  A reference to a mapping of custom method handlers
	 * @param boolean True if onPost is to be immediately called
	 */
	function dom_xmlrpc_server($customMethods = null, $postData = null)
	{
		$this->php_http_server_generic();
		$this->setHTTPEvents();
		$this->setHeaders();

		$this->methodmapper = new dom_xmlrpc_methodmapper();
		$this->addSystemMethods();

		if ($customMethods != null) $this->addCustomMethods($customMethods);
		if ($postData != null) $this->fireHTTPEvent('onPost'); //don't have to call receive() method in this case
	} //dom_xmlrpc_server

	/**
	 * Sets the headers for the client connection
	 */
	function setHeaders()
	{
		$this->setHeader('Content-Type', 'text/xml');
		$this->setHeader('Server', 'DOM XML-RPC Server/0.1');
	} //setHeaders

	/**
	 * Sets the default HTTP event handling
	 */
	function setHTTPEvents()
	{
		$this->setHTTPEvent('onPost', array(&$this, 'onPost'));

		$defaultHandler = array(&$this, 'onWrongRequestMethod');
		$this->setHTTPEvent('onGet', $defaultHandler);
		$this->setHTTPEvent('onHead', $defaultHandler);
		$this->setHTTPEvent('onPut', $defaultHandler);
	} //setHTTPEvents

	/**
	 * Adds the default XML-RPC system methods to the method map
	 */
	function addSystemMethods()
	{
		$this->methodmapper->addMethod(new dom_xmlrpc_method(array(
			'name' => 'system.listMethods',
			'method' => array(&$this, 'listMethods'),
			'help' => 'Lists available server methods.',
			'signature' => array(DOM_XMLRPC_TYPE_ARRAY))));

		$this->methodmapper->addMethod(new dom_xmlrpc_method(array(
			'name' => 'system.methodSignature',
			'method' => array(&$this, 'methodSignature'),
			'help' => 'Returns signature of specified method.',
			'signature' => array(DOM_XMLRPC_TYPE_ARRAY, DOM_XMLRPC_TYPE_STRING))));

		$this->methodmapper->addMethod(new dom_xmlrpc_method(array(
			'name' => 'system.methodHelp',
			'method' => array(&$this, 'methodHelp'),
			'help' => 'Returns help for the specified method.',
			'signature' => array(DOM_XMLRPC_TYPE_STRING, DOM_XMLRPC_TYPE_STRING))));

		$this->methodmapper->addMethod(new dom_xmlrpc_method(array(
			'name' => 'system.getCapabilities',
			'method' => array(&$this, 'getCapabilities'),
			'help' => 'Returns an array of supported server specifications.',
			'signature' => array(DOM_XMLRPC_TYPE_ARRAY))));

		$this->methodmapper->addMethod(new dom_xmlrpc_method(array(
			'name' => 'system.multicall',
			'method' => array(&$this, 'multicall'),
			'help' => 'Handles multiple, asynchronous XML-RPC calls bundled into a single request.',
			'signature' => array(DOM_XMLRPC_TYPE_ARRAY, DOM_XMLRPC_TYPE_ARRAY))));
	} //addSystemMethods

	/**
	 * Adds user defined methods to the method map
	 *
	 * @param object A map of custom methods
	 */
	function addCustomMethods($customMethods)
	{
		foreach ($customMethods as $key => $value)
		{
			$this->methodmapper->addMethod($customMethods[$key]);
		}
	} //addCustomMethods

	/**
	 * Adds user defined methods to the method map
	 *
	 * @param array An array of custom methods
	 */
	function addMethods($methodsArray)
	{
		foreach ($methodsArray as $key => $value)
		{
			$this->methodmapper->addMethod($methodsArray[$key]);
		}
	} //addMethods

	/**
	 * Adds a single user defined method to the method map
	 *
	 * @param array An array of custom methods
	 */
	function addMethod($method)
	{
		//expects an associative array
		$this->methodmapper->addMethod($method);
	} //addMethod

	/**
	 * Adds a map of user defined method to the method map
	 *
	 * @param object A map of custom methods
	 * @param array  A list of method names
	 */
	function addMappedMethods(&$methodmap, $methodNameList)
	{
		$this->methodmapper->addMappedMethods($methodmap, $methodNameList);
	} //addMethodMap

	/**
	 * Specifies whether params should be tokenized
	 *
	 * @param boolean True if params should be tokenized
	 */
	function tokenizeParams($truthVal)
	{
		//if true, params array will be split
		//into individual parameters
		$this->tokenizeParamsArray = $truthVal;
	} //tokenizeParams


	//************************************************************************
	//****************************system methods******************************

	/**
	 * XML-RPC defined method: lists all available methods
	 * @return array A list of method names
	 */
	function listMethods()
	{
		return $this->methodmapper->getMethodNames();
	} //listMethods

	/**
	 * XML-RPC defined method: returns the signature of the specified method
	 *
	 * @param string The method name
	 *
	 * @return array The method signature
	 */
	function methodSignature($name)
	{
		$myMethod =& $this->methodmapper->getMethod($name);
		return $myMethod->signature;
	} //methodSignature

	/**
	 * XML-RPC defined method: returns help on the specified method
	 *
	 * @param string The method name
	 *
	 * @return string Help on the method
	 */
	function methodHelp($name)
	{
		$myMethod =& $this->methodmapper->getMethod($name);
		return $myMethod->help;
	} //methodHelp

	/**
	 * XML-RPC defined method: delineates what XML-RPC services are provided by the server
	 * @return array The XML-RPC services provided by the server
	 */
	function getCapabilities()
	{
		$capabilities = array('xmlrpc' => array(
			'specUrl' => 'http://www.xmlrpc.com/spec',
			'specVersion' => 1),
			'introspect' => array(
				'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html',
				'specVersion' => 1),
			'system.multicall' => array(
				'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
				'specVersion' => 1),
			'faults_interop' => array(
				'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
				'specVersion' => 3));
		return $capabilities;
	} //getCapabilities

	/**
	 * Handles multiple method calls in a single request
	 *
	 * @param array An array of method calls
	 *
	 * @return array An array of method responses
	 */
	function &multicall(&$myArray)
	{
		//call each method and store each result
		//in $this->multiresponse
		foreach ($myArray as $key => $value)
		{
			$currCall =& $myArray[$key];
			$methodName = $currCall[DOM_XMLRPC_TYPE_METHODNAME];
			$method =& $this->methodmapper->getMethod($methodName);
			$params = $currCall[DOM_XMLRPC_TYPE_PARAMS];

			if (!($method == null))
			{
				if ($this->tokenizeParamsArray)
				{
					$this->multiresponse[] =& call_user_func_array($method->method, $params); //should I worry about < PHP 4.04?
				}
				else
				{
					if (count($params) == 1)
					{
						//if only one param, send $params[0]
						//rather than than $params
						$this->multiresponse[] =& call_user_func($method->method, $params[0]);
					}
					else
					{
						//send the entire array
						$this->multiresponse[] =& call_user_func($method->method, $params);
					}
				}
			}
			else
			{ //method doesn't exist
				////return $this->handleMethodNotFound($methodName, $params);
			}
		}

		return $this->multiresponse;
	} //multicall

	//****************************system methods******************************
	//************************************************************************

	/**
	 * Handles the POSTing of data to the server
	 *
	 * @param string The POST data
	 */
	function onPost($postData)
	{
		global $HTTP_RAW_POST_DATA;

		if ($postData == null) $postData = $HTTP_RAW_POST_DATA;
		$this->respond($this->invokeMethod($postData));
	} //onPost

	/**
	 * Invokes the method(s) in the XML request
	 *
	 * @param string The XML text of the request
	 *
	 * @return mixed The method response
	 */
	function &invokeMethod($xmlText)
	{
		$xmlrpcdoc = $this->parseRequest($xmlText);

		if (!$this->isError())
		{
			$methodName = $xmlrpcdoc->getMethodName();
			$method =& $this->methodmapper->getMethod($methodName);
			$params =& $xmlrpcdoc->getParams();

			if (!($method == null))
			{
				if ($this->tokenizeParamsArray)
				{
					$response =& call_user_func_array($method->method, $params); //should I worry about < PHP 4.04?
				}
				else
				{
					if (count($params) == 1)
					{
						//if only one param, send $params[0]
						//rather than than $params
						$response =& call_user_func($method->method, $params[0]);
					}
					else
					{
						//send the entire array
						$response =& call_user_func($method->method, $params);
					}
				}

				return $this->buildResponse($response);
			}
			else
			{ //method doesn't exist
				return $this->handleMethodNotFound($methodName, $params);
			}
		}
	} //invokeMethod

	/**
	 * Default handler for a missing method
	 *
	 * @param string The method name
	 * @param mixed  The method params
	 *
	 * @return mixed A fault or user defined data
	 */
	function &handleMethodNotFound($methodName, &$params)
	{
		if ($this->methodNotFoundHandler == null)
		{
			//raise exception, method doesn't exist
			$this->serverError = DOM_XMLRPC_SERVER_ERROR_REQUESTED_METHOD_NOT_FOUND;
			$this->serverErrorString = 'DOM XML-RPC Server Error - Requested method not found.';
			return $this->raiseFault();
		}
		else
		{
			//fire the custom event; pass in a reference to the server
			//so that a fault can be generated as with default handler above
			return call_user_func($this->methodNotFoundHandler, $this, $methodName, $params);
		}
	} //handleMethodNotFound

	/**
	 * Sets the handler for a missing method
	 *
	 * @param object A reference to the method not found handler
	 */
	function setMethodNotFoundHandler($method)
	{
		$this->methodNotFoundHandler =& $method;
	} //setMethodNotFoundHandler

	/**
	 * Builds a method response or fault
	 *
	 * @param object A reference to the method response data
	 *
	 * @return mixed The method response or fault
	 */
	function &buildResponse(&$response)
	{
		require_once(DOM_XMLRPC_INCLUDE_PATH . 'dom_xmlrpc_fault.php');

		if (is_object($response) && (get_class($response) == 'dom_xmlrpc_fault'))
		{
			return $this->buildFault($response);
		}
		else
		{
			return $this->buildMethodResponse($response);
		}
	} //buildResponse

	/**
	 * Builds a method response
	 *
	 * @param object A reference to the method response data
	 *
	 * @return mixed The method response
	 */
	function buildMethodResponse($response)
	{
		require_once(DOM_XMLRPC_INCLUDE_PATH . 'dom_xmlrpc_methodresponse.php');
		$methodResponse = new dom_xmlrpc_methodresponse($response);

		return $methodResponse->toXML();
	} //buildMethodResponse

	/**
	 * Determines whether an error has occurred
	 * @return boolean True if an error has occurred
	 */
	function isError()
	{
		return ($this->serverError != -1);
	} //isError

	/**
	 * Raises an XML-RPC fault
	 * @return object An XML-RPC fault
	 */
	function &raiseFault()
	{
		require_once(DOM_XMLRPC_INCLUDE_PATH . 'dom_xmlrpc_fault.php');

		$fault = new dom_xmlrpc_fault($this->serverError, $this->serverErrorString);
		return $this->buildFault($fault);
	} //raiseFault

	/**
	 * Builds an XML-RPC fault
	 *
	 * @param object A reference to the method response data
	 *
	 * @return mixed The XML-RPC fault
	 */
	function buildFault($response)
	{
		require_once(DOM_XMLRPC_INCLUDE_PATH . 'dom_xmlrpc_methodresponse_fault.php');
		$fault = new dom_xmlrpc_methodresponse_fault($response);

		return $fault->toXML();
	} //buildFault

	/**
	 * Sets object awareness to the specified value
	 *
	 * @param boolean True if object awareness is to be enabled
	 */
	function setObjectAwareness($truthVal)
	{
		$this->objectAwareness = $truthVal;
	} //setObjectAwareness

	/**
	 * Sets the handler for object handling
	 *
	 * @param object The handler for object handling
	 */
	function setObjectDefinitionHandler($handler)
	{
		$this->objectDefinitionHandler =& $handler;
	} //setObjectDefinitionHandler

	/**
	 * Parses the method request
	 *
	 * @param string The text of the method request
	 *
	 * @return mixed The method response document
	 */
	function &parseRequest($xmlText)
	{
		if ($this->objectAwareness)
		{
			require_once(DOM_XMLRPC_INCLUDE_PATH . 'dom_xmlrpc_object_parser.php');
			$parser = new dom_xmlrpc_object_parser($this->objectDefinitionHandler);
		}
		else
		{
			require_once(DOM_XMLRPC_INCLUDE_PATH . 'dom_xmlrpc_array_parser.php');
			$parser = new dom_xmlrpc_array_parser();
		}

		if ($parser->parseXML($xmlText, false))
		{
			return $parser->getArrayDocument();
		}
		else
		{
			//raise exception, parsing error
			$this->serverError = DOM_XMLRPC_PARSE_ERROR_NOT_WELL_FORMED;
			$this->serverErrorString = 'DOM XML-RPC Parse Error - XML document not well formed.';
			return null;
		}
	} //parseRequest

	/**
	 * Raises fault if POST is not used for method request
	 */
	function onWrongRequestMethod()
	{
		//raise exception, POST method not used
		$this->serverError = DOM_XMLRPC_SERVER_ERROR_INTERNAL_XMLRPC;
		$this->serverErrorString = 'DOM XML-RPC Server Error - ' .
			'Only POST method is allowed by the XML-RPC specification.';

		$this->respond($this->raiseFault());
	} //onWrongRequestMethod
} //dom_xmlrpc_server


/**
 * Represents an XML-RPC server method
 *
 * @package dom-xmlrpc
 * @author  John Heinstein <johnkarl@nbnet.nb.ca>
 */
class dom_xmlrpc_methods
{
	/** @var array A list of methods */
	var $methods = array();

	/**
	 * Adds a method to the method array
	 *
	 * @param object The method to be added
	 */
	function addMethod(&$method)
	{
		$this->methods[$method->name] =& $method;
	} //addMethod

	/**
	 * Retrieves a method from the method array
	 *
	 * @param string The name of the method
	 *
	 * @return object A reference to the requested method
	 */
	function &getMethod($name)
	{
		if (isset($this->methods[$name]))
		{
			return $this->methods[$name];
		}

		return null;
	} //getMethod

	/**
	 * Retrieves a list of methods in the method array
	 * @return array A list of methods in the method array
	 */
	function getMethodNames()
	{
		return array_keys($this->methods);
	} //getMethodNames
} //dom_xmlrpc_methods

/**
 * Represents a map of methods available to the server
 *
 * @package dom-xmlrpc
 * @author  John Heinstein <johnkarl@nbnet.nb.ca>
 */
class dom_xmlrpc_methodmapper
{
	/** @var array The method map */
	var $mappedmethods = array();
	/** @var array A dom_xmlrpc_methods instance */
	var $unmappedmethods;

	/**
	 * Constructor: Instantiates a new methodmapper
	 */
	function dom_xmlrpc_methodmapper()
	{
		$this->unmappedmethods = new dom_xmlrpc_methods();
	} //dom_xmlrpc_methodmapper

	/**
	 * Adds a method to the method map
	 *
	 * @param object The method to be added
	 */
	function addMethod(&$method)
	{
		$this->unmappedmethods->addMethod($method);
		$this->mappedmethods[$method->name] =& $this->unmappedmethods;
	} //addMethod

	/**
	 * Adds a method map to the method map
	 *
	 * @param object The method map to be added
	 */
	function addMappedMethods(&$methodmap, $methodNameList)
	{
		$total = count($methodNameList);

		for ($i = 0; $i < $total; $i++)
		{
			$this->mappedmethods[$methodNameList[$i]] =& $methodmap;
		}
	} //addMappedMethods

	/**
	 * Retrieves a method from the method map
	 *
	 * @param string The name of the method
	 *
	 * @return object A reference to the requested method
	 */
	function &getMethod($name)
	{
		if (isset($this->mappedmethods[$name]))
		{
			$methodmap =& $this->mappedmethods[$name];
			return $methodmap->getMethod($name);
		}

		return null;
	} //getMethod

	/**
	 * Retrieves a list of methods in the method array
	 * @return array A list of methods in the method array
	 */
	function getMethodNames()
	{
		return array_keys($this->mappedmethods);
	} //getMethodNames
} //dom_xmlrpc_methodmapper

/**
 * Represents a server method
 *
 * @package dom-xmlrpc
 * @author  John Heinstein <johnkarl@nbnet.nb.ca>
 */
class dom_xmlrpc_method
{
	/** @var string The method name */
	var $name;
	/** @var object A reference to the metho */
	var $method;
	/** @var string Help for the method */
	var $help = '';
	/** @var array The method signature */
	var $signature = '';

	/**
	 * Constructor: Instantiates a custom method
	 *
	 * @param array A method name, reference, signature, and help
	 */
	function dom_xmlrpc_method($paramArray)
	{
		$this->name = $paramArray['name'];
		$this->method =& $paramArray['method'];

		if (isset($paramArray['help']))
		{
			$this->help = $paramArray['help'];
		}

		if (isset($paramArray['signature']))
		{
			$this->signature = $paramArray['signature'];
		}
	} //dom_xmlrpc_method
} //dom_xmlrpc_method

/**
 * Abstract class for a method map
 *
 * @package dom-xmlrpc
 * @author  John Heinstein <johnkarl@nbnet.nb.ca>
 */
class dom_xmlrpc_methodmap
{
	/**
	 * Retrieves a method from the method array
	 *
	 * @param string The name of the method
	 *
	 * @return object A reference to the requested method
	 */
	function &getMethod($methodName)
	{
		//override this method
	} //getMethod
} //dom_xmlrpc_methodmap


/*
//To invoke the server, do:
$httpServer = new dom_xmlrpc_server();
$httpServer->addMethod($someMethod);
$httpServer->receive(); //will grab $HTTP_RAW_POST_DATA

//abbreviated format, will use $postData instead of $HTTP_RAW_POST_DATA
$httpServer = new dom_xmlrpc_server($methods, $postData);
*/

