1
0

Wrapper.php 59 KB

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