/** * Given a user-defined PHP function, create a PHP 'wrapper' function that can * be exposed as xmlrpc method from an xmlrpc_server object and called from remote * clients. * * Since php is a typeless language, to infer types of input and output parameters, * it relies on parsing the javadoc-style comment block associated with the given * function. Usage of xmlrpc native types (such as datetime.dateTime.iso8601 and base64) * in the @param tag is also allowed, if you need the php function to receive/send * data in that particular format (note that base64 enncoding/decoding is transparently * carried out by the lib, while datetime vals are passed around as strings) * * Known limitations: * - requires PHP 5.0.3 + * - only works for user-defined functions, not for PHP internal functions * (reflection does not support retrieving number/type of params for those) * - functions returning php objects will generate special xmlrpc responses: * when the xmlrpc decoding of those responses is carried out by this same lib, using * the appropriate param in php_xmlrpc_decode, the php objects will be rebuilt. * In short: php objects can be serialized, too (except for their resource members), * using this function. * Other libs might choke on the very same xml that will be generated in this case * (i.e. it has a nonstandard attribute on struct element tags) * - usage of javadoc @param tags using param names in a different order from the * function prototype is not considered valid (to be fixed?) * * @param string $funcname the name of the PHP user function to be exposed as xmlrpc method; array($obj, 'methodname') might be ok too, in the future... * @return false on error, or an array containing the name of the new php function, * its signature and docs, to be used in the server dispatch map * * @todo decide how to deal with params passed by ref: bomb out or allow? * @todo finish using javadoc info to build method sig if all params are named but out of order * @done switch to some automagic object encoding scheme * @todo add a check for params of 'resource' type * @todo add some trigger_errors when returning false? * @todo what to do when the PHP function returns NULL? we are currently returning bogus responses!!! */ function wrap_php_function($funcname, $newfuncname = '') { if (version_compare(phpversion(), '5.0.3') == -1) { // up to php 5.0.3 some useful reflection methods were missing return false; } if (is_array($funcname) && !method_exists($funcname[0], $funcname[1]) || !function_exists($funcname)) { return false; } else { // determine name of new php function if ($newfuncname == '') { if (is_array($funcname)) { $xmlrpcfuncname = "xmlrpc_" . implode('_', $funcname); } else { $xmlrpcfuncname = "xmlrpc_{$funcname}"; } } else { $xmlrpcfuncname = $newfuncname; } while (function_exists($xmlrpcfuncname)) { $xmlrpcfuncname .= 'x'; } $code = "function {$xmlrpcfuncname}(\$msg) {\n"; // start to introspect PHP code $func =& new ReflectionFunction($funcname); if ($func->isInternal()) { // Note: from PHP 5.1.0 onward, we will possibly be able to use invokeargs // instead of getparameters to fully reflect internal php functions ? return false; } // retrieve parameter names, types and description from javadoc comments // function description $desc = ''; // type of return val: by default 'any' $returns = $GLOBALS['xmlrpcValue']; // type + name of function parameters $paramDocs = array(); $docs = $func->getDocComment(); if ($docs != '') { $docs = explode("\n", $docs); $i = 0; foreach ($docs as $doc) { $doc = trim($doc, " \r\t/*"); if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) { if ($desc) { $desc .= "\n"; } $desc .= $doc; } elseif (strpos($doc, '@param') === 0) { // syntax: @param type [$name] desc if (preg_match('/@param\\s+(\\S+)(\\s+\\$\\S+)?\\s+(.+)/', $doc, $matches)) { if (strpos($matches[1], '|')) { //$paramDocs[$i]['type'] = explode('|', $matches[1]); $paramDocs[$i]['type'] = 'mixed'; } else { $paramDocs[$i]['type'] = $matches[1]; } $paramDocs[$i]['name'] = trim($matches[2]); $paramDocs[$i]['doc'] = $matches[3]; } $i++; } elseif (strpos($doc, '@return') === 0) { $returns = preg_split("/\\s+/", $doc); if (isset($returns[1])) { $returns = php_2_xmlrpc_type($returns[1]); } } } } // start introspection of actual function prototype and building of PHP code // to be eval'd $params = $func->getParameters(); $innercode = ''; $i = 0; $parsvariations = array(); $pars = array(); $pnum = count($params); foreach ($params as $param) { if (isset($paramDocs[$i]['name']) && $paramDocs[$i]['name'] && strtolower($paramDocs[$i]['name']) != '$' . strtolower($param->getName())) { // param name from phpdoc info does not match param definition! $paramDocs[$i]['type'] = 'mixed'; } if ($param->isOptional()) { // this particular parameter is optional. save as valid previous list of parameters $innercode .= "if (\$paramcount > {$i}) {\n"; $parsvariations[] = $pars; } $innercode .= "\$p{$i} = \$msg->getParam({$i});\n"; $innercode .= "if (\$p{$i}->kindOf() == 'scalar') \$p{$i} = \$p{$i}->scalarval(); else \$p{$i} = php_xmlrpc_decode(\$p{$i});\n"; $pars[] = "\$p{$i}"; $i++; if ($param->isOptional()) { $innercode .= "}\n"; } if ($i == $pnum) { // last allowed parameters combination $parsvariations[] = $pars; } } $sigs = array(); if (count($parsvariations) == 0) { // only known good synopsis = no parameters $parsvariations[] = array(); $minpars = 0; } else { $minpars = count($parsvariations[0]); } if ($minpars) { // add to code the check for min params number // NB: this check needs to be done BEFORE decoding param values $innercode = "\$paramcount = \$msg->getNumParams();\n" . "if (\$paramcount < {$minpars}) return new xmlrpcresp(0, {$GLOBALS['xmlrpcerr']['incorrect_params']}, '{$GLOBALS['xmlrpcstr']['incorrect_params']}');\n" . $innercode; } else { $innercode = "\$paramcount = \$msg->getNumParams();\n" . $innercode; } $innercode .= "\$np = false;"; foreach ($parsvariations as $pars) { $innercode .= "if (\$paramcount == " . count($pars) . ") \$retval = {$funcname}(" . implode(',', $pars) . "); else\n"; // build a 'generic' signature (only use an appropriate return type) $sig = array($returns); for ($i = 0; $i < count($pars); $i++) { if (isset($paramDocs[$i]['type'])) { $sig[] = php_2_xmlrpc_type($paramDocs[$i]['type']); } else { $sig[] = $GLOBALS['xmlrpcValue']; } } $sigs[] = $sig; } $innercode .= "\$np = true;\n"; $innercode .= "if (\$np) return new xmlrpcresp(0, {$GLOBALS['xmlrpcerr']['incorrect_params']}, '{$GLOBALS['xmlrpcstr']['incorrect_params']}'); else\n"; //$innercode .= "if (\$_xmlrpcs_error_occurred) return new xmlrpcresp(0, $GLOBALS['xmlrpcerr']user, \$_xmlrpcs_error_occurred); else\n"; if ($returns == $GLOBALS['xmlrpcDateTime'] || $returns == $GLOBALS['xmlrpcBase64']) { $innercode .= "return new xmlrpcresp(new xmlrpcval(\$retval, '{$returns}'));"; } else { $innercode .= "return new xmlrpcresp(php_xmlrpc_encode(\$retval, array('encode_php_objs')));"; } // shall we exclude functions returning by ref? // if($func->returnsReference()) // return false; $code = $code . $innercode . "\n}\n \$allOK=1;"; //print_r($code); $allOK = 0; eval($code); // alternative //$xmlrpcfuncname = create_function('$m', $innercode); if (!$allOK) { return false; } /// @todo examine if $paramDocs matches $parsvariations and build array for /// usage as method signature, plus put together a nice string for docs $ret = array('function' => $xmlrpcfuncname, 'signature' => $sigs, 'docstring' => $desc); return $ret; } }
function _xmlrpcs_multicall_do_call_phpvals($server, $call) { if (!is_array($call)) { return _xmlrpcs_multicall_error('notstruct'); } if (!array_key_exists('methodName', $call)) { return _xmlrpcs_multicall_error('nomethod'); } if (!is_string($call['methodName'])) { return _xmlrpcs_multicall_error('notstring'); } if ($call['methodName'] == 'system.multicall') { return _xmlrpcs_multicall_error('recursion'); } if (!array_key_exists('params', $call)) { return _xmlrpcs_multicall_error('noparams'); } if (!is_array($call['params'])) { return _xmlrpcs_multicall_error('notarray'); } // this is a real dirty and simplistic hack, since we might have received a // base64 or datetime values, but they will be listed as strings here... $numParams = count($call['params']); $pt = array(); foreach ($call['params'] as $val) { $pt[] = php_2_xmlrpc_type(gettype($val)); } $result = $server->execute($call['methodName'], $call['params'], $pt); if ($result->faultCode() != 0) { return _xmlrpcs_multicall_error($result); // Method returned fault. } return new xmlrpcval(array($result->value()), 'array'); }
/** * Given a user-defined PHP function, create a PHP 'wrapper' function that can * be exposed as xmlrpc method from an xmlrpc_server object and called from remote * clients (as well as its corresponding signature info). * * Since php is a typeless language, to infer types of input and output parameters, * it relies on parsing the javadoc-style comment block associated with the given * function. Usage of xmlrpc native types (such as datetime.dateTime.iso8601 and base64) * in the @param tag is also allowed, if you need the php function to receive/send * data in that particular format (note that base64 encoding/decoding is transparently * carried out by the lib, while datetime vals are passed around as strings) * * Known limitations: * - requires PHP 5.0.3 + * - only works for user-defined functions, not for PHP internal functions * (reflection does not support retrieving number/type of params for those) * - functions returning php objects will generate special xmlrpc responses: * when the xmlrpc decoding of those responses is carried out by this same lib, using * the appropriate param in php_xmlrpc_decode, the php objects will be rebuilt. * In short: php objects can be serialized, too (except for their resource members), * using this function. * Other libs might choke on the very same xml that will be generated in this case * (i.e. it has a nonstandard attribute on struct element tags) * - usage of javadoc @param tags using param names in a different order from the * function prototype is not considered valid (to be fixed?) * * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' * php functions (ie. functions not expecting a single xmlrpcmsg obj as parameter) * is by making use of the functions_parameters_type class member. * * @param string $funcname the name of the PHP user function to be exposed as xmlrpc method; array($obj, 'methodname') and array('class', 'methodname') are ok too * @param string $newfuncname (optional) name for function to be created * @param array $extra_options (optional) array of options for conversion. valid values include: * bool return_source when true, php code w. function definition will be returned, not evaluated * bool encode_php_objs let php objects be sent to server using the 'improved' xmlrpc notation, so server can deserialize them as php objects * bool decode_php_objs --- WARNING !!! possible security hazard. only use it with trusted servers --- * bool suppress_warnings remove from produced xml any runtime warnings due to the php function being invoked * @return false on error, or an array containing the name of the new php function, * its signature and docs, to be used in the server dispatch map * * @todo decide how to deal with params passed by ref: bomb out or allow? * @todo finish using javadoc info to build method sig if all params are named but out of order * @todo add a check for params of 'resource' type * @todo add some trigger_errors / error_log when returning false? * @todo what to do when the PHP function returns NULL? we are currently returning an empty string value... * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3? * @todo if $newfuncname is empty, we could use create_user_func instead of eval, as it is possibly faster * @todo add a verbatim_object_copy parameter to allow avoiding the same obj instance? */ function wrap_php_function($funcname, $newfuncname = '', $extra_options = array()) { $buildit = isset($extra_options['return_source']) ? !$extra_options['return_source'] : true; $prefix = isset($extra_options['prefix']) ? $extra_options['prefix'] : 'xmlrpc'; $encode_php_objects = isset($extra_options['encode_php_objs']) ? (bool) $extra_options['encode_php_objs'] : false; $decode_php_objects = isset($extra_options['decode_php_objs']) ? (bool) $extra_options['decode_php_objs'] : false; $catch_warnings = isset($extra_options['suppress_warnings']) && $extra_options['suppress_warnings'] ? '@' : ''; if (version_compare(phpversion(), '5.0.3') == -1) { // up to php 5.0.3 some useful reflection methods were missing error_log('XML-RPC: cannot not wrap php functions unless running php version bigger than 5.0.3'); return false; } $exists = false; if (is_string($funcname) && strpos($funcname, '::') !== false) { $funcname = explode('::', $funcname); } if (is_array($funcname)) { if (count($funcname) < 2 || !is_string($funcname[0]) && !is_object($funcname[0])) { error_log('XML-RPC: syntax for function to be wrapped is wrong'); return false; } if (is_string($funcname[0])) { $plainfuncname = implode('::', $funcname); } elseif (is_object($funcname[0])) { $plainfuncname = get_class($funcname[0]) . '->' . $funcname[1]; } $exists = method_exists($funcname[0], $funcname[1]); if (!$exists && version_compare(phpversion(), '5.1') < 0) { // workaround for php 5.0: static class methods are not seen by method_exists $exists = is_callable($funcname); } } else { $plainfuncname = $funcname; $exists = function_exists($funcname); } if (!$exists) { error_log('XML-RPC: function to be wrapped is not defined: ' . $plainfuncname); return false; } else { // determine name of new php function if ($newfuncname == '') { if (is_array($funcname)) { if (is_string($funcname[0])) { $xmlrpcfuncname = "{$prefix}_" . implode('_', $funcname); } else { $xmlrpcfuncname = "{$prefix}_" . get_class($funcname[0]) . '_' . $funcname[1]; } } else { $xmlrpcfuncname = "{$prefix}_{$funcname}"; } } else { $xmlrpcfuncname = $newfuncname; } while ($buildit && function_exists($xmlrpcfuncname)) { $xmlrpcfuncname .= 'x'; } // start to introspect PHP code if (is_array($funcname)) { $func = new ReflectionMethod($funcname[0], $funcname[1]); if ($func->isPrivate()) { error_log('XML-RPC: method to be wrapped is private: ' . $plainfuncname); return false; } if ($func->isProtected()) { error_log('XML-RPC: method to be wrapped is protected: ' . $plainfuncname); return false; } if ($func->isConstructor()) { error_log('XML-RPC: method to be wrapped is the constructor: ' . $plainfuncname); return false; } // php 503 always says isdestructor = true... if (version_compare(phpversion(), '5.0.3') != 0 && $func->isDestructor()) { error_log('XML-RPC: method to be wrapped is the destructor: ' . $plainfuncname); return false; } if ($func->isAbstract()) { error_log('XML-RPC: method to be wrapped is abstract: ' . $plainfuncname); return false; } /// @todo add more checks for static vs. nonstatic? } else { $func = new ReflectionFunction($funcname); } if ($func->isInternal()) { // Note: from PHP 5.1.0 onward, we will possibly be able to use invokeargs // instead of getparameters to fully reflect internal php functions ? error_log('XML-RPC: function to be wrapped is internal: ' . $plainfuncname); return false; } // retrieve parameter names, types and description from javadoc comments // function description $desc = ''; // type of return val: by default 'any' $returns = $GLOBALS['xmlrpcValue']; // desc of return val $returnsDocs = ''; // type + name of function parameters $paramDocs = array(); $docs = $func->getDocComment(); if ($docs != '') { $docs = explode("\n", $docs); $i = 0; foreach ($docs as $doc) { $doc = trim($doc, " \r\t/*"); if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) { if ($desc) { $desc .= "\n"; } $desc .= $doc; } elseif (strpos($doc, '@param') === 0) { // syntax: @param type [$name] desc if (preg_match('/@param\\s+(\\S+)(\\s+\\$\\S+)?\\s+(.+)/', $doc, $matches)) { if (strpos($matches[1], '|')) { //$paramDocs[$i]['type'] = explode('|', $matches[1]); $paramDocs[$i]['type'] = 'mixed'; } else { $paramDocs[$i]['type'] = $matches[1]; } $paramDocs[$i]['name'] = trim($matches[2]); $paramDocs[$i]['doc'] = $matches[3]; } $i++; } elseif (strpos($doc, '@return') === 0) { // syntax: @return type desc //$returns = preg_split('/\s+/', $doc); if (preg_match('/@return\\s+(\\S+)\\s+(.+)/', $doc, $matches)) { $returns = php_2_xmlrpc_type($matches[1]); if (isset($matches[2])) { $returnsDocs = $matches[2]; } } } } } // execute introspection of actual function prototype $params = array(); $i = 0; foreach ($func->getParameters() as $paramobj) { $params[$i] = array(); $params[$i]['name'] = '$' . $paramobj->getName(); $params[$i]['isoptional'] = $paramobj->isOptional(); $i++; } // start building of PHP code to be eval'd $innercode = ''; $i = 0; $parsvariations = array(); $pars = array(); $pnum = count($params); foreach ($params as $param) { if (isset($paramDocs[$i]['name']) && $paramDocs[$i]['name'] && strtolower($paramDocs[$i]['name']) != strtolower($param['name'])) { // param name from phpdoc info does not match param definition! $paramDocs[$i]['type'] = 'mixed'; } if ($param['isoptional']) { // this particular parameter is optional. save as valid previous list of parameters $innercode .= "if (\$paramcount > {$i}) {\n"; $parsvariations[] = $pars; } $innercode .= "\$p{$i} = \$msg->getParam({$i});\n"; if ($decode_php_objects) { $innercode .= "if (\$p{$i}->kindOf() == 'scalar') \$p{$i} = \$p{$i}->scalarval(); else \$p{$i} = php_{$prefix}_decode(\$p{$i}, array('decode_php_objs'));\n"; } else { $innercode .= "if (\$p{$i}->kindOf() == 'scalar') \$p{$i} = \$p{$i}->scalarval(); else \$p{$i} = php_{$prefix}_decode(\$p{$i});\n"; } $pars[] = "\$p{$i}"; $i++; if ($param['isoptional']) { $innercode .= "}\n"; } if ($i == $pnum) { // last allowed parameters combination $parsvariations[] = $pars; } } $sigs = array(); $psigs = array(); if (count($parsvariations) == 0) { // only known good synopsis = no parameters $parsvariations[] = array(); $minpars = 0; } else { $minpars = count($parsvariations[0]); } if ($minpars) { // add to code the check for min params number // NB: this check needs to be done BEFORE decoding param values $innercode = "\$paramcount = \$msg->getNumParams();\n" . "if (\$paramcount < {$minpars}) return new {$prefix}resp(0, {$GLOBALS['xmlrpcerr']['incorrect_params']}, '{$GLOBALS['xmlrpcstr']['incorrect_params']}');\n" . $innercode; } else { $innercode = "\$paramcount = \$msg->getNumParams();\n" . $innercode; } $innercode .= "\$np = false;\n"; // since there are no closures in php, if we are given an object instance, // we store a pointer to it in a global var... if (is_array($funcname) && is_object($funcname[0])) { $GLOBALS['xmlrpcWPFObjHolder'][$xmlrpcfuncname] =& $funcname[0]; $innercode .= "\$obj =& \$GLOBALS['xmlrpcWPFObjHolder']['{$xmlrpcfuncname}'];\n"; $realfuncname = '$obj->' . $funcname[1]; } else { $realfuncname = $plainfuncname; } foreach ($parsvariations as $pars) { $innercode .= "if (\$paramcount == " . count($pars) . ") \$retval = {$catch_warnings}{$realfuncname}(" . implode(',', $pars) . "); else\n"; // build a 'generic' signature (only use an appropriate return type) $sig = array($returns); $psig = array($returnsDocs); for ($i = 0; $i < count($pars); $i++) { if (isset($paramDocs[$i]['type'])) { $sig[] = php_2_xmlrpc_type($paramDocs[$i]['type']); } else { $sig[] = $GLOBALS['xmlrpcValue']; } $psig[] = isset($paramDocs[$i]['doc']) ? $paramDocs[$i]['doc'] : ''; } $sigs[] = $sig; $psigs[] = $psig; } $innercode .= "\$np = true;\n"; $innercode .= "if (\$np) return new {$prefix}resp(0, {$GLOBALS['xmlrpcerr']['incorrect_params']}, '{$GLOBALS['xmlrpcstr']['incorrect_params']}'); else {\n"; //$innercode .= "if (\$_xmlrpcs_error_occurred) return new xmlrpcresp(0, $GLOBALS['xmlrpcerr']user, \$_xmlrpcs_error_occurred); else\n"; $innercode .= "if (is_a(\$retval, '{$prefix}resp')) return \$retval; else\n"; if ($returns == $GLOBALS['xmlrpcDateTime'] || $returns == $GLOBALS['xmlrpcBase64']) { $innercode .= "return new {$prefix}resp(new {$prefix}val(\$retval, '{$returns}'));"; } else { if ($encode_php_objects) { $innercode .= "return new {$prefix}resp(php_{$prefix}_encode(\$retval, array('encode_php_objs')));\n"; } else { $innercode .= "return new {$prefix}resp(php_{$prefix}_encode(\$retval));\n"; } } // shall we exclude functions returning by ref? // if($func->returnsReference()) // return false; $code = "function {$xmlrpcfuncname}(\$msg) {\n" . $innercode . "}\n}"; //print_r($code); if ($buildit) { $allOK = 0; eval($code . '$allOK=1;'); // alternative //$xmlrpcfuncname = create_function('$m', $innercode); if (!$allOK) { error_log('XML-RPC: could not create function ' . $xmlrpcfuncname . ' to wrap php function ' . $plainfuncname); return false; } } /// @todo examine if $paramDocs matches $parsvariations and build array for /// usage as method signature, plus put together a nice string for docs $ret = array('function' => $xmlrpcfuncname, 'signature' => $sigs, 'docstring' => $desc, 'signature_docs' => $psigs, 'source' => $code); return $ret; } }