Server.php 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612
  1. <?php
  2. namespace PhpXmlRpc;
  3. use PhpXmlRpc\Exception\NoSuchMethodException;
  4. use PhpXmlRpc\Exception\ValueErrorException;
  5. use PhpXmlRpc\Helper\Http;
  6. use PhpXmlRpc\Helper\Interop;
  7. use PhpXmlRpc\Helper\Logger;
  8. use PhpXmlRpc\Helper\XMLParser;
  9. use PhpXmlRpc\Traits\CharsetEncoderAware;
  10. use PhpXmlRpc\Traits\DeprecationLogger;
  11. use PhpXmlRpc\Traits\ParserAware;
  12. /**
  13. * Allows effortless implementation of XML-RPC servers
  14. *
  15. * @property string[] $accepted_compression deprecated - public access left in purely for BC. Access via getOption()/setOption()
  16. * @property bool $allow_system_funcs deprecated - public access left in purely for BC. Access via getOption()/setOption()
  17. * @property bool $compress_response deprecated - public access left in purely for BC. Access via getOption()/setOption()
  18. * @property int $debug deprecated - public access left in purely for BC. Access via getOption()/setOption()
  19. * @property int $exception_handling deprecated - public access left in purely for BC. Access via getOption()/setOption()
  20. * @property string $functions_parameters_type deprecated - public access left in purely for BC. Access via getOption()/setOption()
  21. * @property array $phpvals_encoding_options deprecated - public access left in purely for BC. Access via getOption()/setOption()
  22. * @property string $response_charset_encoding deprecated - public access left in purely for BC. Access via getOption()/setOption()
  23. */
  24. class Server
  25. {
  26. use CharsetEncoderAware;
  27. use DeprecationLogger;
  28. use ParserAware;
  29. const OPT_ACCEPTED_COMPRESSION = 'accepted_compression';
  30. const OPT_ALLOW_SYSTEM_FUNCS = 'allow_system_funcs';
  31. const OPT_COMPRESS_RESPONSE = 'compress_response';
  32. const OPT_DEBUG = 'debug';
  33. const OPT_EXCEPTION_HANDLING = 'exception_handling';
  34. const OPT_FUNCTIONS_PARAMETERS_TYPE = 'functions_parameters_type';
  35. const OPT_PHPVALS_ENCODING_OPTIONS = 'phpvals_encoding_options';
  36. const OPT_RESPONSE_CHARSET_ENCODING = 'response_charset_encoding';
  37. /** @var string */
  38. protected static $responseClass = '\\PhpXmlRpc\\Response';
  39. /**
  40. * @var string
  41. * Defines how functions in $dmap will be invoked: either using an xml-rpc Request object or plain php values.
  42. * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals' (only for use by polyfill-xmlrpc).
  43. *
  44. * @todo create class constants for these
  45. */
  46. protected $functions_parameters_type = 'xmlrpcvals';
  47. /**
  48. * @var array
  49. * Option used for fine-tuning the encoding the php values returned from functions registered in the dispatch map
  50. * when the functions_parameters_type member is set to 'phpvals'.
  51. * @see Encoder::encode for a list of values
  52. */
  53. protected $phpvals_encoding_options = array('auto_dates');
  54. /**
  55. * @var int
  56. * Controls whether the server is going to echo debugging messages back to the client as comments in response body.
  57. * SECURITY SENSITIVE!
  58. * Valid values:
  59. * 0 =
  60. * 1 =
  61. * 2 =
  62. * 3 =
  63. */
  64. protected $debug = 1;
  65. /**
  66. * @var int
  67. * Controls behaviour of server when the invoked method-handler function throws an exception (within the `execute` method):
  68. * 0 = catch it and return an 'internal error' xml-rpc response (default)
  69. * 1 = SECURITY SENSITIVE DO NOT ENABLE ON PUBLIC SERVERS!!! catch it and return an xml-rpc response with the error
  70. * corresponding to the exception, both its code and message.
  71. * 2 = allow the exception to float to the upper layers
  72. * Can be overridden per-method-handler in the dispatch map
  73. */
  74. protected $exception_handling = 0;
  75. /**
  76. * @var bool
  77. * When set to true, it will enable HTTP compression of the response, in case the client has declared its support
  78. * for compression in the request.
  79. * Automatically set at constructor time.
  80. */
  81. protected $compress_response = false;
  82. /**
  83. * @var string[]
  84. * List of http compression methods accepted by the server for requests. Automatically set at constructor time.
  85. * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
  86. */
  87. protected $accepted_compression = array();
  88. /**
  89. * @var bool
  90. * Shall we serve calls to system.* methods?
  91. */
  92. protected $allow_system_funcs = true;
  93. /**
  94. * List of charset encodings natively accepted for requests.
  95. * Set at constructor time.
  96. * @deprecated UNUSED so far by this library. It is still accessible by subclasses but will be dropped in the future.
  97. */
  98. private $accepted_charset_encodings = array();
  99. /**
  100. * @var string
  101. * Charset encoding to be used for response.
  102. * NB: if we can, we will convert the generated response from internal_encoding to the intended one.
  103. * Can be:
  104. * - a supported xml encoding (only UTF-8 and ISO-8859-1, unless mbstring is enabled),
  105. * - null (leave unspecified in response, convert output stream to US_ASCII),
  106. * - 'auto' (use client-specified charset encoding or same as request if request headers do not specify it (unless request is US-ASCII: then use library default anyway).
  107. * NB: pretty dangerous if you accept every charset and do not have mbstring enabled)
  108. */
  109. protected $response_charset_encoding = '';
  110. protected static $options = array(
  111. self::OPT_ACCEPTED_COMPRESSION,
  112. self::OPT_ALLOW_SYSTEM_FUNCS,
  113. self::OPT_COMPRESS_RESPONSE,
  114. self::OPT_DEBUG,
  115. self::OPT_EXCEPTION_HANDLING,
  116. self::OPT_FUNCTIONS_PARAMETERS_TYPE,
  117. self::OPT_PHPVALS_ENCODING_OPTIONS,
  118. self::OPT_RESPONSE_CHARSET_ENCODING,
  119. );
  120. /**
  121. * @var mixed
  122. * Extra data passed at runtime to method handling functions. Used only by EPI layer
  123. * @internal
  124. */
  125. public $user_data = null;
  126. /**
  127. * Array defining php functions exposed as xml-rpc methods by this server.
  128. * @var array[] $dmap
  129. */
  130. protected $dmap = array();
  131. /**
  132. * Storage for internal debug info.
  133. */
  134. protected $debug_info = '';
  135. protected static $_xmlrpc_debuginfo = '';
  136. protected static $_xmlrpcs_occurred_errors = '';
  137. protected static $_xmlrpcs_prev_ehandler = '';
  138. /**
  139. * @param array[] $dispatchMap the dispatch map with definition of exposed services
  140. * Array keys are the names of the method names.
  141. * Each array value is an array with the following members:
  142. * - function (callable)
  143. * - docstring (optional)
  144. * - signature (array, optional)
  145. * - signature_docs (array, optional)
  146. * - parameters_type (string, optional)
  147. * - exception_handling (int, optional)
  148. * @param boolean $serviceNow set to false in order to prevent the server from running upon construction
  149. */
  150. public function __construct($dispatchMap = null, $serviceNow = true)
  151. {
  152. // if ZLIB is enabled, let the server by default accept compressed requests,
  153. // and compress responses sent to clients that support them
  154. if (function_exists('gzinflate')) {
  155. $this->accepted_compression[] = 'gzip';
  156. }
  157. if (function_exists('gzuncompress')) {
  158. $this->accepted_compression[] = 'deflate';
  159. }
  160. if (function_exists('gzencode') || function_exists('gzcompress')) {
  161. $this->compress_response = true;
  162. }
  163. // by default the xml parser can support these 3 charset encodings
  164. $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
  165. // dispMap is a dispatch array of methods mapped to function names and signatures.
  166. // If a method doesn't appear in the map then an unknown method error is generated.
  167. // milosch - changed to make passing dispMap optional. Instead, you can use the addToMap() function
  168. // to add functions manually (borrowed from SOAPX4)
  169. if ($dispatchMap) {
  170. $this->setDispatchMap($dispatchMap);
  171. if ($serviceNow) {
  172. $this->service();
  173. }
  174. }
  175. }
  176. /**
  177. * @param string $name see all the OPT_ constants
  178. * @param mixed $value
  179. * @return $this
  180. * @throws ValueErrorException on unsupported option
  181. */
  182. public function setOption($name, $value)
  183. {
  184. switch ($name) {
  185. case self::OPT_ACCEPTED_COMPRESSION :
  186. case self::OPT_ALLOW_SYSTEM_FUNCS:
  187. case self::OPT_COMPRESS_RESPONSE:
  188. case self::OPT_DEBUG:
  189. case self::OPT_EXCEPTION_HANDLING:
  190. case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
  191. case self::OPT_PHPVALS_ENCODING_OPTIONS:
  192. case self::OPT_RESPONSE_CHARSET_ENCODING:
  193. $this->$name = $value;
  194. break;
  195. default:
  196. throw new ValueErrorException("Unsupported option '$name'");
  197. }
  198. return $this;
  199. }
  200. /**
  201. * @param string $name see all the OPT_ constants
  202. * @return mixed
  203. * @throws ValueErrorException on unsupported option
  204. */
  205. public function getOption($name)
  206. {
  207. switch ($name) {
  208. case self::OPT_ACCEPTED_COMPRESSION:
  209. case self::OPT_ALLOW_SYSTEM_FUNCS:
  210. case self::OPT_COMPRESS_RESPONSE:
  211. case self::OPT_DEBUG:
  212. case self::OPT_EXCEPTION_HANDLING:
  213. case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
  214. case self::OPT_PHPVALS_ENCODING_OPTIONS:
  215. case self::OPT_RESPONSE_CHARSET_ENCODING:
  216. return $this->$name;
  217. default:
  218. throw new ValueErrorException("Unsupported option '$name'");
  219. }
  220. }
  221. /**
  222. * Returns the complete list of Server options.
  223. * @return array
  224. */
  225. public function getOptions()
  226. {
  227. $values = array();
  228. foreach(static::$options as $opt) {
  229. $values[$opt] = $this->getOption($opt);
  230. }
  231. return $values;
  232. }
  233. /**
  234. * @param array $options key: see all the OPT_ constants
  235. * @return $this
  236. * @throws ValueErrorException on unsupported option
  237. */
  238. public function setOptions($options)
  239. {
  240. foreach($options as $name => $value) {
  241. $this->setOption($name, $value);
  242. }
  243. return $this;
  244. }
  245. /**
  246. * Set debug level of server.
  247. *
  248. * @param integer $level debug lvl: determines info added to xml-rpc responses (as xml comments)
  249. * 0 = no debug info,
  250. * 1 = msgs set from user with debugmsg(),
  251. * 2 = add complete xml-rpc request (headers and body),
  252. * 3 = add also all processing warnings happened during method processing
  253. * (NB: this involves setting a custom error handler, and might interfere
  254. * with the standard processing of the php function exposed as method. In
  255. * particular, triggering a USER_ERROR level error will not halt script
  256. * execution anymore, but just end up logged in the xml-rpc response)
  257. * Note that info added at level 2 and 3 will be base64 encoded
  258. * @return $this
  259. */
  260. public function setDebug($level)
  261. {
  262. $this->debug = $level;
  263. return $this;
  264. }
  265. /**
  266. * Add a string to the debug info that can be later serialized by the server as part of the response message.
  267. * Note that for best compatibility, the debug string should be encoded using the PhpXmlRpc::$xmlrpc_internalencoding
  268. * character set.
  269. *
  270. * @param string $msg
  271. * @return void
  272. */
  273. public static function xmlrpc_debugmsg($msg)
  274. {
  275. static::$_xmlrpc_debuginfo .= $msg . "\n";
  276. }
  277. /**
  278. * Add a string to the debug info that will be later serialized by the server as part of the response message
  279. * (base64 encoded) when debug level >= 2
  280. *
  281. * @param string $msg
  282. * @return void
  283. */
  284. public static function error_occurred($msg)
  285. {
  286. static::$_xmlrpcs_occurred_errors .= $msg . "\n";
  287. }
  288. /**
  289. * Return a string with the serialized representation of all debug info.
  290. *
  291. * @internal this function will become protected in the future
  292. *
  293. * @param string $charsetEncoding the target charset encoding for the serialization
  294. *
  295. * @return string an XML comment (or two)
  296. */
  297. public function serializeDebug($charsetEncoding = '')
  298. {
  299. // Tough encoding problem: which internal charset should we assume for debug info?
  300. // It might contain a copy of raw data received from client, ie with unknown encoding,
  301. // intermixed with php generated data and user generated data...
  302. // so we split it: system debug is base 64 encoded,
  303. // user debug info should be encoded by the end user using the INTERNAL_ENCODING
  304. $out = '';
  305. if ($this->debug_info != '') {
  306. $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n" . base64_encode($this->debug_info) . "\n-->\n";
  307. }
  308. if (static::$_xmlrpc_debuginfo != '') {
  309. $out .= "<!-- DEBUG INFO:\n" . $this->getCharsetEncoder()->encodeEntities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "\n-->\n";
  310. // NB: a better solution MIGHT be to use CDATA, but we need to insert it
  311. // into return payload AFTER the beginning tag
  312. //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n";
  313. }
  314. return $out;
  315. }
  316. /**
  317. * Execute the xml-rpc request, printing the response.
  318. *
  319. * @param string $data the request body. If null, the http POST request will be examined
  320. * @param bool $returnPayload When true, return the response but do not echo it or any http header
  321. *
  322. * @return Response|string the response object (usually not used by caller...) or its xml serialization
  323. * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
  324. */
  325. public function service($data = null, $returnPayload = false)
  326. {
  327. if ($data === null) {
  328. $data = file_get_contents('php://input');
  329. }
  330. $rawData = $data;
  331. // reset internal debug info
  332. $this->debug_info = '';
  333. // Save what we received, before parsing it
  334. if ($this->debug > 1) {
  335. $this->debugMsg("+++GOT+++\n" . $data . "\n+++END+++");
  336. }
  337. $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
  338. if (!$resp) {
  339. // this actually executes the request
  340. $resp = $this->parseRequest($data, $reqCharset);
  341. // save full body of request into response, for debugging purposes.
  342. // NB: this is the _request_ data, not the response's own data, unlike what happens client-side
  343. /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even
  344. /// better: just avoid setting this, and set debug info of the received http request in the request
  345. /// object instead? It's not like the developer misses access to _SERVER, _COOKIES though...
  346. /// Last but not least: the raw data might be of use to handler functions - but in decompressed form...
  347. $resp->raw_data = $rawData;
  348. }
  349. if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') {
  350. $this->debugMsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
  351. static::$_xmlrpcs_occurred_errors . "+++END+++");
  352. }
  353. $header = $resp->xml_header($respCharset);
  354. if ($this->debug > 0) {
  355. $header .= $this->serializeDebug($respCharset);
  356. }
  357. // Do not create response serialization if it has already happened. Helps to build json magic
  358. /// @todo what if the payload was created targeting a different charset than $respCharset?
  359. /// Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
  360. $payload = $resp->getPayload();
  361. if (empty($payload)) {
  362. $payload = $resp->serialize($respCharset);
  363. }
  364. $payload = $header . $payload;
  365. if ($returnPayload) {
  366. return $payload;
  367. }
  368. // if we get a warning/error that has output some text before here, then we cannot
  369. // add a new header. We cannot say we are sending xml, either...
  370. if (!headers_sent()) {
  371. header('Content-Type: ' . $resp->getContentType());
  372. // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did
  373. header("Vary: Accept-Charset");
  374. // http compression of output: only if we can do it, and we want to do it, and client asked us to,
  375. // and php ini settings do not force it already
  376. $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
  377. if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) {
  378. if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) {
  379. $payload = gzencode($payload);
  380. header("Content-Encoding: gzip");
  381. header("Vary: Accept-Encoding");
  382. } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) {
  383. $payload = gzcompress($payload);
  384. header("Content-Encoding: deflate");
  385. header("Vary: Accept-Encoding");
  386. }
  387. }
  388. // Do not output content-length header if php is compressing output for us: it will mess up measurements.
  389. // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for
  390. // responses up to 8000 bytes
  391. if ($phpNoSelfCompress) {
  392. header('Content-Length: ' . (int)strlen($payload));
  393. }
  394. } else {
  395. $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
  396. }
  397. print $payload;
  398. // return response, in case subclasses want it
  399. return $resp;
  400. }
  401. /**
  402. * Add a method to the dispatch map.
  403. *
  404. * @param string $methodName the name with which the method will be made available
  405. * @param callable $function the php function that will get invoked
  406. * @param array[] $sig the array of valid method signatures.
  407. * Each element is one signature: an array of strings with at least one element
  408. * First element = type of returned value. Elements 2..N = types of parameters 1..N
  409. * @param string $doc method documentation
  410. * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
  411. * descriptions instead of types (one string for return type, one per param)
  412. * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
  413. * @param int $exceptionHandling @see $this->exception_handling
  414. * @return void
  415. *
  416. * @todo raise a warning if the user tries to register a 'system.' method
  417. */
  418. public function addToMap($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
  419. $exceptionHandling = false)
  420. {
  421. $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, $parametersType, $exceptionHandling);
  422. }
  423. /**
  424. * Add a method to the dispatch map.
  425. *
  426. * @param string $methodName the name with which the method will be made available
  427. * @param callable $function the php function that will get invoked
  428. * @param array[] $sig the array of valid method signatures.
  429. * Each element is one signature: an array of strings with at least one element
  430. * First element = type of returned value. Elements 2..N = types of parameters 1..N
  431. * @param string $doc method documentation
  432. * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
  433. * descriptions instead of types (one string for return type, one per param)
  434. * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
  435. * @param int $exceptionHandling @see $this->exception_handling
  436. * @return void
  437. *
  438. * @todo raise a warning if the user tries to register a 'system.' method
  439. * @deprecated use addToMap instead
  440. */
  441. public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
  442. $exceptionHandling = false)
  443. {
  444. $this->logDeprecationUnlessCalledBy('addToMap');
  445. $this->dmap[$methodName] = array(
  446. 'function' => $function,
  447. 'docstring' => $doc,
  448. );
  449. if ($sig) {
  450. $this->dmap[$methodName]['signature'] = $sig;
  451. }
  452. if ($sigDoc) {
  453. $this->dmap[$methodName]['signature_docs'] = $sigDoc;
  454. }
  455. if ($parametersType) {
  456. $this->dmap[$methodName]['parameters_type'] = $parametersType;
  457. }
  458. if ($exceptionHandling !== false) {
  459. $this->dmap[$methodName]['exception_handling'] = $exceptionHandling;
  460. }
  461. }
  462. /**
  463. * Verify type and number of parameters received against a list of known signatures.
  464. *
  465. * @param array|Request $in array of either xml-rpc value objects or xml-rpc type definitions
  466. * @param array $sigs array of known signatures to match against
  467. * @return array int, string
  468. */
  469. protected function verifySignature($in, $sigs)
  470. {
  471. // check each possible signature in turn
  472. if (is_object($in)) {
  473. $numParams = $in->getNumParams();
  474. } else {
  475. $numParams = count($in);
  476. }
  477. foreach ($sigs as $curSig) {
  478. if (count($curSig) == $numParams + 1) {
  479. $itsOK = 1;
  480. for ($n = 0; $n < $numParams; $n++) {
  481. if (is_object($in)) {
  482. $p = $in->getParam($n);
  483. if ($p->kindOf() == 'scalar') {
  484. $pt = $p->scalarTyp();
  485. } else {
  486. $pt = $p->kindOf();
  487. }
  488. } else {
  489. $pt = ($in[$n] == 'i4') ? 'int' : strtolower($in[$n]); // dispatch maps never use i4...
  490. }
  491. // param index is $n+1, as first member of sig is return type
  492. if ($pt != $curSig[$n + 1] && $curSig[$n + 1] != Value::$xmlrpcValue) {
  493. $itsOK = 0;
  494. $pno = $n + 1;
  495. $wanted = $curSig[$n + 1];
  496. $got = $pt;
  497. break;
  498. }
  499. }
  500. if ($itsOK) {
  501. return array(1, '');
  502. }
  503. }
  504. }
  505. if (isset($wanted)) {
  506. return array(0, "Wanted {$wanted}, got {$got} at param {$pno}");
  507. } else {
  508. return array(0, "No method signature matches number of parameters");
  509. }
  510. }
  511. /**
  512. * Parse http headers received along with xml-rpc request. If needed, inflate request.
  513. *
  514. * @return Response|null null on success or an error Response
  515. */
  516. protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression)
  517. {
  518. // check if $_SERVER is populated: it might have been disabled via ini file
  519. // (this is true even when in CLI mode)
  520. if (count($_SERVER) == 0) {
  521. $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': cannot parse request headers as $_SERVER is not populated');
  522. }
  523. if ($this->debug > 1) {
  524. if (function_exists('getallheaders')) {
  525. $this->debugMsg(''); // empty line
  526. foreach (getallheaders() as $name => $val) {
  527. $this->debugMsg("HEADER: $name: $val");
  528. }
  529. }
  530. }
  531. if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
  532. $contentEncoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
  533. } else {
  534. $contentEncoding = '';
  535. }
  536. $rawData = $data;
  537. // check if request body has been compressed and decompress it
  538. if ($contentEncoding != '' && strlen($data)) {
  539. if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') {
  540. // if decoding works, use it. else assume data wasn't gzencoded
  541. /// @todo test separately for gzinflate and gzuncompress
  542. if (function_exists('gzinflate') && in_array($contentEncoding, $this->accepted_compression)) {
  543. if ($contentEncoding == 'deflate' && $degzdata = @gzuncompress($data)) {
  544. $data = $degzdata;
  545. if ($this->debug > 1) {
  546. $this->debugMsg("\n+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
  547. }
  548. } elseif ($contentEncoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
  549. $data = $degzdata;
  550. if ($this->debug > 1) {
  551. $this->debugMsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
  552. }
  553. } else {
  554. $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'],
  555. PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData)
  556. );
  557. return $r;
  558. }
  559. } else {
  560. $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'],
  561. PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData)
  562. );
  563. return $r;
  564. }
  565. }
  566. }
  567. // check if client specified accepted charsets, and if we know how to fulfill the request
  568. if ($this->response_charset_encoding == 'auto') {
  569. $respEncoding = '';
  570. if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
  571. // here we check if we can match the client-requested encoding with the encodings we know we can generate.
  572. // we parse q=0.x preferences instead of preferring the first charset specified
  573. $http = new Http();
  574. $clientAcceptedCharsets = $http->parseAcceptHeader($_SERVER['HTTP_ACCEPT_CHARSET']);
  575. $knownCharsets = $this->getCharsetEncoder()->knownCharsets();
  576. foreach ($clientAcceptedCharsets as $accepted) {
  577. foreach ($knownCharsets as $charset) {
  578. if (strtoupper($accepted) == strtoupper($charset)) {
  579. $respEncoding = $charset;
  580. break 2;
  581. }
  582. }
  583. }
  584. }
  585. } else {
  586. $respEncoding = $this->response_charset_encoding;
  587. }
  588. if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
  589. $respCompression = $_SERVER['HTTP_ACCEPT_ENCODING'];
  590. } else {
  591. $respCompression = '';
  592. }
  593. // 'guestimate' request encoding
  594. /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check???
  595. $parser = $this->getParser();
  596. $reqEncoding = $parser->guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
  597. $data);
  598. return null;
  599. }
  600. /**
  601. * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the
  602. * server.
  603. * @internal this function will become protected in the future
  604. *
  605. * @param string $data the xml request
  606. * @param string $reqEncoding (optional) the charset encoding of the xml request
  607. * @return Response
  608. * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
  609. *
  610. * @todo either rename this function or move the 'execute' part out of it...
  611. */
  612. public function parseRequest($data, $reqEncoding = '')
  613. {
  614. // decompose incoming XML into request structure
  615. /// @todo move this block of code into the XMLParser
  616. if ($reqEncoding != '') {
  617. // Since parsing will fail if
  618. // - charset is not specified in the xml declaration,
  619. // - the encoding is not UTF8 and
  620. // - there are non-ascii chars in the text,
  621. // we try to work round that...
  622. // The following code might be better for mb_string enabled installs, but it makes the lib about 200% slower...
  623. //if (!is_valid_charset($reqEncoding, array('UTF-8')))
  624. if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
  625. if (function_exists('mb_convert_encoding')) {
  626. $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding);
  627. } else {
  628. if ($reqEncoding == 'ISO-8859-1') {
  629. $data = utf8_encode($data);
  630. } else {
  631. $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received request: ' . $reqEncoding);
  632. }
  633. }
  634. }
  635. }
  636. // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
  637. // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
  638. if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
  639. $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
  640. } else {
  641. $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
  642. }
  643. // register a callback with the xml parser for when it finds the method name
  644. $options['methodname_callback'] = array($this, 'methodNameCallback');
  645. $xmlRpcParser = $this->getParser();
  646. try {
  647. $_xh = $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
  648. // BC
  649. if (!is_array($_xh)) {
  650. $_xh = $xmlRpcParser->_xh;
  651. }
  652. } catch (NoSuchMethodException $e) {
  653. return new static::$responseClass(0, $e->getCode(), $e->getMessage());
  654. }
  655. if ($_xh['isf'] == 3) {
  656. // (BC) we return XML error as a faultCode
  657. preg_match('/^XML error ([0-9]+)/', $_xh['isf_reason'], $matches);
  658. return new static::$responseClass(
  659. 0,
  660. PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
  661. $_xh['isf_reason']);
  662. } elseif ($_xh['isf']) {
  663. /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc vs.
  664. /// parsing error
  665. return new static::$responseClass(
  666. 0,
  667. PhpXmlRpc::$xmlrpcerr['invalid_request'],
  668. PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $_xh['isf_reason']);
  669. } else {
  670. // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
  671. // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals)
  672. // that would mean a useless encode+decode pass
  673. if ($this->functions_parameters_type != 'xmlrpcvals' ||
  674. (isset($this->dmap[$_xh['method']]['parameters_type']) &&
  675. ($this->dmap[$_xh['method']]['parameters_type'] != 'xmlrpcvals')
  676. )
  677. ) {
  678. if ($this->debug > 1) {
  679. $this->debugMsg("\n+++PARSED+++\n" . var_export($_xh['params'], true) . "\n+++END+++");
  680. }
  681. return $this->execute($_xh['method'], $_xh['params'], $_xh['pt']);
  682. } else {
  683. // build a Request object with data parsed from xml and add parameters in
  684. $req = new Request($_xh['method']);
  685. /// @todo for more speed, we could just pass in the array to the constructor (and loose the type validation)...
  686. for ($i = 0; $i < count($_xh['params']); $i++) {
  687. $req->addParam($_xh['params'][$i]);
  688. }
  689. if ($this->debug > 1) {
  690. $this->debugMsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
  691. }
  692. return $this->execute($req);
  693. }
  694. }
  695. }
  696. /**
  697. * Execute a method invoked by the client, checking parameters used.
  698. *
  699. * @param Request|string $req either a Request obj or a method name
  700. * @param mixed[] $params array with method parameters as php types (only if $req is method name)
  701. * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name)
  702. * @return Response
  703. *
  704. * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
  705. */
  706. protected function execute($req, $params = null, $paramTypes = null)
  707. {
  708. static::$_xmlrpcs_occurred_errors = '';
  709. static::$_xmlrpc_debuginfo = '';
  710. if (is_object($req)) {
  711. $methodName = $req->method();
  712. } else {
  713. $methodName = $req;
  714. }
  715. $sysCall = $this->isSyscall($methodName);
  716. $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
  717. if (!isset($dmap[$methodName]['function'])) {
  718. // No such method
  719. return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unknown_method'], PhpXmlRpc::$xmlrpcstr['unknown_method']);
  720. }
  721. // Check signature
  722. if (isset($dmap[$methodName]['signature'])) {
  723. $sig = $dmap[$methodName]['signature'];
  724. if (is_object($req)) {
  725. list($ok, $errStr) = $this->verifySignature($req, $sig);
  726. } else {
  727. list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
  728. }
  729. if (!$ok) {
  730. // Didn't match.
  731. return new static::$responseClass(
  732. 0,
  733. PhpXmlRpc::$xmlrpcerr['incorrect_params'],
  734. PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
  735. );
  736. }
  737. }
  738. $func = $dmap[$methodName]['function'];
  739. // let the 'class::function' syntax be accepted in dispatch maps
  740. if (is_string($func) && strpos($func, '::')) {
  741. $func = explode('::', $func);
  742. }
  743. // build string representation of function 'name'
  744. if (is_array($func)) {
  745. if (is_object($func[0])) {
  746. $funcName = get_class($func[0]) . '->' . $func[1];
  747. } else {
  748. $funcName = implode('::', $func);
  749. }
  750. } else if ($func instanceof \Closure) {
  751. $funcName = 'Closure';
  752. } else {
  753. $funcName = $func;
  754. }
  755. // verify that function to be invoked is in fact callable
  756. if (!is_callable($func)) {
  757. $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
  758. return new static::$responseClass(
  759. 0,
  760. PhpXmlRpc::$xmlrpcerr['server_error'],
  761. PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
  762. );
  763. }
  764. if (isset($dmap[$methodName]['exception_handling'])) {
  765. $exception_handling = (int)$dmap[$methodName]['exception_handling'];
  766. } else {
  767. $exception_handling = $this->exception_handling;
  768. }
  769. // If debug level is 3, we should catch all errors generated during processing of user function, and log them
  770. // as part of response
  771. if ($this->debug > 2) {
  772. self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
  773. }
  774. try {
  775. // Allow mixed-convention servers
  776. if (is_object($req)) {
  777. // call an 'xml-rpc aware' function
  778. if ($sysCall) {
  779. $r = call_user_func($func, $this, $req);
  780. } else {
  781. $r = call_user_func($func, $req);
  782. }
  783. if (!is_a($r, 'PhpXmlRpc\Response')) {
  784. $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
  785. if (is_a($r, 'PhpXmlRpc\Value')) {
  786. $r = new static::$responseClass($r);
  787. } else {
  788. $r = new static::$responseClass(
  789. 0,
  790. PhpXmlRpc::$xmlrpcerr['server_error'],
  791. PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
  792. );
  793. }
  794. }
  795. } else {
  796. // call a 'plain php' function
  797. if ($sysCall) {
  798. array_unshift($params, $this);
  799. $r = call_user_func_array($func, $params);
  800. } else {
  801. // 3rd API convention for method-handling functions: EPI-style
  802. if ($this->functions_parameters_type == 'epivals') {
  803. $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
  804. // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
  805. if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
  806. $r = new static::$responseClass(0, (integer)$r['faultCode'], (string)$r['faultString']);
  807. } else {
  808. // functions using EPI api should NOT return resp objects, so make sure we encode the
  809. // return type correctly
  810. $encoder = new Encoder();
  811. $r = new static::$responseClass($encoder->encode($r, array('extension_api')));
  812. }
  813. } else {
  814. $r = call_user_func_array($func, $params);
  815. }
  816. }
  817. // the return type can be either a Response object or a plain php value...
  818. if (!is_a($r, '\PhpXmlRpc\Response')) {
  819. // q: what should we assume here about automatic encoding of datetimes and php classes instances?
  820. // a: let the user decide
  821. $encoder = new Encoder();
  822. $r = new static::$responseClass($encoder->encode($r, $this->phpvals_encoding_options));
  823. }
  824. }
  825. /// @todo bump minimum php version to 7.1 and use a single catch clause instead of the duplicate blocks
  826. } catch (\Exception $e) {
  827. // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
  828. // proper error-response
  829. switch ($exception_handling) {
  830. case 2:
  831. if ($this->debug > 2) {
  832. if (self::$_xmlrpcs_prev_ehandler) {
  833. set_error_handler(self::$_xmlrpcs_prev_ehandler);
  834. } else {
  835. restore_error_handler();
  836. }
  837. }
  838. throw $e;
  839. case 1:
  840. $errCode = $e->getCode();
  841. if ($errCode == 0) {
  842. $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
  843. }
  844. $r = new static::$responseClass(0, $errCode, $e->getMessage());
  845. break;
  846. default:
  847. $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
  848. }
  849. } catch (\Error $e) {
  850. // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
  851. // proper error-response
  852. switch ($exception_handling) {
  853. case 2:
  854. if ($this->debug > 2) {
  855. if (self::$_xmlrpcs_prev_ehandler) {
  856. set_error_handler(self::$_xmlrpcs_prev_ehandler);
  857. } else {
  858. restore_error_handler();
  859. }
  860. }
  861. throw $e;
  862. case 1:
  863. $errCode = $e->getCode();
  864. if ($errCode == 0) {
  865. $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
  866. }
  867. $r = new static::$responseClass(0, $errCode, $e->getMessage());
  868. break;
  869. default:
  870. $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
  871. }
  872. }
  873. if ($this->debug > 2) {
  874. // note: restore the error handler we found before calling the user func, even if it has been changed
  875. // inside the func itself
  876. if (self::$_xmlrpcs_prev_ehandler) {
  877. set_error_handler(self::$_xmlrpcs_prev_ehandler);
  878. } else {
  879. restore_error_handler();
  880. }
  881. }
  882. return $r;
  883. }
  884. /**
  885. * Registered as callback for when the XMLParser has found the name of the method to execute.
  886. * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
  887. * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
  888. *
  889. * @internal
  890. * @param $methodName
  891. * @param XMLParser $xmlParser
  892. * @param resource $parser
  893. * @return void
  894. * @throws NoSuchMethodException
  895. *
  896. * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
  897. * dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
  898. * about the matched method handler, in order to avoid doing the work twice...
  899. */
  900. public function methodNameCallback($methodName, $xmlParser, $parser)
  901. {
  902. $sysCall = $this->isSyscall($methodName);
  903. $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
  904. if (!isset($dmap[$methodName]['function'])) {
  905. // No such method
  906. throw new NoSuchMethodException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
  907. }
  908. // alter on-the-fly the config of the xml parser if needed
  909. if (isset($dmap[$methodName]['parameters_type']) &&
  910. $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
  911. /// @todo this should be done by a method of the XMLParser
  912. switch ($dmap[$methodName]['parameters_type']) {
  913. case XMLParser::RETURN_PHP:
  914. xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
  915. break;
  916. case XMLParser::RETURN_EPIVALS:
  917. xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
  918. break;
  919. /// @todo log a warning on unsupported return type
  920. case XMLParser::RETURN_XMLRPCVALS:
  921. default:
  922. xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
  923. }
  924. }
  925. }
  926. /**
  927. * Add a string to the 'internal debug message' (separate from 'user debug message').
  928. *
  929. * @param string $string
  930. * @return void
  931. */
  932. protected function debugMsg($string)
  933. {
  934. $this->debug_info .= $string . "\n";
  935. }
  936. /**
  937. * @param string $methName
  938. * @return bool
  939. */
  940. protected function isSyscall($methName)
  941. {
  942. return (strpos($methName, "system.") === 0);
  943. }
  944. /**
  945. * @param array $dmap
  946. * @return $this
  947. */
  948. public function setDispatchMap($dmap)
  949. {
  950. $this->dmap = $dmap;
  951. return $this;
  952. }
  953. /**
  954. * @return array[]
  955. */
  956. public function getDispatchMap()
  957. {
  958. return $this->dmap;
  959. }
  960. /**
  961. * @return array[]
  962. */
  963. public function getSystemDispatchMap()
  964. {
  965. if (!$this->allow_system_funcs) {
  966. return array();
  967. }
  968. return array(
  969. 'system.listMethods' => array(
  970. 'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
  971. // listMethods: signature was either a string, or nothing.
  972. // The useless string variant has been removed
  973. 'signature' => array(array(Value::$xmlrpcArray)),
  974. 'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
  975. 'signature_docs' => array(array('list of method names')),
  976. ),
  977. 'system.methodHelp' => array(
  978. 'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
  979. 'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
  980. 'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
  981. 'signature_docs' => array(array('method description', 'name of the method to be described')),
  982. ),
  983. 'system.methodSignature' => array(
  984. 'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
  985. 'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
  986. 'docstring' => 'Returns an array of known signatures (an array of arrays) for the method name passed. If no signatures are known, returns a none-array (test for type != array to detect missing signature)',
  987. 'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
  988. ),
  989. 'system.multicall' => array(
  990. 'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
  991. 'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
  992. 'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
  993. 'signature_docs' => array(array('list of response structs, where each struct has the usual members', 'list of calls, with each call being represented as a struct, with members "methodname" and "params"')),
  994. ),
  995. 'system.getCapabilities' => array(
  996. 'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
  997. 'signature' => array(array(Value::$xmlrpcStruct)),
  998. 'docstring' => 'This method lists all the capabilities that the XML-RPC server has: the (more or less standard) extensions to the xmlrpc spec that it adheres to',
  999. 'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
  1000. ),
  1001. );
  1002. }
  1003. /**
  1004. * @return array[]
  1005. */
  1006. public function getCapabilities()
  1007. {
  1008. $outAr = array(
  1009. // xml-rpc spec: always supported
  1010. 'xmlrpc' => array(
  1011. 'specUrl' => 'http://www.xmlrpc.com/spec', // NB: the spec sits now at http://xmlrpc.com/spec.md
  1012. 'specVersion' => 1
  1013. ),
  1014. // if we support system.xxx functions, we always support multicall, too...
  1015. 'system.multicall' => array(
  1016. // Note that, as of 2006/09/17, the following URL does not respond anymore
  1017. 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
  1018. 'specVersion' => 1
  1019. ),
  1020. // introspection: version 2! we support 'mixed', too.
  1021. // note: the php xml-rpc extension says this instead:
  1022. // url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516
  1023. 'introspection' => array(
  1024. 'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
  1025. 'specVersion' => 2,
  1026. ),
  1027. );
  1028. // NIL extension
  1029. if (PhpXmlRpc::$xmlrpc_null_extension) {
  1030. $outAr['nil'] = array(
  1031. // Note that, as of 2023/01, the following URL does not respond anymore
  1032. 'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
  1033. 'specVersion' => 1
  1034. );
  1035. }
  1036. // support for "standard" error codes
  1037. if (PhpXmlRpc::$xmlrpcerr['unknown_method'] === Interop::$xmlrpcerr['unknown_method']) {
  1038. $outAr['faults_interop'] = array(
  1039. 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
  1040. 'specVersion' => 20010516
  1041. );
  1042. }
  1043. return $outAr;
  1044. }
  1045. /**
  1046. * @internal handler of a system. method
  1047. *
  1048. * @param Server $server
  1049. * @param Request $req
  1050. * @return Response
  1051. */
  1052. public static function _xmlrpcs_getCapabilities($server, $req = null)
  1053. {
  1054. $encoder = new Encoder();
  1055. return new static::$responseClass($encoder->encode($server->getCapabilities()));
  1056. }
  1057. /**
  1058. * @internal handler of a system. method
  1059. *
  1060. * @param Server $server
  1061. * @param Request $req if called in plain php values mode, second param is missing
  1062. * @return Response
  1063. */
  1064. public static function _xmlrpcs_listMethods($server, $req = null)
  1065. {
  1066. $outAr = array();
  1067. foreach ($server->dmap as $key => $val) {
  1068. $outAr[] = new Value($key, 'string');
  1069. }
  1070. foreach ($server->getSystemDispatchMap() as $key => $val) {
  1071. $outAr[] = new Value($key, 'string');
  1072. }
  1073. return new static::$responseClass(new Value($outAr, 'array'));
  1074. }
  1075. /**
  1076. * @internal handler of a system. method
  1077. *
  1078. * @param Server $server
  1079. * @param Request $req
  1080. * @return Response
  1081. */
  1082. public static function _xmlrpcs_methodSignature($server, $req)
  1083. {
  1084. // let's accept as parameter either an xml-rpc value or string
  1085. if (is_object($req)) {
  1086. $methName = $req->getParam(0);
  1087. $methName = $methName->scalarVal();
  1088. } else {
  1089. $methName = $req;
  1090. }
  1091. if ($server->isSyscall($methName)) {
  1092. $dmap = $server->getSystemDispatchMap();
  1093. } else {
  1094. $dmap = $server->dmap;
  1095. }
  1096. if (isset($dmap[$methName])) {
  1097. if (isset($dmap[$methName]['signature'])) {
  1098. $sigs = array();
  1099. foreach ($dmap[$methName]['signature'] as $inSig) {
  1100. $curSig = array();
  1101. foreach ($inSig as $sig) {
  1102. $curSig[] = new Value($sig, 'string');
  1103. }
  1104. $sigs[] = new Value($curSig, 'array');
  1105. }
  1106. $r = new static::$responseClass(new Value($sigs, 'array'));
  1107. } else {
  1108. // NB: according to the official docs, we should be returning a
  1109. // "none-array" here, which means not-an-array
  1110. $r = new static::$responseClass(new Value('undef', 'string'));
  1111. }
  1112. } else {
  1113. $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
  1114. }
  1115. return $r;
  1116. }
  1117. /**
  1118. * @internal handler of a system. method
  1119. *
  1120. * @param Server $server
  1121. * @param Request $req
  1122. * @return Response
  1123. */
  1124. public static function _xmlrpcs_methodHelp($server, $req)
  1125. {
  1126. // let's accept as parameter either an xml-rpc value or string
  1127. if (is_object($req)) {
  1128. $methName = $req->getParam(0);
  1129. $methName = $methName->scalarVal();
  1130. } else {
  1131. $methName = $req;
  1132. }
  1133. if ($server->isSyscall($methName)) {
  1134. $dmap = $server->getSystemDispatchMap();
  1135. } else {
  1136. $dmap = $server->dmap;
  1137. }
  1138. if (isset($dmap[$methName])) {
  1139. if (isset($dmap[$methName]['docstring'])) {
  1140. $r = new static::$responseClass(new Value($dmap[$methName]['docstring'], 'string'));
  1141. } else {
  1142. $r = new static::$responseClass(new Value('', 'string'));
  1143. }
  1144. } else {
  1145. $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
  1146. }
  1147. return $r;
  1148. }
  1149. /**
  1150. * @internal this function will become protected in the future
  1151. *
  1152. * @param $err
  1153. * @return Value
  1154. */
  1155. public static function _xmlrpcs_multicall_error($err)
  1156. {
  1157. if (is_string($err)) {
  1158. $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"];
  1159. $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"];
  1160. } else {
  1161. $code = $err->faultCode();
  1162. $str = $err->faultString();
  1163. }
  1164. $struct = array();
  1165. $struct['faultCode'] = new Value($code, 'int');
  1166. $struct['faultString'] = new Value($str, 'string');
  1167. return new Value($struct, 'struct');
  1168. }
  1169. /**
  1170. * @internal this function will become protected in the future
  1171. *
  1172. * @param Server $server
  1173. * @param Value $call
  1174. * @return Value
  1175. */
  1176. public static function _xmlrpcs_multicall_do_call($server, $call)
  1177. {
  1178. if ($call->kindOf() != 'struct') {
  1179. return static::_xmlrpcs_multicall_error('notstruct');
  1180. }
  1181. $methName = @$call['methodName'];
  1182. if (!$methName) {
  1183. return static::_xmlrpcs_multicall_error('nomethod');
  1184. }
  1185. if ($methName->kindOf() != 'scalar' || $methName->scalarTyp() != 'string') {
  1186. return static::_xmlrpcs_multicall_error('notstring');
  1187. }
  1188. if ($methName->scalarVal() == 'system.multicall') {
  1189. return static::_xmlrpcs_multicall_error('recursion');
  1190. }
  1191. $params = @$call['params'];
  1192. if (!$params) {
  1193. return static::_xmlrpcs_multicall_error('noparams');
  1194. }
  1195. if ($params->kindOf() != 'array') {
  1196. return static::_xmlrpcs_multicall_error('notarray');
  1197. }
  1198. $req = new Request($methName->scalarVal());
  1199. foreach ($params as $i => $param) {
  1200. if (!$req->addParam($param)) {
  1201. $i++; // for error message, we count params from 1
  1202. return static::_xmlrpcs_multicall_error(new static::$responseClass(0,
  1203. PhpXmlRpc::$xmlrpcerr['incorrect_params'],
  1204. PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
  1205. }
  1206. }
  1207. $result = $server->execute($req);
  1208. if ($result->faultCode() != 0) {
  1209. return static::_xmlrpcs_multicall_error($result); // Method returned fault.
  1210. }
  1211. return new Value(array($result->value()), 'array');
  1212. }
  1213. /**
  1214. * @internal this function will become protected in the future
  1215. *
  1216. * @param Server $server
  1217. * @param Value $call
  1218. * @return Value
  1219. */
  1220. public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
  1221. {
  1222. if (!is_array($call)) {
  1223. return static::_xmlrpcs_multicall_error('notstruct');
  1224. }
  1225. if (!array_key_exists('methodName', $call)) {
  1226. return static::_xmlrpcs_multicall_error('nomethod');
  1227. }
  1228. if (!is_string($call['methodName'])) {
  1229. return static::_xmlrpcs_multicall_error('notstring');
  1230. }
  1231. if ($call['methodName'] == 'system.multicall') {
  1232. return static::_xmlrpcs_multicall_error('recursion');
  1233. }
  1234. if (!array_key_exists('params', $call)) {
  1235. return static::_xmlrpcs_multicall_error('noparams');
  1236. }
  1237. if (!is_array($call['params'])) {
  1238. return static::_xmlrpcs_multicall_error('notarray');
  1239. }
  1240. // this is a simplistic hack, since we might have received
  1241. // base64 or datetime values, but they will be listed as strings here...
  1242. $pt = array();
  1243. $wrapper = new Wrapper();
  1244. foreach ($call['params'] as $val) {
  1245. // support EPI-encoded base64 and datetime values
  1246. if ($val instanceof \stdClass && isset($val->xmlrpc_type)) {
  1247. $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type;
  1248. } else {
  1249. $pt[] = $wrapper->php2XmlrpcType(gettype($val));
  1250. }
  1251. }
  1252. $result = $server->execute($call['methodName'], $call['params'], $pt);
  1253. if ($result->faultCode() != 0) {
  1254. return static::_xmlrpcs_multicall_error($result); // Method returned fault.
  1255. }
  1256. return new Value(array($result->value()), 'array');
  1257. }
  1258. /**
  1259. * @internal handler of a system. method
  1260. *
  1261. * @param Server $server
  1262. * @param Request|array $req
  1263. * @return Response
  1264. */
  1265. public static function _xmlrpcs_multicall($server, $req)
  1266. {
  1267. $result = array();
  1268. // let's accept a plain list of php parameters, beside a single xml-rpc msg object
  1269. if (is_object($req)) {
  1270. $calls = $req->getParam(0);
  1271. foreach ($calls as $call) {
  1272. $result[] = static::_xmlrpcs_multicall_do_call($server, $call);
  1273. }
  1274. } else {
  1275. $numCalls = count($req);
  1276. for ($i = 0; $i < $numCalls; $i++) {
  1277. $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]);
  1278. }
  1279. }
  1280. return new static::$responseClass(new Value($result, 'array'));
  1281. }
  1282. /**
  1283. * Error handler used to track errors that occur during server-side execution of PHP code.
  1284. * This allows to report back to the client whether an internal error has occurred or not
  1285. * using an xml-rpc response object, instead of letting the client deal with the html junk
  1286. * that a PHP execution error on the server generally entails.
  1287. *
  1288. * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
  1289. *
  1290. * @internal
  1291. */
  1292. public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null)
  1293. {
  1294. // obey the @ protocol
  1295. if (error_reporting() == 0) {
  1296. return;
  1297. }
  1298. //if ($errCode != E_NOTICE && $errCode != E_WARNING && $errCode != E_USER_NOTICE && $errCode != E_USER_WARNING)
  1299. if ($errCode != E_STRICT) {
  1300. static::error_occurred($errString);
  1301. }
  1302. // Try to avoid as much as possible disruption to the previous error handling mechanism in place
  1303. if (self::$_xmlrpcs_prev_ehandler == '') {
  1304. // The previous error handler was the default: all we should do is log error to the default error log
  1305. // (if level high enough)
  1306. if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) {
  1307. // we can't use the functionality of LoggerAware, because this is a static method
  1308. if (self::$logger === null) {
  1309. self::$logger = Logger::instance();
  1310. }
  1311. self::$logger->error($errString);
  1312. }
  1313. } else {
  1314. // Pass control on to previous error handler, trying to avoid loops...
  1315. if (self::$_xmlrpcs_prev_ehandler != array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler')) {
  1316. if (is_array(self::$_xmlrpcs_prev_ehandler)) {
  1317. // the following works both with static class methods and plain object methods as error handler
  1318. call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context));
  1319. } else {
  1320. $method = self::$_xmlrpcs_prev_ehandler;
  1321. $method($errCode, $errString, $filename, $lineNo, $context);
  1322. }
  1323. }
  1324. }
  1325. }
  1326. // *** BC layer ***
  1327. /**
  1328. * @param string $charsetEncoding
  1329. * @return string
  1330. *
  1331. * @deprecated this method was moved to the Response class
  1332. */
  1333. protected function xml_header($charsetEncoding = '')
  1334. {
  1335. $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
  1336. if ($charsetEncoding != '') {
  1337. return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
  1338. } else {
  1339. return "<?xml version=\"1.0\"?" . ">\n";
  1340. }
  1341. }
  1342. // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
  1343. public function &__get($name)
  1344. {
  1345. switch ($name) {
  1346. case self::OPT_ACCEPTED_COMPRESSION :
  1347. case self::OPT_ALLOW_SYSTEM_FUNCS:
  1348. case self::OPT_COMPRESS_RESPONSE:
  1349. case self::OPT_DEBUG:
  1350. case self::OPT_EXCEPTION_HANDLING:
  1351. case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
  1352. case self::OPT_PHPVALS_ENCODING_OPTIONS:
  1353. case self::OPT_RESPONSE_CHARSET_ENCODING:
  1354. $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
  1355. return $this->$name;
  1356. case 'accepted_charset_encodings':
  1357. // manually implement the 'protected property' behaviour
  1358. $canAccess = false;
  1359. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  1360. if (isset($trace[1]) && isset($trace[1]['class'])) {
  1361. if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
  1362. $canAccess = true;
  1363. }
  1364. }
  1365. if ($canAccess) {
  1366. $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
  1367. return $this->accepted_compression;
  1368. } else {
  1369. trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
  1370. }
  1371. break;
  1372. default:
  1373. /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
  1374. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
  1375. trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
  1376. $result = null;
  1377. return $result;
  1378. }
  1379. }
  1380. public function __set($name, $value)
  1381. {
  1382. switch ($name) {
  1383. case self::OPT_ACCEPTED_COMPRESSION :
  1384. case self::OPT_ALLOW_SYSTEM_FUNCS:
  1385. case self::OPT_COMPRESS_RESPONSE:
  1386. case self::OPT_DEBUG:
  1387. case self::OPT_EXCEPTION_HANDLING:
  1388. case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
  1389. case self::OPT_PHPVALS_ENCODING_OPTIONS:
  1390. case self::OPT_RESPONSE_CHARSET_ENCODING:
  1391. $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
  1392. $this->$name = $value;
  1393. break;
  1394. case 'accepted_charset_encodings':
  1395. // manually implement the 'protected property' behaviour
  1396. $canAccess = false;
  1397. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  1398. if (isset($trace[1]) && isset($trace[1]['class'])) {
  1399. if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
  1400. $canAccess = true;
  1401. }
  1402. }
  1403. if ($canAccess) {
  1404. $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
  1405. $this->accepted_compression = $value;
  1406. } else {
  1407. trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
  1408. }
  1409. break;
  1410. default:
  1411. /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
  1412. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
  1413. trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
  1414. }
  1415. }
  1416. public function __isset($name)
  1417. {
  1418. switch ($name) {
  1419. case self::OPT_ACCEPTED_COMPRESSION :
  1420. case self::OPT_ALLOW_SYSTEM_FUNCS:
  1421. case self::OPT_COMPRESS_RESPONSE:
  1422. case self::OPT_DEBUG:
  1423. case self::OPT_EXCEPTION_HANDLING:
  1424. case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
  1425. case self::OPT_PHPVALS_ENCODING_OPTIONS:
  1426. case self::OPT_RESPONSE_CHARSET_ENCODING:
  1427. $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
  1428. return isset($this->$name);
  1429. case 'accepted_charset_encodings':
  1430. // manually implement the 'protected property' behaviour
  1431. $canAccess = false;
  1432. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  1433. if (isset($trace[1]) && isset($trace[1]['class'])) {
  1434. if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
  1435. $canAccess = true;
  1436. }
  1437. }
  1438. if ($canAccess) {
  1439. $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
  1440. return isset($this->accepted_compression);
  1441. }
  1442. // break through voluntarily
  1443. default:
  1444. return false;
  1445. }
  1446. }
  1447. public function __unset($name)
  1448. {
  1449. switch ($name) {
  1450. case self::OPT_ACCEPTED_COMPRESSION :
  1451. case self::OPT_ALLOW_SYSTEM_FUNCS:
  1452. case self::OPT_COMPRESS_RESPONSE:
  1453. case self::OPT_DEBUG:
  1454. case self::OPT_EXCEPTION_HANDLING:
  1455. case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
  1456. case self::OPT_PHPVALS_ENCODING_OPTIONS:
  1457. case self::OPT_RESPONSE_CHARSET_ENCODING:
  1458. $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
  1459. unset($this->$name);
  1460. break;
  1461. case 'accepted_charset_encodings':
  1462. // manually implement the 'protected property' behaviour
  1463. $canAccess = false;
  1464. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  1465. if (isset($trace[1]) && isset($trace[1]['class'])) {
  1466. if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
  1467. $canAccess = true;
  1468. }
  1469. }
  1470. if ($canAccess) {
  1471. $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
  1472. unset($this->accepted_compression);
  1473. } else {
  1474. trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
  1475. }
  1476. break;
  1477. default:
  1478. /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
  1479. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
  1480. trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
  1481. }
  1482. }
  1483. }