1 | <?php |
||
2 | /** |
||
3 | * @author Gaetano Giunta |
||
4 | * @copyright (C) 2006-2023 G. Giunta |
||
5 | * @license code licensed under the BSD License: see file license.txt |
||
6 | */ |
||
7 | |||
8 | namespace PhpXmlRpc; |
||
9 | |||
10 | use PhpXmlRpc\Exception\ValueErrorException; |
||
11 | use PhpXmlRpc\Traits\LoggerAware; |
||
12 | |||
13 | /** |
||
14 | * PHPXMLRPC "wrapper" class - generate stubs to transparently access xml-rpc methods as php functions and vice-versa. |
||
15 | * Note: this class implements the PROXY pattern, but it is not named so to avoid confusion with http proxies. |
||
16 | * |
||
17 | * @todo use some better templating system for code generation? |
||
18 | * @todo implement method wrapping with preservation of php objs in calls |
||
19 | * @todo add support for 'epivals' mode |
||
20 | * @todo allow setting custom namespace for generated wrapping code |
||
21 | */ |
||
22 | class Wrapper |
||
23 | { |
||
24 | use LoggerAware; |
||
25 | |||
26 | /** |
||
27 | * @var object[] |
||
28 | * Used to hold a reference to object instances whose methods get wrapped by wrapPhpFunction(), in 'create source' mode |
||
29 | 24 | * @internal this property will become protected in the future |
|
30 | */ |
||
31 | 24 | public static $objHolder = array(); |
|
32 | 1 | ||
33 | /** @var string */ |
||
34 | 24 | protected static $namespace = '\\PhpXmlRpc\\'; |
|
35 | |||
36 | /** |
||
37 | * Given a string defining a php type or phpxmlrpc type (loosely defined: strings |
||
38 | * accepted come from javadoc blocks), return corresponding phpxmlrpc type. |
||
39 | * Notes: |
||
40 | * - for php 'resource' types returns empty string, since resources cannot be serialized; |
||
41 | * - for php class names returns 'struct', since php objects can be serialized as xml-rpc structs |
||
42 | * - for php arrays always return array, even though arrays sometimes serialize as structs... |
||
43 | * - for 'void' and 'null' returns 'undefined' |
||
44 | * |
||
45 | * @param string $phpType |
||
46 | * @return string |
||
47 | * |
||
48 | * @todo support notation `something[]` as 'array' |
||
49 | * @todo check if nil support is enabled when finding null |
||
50 | */ |
||
51 | public function php2XmlrpcType($phpType) |
||
52 | { |
||
53 | switch (strtolower($phpType)) { |
||
54 | case 'string': |
||
55 | return Value::$xmlrpcString; |
||
56 | case 'integer': |
||
57 | 559 | case Value::$xmlrpcInt: // 'int' |
|
58 | case Value::$xmlrpcI4: |
||
59 | 559 | case Value::$xmlrpcI8: |
|
60 | 559 | return Value::$xmlrpcInt; |
|
61 | 559 | case Value::$xmlrpcDouble: // 'double' |
|
62 | 559 | return Value::$xmlrpcDouble; |
|
63 | 559 | case 'bool': |
|
64 | 559 | case Value::$xmlrpcBoolean: // 'boolean' |
|
65 | 559 | case 'false': |
|
66 | 559 | case 'true': |
|
67 | 559 | return Value::$xmlrpcBoolean; |
|
68 | case Value::$xmlrpcArray: // 'array': |
||
69 | 559 | case 'array[]': |
|
70 | 559 | return Value::$xmlrpcArray; |
|
71 | 559 | case 'object': |
|
72 | 559 | case Value::$xmlrpcStruct: // 'struct' |
|
73 | return Value::$xmlrpcStruct; |
||
74 | 559 | case Value::$xmlrpcBase64: |
|
75 | 559 | return Value::$xmlrpcBase64; |
|
76 | case 'resource': |
||
77 | 559 | return ''; |
|
78 | 559 | default: |
|
79 | if (class_exists($phpType)) { |
||
80 | 559 | // DateTimeInterface is not present in php 5.4... |
|
81 | if (is_a($phpType, 'DateTimeInterface') || is_a($phpType, 'DateTime')) { |
||
82 | 559 | return Value::$xmlrpcDateTime; |
|
83 | } |
||
84 | return Value::$xmlrpcStruct; |
||
85 | 559 | } else { |
|
86 | 559 | // unknown: might be any 'extended' xml-rpc type |
|
87 | return Value::$xmlrpcValue; |
||
88 | } |
||
89 | 559 | } |
|
90 | } |
||
91 | |||
92 | 559 | /** |
|
93 | * Given a string defining a phpxmlrpc type return the corresponding php type. |
||
94 | * |
||
95 | * @param string $xmlrpcType |
||
96 | * @return string |
||
97 | */ |
||
98 | public function xmlrpc2PhpType($xmlrpcType) |
||
99 | { |
||
100 | switch (strtolower($xmlrpcType)) { |
||
101 | case 'base64': |
||
102 | case 'datetime.iso8601': |
||
103 | case 'string': |
||
104 | 85 | return Value::$xmlrpcString; |
|
105 | case 'int': |
||
106 | 85 | case 'i4': |
|
107 | 85 | case 'i8': |
|
108 | 85 | return 'integer'; |
|
109 | 85 | case 'struct': |
|
110 | 64 | case 'array': |
|
111 | 85 | return 'array'; |
|
112 | 22 | case 'double': |
|
113 | 22 | return 'float'; |
|
114 | 64 | case 'undefined': |
|
115 | 22 | return 'mixed'; |
|
116 | 22 | case 'boolean': |
|
117 | case 'null': |
||
118 | 22 | default: |
|
119 | // unknown: might be any xml-rpc type |
||
120 | 22 | return strtolower($xmlrpcType); |
|
121 | 22 | } |
|
122 | } |
||
123 | |||
124 | /** |
||
125 | * Given a user-defined PHP function, create a PHP 'wrapper' function that can be exposed as xml-rpc method from an |
||
126 | * xml-rpc server object and called from remote clients (as well as its corresponding signature info). |
||
127 | * |
||
128 | * Since php is a typeless language, to infer types of input and output parameters, it relies on parsing the |
||
129 | * javadoc-style comment block associated with the given function. Usage of xml-rpc native types (such as |
||
130 | * datetime.dateTime.iso8601 and base64) in the '@param' tag is also allowed, if you need the php function to |
||
131 | * receive/send data in that particular format (note that base64 encoding/decoding is transparently carried out by |
||
132 | * the lib, while datetime values are passed around as strings) |
||
133 | * |
||
134 | * Known limitations: |
||
135 | * - only works for user-defined functions, not for PHP internal functions (reflection does not support retrieving |
||
136 | * number/type of params for those) |
||
137 | * - functions returning php objects will generate special structs in xml-rpc responses: when the xml-rpc decoding of |
||
138 | * those responses is carried out by this same lib, using the appropriate param in php_xmlrpc_decode, the php |
||
139 | * objects will be rebuilt. |
||
140 | * In short: php objects can be serialized, too (except for their resource members), using this function. |
||
141 | * Other libs might choke on the very same xml that will be generated in this case (i.e. it has a nonstandard |
||
142 | * attribute on struct element tags) |
||
143 | * |
||
144 | * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' php functions (i.e. functions |
||
145 | * not expecting a single Request obj as parameter) is by making use of the $functions_parameters_type and |
||
146 | * $exception_handling properties. |
||
147 | * |
||
148 | * @param \Callable $callable the PHP user function to be exposed as xml-rpc method: a closure, function name, array($obj, 'methodname') or array('class', 'methodname') are ok |
||
149 | * @param string $newFuncName (optional) name for function to be created. Used only when return_source in $extraOptions is true |
||
150 | * @param array $extraOptions (optional) array of options for conversion. valid values include: |
||
151 | * - bool return_source when true, php code w. function definition will be returned, instead of a closure |
||
152 | * - bool encode_nulls let php objects be sent to server using <nil> elements instead of empty strings |
||
153 | * - bool encode_php_objs let php objects be sent to server using the 'improved' xml-rpc notation, so server can deserialize them as php objects |
||
154 | * - bool decode_php_objs --- WARNING !!! possible security hazard. only use it with trusted servers --- |
||
155 | * - bool suppress_warnings remove from produced xml any warnings generated at runtime by the php function being invoked |
||
156 | * @return array|false false on error, or an array containing the name of the new php function, |
||
157 | * its signature and docs, to be used in the server dispatch map |
||
158 | * |
||
159 | * @todo decide how to deal with params passed by ref in function definition: bomb out or allow? |
||
160 | * @todo finish using phpdoc info to build method sig if all params are named but out of order |
||
161 | * @todo add a check for params of 'resource' type |
||
162 | * @todo add some error logging when returning false? |
||
163 | * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value... |
||
164 | * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3? |
||
165 | * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance? |
||
166 | * @todo add an option to allow generated function to skip validation of number of parameters, as that is done by the server anyway |
||
167 | */ |
||
168 | public function wrapPhpFunction($callable, $newFuncName = '', $extraOptions = array()) |
||
169 | { |
||
170 | $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true; |
||
171 | |||
172 | if (is_string($callable) && strpos($callable, '::') !== false) { |
||
173 | $callable = explode('::', $callable); |
||
174 | } |
||
175 | if (is_array($callable)) { |
||
176 | if (count($callable) < 2 || (!is_string($callable[0]) && !is_object($callable[0]))) { |
||
177 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': syntax for function to be wrapped is wrong'); |
|
178 | return false; |
||
179 | 559 | } |
|
180 | if (is_string($callable[0])) { |
||
181 | 559 | $plainFuncName = implode('::', $callable); |
|
182 | 559 | } elseif (is_object($callable[0])) { |
|
183 | $plainFuncName = get_class($callable[0]) . '->' . $callable[1]; |
||
184 | 559 | } |
|
185 | 559 | $exists = method_exists($callable[0], $callable[1]); |
|
186 | } else if ($callable instanceof \Closure) { |
||
187 | // we do not support creating code which wraps closures, as php does not allow to serialize them |
||
188 | if (!$buildIt) { |
||
189 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': a closure can not be wrapped in generated source code'); |
|
190 | 559 | return false; |
|
191 | 559 | } |
|
192 | 559 | ||
193 | $plainFuncName = 'Closure'; |
||
194 | 559 | $exists = true; |
|
195 | 559 | } else { |
|
196 | $plainFuncName = $callable; |
||
197 | 559 | $exists = function_exists($callable); |
|
198 | } |
||
199 | |||
200 | if (!$exists) { |
||
201 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is not defined: ' . $plainFuncName); |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
202 | 559 | return false; |
|
203 | 559 | } |
|
204 | |||
205 | 559 | $funcDesc = $this->introspectFunction($callable, $plainFuncName); |
|
206 | 559 | if (!$funcDesc) { |
|
207 | return false; |
||
208 | } |
||
209 | 559 | ||
210 | $funcSigs = $this->buildMethodSignatures($funcDesc); |
||
211 | |||
212 | if ($buildIt) { |
||
213 | $callable = $this->buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc); |
||
214 | 559 | } else { |
|
215 | 559 | $newFuncName = $this->newFunctionName($callable, $newFuncName, $extraOptions); |
|
216 | $code = $this->buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc); |
||
217 | } |
||
218 | |||
219 | 559 | $ret = array( |
|
220 | 'function' => $callable, |
||
221 | 559 | 'signature' => $funcSigs['sigs'], |
|
222 | 559 | 'docstring' => $funcDesc['desc'], |
|
223 | 'signature_docs' => $funcSigs['sigsDocs'], |
||
224 | 559 | ); |
|
225 | 559 | if (!$buildIt) { |
|
226 | $ret['function'] = $newFuncName; |
||
227 | $ret['source'] = $code; |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
228 | } |
||
229 | 559 | return $ret; |
|
230 | 559 | } |
|
231 | 559 | ||
232 | 559 | /** |
|
233 | * Introspect a php callable and its phpdoc block and extract information about its signature |
||
234 | 559 | * |
|
235 | 559 | * @param callable $callable |
|
236 | 559 | * @param string $plainFuncName |
|
237 | * @return array|false |
||
238 | 559 | */ |
|
239 | protected function introspectFunction($callable, $plainFuncName) |
||
240 | { |
||
241 | // start to introspect PHP code |
||
242 | if (is_array($callable)) { |
||
243 | $func = new \ReflectionMethod($callable[0], $callable[1]); |
||
244 | if ($func->isPrivate()) { |
||
245 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName); |
||
246 | return false; |
||
247 | } |
||
248 | 559 | if ($func->isProtected()) { |
|
249 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName); |
||
250 | return false; |
||
251 | 559 | } |
|
252 | 559 | if ($func->isConstructor()) { |
|
253 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName); |
|
254 | return false; |
||
255 | } |
||
256 | if ($func->isDestructor()) { |
||
257 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName); |
|
258 | return false; |
||
259 | } |
||
260 | if ($func->isAbstract()) { |
||
261 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is abstract: ' . $plainFuncName); |
|
262 | return false; |
||
263 | } |
||
264 | /// @todo add more checks for static vs. nonstatic? |
||
265 | 559 | } else { |
|
266 | $func = new \ReflectionFunction($callable); |
||
267 | } |
||
268 | if ($func->isInternal()) { |
||
269 | 559 | /// @todo from PHP 5.1.0 onward, we should be able to use invokeargs instead of getparameters to fully |
|
270 | /// reflect internal php functions |
||
271 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is internal: ' . $plainFuncName); |
||
272 | return false; |
||
273 | } |
||
274 | |||
275 | 559 | // retrieve parameter names, types and description from javadoc comments |
|
276 | |||
277 | 559 | // function description |
|
278 | $desc = ''; |
||
279 | // type of return val: by default 'any' |
||
280 | $returns = Value::$xmlrpcValue; |
||
281 | // desc of return val |
||
282 | $returnsDocs = ''; |
||
283 | // type + name of function parameters |
||
284 | $paramDocs = array(); |
||
285 | |||
286 | $docs = $func->getDocComment(); |
||
287 | 559 | if ($docs != '') { |
|
288 | $docs = explode("\n", $docs); |
||
289 | 559 | $i = 0; |
|
290 | foreach ($docs as $doc) { |
||
291 | 559 | $doc = trim($doc, " \r\t/*"); |
|
292 | if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) { |
||
293 | 559 | if ($desc) { |
|
294 | $desc .= "\n"; |
||
295 | 559 | } |
|
296 | 559 | $desc .= $doc; |
|
297 | 559 | } elseif (strpos($doc, '@param') === 0) { |
|
298 | 559 | // syntax: @param type $name [desc] |
|
299 | 559 | if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s*(.+)?/', $doc, $matches)) { |
|
300 | 559 | $name = strtolower(trim($matches[2])); |
|
301 | 559 | //$paramDocs[$name]['name'] = trim($matches[2]); |
|
302 | 559 | $paramDocs[$name]['doc'] = isset($matches[3]) ? $matches[3] : ''; |
|
303 | 559 | $paramDocs[$name]['type'] = $matches[1]; |
|
304 | } |
||
305 | 559 | $i++; |
|
306 | 559 | } elseif (strpos($doc, '@return') === 0) { |
|
307 | // syntax: @return type [desc] |
||
308 | 559 | if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) { |
|
309 | 559 | $returns = $matches[1]; |
|
310 | if (isset($matches[2])) { |
||
311 | 559 | $returnsDocs = trim($matches[2]); |
|
312 | 559 | } |
|
313 | } |
||
314 | 559 | } |
|
315 | 559 | } |
|
316 | } |
||
317 | 559 | ||
318 | 559 | // execute introspection of actual function prototype |
|
319 | 559 | $params = array(); |
|
320 | 559 | $i = 0; |
|
321 | foreach ($func->getParameters() as $paramObj) { |
||
322 | $params[$i] = array(); |
||
323 | $params[$i]['name'] = '$' . $paramObj->getName(); |
||
324 | $params[$i]['isoptional'] = $paramObj->isOptional(); |
||
325 | $i++; |
||
326 | } |
||
327 | |||
328 | 559 | return array( |
|
329 | 559 | 'desc' => $desc, |
|
330 | 559 | 'docs' => $docs, |
|
331 | 559 | 'params' => $params, // array, positionally indexed |
|
332 | 559 | 'paramDocs' => $paramDocs, // array, indexed by name |
|
333 | 559 | 'returns' => $returns, |
|
334 | 559 | 'returnsDocs' =>$returnsDocs, |
|
335 | ); |
||
336 | } |
||
337 | |||
338 | 559 | /** |
|
339 | 559 | * Given the method description given by introspection, create method signature data |
|
340 | 559 | * |
|
341 | 559 | * @param array $funcDesc as generated by self::introspectFunction() |
|
342 | 559 | * @return array |
|
343 | 559 | * |
|
344 | * @todo support better docs with multiple types separated by pipes by creating multiple signatures |
||
345 | * (this is questionable, as it might produce a big matrix of possible signatures with many such occurrences) |
||
346 | */ |
||
347 | protected function buildMethodSignatures($funcDesc) |
||
348 | { |
||
349 | $i = 0; |
||
350 | $parsVariations = array(); |
||
351 | $pars = array(); |
||
352 | $pNum = count($funcDesc['params']); |
||
353 | foreach ($funcDesc['params'] as $param) { |
||
354 | /* // match by name real param and documented params |
||
355 | $name = strtolower($param['name']); |
||
356 | if (!isset($funcDesc['paramDocs'][$name])) { |
||
357 | 559 | $funcDesc['paramDocs'][$name] = array(); |
|
358 | } |
||
359 | 559 | if (!isset($funcDesc['paramDocs'][$name]['type'])) { |
|
360 | 559 | $funcDesc['paramDocs'][$name]['type'] = 'mixed'; |
|
361 | 559 | }*/ |
|
362 | 559 | ||
363 | 559 | if ($param['isoptional']) { |
|
364 | // this particular parameter is optional. save as valid previous list of parameters |
||
365 | $parsVariations[] = $pars; |
||
366 | } |
||
367 | |||
368 | $pars[] = "\$p$i"; |
||
369 | $i++; |
||
370 | if ($i == $pNum) { |
||
371 | // last allowed parameters combination |
||
372 | $parsVariations[] = $pars; |
||
373 | 559 | } |
|
374 | } |
||
375 | |||
376 | if (count($parsVariations) == 0) { |
||
377 | // only known good synopsis = no parameters |
||
378 | 559 | $parsVariations[] = array(); |
|
379 | 559 | } |
|
380 | 559 | ||
381 | $sigs = array(); |
||
382 | 559 | $sigsDocs = array(); |
|
383 | foreach ($parsVariations as $pars) { |
||
384 | // build a signature |
||
385 | $sig = array($this->php2XmlrpcType($funcDesc['returns'])); |
||
386 | 559 | $pSig = array($funcDesc['returnsDocs']); |
|
387 | for ($i = 0; $i < count($pars); $i++) { |
||
388 | 559 | $name = strtolower($funcDesc['params'][$i]['name']); |
|
389 | if (isset($funcDesc['paramDocs'][$name]['type'])) { |
||
390 | $sig[] = $this->php2XmlrpcType($funcDesc['paramDocs'][$name]['type']); |
||
391 | 559 | } else { |
|
392 | 559 | $sig[] = Value::$xmlrpcValue; |
|
393 | 559 | } |
|
394 | $pSig[] = isset($funcDesc['paramDocs'][$name]['doc']) ? $funcDesc['paramDocs'][$name]['doc'] : ''; |
||
395 | 559 | } |
|
396 | 559 | $sigs[] = $sig; |
|
397 | 559 | $sigsDocs[] = $pSig; |
|
398 | 559 | } |
|
399 | 559 | ||
400 | 559 | return array( |
|
401 | 'sigs' => $sigs, |
||
402 | 559 | 'sigsDocs' => $sigsDocs |
|
403 | ); |
||
404 | 559 | } |
|
405 | |||
406 | 559 | /** |
|
407 | 559 | * Creates a closure that will execute $callable |
|
408 | * |
||
409 | * @param $callable |
||
410 | * @param array $extraOptions |
||
411 | 559 | * @param string $plainFuncName |
|
412 | 559 | * @param array $funcDesc |
|
413 | * @return \Closure |
||
414 | * |
||
415 | * @todo validate params? In theory all validation is left to the dispatch map... |
||
416 | * @todo add support for $catchWarnings |
||
417 | */ |
||
418 | protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc) |
||
419 | { |
||
420 | /** |
||
421 | * @param Request $req |
||
422 | * |
||
423 | * @return mixed |
||
424 | */ |
||
425 | $function = function($req) use($callable, $extraOptions, $funcDesc) |
||
426 | { |
||
427 | 559 | $encoderClass = static::$namespace.'Encoder'; |
|
428 | $responseClass = static::$namespace.'Response'; |
||
429 | $valueClass = static::$namespace.'Value'; |
||
430 | |||
431 | // validate number of parameters received |
||
432 | // this should be optional really, as we assume the server does the validation |
||
433 | $minPars = count($funcDesc['params']); |
||
434 | $maxPars = $minPars; |
||
435 | 108 | foreach ($funcDesc['params'] as $i => $param) { |
|
436 | 108 | if ($param['isoptional']) { |
|
437 | 108 | // this particular parameter is optional. We assume later ones are as well |
|
438 | 108 | $minPars = $i; |
|
439 | break; |
||
440 | } |
||
441 | } |
||
442 | 108 | $numPars = $req->getNumParams(); |
|
443 | 108 | if ($numPars < $minPars || $numPars > $maxPars) { |
|
444 | 108 | return new $responseClass(0, 3, 'Incorrect parameters passed to method'); |
|
445 | 108 | } |
|
446 | |||
447 | $encoder = new $encoderClass(); |
||
448 | $options = array(); |
||
449 | if (isset($extraOptions['decode_php_objs']) && $extraOptions['decode_php_objs']) { |
||
450 | $options[] = 'decode_php_objs'; |
||
451 | 108 | } |
|
452 | 108 | $params = $encoder->decode($req, $options); |
|
453 | |||
454 | $result = call_user_func_array($callable, $params); |
||
455 | |||
456 | 108 | if (! is_a($result, $responseClass)) { |
|
457 | 108 | // q: why not do the same for int, float, bool, string? |
|
458 | 108 | if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) { |
|
459 | $result = new $valueClass($result, $funcDesc['returns']); |
||
460 | } else { |
||
461 | 108 | $options = array(); |
|
462 | if (isset($extraOptions['encode_php_objs']) && $extraOptions['encode_php_objs']) { |
||
463 | 108 | $options[] = 'encode_php_objs'; |
|
464 | } |
||
465 | 108 | if (isset($extraOptions['encode_nulls']) && $extraOptions['encode_nulls']) { |
|
466 | 108 | $options[] = 'null_extension'; |
|
467 | } |
||
468 | |||
469 | 108 | $result = $encoder->encode($result, $options); |
|
470 | 108 | } |
|
471 | 1 | $result = new $responseClass($result); |
|
472 | } |
||
473 | |||
474 | 108 | return $result; |
|
475 | }; |
||
476 | 108 | ||
477 | return $function; |
||
478 | } |
||
479 | 108 | ||
480 | 559 | /** |
|
481 | * Return a name for a new function, based on $callable, insuring its uniqueness |
||
482 | 559 | * @param mixed $callable a php callable, or the name of an xml-rpc method |
|
483 | * @param string $newFuncName when not empty, it is used instead of the calculated version |
||
484 | * @return string |
||
485 | */ |
||
486 | protected function newFunctionName($callable, $newFuncName, $extraOptions) |
||
487 | { |
||
488 | // determine name of new php function |
||
489 | |||
490 | $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc'; |
||
491 | 643 | ||
492 | if ($newFuncName == '') { |
||
493 | if (is_array($callable)) { |
||
494 | if (is_string($callable[0])) { |
||
495 | 643 | $xmlrpcFuncName = "{$prefix}_" . implode('_', $callable); |
|
496 | } else { |
||
497 | 643 | $xmlrpcFuncName = "{$prefix}_" . get_class($callable[0]) . '_' . $callable[1]; |
|
498 | 622 | } |
|
499 | 559 | } else { |
|
500 | 559 | if ($callable instanceof \Closure) { |
|
501 | $xmlrpcFuncName = "{$prefix}_closure"; |
||
502 | 559 | } else { |
|
503 | $callable = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), |
||
504 | array('_', ''), $callable); |
||
505 | 622 | $xmlrpcFuncName = "{$prefix}_$callable"; |
|
506 | } |
||
507 | } |
||
508 | 622 | } else { |
|
509 | 622 | $xmlrpcFuncName = $newFuncName; |
|
510 | 622 | } |
|
511 | |||
512 | while (function_exists($xmlrpcFuncName)) { |
||
513 | $xmlrpcFuncName .= 'x'; |
||
514 | 22 | } |
|
515 | |||
516 | return $xmlrpcFuncName; |
||
517 | 643 | } |
|
518 | 620 | ||
519 | /** |
||
520 | * @param $callable |
||
521 | 643 | * @param string $newFuncName |
|
522 | * @param array $extraOptions |
||
523 | * @param string $plainFuncName |
||
524 | * @param array $funcDesc |
||
525 | * @return string |
||
526 | */ |
||
527 | protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc) |
||
528 | { |
||
529 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
||
530 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
||
531 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
||
532 | $catchWarnings = isset($extraOptions['suppress_warnings']) && $extraOptions['suppress_warnings'] ? '@' : ''; |
||
533 | |||
534 | 559 | $i = 0; |
|
535 | $parsVariations = array(); |
||
536 | 559 | $pars = array(); |
|
537 | $pNum = count($funcDesc['params']); |
||
538 | 559 | foreach ($funcDesc['params'] as $param) { |
|
539 | 559 | ||
540 | 559 | if ($param['isoptional']) { |
|
541 | // this particular parameter is optional. save as valid previous list of parameters |
||
542 | 559 | $parsVariations[] = $pars; |
|
543 | 559 | } |
|
544 | 559 | ||
545 | 559 | $pars[] = "\$params[$i]"; |
|
546 | 559 | $i++; |
|
547 | if ($i == $pNum) { |
||
548 | 559 | // last allowed parameters combination |
|
549 | $parsVariations[] = $pars; |
||
550 | } |
||
551 | } |
||
552 | |||
553 | 559 | if (count($parsVariations) == 0) { |
|
554 | 559 | // only known good synopsis = no parameters |
|
555 | 559 | $parsVariations[] = array(); |
|
556 | $minPars = 0; |
||
557 | 559 | $maxPars = 0; |
|
558 | } else { |
||
559 | $minPars = count($parsVariations[0]); |
||
560 | $maxPars = count($parsVariations[count($parsVariations)-1]); |
||
561 | 559 | } |
|
562 | |||
563 | // build body of new function |
||
564 | |||
565 | $innerCode = " \$paramCount = \$req->getNumParams();\n"; |
||
566 | $innerCode .= " if (\$paramCount < $minPars || \$paramCount > $maxPars) return new " . static::$namespace . "Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n"; |
||
567 | 559 | ||
568 | 559 | $innerCode .= " \$encoder = new " . static::$namespace . "Encoder();\n"; |
|
569 | if ($decodePhpObjects) { |
||
570 | $innerCode .= " \$params = \$encoder->decode(\$req, array('decode_php_objs'));\n"; |
||
571 | } else { |
||
572 | $innerCode .= " \$params = \$encoder->decode(\$req);\n"; |
||
573 | 559 | } |
|
574 | 559 | ||
575 | // since we are building source code for later use, if we are given an object instance, |
||
576 | 559 | // we go out of our way and store a pointer to it in a static class var... |
|
577 | 559 | if (is_array($callable) && is_object($callable[0])) { |
|
578 | static::holdObject($newFuncName, $callable[0]); |
||
579 | $class = get_class($callable[0]); |
||
580 | 559 | if ($class[0] !== '\\') { |
|
581 | $class = '\\' . $class; |
||
582 | } |
||
583 | $innerCode .= " /// @var $class \$obj\n"; |
||
584 | $innerCode .= " \$obj = PhpXmlRpc\\Wrapper::getHeldObject('$newFuncName');\n"; |
||
585 | 559 | $realFuncName = '$obj->' . $callable[1]; |
|
586 | 559 | } else { |
|
587 | 559 | $realFuncName = $plainFuncName; |
|
588 | 559 | } |
|
589 | foreach ($parsVariations as $i => $pars) { |
||
590 | 559 | $innerCode .= " if (\$paramCount == " . count($pars) . ") \$retVal = {$catchWarnings}$realFuncName(" . implode(',', $pars) . ");\n"; |
|
591 | if ($i < (count($parsVariations) - 1)) |
||
592 | 559 | $innerCode .= " else\n"; |
|
593 | 559 | } |
|
594 | 559 | $innerCode .= " if (is_a(\$retVal, '" . static::$namespace . "Response'))\n return \$retVal;\n else\n"; |
|
595 | /// q: why not do the same for int, float, bool, string? |
||
596 | if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) { |
||
597 | 559 | $innerCode .= " return new " . static::$namespace . "Response(new " . static::$namespace . "Value(\$retVal, '{$funcDesc['returns']}'));"; |
|
598 | 559 | } else { |
|
599 | $encodeOptions = array(); |
||
600 | if ($encodeNulls) { |
||
601 | 559 | $encodeOptions[] = 'null_extension'; |
|
602 | } |
||
603 | if ($encodePhpObjects) { |
||
604 | 559 | $encodeOptions[] = 'encode_php_objs'; |
|
605 | } |
||
606 | |||
607 | if ($encodeOptions) { |
||
608 | $innerCode .= " return new " . static::$namespace . "Response(\$encoder->encode(\$retVal, array('" . |
||
609 | implode("', '", $encodeOptions) . "')));"; |
||
610 | } else { |
||
611 | 559 | $innerCode .= " return new " . static::$namespace . "Response(\$encoder->encode(\$retVal));"; |
|
612 | } |
||
613 | 559 | } |
|
614 | // shall we exclude functions returning by ref? |
||
615 | // if ($func->returnsReference()) |
||
616 | // return false; |
||
617 | |||
618 | $code = "/**\n * @param \PhpXmlRpc\Request \$req\n * @return \PhpXmlRpc\Response\n * @throws \\Exception\n */\n" . |
||
619 | "function $newFuncName(\$req)\n{\n" . $innerCode . "\n}"; |
||
620 | |||
621 | return $code; |
||
622 | } |
||
623 | |||
624 | /** |
||
625 | * Given a user-defined PHP class or php object, map its methods onto a list of |
||
626 | * PHP 'wrapper' functions that can be exposed as xml-rpc methods from an xml-rpc server |
||
627 | * object and called from remote clients (as well as their corresponding signature info). |
||
628 | * |
||
629 | 559 | * @param string|object $className the name of the class whose methods are to be exposed as xml-rpc methods, or an object instance of that class |
|
630 | * @param array $extraOptions see the docs for wrapPhpFunction for basic options, plus |
||
631 | 559 | * - string method_type 'static', 'nonstatic', 'all' and 'auto' (default); the latter will switch between static and non-static depending on whether $className is a class name or object instance |
|
632 | 559 | * - string method_filter a regexp used to filter methods to wrap based on their names |
|
633 | * - string prefix used for the names of the xml-rpc methods created. |
||
634 | 559 | * - string replace_class_name use to completely replace the class name with the prefix in the generated method names. e.g. instead of \Some\Namespace\Class.method use prefixmethod |
|
635 | 559 | * @return array|false false on failure, or on array useable for the dispatch map |
|
636 | 559 | * |
|
637 | 559 | * @todo allow the generated function to be able to reuse an external Encoder instance instead of creating one on |
|
638 | 559 | * each invocation, for the case where all the generated functions will be saved as methods of a class |
|
639 | 559 | */ |
|
640 | 559 | public function wrapPhpClass($className, $extraOptions = array()) |
|
641 | 559 | { |
|
642 | $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : ''; |
||
643 | 559 | $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto'; |
|
644 | |||
645 | 559 | $results = array(); |
|
646 | 559 | $mList = get_class_methods($className); |
|
647 | foreach ($mList as $mName) { |
||
648 | if ($methodFilter == '' || preg_match($methodFilter, $mName)) { |
||
649 | $func = new \ReflectionMethod($className, $mName); |
||
650 | if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) { |
||
651 | if (($func->isStatic() && ($methodType == 'all' || $methodType == 'static' || ($methodType == 'auto' && is_string($className)))) || |
||
652 | (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className)))) |
||
653 | 559 | ) { |
|
654 | $methodWrap = $this->wrapPhpFunction(array($className, $mName), '', $extraOptions); |
||
655 | |||
656 | if ($methodWrap) { |
||
657 | $results[$this->generateMethodNameForClassMethod($className, $mName, $extraOptions)] = $methodWrap; |
||
658 | } |
||
659 | } |
||
660 | } |
||
661 | } |
||
662 | 559 | } |
|
663 | |||
664 | 559 | return $results; |
|
665 | 559 | } |
|
666 | |||
667 | /** |
||
668 | 559 | * @param string|object $className |
|
669 | 559 | * @param string $classMethod |
|
670 | * @param array $extraOptions |
||
671 | * @return string |
||
672 | * |
||
673 | 559 | * @todo php allows many more characters in identifiers than the xml-rpc spec does. We should make sure to |
|
674 | * replace those (while trying to make sure we are not running in collisions) |
||
675 | */ |
||
676 | protected function generateMethodNameForClassMethod($className, $classMethod, $extraOptions = array()) |
||
677 | { |
||
678 | if (isset($extraOptions['replace_class_name']) && $extraOptions['replace_class_name']) { |
||
679 | return (isset($extraOptions['prefix']) ? $extraOptions['prefix'] : '') . $classMethod; |
||
680 | } |
||
681 | |||
682 | if (is_object($className)) { |
||
683 | $realClassName = get_class($className); |
||
684 | } else { |
||
685 | $realClassName = $className; |
||
686 | } |
||
687 | return (isset($extraOptions['prefix']) ? $extraOptions['prefix'] : '') . "$realClassName.$classMethod"; |
||
688 | } |
||
689 | |||
690 | /** |
||
691 | * Given an xml-rpc client and a method name, register a php wrapper function that will call it and return results |
||
692 | * using native php types for both arguments and results. The generated php function will return a Response |
||
693 | * object for failed xml-rpc calls. |
||
694 | * |
||
695 | * Known limitations: |
||
696 | * - server must support system.methodSignature for the target xml-rpc method |
||
697 | * - for methods that expose many signatures, only one can be picked (we could in principle check if signatures |
||
698 | * differ only by number of params and not by type, but it would be more complication than we can spare time for) |
||
699 | * - nested xml-rpc params: the caller of the generated php function has to encode on its own the params passed to |
||
700 | * the php function if these are structs or arrays whose (sub)members include values of type base64 |
||
701 | * |
||
702 | * Notes: the connection properties of the given client will be copied and reused for the connection used during |
||
703 | * the call to the generated php function. |
||
704 | * Calling the generated php function 'might' be slightly slow: a new xml-rpc client is created on every invocation |
||
705 | * and an xmlrpc-connection opened+closed. |
||
706 | * An extra 'debug' argument, defaulting to 0, is appended to the argument list of the generated function, useful |
||
707 | * for debugging purposes. |
||
708 | * |
||
709 | * @param Client $client an xml-rpc client set up correctly to communicate with target server |
||
710 | * @param string $methodName the xml-rpc method to be mapped to a php function |
||
711 | * @param array $extraOptions array of options that specify conversion details. Valid options include |
||
712 | * - integer signum the index of the method signature to use in mapping (if |
||
713 | * method exposes many sigs) |
||
714 | * - integer timeout timeout (in secs) to be used when executing function/calling remote method |
||
715 | * - string protocol 'http' (default), 'http11', 'https', 'h2' or 'h2c' |
||
716 | * - string new_function_name the name of php function to create, when return_source is used. |
||
717 | * If unspecified, lib will pick an appropriate name |
||
718 | * - string return_source if true return php code w. function definition instead of |
||
719 | * the function itself (closure) |
||
720 | * - bool encode_nulls if true, use `<nil/>` elements instead of empty string xml-rpc |
||
721 | 109 | * values for php null values |
|
722 | * - bool encode_php_objs let php objects be sent to server using the 'improved' xml-rpc |
||
723 | 109 | * notation, so server can deserialize them as php objects |
|
724 | * - bool decode_php_objs --- WARNING !!! possible security hazard. only use it with |
||
725 | 109 | * trusted servers --- |
|
726 | * - mixed return_on_fault a php value to be returned when the xml-rpc call fails/returns |
||
727 | 109 | * a fault response (by default the Response object is returned |
|
728 | 109 | * in this case). If a string is used, '%faultCode%' and |
|
729 | 24 | * '%faultString%' tokens will be substituted with actual error values |
|
730 | * - bool throw_on_fault if true, throw an exception instead of returning a Response |
||
731 | * in case of errors/faults; |
||
732 | 86 | * if a string, do the same and assume it is the exception class to throw |
|
733 | 1 | * - bool debug set it to 1 or 2 to see debug results of querying server for |
|
734 | * method synopsis |
||
735 | * - int simple_client_copy set it to 1 to have a lightweight copy of the $client object |
||
736 | * made in the generated code (only used when return_source = true) |
||
737 | 85 | * @return \Closure|string[]|false false on failure, closure by default and array for return_source = true |
|
738 | * |
||
739 | 85 | * @todo allow caller to give us the method signature instead of querying for it, or just say 'skip it' |
|
740 | * @todo if we can not retrieve method signature, create a php function with varargs |
||
741 | 85 | * @todo if caller did not specify a specific sig, shall we support all of them? |
|
742 | * It might be hard (hence slow) to match based on type and number of arguments... |
||
743 | * @todo when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster) |
||
744 | * @todo allow creating functions which have an extra `$debug=0` parameter |
||
745 | */ |
||
746 | public function wrapXmlrpcMethod($client, $methodName, $extraOptions = array()) |
||
747 | 85 | { |
|
748 | $newFuncName = isset($extraOptions['new_function_name']) ? $extraOptions['new_function_name'] : ''; |
||
749 | 85 | ||
750 | $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true; |
||
751 | |||
752 | $mSig = $this->retrieveMethodSignature($client, $methodName, $extraOptions); |
||
753 | if (!$mSig) { |
||
754 | return false; |
||
755 | } |
||
756 | |||
757 | if ($buildIt) { |
||
758 | return $this->buildWrapMethodClosure($client, $methodName, $extraOptions, $mSig); |
||
759 | } else { |
||
760 | 109 | // if in 'offline' mode, retrieve method description too. |
|
761 | // in online mode, favour speed of operation |
||
762 | 109 | $mDesc = $this->retrieveMethodHelp($client, $methodName, $extraOptions); |
|
763 | 109 | ||
764 | 109 | $newFuncName = $this->newFunctionName($methodName, $newFuncName, $extraOptions); |
|
765 | 109 | ||
766 | $results = $this->buildWrapMethodSource($client, $methodName, $extraOptions, $newFuncName, $mSig, $mDesc); |
||
767 | 109 | ||
768 | 109 | $results['function'] = $newFuncName; |
|
769 | 109 | ||
770 | 109 | return $results; |
|
771 | } |
||
772 | 109 | } |
|
773 | 109 | ||
774 | 109 | /** |
|
775 | 109 | * Retrieves an xml-rpc method signature from a server which supports system.methodSignature |
|
776 | 109 | * @param Client $client |
|
777 | 24 | * @param string $methodName |
|
778 | 24 | * @param array $extraOptions |
|
779 | * @return false|array |
||
780 | */ |
||
781 | 86 | protected function retrieveMethodSignature($client, $methodName, array $extraOptions = array()) |
|
782 | 86 | { |
|
783 | 85 | $reqClass = static::$namespace . 'Request'; |
|
784 | 85 | $valClass = static::$namespace . 'Value'; |
|
785 | $decoderClass = static::$namespace . 'Encoder'; |
||
786 | |||
787 | 86 | $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0; |
|
788 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
||
789 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
||
790 | $sigNum = isset($extraOptions['signum']) ? (int)$extraOptions['signum'] : 0; |
||
791 | |||
792 | 86 | $req = new $reqClass('system.methodSignature'); |
|
793 | $req->addParam(new $valClass($methodName)); |
||
794 | $origDebug = $client->getOption(Client::OPT_DEBUG); |
||
795 | $client->setDebug($debug); |
||
796 | /// @todo move setting of timeout, protocol to outside the send() call |
||
797 | $response = $client->send($req, $timeout, $protocol); |
||
798 | $client->setDebug($origDebug); |
||
799 | if ($response->faultCode()) { |
||
800 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature from remote server for method ' . $methodName); |
||
801 | 85 | return false; |
|
802 | } |
||
803 | 85 | ||
804 | 85 | $mSig = $response->value(); |
|
805 | 85 | /// @todo what about return xml? |
|
806 | if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') { |
||
807 | 85 | $decoder = new $decoderClass(); |
|
808 | 85 | $mSig = $decoder->decode($mSig); |
|
809 | 85 | } |
|
810 | |||
811 | 85 | if (!is_array($mSig) || count($mSig) <= $sigNum) { |
|
812 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature nr.' . $sigNum . ' from remote server for method ' . $methodName); |
||
813 | 85 | return false; |
|
814 | 85 | } |
|
815 | 85 | ||
816 | 85 | return $mSig[$sigNum]; |
|
817 | 85 | } |
|
818 | 85 | ||
819 | 85 | /** |
|
820 | 85 | * @param Client $client |
|
821 | * @param string $methodName |
||
822 | * @param array $extraOptions |
||
823 | * @return string in case of any error, an empty string is returned, no warnings generated |
||
824 | 85 | */ |
|
825 | protected function retrieveMethodHelp($client, $methodName, array $extraOptions = array()) |
||
826 | { |
||
827 | $reqClass = static::$namespace . 'Request'; |
||
828 | $valClass = static::$namespace . 'Value'; |
||
829 | |||
830 | $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0; |
||
831 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
||
832 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
||
833 | |||
834 | $mDesc = ''; |
||
835 | |||
836 | 1 | $req = new $reqClass('system.methodHelp'); |
|
837 | $req->addParam(new $valClass($methodName)); |
||
838 | $origDebug = $client->getOption(Client::OPT_DEBUG); |
||
839 | 1 | $client->setDebug($debug); |
|
840 | /// @todo move setting of timeout, protocol to outside the send() call |
||
841 | $response = $client->send($req, $timeout, $protocol); |
||
842 | 1 | $client->setDebug($origDebug); |
|
843 | 1 | if (!$response->faultCode()) { |
|
844 | 1 | $mDesc = $response->value(); |
|
845 | 1 | if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') { |
|
846 | 1 | $mDesc = $mDesc->scalarVal(); |
|
847 | } |
||
848 | } |
||
849 | |||
850 | 1 | return $mDesc; |
|
851 | } |
||
852 | |||
853 | 1 | /** |
|
854 | 1 | * @param Client $client |
|
855 | 1 | * @param string $methodName |
|
856 | 1 | * @param array $extraOptions @see wrapXmlrpcMethod |
|
857 | * @param array $mSig |
||
858 | 1 | * @return \Closure |
|
859 | 1 | * |
|
860 | 1 | * @todo should we allow usage of parameter simple_client_copy to mean 'do not clone' in this case? |
|
861 | */ |
||
862 | protected function buildWrapMethodClosure($client, $methodName, array $extraOptions, $mSig) |
||
863 | 1 | { |
|
864 | 1 | // we clone the client, so that we can modify it a bit independently of the original |
|
865 | $clientClone = clone $client; |
||
866 | $function = function() use($clientClone, $methodName, $extraOptions, $mSig) |
||
867 | { |
||
868 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
||
869 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
||
870 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
||
871 | 1 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
|
872 | 1 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
|
873 | 1 | $throwFault = false; |
|
874 | 1 | $decodeFault = false; |
|
875 | 1 | $faultResponse = null; |
|
876 | if (isset($extraOptions['throw_on_fault'])) { |
||
877 | $throwFault = $extraOptions['throw_on_fault']; |
||
878 | 1 | } else if (isset($extraOptions['return_on_fault'])) { |
|
879 | 1 | $decodeFault = true; |
|
880 | 1 | $faultResponse = $extraOptions['return_on_fault']; |
|
881 | } |
||
882 | |||
883 | 1 | $reqClass = static::$namespace . 'Request'; |
|
884 | 1 | $encoderClass = static::$namespace . 'Encoder'; |
|
885 | $valueClass = static::$namespace . 'Value'; |
||
886 | |||
887 | $encoder = new $encoderClass(); |
||
888 | $encodeOptions = array(); |
||
889 | 1 | if ($encodePhpObjects) { |
|
890 | $encodeOptions[] = 'encode_php_objs'; |
||
891 | } |
||
892 | if ($encodeNulls) { |
||
893 | $encodeOptions[] = 'null_extension'; |
||
894 | } |
||
895 | 1 | $decodeOptions = array(); |
|
896 | if ($decodePhpObjects) { |
||
897 | 1 | $decodeOptions[] = 'decode_php_objs'; |
|
898 | 1 | } |
|
899 | 1 | ||
900 | /// @todo check for insufficient nr. of args besides excess ones? note that 'source' version does not... |
||
901 | |||
902 | // support one extra parameter: debug |
||
903 | $maxArgs = count($mSig)-1; // 1st element is the return type |
||
904 | $currentArgs = func_get_args(); |
||
905 | if (func_num_args() == ($maxArgs+1)) { |
||
906 | $debug = array_pop($currentArgs); |
||
907 | $clientClone->setDebug($debug); |
||
908 | } |
||
909 | |||
910 | $xmlrpcArgs = array(); |
||
911 | 1 | foreach ($currentArgs as $i => $arg) { |
|
912 | if ($i == $maxArgs) { |
||
913 | 1 | break; |
|
914 | } |
||
915 | 1 | $pType = $mSig[$i+1]; |
|
916 | if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' || |
||
917 | $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null' |
||
918 | ) { |
||
919 | // by building directly xml-rpc values when type is known and scalar (instead of encode() calls), |
||
920 | // we make sure to honour the xml-rpc signature |
||
921 | $xmlrpcArgs[] = new $valueClass($arg, $pType); |
||
922 | } else { |
||
923 | $xmlrpcArgs[] = $encoder->encode($arg, $encodeOptions); |
||
924 | } |
||
925 | } |
||
926 | |||
927 | 85 | $req = new $reqClass($methodName, $xmlrpcArgs); |
|
928 | // use this to get the maximum decoding flexibility |
||
929 | 85 | $clientClone->setOption(Client::OPT_RETURN_TYPE, 'xmlrpcvals'); |
|
930 | 85 | $resp = $clientClone->send($req, $timeout, $protocol); |
|
931 | 85 | if ($resp->faultcode()) { |
|
932 | 85 | if ($throwFault) { |
|
933 | 85 | if (is_string($throwFault)) { |
|
934 | 85 | throw new $throwFault($resp->faultString(), $resp->faultCode()); |
|
935 | 85 | } else { |
|
936 | throw new \PhpXmlRpc\Exception($resp->faultString(), $resp->faultCode()); |
||
937 | } |
||
938 | } else if ($decodeFault) { |
||
939 | 85 | if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || |
|
940 | 85 | (strpos($faultResponse, '%faultString%') !== false))) { |
|
941 | $faultResponse = str_replace(array('%faultCode%', '%faultString%'), |
||
942 | array($resp->faultCode(), $resp->faultString()), $faultResponse); |
||
943 | 85 | } |
|
944 | return $faultResponse; |
||
945 | 85 | } else { |
|
946 | 85 | return $resp; |
|
947 | } |
||
948 | 64 | } else { |
|
949 | 64 | return $encoder->decode($resp->value(), $decodeOptions); |
|
950 | 64 | } |
|
951 | 64 | }; |
|
952 | |||
953 | return $function; |
||
954 | 22 | } |
|
955 | 22 | ||
956 | /** |
||
957 | 85 | * @internal made public just for Debugger usage |
|
958 | * |
||
959 | 85 | * @param Client $client |
|
960 | * @param string $methodName |
||
961 | 85 | * @param array $extraOptions @see wrapXmlrpcMethod |
|
962 | * @param string $newFuncName |
||
963 | * @param array $mSig |
||
964 | * @param string $mDesc |
||
965 | * @return string[] keys: source, docstring |
||
966 | */ |
||
967 | 85 | public function buildWrapMethodSource($client, $methodName, array $extraOptions, $newFuncName, $mSig, $mDesc='') |
|
968 | 85 | { |
|
969 | 85 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
|
970 | 85 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
|
971 | 64 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
|
972 | 64 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
|
973 | 64 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
|
974 | 64 | $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0; |
|
975 | $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc'; |
||
976 | $throwFault = false; |
||
977 | 64 | $decodeFault = false; |
|
978 | $faultResponse = null; |
||
979 | if (isset($extraOptions['throw_on_fault'])) { |
||
980 | $throwFault = $extraOptions['throw_on_fault']; |
||
981 | } else if (isset($extraOptions['return_on_fault'])) { |
||
982 | $decodeFault = true; |
||
983 | $faultResponse = $extraOptions['return_on_fault']; |
||
984 | } |
||
985 | 64 | ||
986 | 64 | $code = "function $newFuncName("; |
|
987 | if ($clientCopyMode < 2) { |
||
988 | 85 | // client copy mode 0 or 1 == full / partial client copy in emitted code |
|
989 | 64 | $verbatimClientCopy = !$clientCopyMode; |
|
990 | 64 | $innerCode = ' ' . str_replace("\n", "\n ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace)); |
|
991 | $innerCode .= "\$client->setDebug(\$debug);\n"; |
||
992 | 85 | $this_ = ''; |
|
993 | 85 | } else { |
|
994 | // client copy mode 2 == no client copy in emitted code |
||
995 | 85 | $innerCode = ''; |
|
996 | 85 | $this_ = 'this->'; |
|
997 | } |
||
998 | $innerCode .= " \$req = new " . static::$namespace . "Request('$methodName');\n"; |
||
999 | |||
1000 | if ($mDesc != '') { |
||
1001 | // take care that PHP comment is not terminated unwillingly by method description |
||
1002 | /// @todo according to the spec, method desc can have html in it. We should run it through strip_tags... |
||
1003 | 85 | $mDesc = "/**\n * " . str_replace(array("\n", '*/'), array("\n * ", '* /'), $mDesc) . "\n"; |
|
1004 | } else { |
||
1005 | 85 | $mDesc = "/**\n * Function $newFuncName.\n"; |
|
1006 | 22 | } |
|
1007 | |||
1008 | 64 | // param parsing |
|
1009 | $innerCode .= " \$encoder = new " . static::$namespace . "Encoder();\n"; |
||
1010 | $plist = array(); |
||
1011 | 85 | $pCount = count($mSig); |
|
1012 | for ($i = 1; $i < $pCount; $i++) { |
||
1013 | 85 | $plist[] = "\$p$i"; |
|
1014 | $pType = $mSig[$i]; |
||
1015 | if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' || |
||
1016 | $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null' |
||
1017 | ) { |
||
1018 | // only build directly xml-rpc values when type is known and scalar |
||
1019 | $innerCode .= " \$p$i = new " . static::$namespace . "Value(\$p$i, '$pType');\n"; |
||
1020 | } else { |
||
1021 | if ($encodePhpObjects || $encodeNulls) { |
||
1022 | $encOpts = array(); |
||
1023 | if ($encodePhpObjects) { |
||
1024 | $encOpts[] = 'encode_php_objs'; |
||
1025 | } |
||
1026 | if ($encodeNulls) { |
||
1027 | $encOpts[] = 'null_extension'; |
||
1028 | } |
||
1029 | |||
1030 | $innerCode .= " \$p$i = \$encoder->encode(\$p$i, array( '" . implode("', '", $encOpts) . "'));\n"; |
||
1031 | } else { |
||
1032 | $innerCode .= " \$p$i = \$encoder->encode(\$p$i);\n"; |
||
1033 | } |
||
1034 | 22 | } |
|
1035 | $innerCode .= " \$req->addParam(\$p$i);\n"; |
||
1036 | 22 | $mDesc .= " * @param " . $this->xmlrpc2PhpType($pType) . " \$p$i\n"; |
|
1037 | 22 | } |
|
1038 | 22 | if ($clientCopyMode < 2) { |
|
1039 | 22 | $plist[] = '$debug = 0'; |
|
1040 | 22 | $mDesc .= " * @param int \$debug when 1 (or 2) will enable debugging of the underlying {$prefix} call (defaults to 0)\n"; |
|
1041 | 22 | } |
|
1042 | 22 | $plist = implode(', ', $plist); |
|
1043 | 22 | $mDesc .= ' * @return ' . $this->xmlrpc2PhpType($mSig[0]); |
|
1044 | 22 | if ($throwFault) { |
|
1045 | 22 | $mDesc .= "\n * @throws " . (is_string($throwFault) ? $throwFault : '\\PhpXmlRpc\\Exception'); |
|
1046 | } else if ($decodeFault) { |
||
1047 | 22 | $mDesc .= '|' . gettype($faultResponse) . " (a " . gettype($faultResponse) . " if call fails)"; |
|
1048 | 22 | } else { |
|
1049 | $mDesc .= '|' . static::$namespace . "Response (a " . static::$namespace . "Response obj instance if call fails)"; |
||
1050 | 22 | } |
|
1051 | 22 | $mDesc .= "\n */\n"; |
|
1052 | 22 | ||
1053 | /// @todo move setting of timeout, protocol to outside the send() call |
||
1054 | $innerCode .= " \$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n"; |
||
1055 | if ($throwFault) { |
||
1056 | if (!is_string($throwFault)) { |
||
1057 | 22 | $throwFault = '\\PhpXmlRpc\\Exception'; |
|
1058 | 22 | } |
|
1059 | 22 | $respCode = "throw new $throwFault(\$res->faultString(), \$res->faultCode())"; |
|
1060 | 22 | } else if ($decodeFault) { |
|
1061 | if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) { |
||
1062 | 22 | $respCode = "return str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')"; |
|
1063 | } else { |
||
1064 | $respCode = 'return ' . var_export($faultResponse, true); |
||
1065 | } |
||
1066 | } else { |
||
1067 | $respCode = 'return $res'; |
||
1068 | 22 | } |
|
1069 | if ($decodePhpObjects) { |
||
1070 | $innerCode .= " if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));"; |
||
1071 | 22 | } else { |
|
1072 | 22 | $innerCode .= " if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value());"; |
|
1073 | } |
||
1074 | 22 | ||
1075 | 21 | $code = $code . $plist . ")\n{\n" . $innerCode . "\n}\n"; |
|
1076 | |||
1077 | return array('source' => $code, 'docstring' => $mDesc); |
||
1078 | } |
||
1079 | 22 | ||
1080 | 22 | /** |
|
1081 | 22 | * Similar to wrapXmlrpcMethod, but will generate a php class that wraps all xml-rpc methods exposed by the remote |
|
1082 | 22 | * server as own methods. |
|
1083 | * For a slimmer alternative, see the code in demo/client/proxy.php. |
||
1084 | 22 | * Note that unlike wrapXmlrpcMethod, we always have to generate php code here. Since php 7 anon classes exist, but |
|
1085 | 22 | * we do not support them yet... |
|
1086 | 22 | * |
|
1087 | 22 | * @see wrapXmlrpcMethod for more details. |
|
1088 | 22 | * |
|
1089 | 22 | * @param Client $client the client obj all set to query the desired server |
|
1090 | 22 | * @param array $extraOptions list of options for wrapped code. See the ones from wrapXmlrpcMethod, plus |
|
1091 | * - string method_filter regular expression |
||
1092 | * - string new_class_name |
||
1093 | 22 | * - string prefix |
|
1094 | 22 | * - bool simple_client_copy set it to true to avoid copying all properties of $client into the copy made in the new class |
|
1095 | * @return string|array|false false on error, the name of the created class if all ok or an array with code, class name and comments (if the appropriate option is set in extra_options) |
||
1096 | 22 | * |
|
1097 | 22 | * @todo add support for anonymous classes in the 'buildIt' case for php > 7 |
|
1098 | 22 | * @todo add method setDebug() to new class, to enable/disable debugging |
|
1099 | 22 | * @todo optimization - move the generated Encoder instance to be a property of the created class, instead of creating |
|
1100 | 22 | * it on every generated method invocation |
|
1101 | */ |
||
1102 | public function wrapXmlrpcServer($client, $extraOptions = array()) |
||
1103 | 22 | { |
|
1104 | $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : ''; |
||
1105 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
||
1106 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
||
1107 | $newClassName = isset($extraOptions['new_class_name']) ? $extraOptions['new_class_name'] : ''; |
||
1108 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
||
1109 | 22 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
|
1110 | 22 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
|
1111 | 22 | $verbatimClientCopy = isset($extraOptions['simple_client_copy']) ? !($extraOptions['simple_client_copy']) : true; |
|
1112 | 22 | $throwOnFault = isset($extraOptions['throw_on_fault']) ? (bool)$extraOptions['throw_on_fault'] : false; |
|
1113 | 22 | $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true; |
|
1114 | 22 | $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc'; |
|
1115 | |||
1116 | $reqClass = static::$namespace . 'Request'; |
||
1117 | $decoderClass = static::$namespace . 'Encoder'; |
||
1118 | |||
1119 | // retrieve the list of methods |
||
1120 | $req = new $reqClass('system.listMethods'); |
||
1121 | /// @todo move setting of timeout, protocol to outside the send() call |
||
1122 | $response = $client->send($req, $timeout, $protocol); |
||
1123 | if ($response->faultCode()) { |
||
1124 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method list from remote server'); |
||
1125 | |||
1126 | return false; |
||
1127 | } |
||
1128 | $mList = $response->value(); |
||
1129 | /// @todo what about return_type = xml? |
||
1130 | if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') { |
||
1131 | $decoder = new $decoderClass(); |
||
1132 | $mList = $decoder->decode($mList); |
||
1133 | } |
||
1134 | if (!is_array($mList) || !count($mList)) { |
||
1135 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve meaningful method list from remote server'); |
||
1136 | |||
1137 | 85 | return false; |
|
1138 | } |
||
1139 | 85 | ||
1140 | 85 | // pick a suitable name for the new function, avoiding collisions |
|
1141 | if ($newClassName != '') { |
||
1142 | $xmlrpcClassName = $newClassName; |
||
1143 | } else { |
||
1144 | 85 | /// @todo direct access to $client->server is now deprecated |
|
1145 | 85 | $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), array('_', ''), |
|
1146 | $client->server) . '_client'; |
||
1147 | } |
||
1148 | while ($buildIt && class_exists($xmlrpcClassName)) { |
||
1149 | $xmlrpcClassName .= 'x'; |
||
1150 | } |
||
1151 | 85 | ||
1152 | 85 | $source = "class $xmlrpcClassName\n{\n public \$client;\n\n"; |
|
1153 | 85 | $source .= " function __construct()\n {\n"; |
|
1154 | $source .= ' ' . str_replace("\n", "\n ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace)); |
||
1155 | $source .= "\$this->client = \$client;\n }\n\n"; |
||
1156 | $opts = array( |
||
1157 | 'return_source' => true, |
||
1158 | 85 | 'simple_client_copy' => 2, // do not produce code to copy the client object |
|
1159 | 'timeout' => $timeout, |
||
1160 | 85 | 'protocol' => $protocol, |
|
1161 | 'encode_nulls' => $encodeNulls, |
||
1162 | 'encode_php_objs' => $encodePhpObjects, |
||
1163 | 'decode_php_objs' => $decodePhpObjects, |
||
1164 | 'throw_on_fault' => $throwOnFault, |
||
1165 | 'prefix' => $prefix, |
||
1166 | ); |
||
1167 | |||
1168 | /// @todo build phpdoc for class definition, too |
||
1169 | foreach ($mList as $mName) { |
||
1170 | if ($methodFilter == '' || preg_match($methodFilter, $mName)) { |
||
1171 | /// @todo this will fail if server exposes 2 methods called f.e. do.something and do_something |
||
1172 | $opts['new_function_name'] = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), |
||
1173 | array('_', ''), $mName); |
||
1174 | $methodWrap = $this->wrapXmlrpcMethod($client, $mName, $opts); |
||
1175 | if ($methodWrap) { |
||
1176 | if ($buildIt) { |
||
1177 | $source .= $methodWrap['source'] . "\n"; |
||
1178 | |||
1179 | } else { |
||
1180 | $source .= ' ' . str_replace("\n", "\n ", $methodWrap['docstring']); |
||
1181 | $source .= str_replace("\n", "\n ", $methodWrap['source']). "\n"; |
||
1182 | } |
||
1183 | |||
1184 | } else { |
||
1185 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': will not create class method to wrap remote method ' . $mName); |
||
1186 | } |
||
1187 | } |
||
1188 | } |
||
1189 | $source .= "}\n"; |
||
1190 | if ($buildIt) { |
||
1191 | $allOK = 0; |
||
1192 | eval($source . '$allOK=1;'); |
||
1193 | if ($allOK) { |
||
1194 | return $xmlrpcClassName; |
||
1195 | } else { |
||
1196 | /// @todo direct access to $client->server is now deprecated |
||
1197 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not create class ' . $xmlrpcClassName . |
||
1198 | ' to wrap remote server ' . $client->server); |
||
1199 | return false; |
||
1200 | } |
||
1201 | } else { |
||
1202 | return array('class' => $xmlrpcClassName, 'code' => $source, 'docstring' => ''); |
||
1203 | } |
||
1204 | } |
||
1205 | |||
1206 | /** |
||
1207 | * Given necessary info, generate php code that will build a client object just like the given one. |
||
1208 | * Take care that no full checking of input parameters is done to ensure that valid php code is emitted. |
||
1209 | * @param Client $client |
||
1210 | * @param bool $verbatimClientCopy when true, copy the whole options of the client, except for 'debug' and 'return_type' |
||
1211 | * @param string $prefix used for the return_type of the created client |
||
1212 | * @param string $namespace |
||
1213 | * @return string |
||
1214 | */ |
||
1215 | protected function buildClientWrapperCode($client, $verbatimClientCopy, $prefix = 'xmlrpc', $namespace = '\\PhpXmlRpc\\') |
||
1216 | { |
||
1217 | $code = "\$client = new {$namespace}Client('" . str_replace(array("\\", "'"), array("\\\\", "\'"), $client->getUrl()) . |
||
1218 | "');\n"; |
||
1219 | |||
1220 | // copy all client fields to the client that will be generated runtime |
||
1221 | // (this provides for future expansion or subclassing of client obj) |
||
1222 | if ($verbatimClientCopy) { |
||
1223 | foreach ($client->getOptions() as $opt => $val) { |
||
1224 | if ($opt != 'debug' && $opt != 'return_type') { |
||
1225 | $val = var_export($val, true); |
||
1226 | $code .= "\$client->setOption('$opt', $val);\n"; |
||
1227 | } |
||
1228 | } |
||
1229 | } |
||
1230 | // only make sure that client always returns the correct data type |
||
1231 | $code .= "\$client->setOption(\PhpXmlRpc\Client::OPT_RETURN_TYPE, '{$prefix}vals');\n"; |
||
1232 | return $code; |
||
1233 | } |
||
1234 | |||
1235 | /** |
||
1236 | * @param string $index |
||
1237 | * @param object $object |
||
1238 | * @return void |
||
1239 | */ |
||
1240 | public static function holdObject($index, $object) |
||
1241 | { |
||
1242 | self::$objHolder[$index] = $object; |
||
1243 | } |
||
1244 | |||
1245 | /** |
||
1246 | * @param string $index |
||
1247 | * @return object |
||
1248 | * @throws ValueErrorException |
||
1249 | */ |
||
1250 | public static function getHeldObject($index) |
||
1251 | { |
||
1252 | if (isset(self::$objHolder[$index])) { |
||
1253 | return self::$objHolder[$index]; |
||
1254 | } |
||
1255 | |||
1256 | throw new ValueErrorException("No object held for index '$index'"); |
||
1257 | } |
||
1258 | } |
||
1259 |