Приглашаем посетить
Культурология (cult-lib.ru)

XML-RPC

Previous
Table of Contents
Next

XML-RPC

XML-RPC is the grandfather of XML-based RPC protocols. XML-RPC is most often encapsulated in an HTTP POST request and response, although as discussed briefly in Chapter 15, "Building a Distributed Environment," this is not a requirement. A simple XML-RPC request is an XML document that looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>system.load</methodName>
  <params>
  </params>
</methodCall>

This request is sent via a POST method to the XML-RPC server. The server then looks up and executes the specified method (in this case, system.load), and passes the specified parameters (in this case, no parameters are passed). The result is then passed back to the caller. The return value of this request is a string that contains the current machine load, taken from the result of the Unix shell command uptime. Here is sample output:

<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
  <params>
    <param>
      <value>
        <string>0.34</string>
      </value>
    </param>
  </params>
</methodResponse>

Of course you don't have to build and interpret these documents yourself. There are a number of different XML-RPC implementations for PHP. I generally prefer to use the PEAR XML-RPC classes because they are distributed with PHP itself. (They are used by the PEAR installer.) Thus, they have almost 100% deployment. Because of this, there is little reason to look elsewhere. An XML-RPC dialogue consists of two parts: the client request and the server response.

First let's talk about the client code. The client creates a request document, sends it to a server, and parses the response. The following code generates the request document shown earlier in this section and parses the resulting response:

require_once 'XML/RPC.php';

$client = new XML_RPC_Client('/xmlrpc.php', 'www.example.com');
$msg = new XML_RPC_Message('system.load');
$result = $client->send($msg);
if ($result->faultCode()) {
    print "Error\n";
}
print XML_RPC_decode($result->value());

You create a new XML_RPC_Client object, passing in the remote service URI and address.

Then an XML_RPC_Message is created, containing the name of the method to be called (in this case, system.load). Because no parameters are passed to this method, no additional data needs to be added to the message.

Next, the message is sent to the server via the send() method. The result is checked to see whether it is an error. If it is not an error, the value of the result is decoded from its XML format into a native PHP type and printed, using XML_RPC_decode().

You need the supporting functionality on the server side to receive the request, find and execute an appropriate callback, and return the response. Here is a sample implementation that handles the system.load method you requested in the client code:

require_once 'XML/RPC/Server.php';

function system_load()
{
  $uptime = `uptime`;
  if(preg_match("/load average: ([\d.]+)/", $uptime, $matches)) {
    return new XML_RPC_Response( new XML_RPC_Value($matches[1], 'string'));
  }
}

$dispatches = array('system.load' => array('function' => 'system_uptime'));
new XML_RPC_Server($dispatches, 1);

The PHP functions required to support the incoming requests are defined. You only need to deal with the system.load request, which is implemented through the function system_load(). system_load() runs the Unix command uptime and extracts the one-minute load average of the machine. Next, it serializes the extracted load into an XML_RPC_Value and wraps that in an XML_RPC_Response for return to the user.

Next, the callback function is registered in a dispatch map that instructs the server how to dispatch incoming requests to particular functions. You create a $dispatches array of functions that will be called. This is an array that maps XML-RPC method names to PHP function names. Finally, an XML_RPC_Server object is created, and the dispatch array $dispatches is passed to it. The second parameter, 1, indicates that it should immediately service a request, using the service() method (which is called internally).

service() looks at the raw HTTP POST data, parses it for an XML-RPC request, and then performs the dispatching. Because it relies on the PHP autoglobal $HTTP_RAW_POST_DATA, you need to make certain that you do not turn off always_populate_raw_post_data in your php.ini file.

Now, if you place the server code at www.example.com/xmlrpc.php and execute the client code from any machine, you should get back this:

> php system_load.php
0.34

or whatever your one-minute load average is.

Building a Server: Implementing the MetaWeblog API

The power of XML-RPC is that it provides a standardized method for communicating between services. This is especially useful when you do not control both ends of a service request. XML-RPC allows you to easily set up a well-defined way of interfacing with a service you provide. One example of this is Web log submission APIs.

There are many Web log systems available, and there are many tools for helping people organize and post entries to them. If there were no standardize procedures, every tool would have to support every Web log in order to be widely usable, or every Web log would need to support every tool. This sort of tangle of relationships would be impossible to scale.

Although the feature sets and implementations of Web logging systems vary considerably, it is possible to define a set of standard operations that are necessary to submit entries to a Web logging system. Then Web logs and tools only need to implement this interface to have tools be cross-compatible with all Web logging systems.

In contrast to the huge number of Web logging systems available, there are only three real Web log submission APIs in wide usage: the Blogger API, the MetaWeblog API, and the MovableType API (which is actually just an extension of the MetaWeblog API). All the Web log posting tools available speak one of these three protocols, so if you implement these APIs, your Web log will be able to interact with any tool out there. This is a tremendous asset for making a new blogging system easily adoptable.

Of course, you first need to have a Web logging system that can be targeted by one of the APIs. Building an entire Web log system is beyond the scope of this chapter, so instead of creating it from scratch, you can add an XML-RPC layer to the Serendipity Web logging system. The APIs in question handle posting, so they will likely interface with the following routines from Serendipity:

function serendipity_updertEntry($entry) {}
function serendipity_fetchEntry($key, $match) {}

serendipity_updertEntry() is a function that either updates an existing entry or inserts a new one, depending on whether id is passed into it. Its $enTRy parameter is an array that is a row gateway (a one-to-one correspondence of array elements to table columns) to the following database table:

CREATE TABLE serendipity_entries (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(200) DEFAULT NULL,
  timestamp INT(10) DEFAULT NULL,
  body TEXT,
  author VARCHAR(20) DEFAULT NULL,
  isdraft INT
);

serendipity_fetchEntry() fetches an entry from that table by matching the specified key/value pair.

The MetaWeblog API provides greater depth of features than the Blogger API, so that is the target of our implementation. The MetaWeblog API implements three main methods:

metaWeblog.newPost(blogid,username,password,item_struct,publish) returns string
metaWeblog.editPost(postid,username,password,item_struct,publish) returns true
metaWeblog.getPost(postid,username,password) returns item_struct

blogid is an identifier for the Web log you are targeting (which is useful if the system supports multiple separate Web logs). username and password are authentication criteria that identify the poster. publish is a flag that indicates whether the entry is a draft or should be published live.

item_struct is an array of data for the post.

Instead of implementing a new data format for entry data, Dave Winer, the author of the MetaWeblog spec, chose to use the item element definition from the Really Simple Syndication (RSS) 2.0 specification, available at http://blogs.law.harvard.edu/tech/rss. RSS is a standardized XML format developed for representing articles and journal entries. Its item node contains the following elements:

Element

Description

title

The title of the item

link

A URL that links to a formatted form of the item.

description

A summary of the item.

author

The name of the author of the item. In the RSS spec, this is specified to be an email address, although nicknames are more commonly used.

pubDate

The date the entry was published.


The specification also optionally allows for fields for links to comment threads, unique identifiers, and categories. In addition, many Web logs extend the RSS item definition to include a content:encoded element, which contains the full post, not just the post summary that is traditionally found in the RSS description element.

To implement the MetaWeblog API, you need to define functions to implement the three methods in question. First is the function to handle posting new entries:

function metaWeblog_newPost($message) {
  $username = $message->params[1]->getval();
  $password = $message->params[2]->getval();
  if(!serendipity_authenticate_author($username, $password)) {
    return new XML_RPC_Response('', 4, 'Authentication Failed');
  }
  $item_struct = $message->params[3]->getval();
  $publish = $message->params[4]->getval();
  $entry['title'] = $item_struct['title'];
  $entry['body'] = $item_struct['description'];
  $entry['author'] = $username;
  $entry['isdraft'] = ($publish == 0)?'true':'false';
  $id = serendipity_updertEntry($entry);
  return  new XML_RPC_Response( new XML_RPC_Value($id, 'string'));
}

metaWeblog_newPost() extracts the username and password parameters from the request and deserializes their XML representations into PHP types by using the getval() method. Then metaWeblog_newPost() authenticates the specified user. If the user fails to authenticate, metaWeblog_newPost() returns an empty XML_RPC_Response object, with an "Authentication Failed" error message.

If the authentication is successful, metaWeblog_newPost() reads in the item_struct parameter and deserializes it into the PHP array $item_struct, using getval(). An array $entry defining Serendipity's internal entry representation is constructed from $item_struct, and that is passed to serendipity_updertEntry().XML_RPC_Response, consisting of the ID of the new entry, is returned to the caller.

The back end for MetaWeblog.editPost is very similar to MetaWeblog.newPost. Here is the code:

function metaWeblog_editPost($message) {
  $postid = $message->params[0]->getval();
  $username = $message->params[1]->getval();
  $password = $message->params[2]->getval();
  if(!serendipity_authenticate_author($username, $password)) {
    return new XML_RPC_Response('', 4, 'Authentication Failed');
  }
  $item_struct = $message->params[3]->getval();
  $publish = $message->params[4]->getval();
  $entry['title']    = $item_struct['title'];
  $entry['body']     = $item_struct['description'];
  $entry['author']     = $username;
  $entry['id']       = $postid;
  $entry['isdraft'] = ($publish == 0)?'true':'false';
  $id = serendipity_updertEntry($entry);
  return new XML_RPC_Response( new XML_RPC_Value($id?true:false, 'boolean'));
}

The same authentication is performed, and $enTRy is constructed and updated. If serendipity_updertEntry returns $id, then it was successful, and the response is set to TRue; otherwise, the response is set to false.

The final function to implement is the callback for MetaWeblog.getPost. This uses serendipity_fetchEntry() to get the details of the post, and then it formats an XML response containing item_struct. Here is the implementation:

function metaWeblog_getPost($message) {
  $postid = $message->params[0]->getval();
  $username = $message->params[1]->getval();
  $password = $message->params[2]->getval();
  if(!serendipity_authenticate_author($username, $password)) {
    return new XML_RPC_Response('', 4, 'Authentication Failed');
  }
  $entry = serendipity_fetchEntry('id', $postid);

  $tmp = array(
    'pubDate' => new XML_RPC_Value(
       XML_RPC_iso8601_encode($entry['timestamp']), 'dateTime.iso8601'),
    'postid' => new XML_RPC_Value($postid, 'string'),
    'author' => new XML_RPC_Value($entry['author'], 'string'),
    'description' => new XML_RPC_Value($entry['body'], 'string'),
    'title' => new XML_RPC_Value($entry['title'],'string'),
    'link' => new XML_RPC_Value(serendipity_url($postid), 'string')
  );
  $entry = new XML_RPC_Value($tmp, 'struct');
  return  new XML_RPC_Response($entry);
}

Notice that after the entry is fetched, an array of all the data in item is prepared. XML_RPC_iso8601() takes care of formatting the Unix timestamp that Serendipity uses into the ISO 8601-compliant format that the RSS item needs. The resulting array is then serialized as a struct XML_RPC_Value. This is the standard way of building an XML-RPC struct type from PHP base types.

So far you have seen string, boolean, dateTime.iso8601, and struct identifiers, which can be passed as types into XML_RPC_Value. This is the complete list of possibilities:

Type

Description

i4/int

A 32-bit integer

boolean

A Boolean type

double

A floating-point number

string

A string

dateTime.iso8601

An ISO 8601-format timestamp

base64

A base 64-encoded string

struct

An associative array implementation

array

A nonassociative (indexed) array


structs and arrays can contain any type (including other struct and array elements) as their data. If no type is specified, string is used. While all PHP data can be represented as either a string, a struct, or an array, the other types are supported because remote applications written in other languages may require the data to be in a more specific type.

To register these functions, you create a dispatch, as follows:

$dispatches = array(
 metaWeblog.newPost' =>
                        array('function' => 'metaWeblog_newPost'),
                    'metaWeblog.editPost' =>
                        array('function' => 'metaWeblog_editPost'),
                    'metaWeblog.getPost' =>
                        array('function' => 'metaWeblog_getPost'));
$server = new XML_RPC_Server($dispatches,1);

Congratulations! Your software is now MetaWeblog API compatible!

Auto-Discovery of XML-RPC Services

It is nice for a consumer of XML-RPC services to be able to ask the server for details on all the services it provides. XML-RPC defines three standard, built-in methods for this introspection:

  • system.listMethods Returns an array of all methods implemented by the server (all callbacks registered in the dispatch map).

  • system.methodSignature Takes one parameterthe name of a methodand returns an array of possible signatures (prototypes) for the method.

  • system.methodHelp Takes a method name and returns a documentation string for the method.

Because PHP is a dynamic language and does not enforce the number or type of arguments passed to a function, the data to be returned by system.methodSignature must be specified by the user. Methods in XML-RPC can have varying parameters, so the return set is an array of all possible signatures. Each signature is itself an array; the array's first element is the return type of the method, and the remaining elements are the parameters of the method.

To provide this additional information, the server needs to augment its dispatch map to include the additional info, as shown here for the metaWeblog.newPost method:

$dispatches = array(
'metaWeblog.newPost' =>
  array('function'  => 'metaWeblog_newPost',
        'signature' =>  array(
                          array($GLOBALS['XML_RPC_String'],
                                $GLOBALS['XML_RPC_String'],
                                $GLOBALS['XML_RPC_String'],
                                $GLOBALS['XML_RPC_String'],
                                $GLOBALS['XML_RPC_Struct'],
                                $GLOBALS['XML_RPC_String']
                          )
                        ),
        'docstring' => 'Takes blogid, username, password, item_struct '.
           'publish_flag and returns the postid of the new entry'),
/* ... */
);

You can use these three methods combined to get a complete picture of what an XML-RPC server implements. Here is a script that lists the documentation and signatures for every method on a given XML-RPC server:

<?php
require_once 'XML/RPC.php';
if($argc != 2) {
  print "Must specify a url.\n";
  exit;
}
$url = parse_url($argv[1]);

$client = new XML_RPC_Client($url['path'], $url['host']);
$msg = new XML_RPC_Message('system.listMethods');
$result = $client->send($msg);
if ($result->faultCode()) {
    echo "Error\n";
}
$methods = XML_RPC_decode($result->value());
foreach($methods as $method) {
  $message = new XML_RPC_Message('system.methodSignature',
                                 array(new XML_RPC_Value($method)));
  $response = $client->send($message)->value();
  print "Method $method:\n";
  $docstring = XML_RPC_decode(
                 $client->send(
                   new XML_RPC_Message('system.methodHelp',
                                       array(new XML_RPC_Value($method))
                                      )
                 )->value()
               );
  if($docstring) {
    print "$docstring\n";
  }
  else {
    print "NO DOCSTRING\n";
  }
  $response = $client->send($message)->value();
  if($response->kindOf() == 'array') {
    $signatures = XML_RPC_decode($response);
    for($i = 0; $i < count($signatures); $i++) {
      $return = array_shift($signatures[$i]);
      $params = implode(", ", $signatures[$i]);
      print "Signature #$i: $return $method($params)\n";
    }
  } else {
    print "NO SIGNATURE\n";
  }
  print "\n";
}
?>

Running this against a Serendipity installation generates the following:

> xmlrpc-listmethods.php http://www.example.org/serendipity_xmlrpc.php

/* ... */

Method metaWeblog.newPost:
Takes blogid, username, password, item_struct, publish_flag
and returns the postid of the new entry
Signature #0: string metaWeblog.newPost(string, string, string, struct, string)

/* ... */

Method system.listMethods:
This method lists all the methods that the XML-RPC server knows
how to dispatch
Signature #0: array system.listMethods(string)
Signature #1: array system.listMethods()

Method system.methodHelp:
Returns help text if defined for the method passed, otherwise
returns an empty string
Signature #0: string system.methodHelp(string)

Method system.methodSignature:
Returns an array of known signatures (an array of arrays) for
the method name passed. If no signatures are known, returns a
none-array (test for type != array to detect missing signature)
Signature #0: array system.methodSignature(string)


Previous
Table of Contents
Next