1.1 instead of 1.1000000000000
* - the native extension encodes chars "<", ">" and "&" using numeric entities. We use xml named entities, eg:
* value '&' is encoded as & instead of &.
* Also, we do encode the single quote character "'" as ", whereas the extension does not encode it at all
* - when encoding base64 values, we don't add encoded newline characters (
)
* - some versions of the extension have a bug encoding Latin-1 characters with code points between 200 and 209
* (see https://bugs.php.net/bug.php?id=80559). We do not
* - calling `xmlrpc_encode_request($methodName, $utf8text, options(array('encoding' => 'UTF-8')))` is buggy with
* the extension (wrong character entities are generated). It works with us
* - differences in parsing xml
* - some invalid requests / responses will not be accepted that the native extension allows through:
* - missing 'param' inside 'params'
* eg. heyhey
* - differences in the API:
* - arrays which look like an xmlrpc fault and are passed to xmlrpc_encode_request() will be encoded
* as structs (the extension generates an invalid xmlrpc request in this case)
* - sending a request for `system.methodHelp` and `system.methodSignature` for a method registered with a Server
* without adding any related introspection data results in an invalid response with the native extension; it does
* not with our code
* - calling `xmlrpc_server_add_introspection_data` with method signatures makes the server validate the number
* and type of incoming parameters in later calls to `xmlrpc_server_call_method`, relieving the developer from
* having to implement the same checks manually in her php functions
* - marking input parameters as optional in the data passed to calls to `xmlrpc_server_add_introspection_data` and
* `xmlrpc_server_register_introspection_callback` will change the number of method signatures displayed by the
* server in responses to calls to `system.methodSignature`.
* Eg. passing in one signature with one optional param will result in two signatures displayed, one with no params
* and one with one param
*/
namespace PhpXmlRpc\Polyfill\XmlRpc;
use PhpXmlRpc\Encoder;
use PhpXmlRpc\PhpXmlRpc;
use PhpXmlRpc\Request;
use PhpXmlRpc\Response;
use PhpXmlRpc\Server as BaseServer;
use PhpXmlRpc\Value;
final class XmlRpc
{
public static $xmlpc_double_precision = 13;
/**
* Decode the xml generated by xmlrpc_encode() into native php types
* @param string $xml
* @param string $encoding target charset encoding for the returned data. Note: when the xml string contains any
* characters which can not be represented in the target encoding, the returned data will
* be in utf8
* @return mixed
*/
public static function xmlrpc_decode($xml, $encoding = "iso-8859-1")
{
$encoder = new Encoder();
if (strpos($xml, '') === false) {
// strip out unnecessary xml in case we're deserializing a single param.
// in case of a complete response, we do not have to strip anything
// please note that the test below has LARGE space for improvement (eg. it might trip on xml comments...)
$xml = preg_replace(array('!\s*\s*\s*!', '!\s*\s*\s*$!'), array('', ''), $xml);
}
$defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
PhpXmlRpc::$xmlrpc_internalencoding = 'UTF-8';
$options = array('extension_api');
if (strtoupper($encoding) != 'UTF-8') {
// NB: always set xmlrpc_internalencoding = 'UTF-8' when setting 'extension_api_encoding'
$options['extension_api_encoding'] = $encoding;
}
$val = $encoder->decodeXml($xml);
if (!$val) {
$out = null; // instead of false
} else {
if ($val instanceof Response) {
if ($fc = $val->faultCode()) {
$fs = $val->faultString();
$out = array('faultCode' => $fc, 'faultString' => self::fromUtf8($encoding, $fs));
} else {
$out = $encoder->decode($val->value(), $options);
}
} else {
$out = $encoder->decode($val, $options);
}
}
PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
return $out;
}
/**
* Decode an xmlrpc request (or response) into native PHP types
* @param string $xml
* @param string $method (will not be set when decoding responses)
* @param string $encoding target charset encoding for the returned data. Note: when the xml string contains any
* characters which can not be represented in the target encoding, the returned data will
* be in utf8
* @return mixed
*
* @bug fails for $xml === true, $xml === false, $xml === integer, $xml === float
*/
public static function xmlrpc_decode_request($xml, &$method, $encoding = "iso-8859-1")
{
$encoder = new Encoder();
$defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
PhpXmlRpc::$xmlrpc_internalencoding = 'UTF-8';
$options = array('extension_api');
if (strtoupper($encoding) != 'UTF-8') {
// NB: always set xmlrpc_internalencoding = 'UTF-8' when setting 'extension_api_encoding'
$options['extension_api_encoding'] = $encoding;
}
$val = $encoder->decodeXml($xml);
if (!$val) {
$out = null; // instead of false
} else {
if ($val instanceof Response) {
if ($fc = $val->faultCode()) {
$out = array('faultCode' => $fc, 'faultString' => self::fromUtf8($encoding, $val->faultString()));
} else {
$out = $encoder->decode($val->value(), $options);
}
} else if ($val instanceof Request) {
$method = self::fromUtf8($encoding, $val->method());
$out = array();
$pn = $val->getNumParams();
for ($i = 0; $i < $pn; $i++)
$out[] = $encoder->decode($val->getParam($i), $options);
} else {
/// @todo copy lib behaviour in this case
$out = null;
}
}
PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
return $out;
}
/**
* Given a PHP val, convert it to xmlrpc code (wrapped up in either params/param elements or a fault element).
* @param mixed $val
* @return string
* @todo test what happens with arrays with faultCode === 0|''|null
*/
public static function xmlrpc_encode($val)
{
$encoder = new Encoder();
$defaultPrecision = PhpXmlRpc::$xmlpc_double_precision;
PhpXmlRpc::$xmlpc_double_precision = self::$xmlpc_double_precision;
$defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
PhpXmlRpc::$xmlrpc_internalencoding = 'ISO-8859-1';
$eval = $encoder->encode($val, array('extension_api'));
if (is_array($val) && isset($val['faultCode'])) {
$out = "\n\n " . $eval->serialize('US-ASCII') . "";
} else {
$out = "\n\n\n " . $eval->serialize('US-ASCII') . "\n";
}
PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
PhpXmlRpc::$xmlpc_double_precision = $defaultPrecision;
return $out;
}
/**
* Given a method name and array of php values, create an xmlrpc request out
* of them. If method name === null, create an xmlrpc response instead
* @param string $method
* @param array $params
* @param array $output_options options array. At the moment only partial support for 'encoding' and 'escaping' is
* provided.
* encoding: iso-8859-1, utf-8
* escaping: [markup], [markup,non-print]. non-ascii is treated the same as non-print
* @return string
*
* @todo complete parsing/usage of options: encoding, escaping.
* @todo might, or not, implement support for options: output_type, verbosity
*/
public static function xmlrpc_encode_request($method, $params, $output_options = array())
{
$encoder = new Encoder();
$internalEncoding = 'ISO-8859-1';
$targetEncoding = 'iso-8859-1';
$targetCharset = 'US-ASCII';
if (isset($output_options['encoding'])) {
$targetEncoding = $output_options['encoding'];
$internalEncoding = $targetEncoding;
}
if (isset($output_options['escaping'])) {
switch(true) {
/// @todo improve this:
/// escaping strategies supported by the native extension can be combined, and are:
/// - cdata: wraps text in a cdata section
/// - non-print: uses utf8 char entities for chars <32 and >126
/// - non-ascii: uses utf8 char entities for chars > 127
/// - markup: uses utf8 char entities for & " < >
/// If not specified, it defaults to markup | non-ascii | non-print
/// Otoh the default serialization strategy from phpxmlrpc is to
/// - always escape & " < > and '
/// - when going ut8 -> utf8, touch nothing else
/// - when going iso-8859-1 -> ascii, convert chars <32 and 160-255 (but not 127-159)
/// - when going utf8 -> ascii, convert chars <32 and >= 128 (but not 127)
/// - never wrap the text in a cdata section
/// We should:
/// 1. log a warning if being passed options which do not make sense, eg.
/// - cdata along with any other option
/// - any strategy, apart cdata, missing markup (as we always escape markup)
/// - an empty array (same)
/// 2. support cdata escaping (done correctly)
/// 3. support different escaping for non-print and non-ascii
case is_array($output_options['escaping']) && !in_array('non-print', $output_options['escaping']) && !in_array('non-ascii', $output_options['escaping']):
case $output_options['escaping'] == 'markup':
case $output_options['escaping'] == 'cdata':
$targetCharset = $targetEncoding;
}
}
$output_options = array('extension_api');
$defaultPrecision = PhpXmlRpc::$xmlpc_double_precision;
PhpXmlRpc::$xmlpc_double_precision = self::$xmlpc_double_precision;
$defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
PhpXmlRpc::$xmlrpc_internalencoding = $internalEncoding;
if ($method !== null) {
// mimic EPI behaviour: if ($val === NULL) then send NO parameters
if (!is_array($params)) {
if ($params === NULL) {
$params = array();
} else {
$params = array($params);
}
} else {
/// @todo fix corner cases
// if given a 'hash' array, encode it as a single param
$i = 0;
$ok = true;
foreach ($params as $key => $value)
if ($key !== $i) {
$ok = false;
break;
} else
$i++;
if (!$ok) {
$params = array($params);
}
}
$values = array();
foreach ($params as $key => $value) {
$values[] = $encoder->encode($value, $output_options);
}
// create request
$req = new Request($method, $values);
$out = preg_replace('!^<\\?xml version="1\\.0" encoding="'.$targetCharset.'" \\?>!', "", $req->serialize($targetCharset));
} else {
// create response
if (is_array($params) && self::xmlrpc_is_fault($params))
$resp = new Response(0, (integer)$params['faultCode'], (string)$params['faultString']);
else
$resp = new Response($encoder->encode($params, $output_options));
$out = "\n" . $resp->serialize($targetCharset);
}
PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
PhpXmlRpc::$xmlpc_double_precision = $defaultPrecision;
return $out;
}
/**
* Given a php value, return its corresponding xmlrpc type
* @param mixed $value
* @return string
*
* @bug fails compatibility for array('2' => true, false)
* @bug fails compatibility for array(true, 'world')
*/
public static function xmlrpc_get_type($value)
{
switch (strtolower(gettype($value))) {
case 'string':
return Value::$xmlrpcString;
case 'integer':
case 'resource':
return Value::$xmlrpcInt;
case 'double':
return Value::$xmlrpcDouble;
case 'boolean':
return Value::$xmlrpcBoolean;
case 'array':
$i = 0;
$ok = true;
foreach ($value as $key => $valueue)
if ($key !== $i) {
$ok = false;
break;
} else
$i++;
return $ok ? Value::$xmlrpcArray : Value::$xmlrpcStruct;
case 'object':
if ($value instanceof Value) {
$type = $value->scalarTyp();
return str_replace('dateTime.iso8601', 'datetime', $type);
} elseif ($value instanceof \stdClass && isset($value->xmlrpc_type)) {
switch($value->xmlrpc_type) {
case 'datetime':
case 'base64':
return $value->xmlrpc_type;
default:
return 'none';
}
}
return Value::$xmlrpcStruct;
case 'null':
return Value::$xmlrpcBase64; // go figure why...
}
}
/**
* Checks if a given php array corresponds to an xmlrpc fault response
* @param array $arg
* @return boolean
*/
public static function xmlrpc_is_fault($arg)
{
return is_array($arg) && array_key_exists('faultCode', $arg) && array_key_exists('faultString', $arg);
}
/**
* @param string $xml
* @return array
*/
public static function xmlrpc_parse_method_descriptions($xml)
{
return Server::parse_method_descriptions($xml);
}
/** Server side ***************************************************************/
/**
* @param Server $server
* @param array $desc
* @return int
*/
public static function xmlrpc_server_add_introspection_data($server, $desc)
{
if ($server instanceof Server) {
return $server->add_introspection_data($desc);
}
return 0;
}
/**
* Parses XML request and calls corresponding method
* @param Server $server
* @param string $xml
* @param mixed $user_data
* @param array $output_options
* @return string
*/
public static function xmlrpc_server_call_method($server, $xml, $user_data, $output_options = array())
{
$server->user_data = $user_data;
return $server->service($xml, true);
}
/**
* Create a new xmlrpc server instance
* @return Server
*/
public static function xmlrpc_server_create()
{
$s = new Server();
$s->functions_parameters_type = 'epivals';
$s->compress_response = false; // since we will not be outputting any http headers to go with it
return $s;
}
/**
* This function actually does nothing, but it is kept for compatibility.
* To destroy a server object, just unset() it, or send it out of scope...
* @param Server $server
* @return integer
*/
public static function xmlrpc_server_destroy($server)
{
if ($server instanceof Server)
return 1;
return 0;
}
/**
* @param Server $server
* @param string $function
* @return bool
*/
public static function xmlrpc_server_register_introspection_callback($server, $function)
{
if ($server instanceof Server) {
return $server->register_introspection_callback($function);
}
return false;
}
/**
* Add a php function as xmlrpc method handler to an existing server.
* PHP function sig: f(string $methodname, array $params, mixed $extra_data)
* @param Server $server
* @param string $method_name
* @param string $function
* @return boolean true on success or false
*/
public static function xmlrpc_server_register_method($server, $method_name, $function)
{
if ($server instanceof BaseServer) {
$server->add_to_map($method_name, $function);
return true;
}
return false;
}
/**
* Set string $val to a known xmlrpc type (base64 or datetime only), for serializing it later
* (NB: this will turn the string into an object!).
* @param string $val
* @param string $type
* @return boolean false if conversion did not take place
*/
public static function xmlrpc_set_type(&$val, $type)
{
if (is_string($val)) {
if ($type == 'base64') {
$value = array(
'scalar' => $val,
'xmlrpc_type' => 'base64'
);
$val = (object)$value;
} elseif ($type == 'datetime') {
if (preg_match('/([0-9]{4}[0-1][0-9][0-3][0-9])T([0-5][0-9]):([0-5][0-9]):([0-5][0-9])/', $val)) {
// add 3 object members to make it more compatible to user code
$value = array(
'scalar' => $val,
'xmlrpc_type' => 'datetime',
'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($val)
);
$val = (object)$value;
} else {
return false;
}
} else {
// @todo EPI will NOT raise a warning for good type names, eg. 'boolean', etc...
trigger_error("invalid type '$type' passed to xmlrpc_set_type()");
return false;
}
return true;
} else {
return false;
}
}
protected static function fromUtf8($to, $str)
{
if (strtoupper($to) != 'UTF-8') {
/// @todo support mbstring as an alternative, as well as plain utf8_decode if none are available and target is latin-1
$dstr = @iconv('UTF-8', $to, $str);
if ($dstr !== false) {
return $dstr;
}
}
return $str;
}
}