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