Created
February 28, 2012 21:46
-
-
Save hakre/1935355 to your computer and use it in GitHub Desktop.
XMLRPC Discovery Service
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* XMLRPC Discovery and XMLRPC Introspection Service | |
* | |
* <http://xmlrpc-c.sourceforge.net/introspection.html> | |
* | |
* Uses the PHP XML-RPC extension <http://php.net/manual/en/book.xmlrpc.php> | |
* | |
* Has an adapter interface to other libraries as well, a working example implementation | |
* exists for XML-RPC for PHP <http://phpxmlrpc.sourceforge.net/doc-2/> | |
* | |
* Can use system.multicall <http://web.archive.org/web/20060708115328/http://www.xmlrpc.com/discuss/msgReader$1208> | |
* | |
* Scroll to end of file for usage example. | |
* | |
* @author hakre <http://hakre.wordpress.com> | |
* @link http://stackoverflow.com/questions/9485085/xmlrpc-showing-32601-error-using-php/9485163 | |
*/ | |
namespace XMLRPCIntrospection; | |
// Dependencies for XML-RPC for PHP <http://phpxmlrpc.sourceforge.net/doc-2/> | |
// | |
// use SendMessageLibXMLRPCForPHP instead of SendMessagePHP: | |
// | |
// new Discovery($path, 'XMLRPCIntrospection\SendMessageLibXMLRPCForPHP'); | |
// | |
//include("lib/xmlrpc.php"); # note: original file-name is xmlrpc.inc | |
//use xmlrpc_client, xmlrpcval, xmlrpcmsg, xmlrpcresp; | |
use ArrayIterator; | |
use Countable; | |
use Iterator; | |
use IteratorIterator; | |
use RuntimeException; | |
use InvalidArgumentException; | |
use UnexpectedValueException; | |
/** | |
* XML-PRC sendMessage Implementation | |
*/ | |
Interface IXMLRPCSendMessage | |
{ | |
/** | |
* @abstract | |
* @param string $method | |
* @param array $parameters | |
* @return mixed | |
*/ | |
public function sendMessage($method, array $parameters = array()); | |
} | |
/** | |
* SendMessage implementation for PHP XML-RPC extension <http://php.net/manual/en/book.xmlrpc.php> | |
* | |
* @todo add base class with error handler function / debug flag | |
*/ | |
class SendMessagePHP implements IXMLRPCSendMessage | |
{ | |
/** | |
* @var string | |
*/ | |
private $path; | |
public function __construct($path) | |
{ | |
$this->path = (string)$path; | |
} | |
/** | |
* @param string $method | |
* @param array $parameters | |
* @return mixed | |
*/ | |
public function sendMessage($method, array $parameters = array()) | |
{ | |
$encoding = 'utf-8'; // default: iso-8859-1 | |
$request = xmlrpc_encode_request($method, $parameters, array('encoding' => $encoding, 'verbosity' => 'no_white_space')); | |
$context = stream_context_create(array('http' => array( | |
'method' => "POST", | |
'header' => "Content-Type: text/xml", | |
'ignore_errors' => TRUE, | |
'content' => $request, | |
))); | |
$file = file_get_contents($this->path, FALSE, $context); | |
$code = FALSE; | |
if (!empty($http_response_header)) { | |
sscanf($http_response_header[0], 'HTTP/%*d.%*d %d', $code); | |
} | |
$debug = 0; | |
if ($debug) { | |
echo "\nDEBUG ($method) Status #$code:\n", var_dump($parameters), "\nRequest:\n", var_dump($request), "\nResponse:\n", var_dump($file), "\n\n"; | |
} | |
$response = xmlrpc_decode($file, $encoding); | |
if ($response && is_array($response) && xmlrpc_is_fault($response)) { | |
$error = sprintf("Failure: Fault on sending '%s': (%d) %s." | |
, $method, $response['faultCode'], $response['faultString']); | |
trigger_error($error, E_USER_WARNING); | |
return NULL; | |
} | |
return $response; | |
} | |
} | |
/** | |
* SendMessage implementation for XML-RPC for PHP <http://phpxmlrpc.sourceforge.net/doc-2/> | |
*/ | |
class SendMessageLibXMLRPCForPHP implements IXMLRPCSendMessage | |
{ | |
/** | |
* @var xmlrpc_client | |
*/ | |
private $client; | |
/** | |
* @param string|xmlrpc_client $path | |
* @param string $server (optional) | |
* @param int|string $port (optional) | |
* @param string $method (optional) | |
*/ | |
public function __construct($path, $server = '', $port = '', $method = '') | |
{ | |
if ($path instanceof xmlrpc_client) { | |
$this->client = $path; | |
} else { | |
$this->client = new xmlrpc_client($path, $server, $port, $method); | |
} | |
} | |
/** | |
* send message via client | |
* | |
* @param string $method | |
* @param array $parameters | |
* @return mixed | |
*/ | |
public function sendMessage($method, array $parameters = array()) | |
{ | |
$client = $this->client; | |
$parameters && $parameters = $this->overloadParameters($parameters); | |
$message = new xmlrpcmsg($method, $parameters); | |
$client->return_type = 'phpvals'; | |
$debug = TRUE; | |
if ($debug) { | |
$client->debug = 1; | |
$message->createPayload(); | |
echo "DEBUG:\nRequest:\n", $message->payload, "\n"; | |
ob_start(); | |
} | |
$response = $client->send($message); | |
if ($debug) { | |
$buffer = ob_get_clean(); | |
$buffer = htmlspecialchars_decode($buffer); | |
echo $buffer; | |
} | |
if ($response->faultCode()) { | |
$error = sprintf("Failure: Fault on sending '%s': (%d) %s." | |
, $method, $response->faultCode(), $response->faultString()); | |
trigger_error($error, E_USER_WARNING); | |
return NULL; | |
} | |
return $response->value(); | |
} | |
/** | |
* @param mixed $parameter | |
* @param string $type (optional) | |
* @return xmlrpcval | |
*/ | |
private function overloadParameter($parameter, $type = NULL) | |
{ | |
if ($parameter instanceof xmlrpcval) { | |
return $parameter; | |
} | |
if (NULL !== $type) { | |
return new xmlrpcval($parameter, $type); | |
} | |
if (is_string($parameter)) { | |
return new xmlrpcval($parameter, 'string'); | |
} | |
if (is_array($parameter)) { | |
if (array_keys($parameter) === range(0, count($parameter) - 1)) { | |
$type = 'array'; | |
} else { | |
$type = 'struct'; | |
} | |
foreach ($parameter as $index => $value) { | |
$parameter[$index] = $this->overloadParameter($value); | |
} | |
return new xmlrpcval($parameter, $type); | |
} | |
throw new RuntimeException(sprintf('Unable to overload parameter %s.', print_r($parameter, 1))); | |
} | |
/** | |
* @param array $parameters | |
* @return array | |
*/ | |
private function overloadParameters(array $parameters) | |
{ | |
$overloaded = array(); | |
foreach ($parameters as $parameter) { | |
$overloaded[] = $this->overloadParameter($parameter); | |
} | |
return $overloaded; | |
} | |
} | |
Interface IDiscovery | |
{ | |
/** | |
* @param string $name | |
* @return bool | |
*/ | |
public function hasCapability($name); | |
/** | |
* @param string $name | |
* @return ICapability | |
*/ | |
public function getCapability($name); | |
/** | |
* @return ICapability[] | |
*/ | |
public function getCapabilities(); | |
/** | |
* @param string $name | |
* @return boolean | |
*/ | |
public function hasMethod($name); | |
/** | |
* @param string $name | |
* @return IMethod | |
*/ | |
public function getMethod($name); | |
/** | |
* @return IMethod[] | |
*/ | |
public function getMethods(); | |
} | |
interface ICapability | |
{ | |
/** | |
* @return string | |
*/ | |
public function getName(); | |
/** | |
* @return int | |
*/ | |
public function getVersion(); | |
/** | |
* @return string | |
*/ | |
public function getUrl(); | |
} | |
interface IMethod | |
{ | |
/** | |
* @return string | |
*/ | |
public function getName(); | |
/** | |
* @return string | |
*/ | |
public function getHelp(); | |
/** | |
* @return string | |
*/ | |
public function getReturnType(); | |
/** | |
* @return array 0=> firstParameterType, ... | |
*/ | |
public function getParameters(); | |
} | |
interface ISelfAsString | |
{ | |
/** | |
* @return string | |
*/ | |
public function __toString(); | |
} | |
/** | |
* XMLRPC Discovery Service Client Class | |
* | |
* Third Party Dependencies: | |
* | |
* @uses xmlrpc_client XMLRPC Client class | |
* @uses xmlrpcmsg XMLRPC Message class | |
* @uses xmlrpcval XMLRPC Value class | |
*/ | |
class Discovery implements IDiscovery | |
{ | |
const METHOD_CAPABILITIES = 'system.getCapabilities'; | |
const METHOD_LIST = 'system.listMethods'; | |
const METHOD_HELP = 'system.methodHelp'; | |
const METHOD_SIGNATURE = 'system.methodSignature'; | |
/** | |
* @var array | |
*/ | |
private $registry; | |
/** | |
* @var IXMLRPCSendMessage | |
*/ | |
private $sender; | |
/** | |
* @param string|IXMLRPCSendMessage $path | |
* @param string|null $impl | |
*/ | |
public function __construct($path, $impl = NULL) | |
{ | |
if ($path instanceof IXMLRPCSendMessage) { | |
$this->sender = $path; | |
return; | |
} | |
if ($impl === NULL) { | |
$this->sender = new SendMessagePHP($path); | |
return; | |
} | |
if (!is_subclass_of($impl, 'XMLRPCIntrospection\IXMLRPCSendMessage')) { | |
throw new InvalidArgumentException(sprintf('%s not a IXMLRPCSendMessage', $impl)); | |
} | |
$this->sender = new $impl($path); | |
} | |
/** | |
* eager loading | |
* | |
* (system.multicall test) | |
*/ | |
public function loadAll() | |
{ | |
$methods = $this->getMethodNames(); | |
$calls = array(); | |
foreach ($methods as $method) { | |
$calls[] = array('methodName' => self::METHOD_HELP, 'params' => array($method)); | |
$calls[] = array('methodName' => self::METHOD_SIGNATURE, 'params' => array($method)); | |
} | |
if (!$result = $this->sender->sendMessage('system.multicall', $calls)) { | |
return; | |
} | |
foreach ($methods as $method) { | |
list(, $return) = each($result); | |
if (!isset($return[0]) || !is_string($return[0])) { | |
// throw new UnexpectedValueException(sprintf('%s', print_r($return, 1))); | |
// Array | |
// ( | |
// [faultCode] => -32601 | |
// [faultString] => server error. requested method system.methodHelp does not exist. | |
// ) | |
$return[0] = NULL; | |
} | |
$this->registry['help'][$method] = $return[0]; | |
list(, $return) = each($result); | |
if (!isset($return[0]) || !is_array($return[0])) { | |
// throw new UnexpectedValueException(sprintf('%s', print_r($return, 1))); | |
$return[0] = NULL; | |
} | |
$this->registry['signature'][$method] = $return[0]; | |
} | |
} | |
/** | |
* @param string $name | |
* @return bool | |
*/ | |
public function hasCapability($name) | |
{ | |
$this->getCapabilitiesArray(); | |
} | |
/** | |
* @return Capabilities | |
*/ | |
public function getCapabilities() | |
{ | |
$capabilityNames = array_keys($this->getCapabilitiesArray()); | |
return new Capabilities($this, $capabilityNames); | |
} | |
/** | |
* @return array capability => array (specUrl =>, specVersion =>), ... | |
*/ | |
private function getCapabilitiesArray() | |
{ | |
if (isset($this->registry['capabilities'])) { | |
return $this->registry['capabilities']; | |
} | |
$list = $this->sender->sendMessage(self::METHOD_CAPABILITIES); | |
if (!is_array($list) && $list !== NULL) { | |
throw new RuntimeException('Unexpected return type.'); | |
} | |
$list || $list = array(); | |
$this->registry['capabilities'] = $list; | |
return $list; | |
} | |
/** | |
* @param string $name | |
* @return array (specUrl =>, specVersion =>) | |
*/ | |
public function getCapabilityArray($name) | |
{ | |
$capabilities = $this->getCapabilitiesArray(); | |
$capability = isset($capabilities[$name]) ? $capabilities[$name] : array(); | |
return $capability; | |
} | |
/** | |
* @param string $name | |
* @return bool | |
*/ | |
public function hasMethod($name) | |
{ | |
$methods = array_flip($this->getMethodNames()); | |
return isset($methods[$name]); | |
} | |
/** | |
* Does the endpoint provide additional information like | |
* method signatures or help? | |
* | |
* @return bool | |
*/ | |
public function hasMoreInfo() | |
{ | |
return $this->hasMethod(self::METHOD_HELP) || $this->hasMethod(self::METHOD_SIGNATURE); | |
} | |
/** | |
* @return Method[] | |
*/ | |
public function getMethods() | |
{ | |
return new Methods($this, $this->getMethodNames()); | |
} | |
/** | |
* get method names from server | |
* | |
* @return array | |
*/ | |
public function getMethodNames() | |
{ | |
if (isset($this->registry['methods'])) { | |
return $this->registry['methods']; | |
} | |
$list = $this->sender->sendMessage(self::METHOD_LIST); | |
if (is_array($list)) { | |
sort($list); | |
} else { | |
$list = array(); | |
} | |
$this->registry['methods'] = $list; | |
return $list; | |
} | |
/** | |
* @param $method | |
* @return string | |
*/ | |
public function getMethodHelp($method) | |
{ | |
return (string)$this->sendRegisteredMethodMessage($method, 'help', self::METHOD_HELP); | |
} | |
/** | |
* @param $method | |
* @return array 0 => returnType, 1=> firstParameterType, ... | |
*/ | |
public function getMethodSignature($method) | |
{ | |
$signature = $this->sendRegisteredMethodMessage($method, 'signature', self::METHOD_SIGNATURE); | |
if (!is_array($signature)) { | |
$signature = array('<unknown>', '<unknown>'); | |
} else { | |
list($signature) = $signature; | |
} | |
return $signature; | |
} | |
/** | |
* @param string $introspectMethod | |
* @param string $register | |
* @param string $method | |
* @return mixed | |
*/ | |
private function sendRegisteredMethodMessage($introspectMethod, $register, $method) | |
{ | |
$introspectMethod = (string)$introspectMethod; | |
if (isset($this->registry[$register][$introspectMethod])) { | |
return $this->registry[$register][$introspectMethod]; | |
} | |
return $this->registry[$register][$introspectMethod] = $this->sender->sendMessage($method, array($introspectMethod)); | |
} | |
/** | |
* @param string $name | |
* @return ICapability | |
*/ | |
public function getCapability($name) | |
{ | |
return new Capability($this, (string)$name); | |
} | |
/** | |
* @param string $name | |
* @return IMethod | |
*/ | |
public function getMethod($name) | |
{ | |
return new Method($this, (string)$name); | |
} | |
} | |
abstract class DiscoveryNamedList extends IteratorIterator implements Countable, ISelfAsString | |
{ | |
private $discovery; | |
/** | |
* @var ArrayIterator | |
*/ | |
private $names; | |
public function __construct(Discovery $discovery, Array $names) | |
{ | |
$names = array_map('strval', $names); // provoke fatal error if applicable | |
$names = new ArrayIterator($names); | |
$this->discovery = $discovery; | |
$this->names = $names; | |
parent::__construct($names); | |
} | |
/** | |
* @abstract | |
* @param $name string | |
*/ | |
abstract public function getByName($name); | |
public function current() | |
{ | |
return $this->getByName($this->getInnerIterator()->current()); | |
} | |
/** | |
* @return Discovery | |
*/ | |
public function getDiscovery() | |
{ | |
return $this->discovery; | |
} | |
public function valid() | |
{ | |
return $this->getInnerIterator()->valid(); | |
} | |
/** | |
* @return int | |
*/ | |
public function count() | |
{ | |
return $this->names->count(); | |
} | |
/** | |
* @return string | |
*/ | |
public function __toString() | |
{ | |
return sprintf('%s (%d)', basename(get_called_class()), $this->count()); | |
} | |
} | |
/** | |
* Discovery Methods | |
*/ | |
class Methods extends DiscoveryNamedList | |
{ | |
/** | |
* @return Method | |
*/ | |
public function current() | |
{ | |
return $this->getByName($this->getInnerIterator()->current()); | |
} | |
/** | |
* @param $name string | |
* @return Method | |
*/ | |
public function getByName($name) | |
{ | |
return new Method($this->getDiscovery(), (string)$name); | |
} | |
} | |
/** | |
* XMLRPC Method | |
*/ | |
class Method implements IMethod, ISelfAsString | |
{ | |
/** | |
* @var Discovery | |
*/ | |
private $discovery; | |
/** | |
* @var string | |
*/ | |
private $name; | |
/** | |
* @param Discovery $discovery | |
* @param string $name | |
*/ | |
public function __construct(Discovery $discovery, $name) | |
{ | |
$this->discovery = $discovery; | |
$this->name = (string)$name; | |
} | |
/** | |
* @return string | |
*/ | |
public function getName() | |
{ | |
return $this->name; | |
} | |
/** | |
* @return string | |
*/ | |
public function getHelp() | |
{ | |
return $this->discovery->getMethodHelp($this->name); | |
} | |
/** | |
* @return array 0 => returnType, 1=> firstParameterType, ... | |
*/ | |
private function getSignature() | |
{ | |
return $this->discovery->getMethodSignature($this->name); | |
} | |
/** | |
* @return string | |
*/ | |
public function __toString() | |
{ | |
if ($params = $this->getParameters()) { | |
$params = '<' . implode('>, <', $params) . '>'; | |
} else { | |
$params = ''; | |
} | |
return sprintf('<%s> %s (%s)', $this->getReturnType(), $this->getName(), $params); | |
} | |
/** | |
* @return string | |
*/ | |
public function getReturnType() | |
{ | |
if ($return = $this->getSignature()) { | |
list($return) = $return; | |
} | |
return (string)$return; | |
} | |
/** | |
* @return array 0 => parameterType, ... | |
*/ | |
public function getParameters() | |
{ | |
$full = $this->getSignature(); | |
$full && array_shift($full); | |
return array_values($full); | |
} | |
} | |
class Capability implements ICapability, ISelfAsString | |
{ | |
const SPEC_URL = 'specUrl'; | |
const SPEC_VERSION = 'specVersion'; | |
/** | |
* @var Discovery | |
*/ | |
private $discovery; | |
/** | |
* @var string | |
*/ | |
private $name; | |
/** | |
* @param Discovery $discovery | |
* @param string $name | |
*/ | |
public function __construct(Discovery $discovery, $name) | |
{ | |
$this->discovery = $discovery; | |
$this->name = (string)$name; | |
} | |
/** | |
* @return string | |
*/ | |
public function getName() | |
{ | |
return $this->name; | |
} | |
/** | |
* @param $key | |
* @return string | |
*/ | |
private function getByKey($key) | |
{ | |
$capability = $this->discovery->getCapabilityArray($this->name); | |
return isset($capability[$key]) ? $capability[$key] : ''; | |
} | |
/** | |
* @return int | |
*/ | |
public function getVersion() | |
{ | |
return $this->getByKey(self::SPEC_VERSION); | |
} | |
/** | |
* @return string | |
*/ | |
public function getUrl() | |
{ | |
return $this->getByKey(self::SPEC_URL); | |
} | |
/** | |
* @return string | |
*/ | |
public function __toString() | |
{ | |
return sprintf('%s (Version %d) <%s>', $this->name, $this->getVersion(), $this->getUrl()); | |
} | |
} | |
class Capabilities extends DiscoveryNamedList | |
{ | |
/** | |
* @return Capability | |
*/ | |
public function current() | |
{ | |
return $this->getByName($this->getInnerIterator()->current()); | |
} | |
/** | |
* @param $name string | |
* @return Capability | |
*/ | |
public function getByName($name) | |
{ | |
return new Capability($this->getDiscovery(), (string)$name); | |
} | |
} | |
### | |
### Usage Example: | |
### | |
$path = 'http://xmlrpc-c.sourceforge.net/api/sample.php'; | |
$path = 'http://hakre.wordpress.com/xmlrpc.php/'; | |
printf("\n XMLRPC Service Discovery\n\n For <%s>\n\n", $path); | |
$discovery = new Discovery($path); | |
printf(" Capabilities:\n =============\n"); | |
$capabilities = $discovery->getCapabilities(); | |
foreach ($capabilities as $i => $capability) | |
{ | |
printf(" %'.-2d %s\n", $i + 1, $capability); | |
} | |
if (!$capabilities->count()) { | |
print " No capability details are available.\n"; | |
} | |
$discovery->loadAll(); // eager loading via system.multicall - experimental | |
$methods = $discovery->getMethods(); | |
printf("\n %s:\n %s\n", $methods, str_repeat('=', strlen($methods) + 1)); | |
foreach ($methods as $i => $method) | |
{ | |
printf(" %'.-3d %s\n", $i + 1, $method->getName()); | |
} | |
printf("\n Method Details (%d):\n ===================\n", count($methods)); | |
if ($discovery->hasMoreInfo()) { | |
foreach ($methods as $i => $method) | |
{ | |
printf(" %'.-3d %s\n", $i + 1, $method->getName()); | |
printf("\n %s\n", $method); | |
printf("\n%s\n\n", preg_replace('/^/um', ' ', wordwrap($method->getHelp(), 46))); | |
} | |
} else { | |
print(" No method details are available.\n\n"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment