Writings

PHP : Adding Some Structure

We now create a PHP object which will allow us to access our Drupal site from elsewhere. I’ve taken some short cuts (assuming https, predefining the services, etc.) to make this presentation more straight forward, but most of the design principles could apply to any set-up. The full code is attached, so you can follow along. Let’s break it into some understandable chunks:

  1. class Services_Drupal_Exception extends Exception
  2. {
  3. public $faultString;
  4. public $faultCode;
  5.  
  6. public function __construct($function, $faultString, $faultCode) {
  7. $this->faultString = $faultString;
  8. $this->faultCode = $faultCode;
  9. $message = ‘Failed request for ’.$function." fault: $faultString ($faultCode)";
  10. parent::__construct($message);
  11. }
  12. }

First, note that lines 7-18 create new exception class, “Services_Drupal_Exception” which will be raised whenever there is a problem. Creating a unique exception class allows us to easily catch exceptions arising from the xmlrpc library rather than other chunks of our source.

  1. // Connection Information
  2. protected $protocol = ‘https’; // SSL because we will be logging in
  3. protected $server;
  4. private $timeout; // Useful for long operations as default timeout is about a minute
  5.  
  6. // API Information
  7. private $domain;
  8. private $apikey;
  9. private $session_id;

Lines 22-30 include member fields which describe the configuration of the xmlrpc connection, including the url components for the service, the api key, and a timeout amount.

  1. public static function factory($username, $password, $server, $apikey, $domain)
  2. {
  3. return new Services_Drupal($username, $password, $server, $apikey, $domain);
  4. }

Lines 32-35 describe the “factory” static method, which is useful but uninteresting.

  1. public function __construct($username, $password, $server, $apikey, $domain, $timeout = null)
  2. {
  3. if (!(function_exists(‘xmlrpc_encode_request’))){
  4. trigger_error(‘This class requires php5-xmlrpc’);
  5. }
  6. $this->server = $server;
  7. $this->timeout = $timeout;
  8. $this->connect();
  9. $this->apikey = $apikey;
  10. $this->domain = $domain;
  11. $this->login($username, $password);
  12. }

Lines 36-47, the Services_Drupal constructor, follow the basic pattern mentioned before. They start by making a connection to the server via the connect method. At this point, the service receiver has the same privileges as an anonymous user, and will access/edit content as that user. Anonymous users miss out on all the fun, however, so we quickly call the login method, which will allow the receiver to have the privileges of a particular, authenticated user.

  1. private function connect()
  2. {
  3. $response = $this->doRequest('system.connect', array());
  4. // This is not the final session_id, but a temporary id until we log in
  5. $this->session_id = $response['sessid'];
  6. }

Let’s take a look at lines 73-78, the connectmethod next. First, it calls a common method, doRequest, which calls the specified remote method (here, “system.connect”) with the given parameters (here, an empty array). We’ll discuss doRequest in a moment, but first, notice that this function will return a connection object, with the most important bit being the ‘sessid’ (session id) field. This field (or another session id) will be used for all future operations so that the Drupal site can log user access. The connect method, like the node_get (lines 92-95) and matching_by_fields (lines 106-109) methods, serves more or less as a passthrough to the doRequest method.

  1. private function login($username, $password)
  2. {
  3. $timestamp = (string)time();
  4. $nonce = (string)rand();
  5. $hash = hash_hmac('sha256', $timestamp . ';' . $this->domain . ';' . $nonce . ';' . 'user.login',
  6. $this->apikey);
  7. $response = $this->doRequest('user.login', array($hash, $this->domain, $timestamp, $nonce,
  8. $this->session_id, $username, $password));
  9. $this->session_id = $response['sessid'];
  10. }

Lines 79-88, the login method, also call doRequest, but perform some fancy footwork before they do. login, like node_save is a method which requires extra security and identity verification. To provide this extra security, it sends a hashed string which contains the current time, the domain of access (as seen on the API key), a special value called a nonce, the method name, and the API key. The nonce is an arbitrary, (usually) generated string which can be discarded after use. At this point, your hash includes 5 pieces of data (the timestamp, domain, nonce, method, and api key); you are sending this hash with four of the other pieces of data (everything but the api). Since Drupal is aware of all of the different API keys, it can deduce your API key without you sending it over the wire, preventing anyone else from discovering your key. Notice that, as it can alter/destroy data, the node_save method requires a similar hash as additional verification that you are who you claim to be.

  1. public function doRequest($function, array $arguments)
  2. {
  3. $request = xmlrpc_encode_request($function, $arguments);
  4. $stream_options = array('http' => array(
  5. 'method' => "POST",
  6. 'header' => "Content-Type: text/xml",
  7. 'content' => $request
  8. ));
  9. if ($this->timeout) {
  10. $stream_options['http']['timeout'] = $this->timeout;
  11. }
  12. $context = stream_context_create($stream_options);
  13. $file = file_get_contents($this->getUrl(), false, $context);
  14. $response = xmlrpc_decode($file);
  15.  
  16. if (is_array($response) && xmlrpc_is_fault($response)) {
  17. throw new Services_Drupal_Exception($function, $response["faultString"], $response["faultCode"]);
  18. }
  19.  
  20. return $response;
  21. }

Finally, let’s look at the doRequest method, which performs all of your encoding/decoding needs. We rely on php’s native xmlrpc library (see lines 38-40) to do most of the work. We use its xmlrpc_encode_request function to format the service call and it’s parameters into something which can be sent over xmlrpc. We then create a stream wrapper with a bit of context to mimic POSTing the data to the Drupal site; you can do the same with curl, sockets, or some other library if you feel more comfortable. We then get back the response page (here, using file_get_contents), which we decode. It’s possible that there was some fault with our xmlrpc process (such as using the wrong parameters or not having proper access), which we account for by checking xmlrpc_is_fault and raising an exception if so.

Now, how can we use this class to facilitate access?

<?php
include(‘Services_Drupal.php’);
$s = new Services_Drupal(‘xmlrpc’, ‘XM1p4$$’, ‘localhost’, ‘bbcb58febc34fa9a69888b6e9aaaed0c’, ‘example’);
$node = $s->node_get(1);

Assuming everything goes okay, $node will now contain all of the data as if you had performed a node_load within drupal. Note that xmlrpc is the user with password XM1p4$$, accessing the server at localhost using the API key bbcb58febc34fa9a69888b6e9aaaed0c and the domain example.

© C.M. Lubinski 2008-2021