1
0

XmlRPC.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <?php
  2. /**
  3. * (Try to) implement the same API of the PHP native XMLRPC extension, so that
  4. * projects relying on it can be ported to php installs where the extension is
  5. * missing.
  6. *
  7. * @author Gaetano Giunta
  8. * @copyright (c) 2020-2021 G. Giunta
  9. * @license code licensed under the BSD License: see license.txt
  10. *
  11. * Known differences from the observed behaviour of the PHP extension:
  12. * Definitely to fix:
  13. * - the $output_options argument in xmlrpc_encode_request() is only partially supported
  14. * - two functions are not implemented yet - they exist but do nothing: xmlrpc_parse_method_descriptions and
  15. * xmlrpc_server_register_introspection_callback
  16. * - php arrays indexed with integer keys starting above zero or whose keys are
  17. * not in a strict sequence will be converted into xmlrpc structs, not arrays
  18. * - error codes and error strings in Fault responses generated by the Server for invalid calls are different
  19. * Possibly to fix:
  20. * - xmlrpc_server_create() returns an object instead of a resource
  21. * - a single NULL value passed to xmlrpc_encode_request(null, $val) will be decoded as '', not NULL
  22. * (the extension generates an invalid xmlrpc response in this case)
  23. * - php arrays indexed with mixed string/integer keys will preserve the integer keys in the generated structs
  24. * - server method `system.getCapabilities` returns different results
  25. * - server method `system.describeMethods` returns partial data compared to what can be added via
  26. * xmlrpc_parse_method_descriptions and xmlrpc_server_register_introspection_callback - but the native extension
  27. * version of the same method is buggy anyway: it does not list any method's definition...
  28. * Won't fix:
  29. * - differences in the generated xml
  30. * - the native extension always encodes double values using 13 decimal digits (or 6, depending on version), and pads
  31. * with zeros. We use 13 decimal digits and do not pad. Eg:
  32. * value 1.1 is encoded as <double>1.1</double> instead of <double>1.1000000000000</double>
  33. * - the native extension encodes chars "<", ">" and "&" using numeric entities. We use xml named entities, eg:
  34. * value '&' is encoded as <string>&amp;</string> instead of <string>&#38;</string>.
  35. * Also, we do encode the single quote character "'" as &quot;, whereas the extension does not encode it at all
  36. * - when encoding base64 values, we don't add encoded newline characters (&#10;)
  37. * - some versions of the extension have a bug encoding Latin-1 characters with code points between 200 and 209
  38. * (see https://bugs.php.net/bug.php?id=80559). We do not
  39. * - calling `xmlrpc_encode_request($methodName, $utf8text, options(array('encoding' => 'UTF-8')))` is buggy with
  40. * the extension (wrong character entities are generated). It works with us
  41. * - differences in parsing xml
  42. * - some invalid requests / responses will not be accepted that the native extension allows through:
  43. * - missing 'param' inside 'params'
  44. * eg. <methodCall><methodName>hey</methodName><params><value><string>hey</string></value></params></methodCall>
  45. * - differences in the API:
  46. * - arrays which look like an xmlrpc fault and are passed to xmlrpc_encode_request() will be encoded
  47. * as structs (the extension generates an invalid xmlrpc request in this case)
  48. * - sending a request for `system.methodHelp` and `system.methodSignature` for a method registered with a Server
  49. * without adding any related introspection data results in an invalid response with the native extension; it does
  50. * not with our code
  51. * - calling `xmlrpc_server_add_introspection_data` with method signatures makes the server validate the number
  52. * and type of incoming parameters in later calls to `xmlrpc_server_call_method`, relieving the developer from
  53. * having to implement the same checks manually in her php functions
  54. * - marking input parameters as optional in the data passed to calls to `xmlrpc_server_add_introspection_data` and
  55. * `xmlrpc_server_register_introspection_callback` will change the number of method signatures displayed by the
  56. * server in responses to calls to `system.methodSignature`.
  57. * Eg. passing in one signature with one optional param will result in two signatures displayed, one with no params
  58. * and one with one param
  59. */
  60. namespace PhpXmlRpc\Polyfill\XmlRpc;
  61. use PhpXmlRpc\Encoder;
  62. use PhpXmlRpc\PhpXmlRpc;
  63. use PhpXmlRpc\Request;
  64. use PhpXmlRpc\Response;
  65. use PhpXmlRpc\Server as BaseServer;
  66. use PhpXmlRpc\Value;
  67. final class XmlRpc
  68. {
  69. public static $xmlpc_double_precision = 13;
  70. /**
  71. * Decode the xml generated by xmlrpc_encode() into native php types
  72. * @param string $xml
  73. * @param string $encoding target charset encoding for the returned data. Note: when the xml string contains any
  74. * characters which can not be represented in the target encoding, the returned data will
  75. * be in utf8
  76. * @return mixed
  77. */
  78. public static function xmlrpc_decode($xml, $encoding = "iso-8859-1")
  79. {
  80. $encoder = new Encoder();
  81. if (strpos($xml, '<methodResponse>') === false) {
  82. // strip out unnecessary xml in case we're deserializing a single param.
  83. // in case of a complete response, we do not have to strip anything
  84. // please note that the test below has LARGE space for improvement (eg. it might trip on xml comments...)
  85. $xml = preg_replace(array('!\s*<params>\s*<param>\s*!', '!\s*</param>\s*</params>\s*$!'), array('', ''), $xml);
  86. }
  87. $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
  88. PhpXmlRpc::$xmlrpc_internalencoding = 'UTF-8';
  89. $options = array('extension_api');
  90. if (strtoupper($encoding) != 'UTF-8') {
  91. // NB: always set xmlrpc_internalencoding = 'UTF-8' when setting 'extension_api_encoding'
  92. $options['extension_api_encoding'] = $encoding;
  93. }
  94. $val = $encoder->decodeXml($xml);
  95. if (!$val) {
  96. $out = null; // instead of false
  97. } else {
  98. if ($val instanceof Response) {
  99. if ($fc = $val->faultCode()) {
  100. $fs = $val->faultString();
  101. $out = array('faultCode' => $fc, 'faultString' => self::fromUtf8($encoding, $fs));
  102. } else {
  103. $out = $encoder->decode($val->value(), $options);
  104. }
  105. } else {
  106. $out = $encoder->decode($val, $options);
  107. }
  108. }
  109. PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
  110. return $out;
  111. }
  112. /**
  113. * Decode an xmlrpc request (or response) into native PHP types
  114. * @param string $xml
  115. * @param string $method (will not be set when decoding responses)
  116. * @param string $encoding target charset encoding for the returned data. Note: when the xml string contains any
  117. * characters which can not be represented in the target encoding, the returned data will
  118. * be in utf8
  119. * @return mixed
  120. *
  121. * @bug fails for $xml === true, $xml === false, $xml === integer, $xml === float
  122. */
  123. public static function xmlrpc_decode_request($xml, &$method, $encoding = "iso-8859-1")
  124. {
  125. $encoder = new Encoder();
  126. $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
  127. PhpXmlRpc::$xmlrpc_internalencoding = 'UTF-8';
  128. $options = array('extension_api');
  129. if (strtoupper($encoding) != 'UTF-8') {
  130. // NB: always set xmlrpc_internalencoding = 'UTF-8' when setting 'extension_api_encoding'
  131. $options['extension_api_encoding'] = $encoding;
  132. }
  133. $val = $encoder->decodeXml($xml);
  134. if (!$val) {
  135. $out = null; // instead of false
  136. } else {
  137. if ($val instanceof Response) {
  138. if ($fc = $val->faultCode()) {
  139. $out = array('faultCode' => $fc, 'faultString' => self::fromUtf8($encoding, $val->faultString()));
  140. } else {
  141. $out = $encoder->decode($val->value(), $options);
  142. }
  143. } else if ($val instanceof Request) {
  144. $method = self::fromUtf8($encoding, $val->method());
  145. $out = array();
  146. $pn = $val->getNumParams();
  147. for ($i = 0; $i < $pn; $i++)
  148. $out[] = $encoder->decode($val->getParam($i), $options);
  149. } else {
  150. /// @todo copy lib behaviour in this case
  151. $out = null;
  152. }
  153. }
  154. PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
  155. return $out;
  156. }
  157. /**
  158. * Given a PHP val, convert it to xmlrpc code (wrapped up in either params/param elements or a fault element).
  159. * @param mixed $val
  160. * @return string
  161. * @todo test what happens with arrays with faultCode === 0|''|null
  162. */
  163. public static function xmlrpc_encode($val)
  164. {
  165. $encoder = new Encoder();
  166. $defaultPrecision = PhpXmlRpc::$xmlpc_double_precision;
  167. PhpXmlRpc::$xmlpc_double_precision = self::$xmlpc_double_precision;
  168. $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
  169. PhpXmlRpc::$xmlrpc_internalencoding = 'ISO-8859-1';
  170. $eval = $encoder->encode($val, array('extension_api'));
  171. if (is_array($val) && isset($val['faultCode'])) {
  172. $out = "<?xml version=\"1.0\" encoding=\"utf-8\"?" . ">\n<fault>\n " . $eval->serialize('US-ASCII') . "</fault>";
  173. } else {
  174. $out = "<?xml version=\"1.0\" encoding=\"utf-8\"?" . ">\n<params>\n<param>\n " . $eval->serialize('US-ASCII') . "</param>\n</params>";
  175. }
  176. PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
  177. PhpXmlRpc::$xmlpc_double_precision = $defaultPrecision;
  178. return $out;
  179. }
  180. /**
  181. * Given a method name and array of php values, create an xmlrpc request out
  182. * of them. If method name === null, create an xmlrpc response instead
  183. * @param string $method
  184. * @param array $params
  185. * @param array $output_options options array. At the moment only partial support for 'encoding' and 'escaping' is
  186. * provided.
  187. * encoding: iso-8859-1, utf-8
  188. * escaping: [markup], [markup,non-print]. non-ascii is treated the same as non-print
  189. * @return string
  190. *
  191. * @todo complete parsing/usage of options: encoding, escaping.
  192. * @todo might, or not, implement support for options: output_type, verbosity
  193. */
  194. public static function xmlrpc_encode_request($method, $params, $output_options = array())
  195. {
  196. $encoder = new Encoder();
  197. $internalEncoding = 'ISO-8859-1';
  198. $targetEncoding = 'iso-8859-1';
  199. $targetCharset = 'US-ASCII';
  200. if (isset($output_options['encoding'])) {
  201. $targetEncoding = $output_options['encoding'];
  202. $internalEncoding = $targetEncoding;
  203. }
  204. if (isset($output_options['escaping'])) {
  205. switch(true) {
  206. /// @todo improve this:
  207. /// escaping strategies supported by the native extension can be combined, and are:
  208. /// - cdata: wraps text in a cdata section
  209. /// - non-print: uses utf8 char entities for chars <32 and >126
  210. /// - non-ascii: uses utf8 char entities for chars > 127
  211. /// - markup: uses utf8 char entities for & " < >
  212. /// If not specified, it defaults to markup | non-ascii | non-print
  213. /// Otoh the default serialization strategy from phpxmlrpc is to
  214. /// - always escape & " < > and '
  215. /// - when going ut8 -> utf8, touch nothing else
  216. /// - when going iso-8859-1 -> ascii, convert chars <32 and 160-255 (but not 127-159)
  217. /// - when going utf8 -> ascii, convert chars <32 and >= 128 (but not 127)
  218. /// - never wrap the text in a cdata section
  219. /// We should:
  220. /// 1. log a warning if being passed options which do not make sense, eg.
  221. /// - cdata along with any other option
  222. /// - any strategy, apart cdata, missing markup (as we always escape markup)
  223. /// - an empty array (same)
  224. /// 2. support cdata escaping (done correctly)
  225. /// 3. support different escaping for non-print and non-ascii
  226. case is_array($output_options['escaping']) && !in_array('non-print', $output_options['escaping']) && !in_array('non-ascii', $output_options['escaping']):
  227. case $output_options['escaping'] == 'markup':
  228. case $output_options['escaping'] == 'cdata':
  229. $targetCharset = $targetEncoding;
  230. }
  231. }
  232. $output_options = array('extension_api');
  233. $defaultPrecision = PhpXmlRpc::$xmlpc_double_precision;
  234. PhpXmlRpc::$xmlpc_double_precision = self::$xmlpc_double_precision;
  235. $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
  236. PhpXmlRpc::$xmlrpc_internalencoding = $internalEncoding;
  237. if ($method !== null) {
  238. // mimic EPI behaviour: if ($val === NULL) then send NO parameters
  239. if (!is_array($params)) {
  240. if ($params === NULL) {
  241. $params = array();
  242. } else {
  243. $params = array($params);
  244. }
  245. } else {
  246. /// @todo fix corner cases
  247. // if given a 'hash' array, encode it as a single param
  248. $i = 0;
  249. $ok = true;
  250. foreach ($params as $key => $value)
  251. if ($key !== $i) {
  252. $ok = false;
  253. break;
  254. } else
  255. $i++;
  256. if (!$ok) {
  257. $params = array($params);
  258. }
  259. }
  260. $values = array();
  261. foreach ($params as $key => $value) {
  262. $values[] = $encoder->encode($value, $output_options);
  263. }
  264. // create request
  265. $req = new Request($method, $values);
  266. $out = preg_replace('!^<\\?xml version="1\\.0" encoding="'.$targetCharset.'" \\?>!', "<?xml version=\"1.0\" encoding=\"$targetEncoding\"?>", $req->serialize($targetCharset));
  267. } else {
  268. // create response
  269. if (is_array($params) && self::xmlrpc_is_fault($params))
  270. $resp = new Response(0, (integer)$params['faultCode'], (string)$params['faultString']);
  271. else
  272. $resp = new Response($encoder->encode($params, $output_options));
  273. $out = "<?xml version=\"1.0\" encoding=\"$targetEncoding\"?" . ">\n" . $resp->serialize($targetCharset);
  274. }
  275. PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
  276. PhpXmlRpc::$xmlpc_double_precision = $defaultPrecision;
  277. return $out;
  278. }
  279. /**
  280. * Given a php value, return its corresponding xmlrpc type
  281. * @param mixed $value
  282. * @return string
  283. *
  284. * @bug fails compatibility for array('2' => true, false)
  285. * @bug fails compatibility for array(true, 'world')
  286. */
  287. public static function xmlrpc_get_type($value)
  288. {
  289. switch (strtolower(gettype($value))) {
  290. case 'string':
  291. return Value::$xmlrpcString;
  292. case 'integer':
  293. case 'resource':
  294. return Value::$xmlrpcInt;
  295. case 'double':
  296. return Value::$xmlrpcDouble;
  297. case 'boolean':
  298. return Value::$xmlrpcBoolean;
  299. case 'array':
  300. $i = 0;
  301. $ok = true;
  302. foreach ($value as $key => $valueue)
  303. if ($key !== $i) {
  304. $ok = false;
  305. break;
  306. } else
  307. $i++;
  308. return $ok ? Value::$xmlrpcArray : Value::$xmlrpcStruct;
  309. case 'object':
  310. if ($value instanceof Value) {
  311. $type = $value->scalarTyp();
  312. return str_replace('dateTime.iso8601', 'datetime', $type);
  313. } elseif ($value instanceof \stdClass && isset($value->xmlrpc_type)) {
  314. switch($value->xmlrpc_type) {
  315. case 'datetime':
  316. case 'base64':
  317. return $value->xmlrpc_type;
  318. default:
  319. return 'none';
  320. }
  321. }
  322. return Value::$xmlrpcStruct;
  323. case 'null':
  324. return Value::$xmlrpcBase64; // go figure why...
  325. }
  326. }
  327. /**
  328. * Checks if a given php array corresponds to an xmlrpc fault response
  329. * @param array $arg
  330. * @return boolean
  331. */
  332. public static function xmlrpc_is_fault($arg)
  333. {
  334. return is_array($arg) && array_key_exists('faultCode', $arg) && array_key_exists('faultString', $arg);
  335. }
  336. /**
  337. * @param string $xml
  338. * @return array
  339. */
  340. public static function xmlrpc_parse_method_descriptions($xml)
  341. {
  342. return Server::parse_method_descriptions($xml);
  343. }
  344. /** Server side ***************************************************************/
  345. /**
  346. * @param Server $server
  347. * @param array $desc
  348. * @return int
  349. */
  350. public static function xmlrpc_server_add_introspection_data($server, $desc)
  351. {
  352. if ($server instanceof Server) {
  353. return $server->add_introspection_data($desc);
  354. }
  355. return 0;
  356. }
  357. /**
  358. * Parses XML request and calls corresponding method
  359. * @param Server $server
  360. * @param string $xml
  361. * @param mixed $user_data
  362. * @param array $output_options
  363. * @return string
  364. */
  365. public static function xmlrpc_server_call_method($server, $xml, $user_data, $output_options = array())
  366. {
  367. $server->user_data = $user_data;
  368. return $server->service($xml, true);
  369. }
  370. /**
  371. * Create a new xmlrpc server instance
  372. * @return Server
  373. */
  374. public static function xmlrpc_server_create()
  375. {
  376. $s = new Server();
  377. $s->functions_parameters_type = 'epivals';
  378. $s->compress_response = false; // since we will not be outputting any http headers to go with it
  379. return $s;
  380. }
  381. /**
  382. * This function actually does nothing, but it is kept for compatibility.
  383. * To destroy a server object, just unset() it, or send it out of scope...
  384. * @param Server $server
  385. * @return integer
  386. */
  387. public static function xmlrpc_server_destroy($server)
  388. {
  389. if ($server instanceof Server)
  390. return 1;
  391. return 0;
  392. }
  393. /**
  394. * @param Server $server
  395. * @param string $function
  396. * @return bool
  397. */
  398. public static function xmlrpc_server_register_introspection_callback($server, $function)
  399. {
  400. if ($server instanceof Server) {
  401. return $server->register_introspection_callback($function);
  402. }
  403. return false;
  404. }
  405. /**
  406. * Add a php function as xmlrpc method handler to an existing server.
  407. * PHP function sig: f(string $methodname, array $params, mixed $extra_data)
  408. * @param Server $server
  409. * @param string $method_name
  410. * @param string $function
  411. * @return boolean true on success or false
  412. */
  413. public static function xmlrpc_server_register_method($server, $method_name, $function)
  414. {
  415. if ($server instanceof BaseServer) {
  416. $server->add_to_map($method_name, $function);
  417. return true;
  418. }
  419. return false;
  420. }
  421. /**
  422. * Set string $val to a known xmlrpc type (base64 or datetime only), for serializing it later
  423. * (NB: this will turn the string into an object!).
  424. * @param string $val
  425. * @param string $type
  426. * @return boolean false if conversion did not take place
  427. */
  428. public static function xmlrpc_set_type(&$val, $type)
  429. {
  430. if (is_string($val)) {
  431. if ($type == 'base64') {
  432. $value = array(
  433. 'scalar' => $val,
  434. 'xmlrpc_type' => 'base64'
  435. );
  436. $val = (object)$value;
  437. } elseif ($type == 'datetime') {
  438. 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)) {
  439. // add 3 object members to make it more compatible to user code
  440. $value = array(
  441. 'scalar' => $val,
  442. 'xmlrpc_type' => 'datetime',
  443. 'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($val)
  444. );
  445. $val = (object)$value;
  446. } else {
  447. return false;
  448. }
  449. } else {
  450. // @todo EPI will NOT raise a warning for good type names, eg. 'boolean', etc...
  451. trigger_error("invalid type '$type' passed to xmlrpc_set_type()");
  452. return false;
  453. }
  454. return true;
  455. } else {
  456. return false;
  457. }
  458. }
  459. protected static function fromUtf8($to, $str)
  460. {
  461. if (strtoupper($to) != 'UTF-8') {
  462. /// @todo support mbstring as an alternative, as well as plain utf8_decode if none are available and target is latin-1
  463. $dstr = @iconv('UTF-8', $to, $str);
  464. if ($dstr !== false) {
  465. return $dstr;
  466. }
  467. }
  468. return $str;
  469. }
  470. }