Skip to content

Instantly share code, notes, and snippets.

@hakre
Created February 28, 2012 21:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hakre/1935355 to your computer and use it in GitHub Desktop.
Save hakre/1935355 to your computer and use it in GitHub Desktop.
XMLRPC Discovery Service
<?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