Drupal Services: API key authentication using XML-RPC

I recently started building a site that allows authorized users to remotely post stories using the Drupal Services module.

While there are a number of examples, the were all sufficiently abstract so that for someone new to the Drupal Services module, it wasn't easy to build a basic service call.

I put together this example to make it a little easier for the next guy/gal. (It can also be found int he Services hand book page on drupal.org http://drupal.org/node/819826)

This is some sample code from an "API" we're using to allow remote non Drupal, PHP client applications to utilize services to add stories to our drupal 6.x site.

The "API" isn't really an API but more of an example of how to interact with our services, with the helper functions abstracted into a file called api.inc. The goal being that by clients including our libraries, they can have simple, abstracted functions to drop into their remote applications.

We utilized several drupal helper functions for making the xmlrpc requests so we've added bootstrap.inc, common.inc and xmlrpc.inc into an includes folder we distribute with the API.

Obviously there are more elegant ways to handle this but this method provides an efficient (in terms of programming time) method for getting some working examples together for our clients to use, both inside and outside Drupal sites.

Attached file, services_api.tar.gz, contains the example files to get you started, or view the code printed below for reference.

NOTE: Due to the no follow tags auto added by drupal.org to some of the documentation, verify actual usage for urls in the attached files.

*** README.TXT ***


File API Contents

getNodeTest.php - a simple testing file to test retrieving a node via the API.
saveNodeTest.php - a simple testing file to test retrieving a node via the API.
api.inc - actual api

includes - folder of included libraries
- boostrap.inc
- common.inc
- xmlrpc.inc

Test: from command line "php getNodeTest.php" or "php saveNodeTest.php" to run
a simple test to make sure everything works as expected.
getNodeTest.php will return the contents of a node (nid value hard coded).
saveNodeTest.php will programatically create a node with location (values hard coded).

Usage:

Place the api contents in a directory reachable by your program.

Include api.inc in your program.

require_once 'path/to/api/api.inc';

*** api.inc ***

// Author: David Hazel dave@hazelconsulting.com 6/1/2010

require_once './includes/bootstrap.inc';
require_once './includes/common.inc';

/**
* Submits a new story to api.example.com.
*
* Examples:
* @code
* $domain = 'api.example.com';
* $api_key = '7e0246f3dd05ab2cde860a46c06ffc16';
* $url = $domain;
* date_default_timezone_set('America/Los_Angeles');
* $title = ' title + ' . date("F j, Y, g:i a");
* $body = ' body text + ' . date("F j, Y, g:i a");
* $location_long_lat = array(
* 'longitude' => '37.533',
* 'latitude' => '77.467'
* );
* $location_addr = array(
* 'street' => '100 North 17th St',
* 'city' => 'richmond',
* 'province' => 'virginia',
* 'postal_code' => '23219',
* );
* echo submitStory($url, 'long/lat location test ' . $title, $body, $location_long_lat, $domain, $api_key);
* @endcode
*
* @param string $url
* The root url of the service we are calling.
* Should NOT include trailing slash. Should include http://.
* We attempt to clean this up in the cleanUrl($url) function
* @param string $title
* The title of the story. Title field in database is varchar(255)
* but it should probably be a shorter string than that.
* @param string $body
* The Body of the story is a text field.
* @param struct $location
* $location is an array containing location paramaters. Location can be
* defined using either longitude/latitude or address.
* If address is provided, system will attempted to validate and
* calculate long/lat.
* - $location = array(
* 'longitude' => '37.533',
* 'latitude' => '77.467',
* );
* - $location = array(
* 'street' => '100 North 17th St',
* 'city' => 'richmond',
* 'province' => 'virginia',
* 'postal_code' => '23219',
* );
*
* @return mixed
* Returns the results of the xmlrpc Request to the service. This could
* be the nid of the story node we just created, or any errors from the
* xmlrpc call.
* Typical errors:
* - Invalid API Key. Usually the $domain value supplied is not correct
* for the $api_key supplied.
* - Access Denied. The $api_key supplied is not authorized to perform
* the service call requested.
* - Missing Schema. The node object constructed and passed to the
* service was not valid.
*/
function submitStory($url, $title, $body, $location, $domain, $api_key){
$node = buildNode($title, $body, $location);
return xmlrpcRequest(cleanUrl($url), 'node.save', $node, $domain, $api_key);
}

/**
* Retrieve a story from the api.example.com.
*
* Examples:
* @code
* $domain = 'api.example.com';
* $api_key = '7e0246f3dd05ab2cde860a46c06ffc16';
* $url = $domain;
* $nid = '3298';
* echo getStory($url, $nid, $domain, $api_key);
* @endcode
*
* @param string $url
* The root url of the service we are calling.
* Should NOT include trailing slash. Should include http://.
* We attempt to clean this up in the cleanUrl($url) function
* @param mixed $nid
* The NID or node id/identifier of the story we want to retreive.
*
* @return mixed
* Returns the results of the xmlrpc Request to the service. This could
* be the node struct of the story we requested, or any errors from the
* xmlrpc call.
* Typical errors:
* Invalid API Key. Usually the $domain value supplied is not correct
* for the $api_key supplied.
* Access Denied. The $api_key supplied is not authorized to perform
* the service call requested.
*/
function getStory($url, $nid, $domain, $api_key){
return xmlrpcRequest(cleanUrl($url), 'node.get', $nid, $domain, $api_key);
}

/**
* Do some basic checking of the $url we are going to submit.
*
* @param string $url
* The root url of the service we are calling.
* Should NOT include trailing slash. Should include http://.
* We attempt to clean this up in the cleanUrl($url) function
*
* @return string
* Returns the cleaned $url.
*/
function cleanUrl($url){

// Check if $url contains trailing slash. If it does, remove it.
$last = substr($url, -1);;
if($last == '/'){
$url = substr($url, 0, $url -1);
}
// Check if $url has prepended http://. If it does NOT add it.
if(substr_count($url, 'http://') != 1){
$url = 'http://' . $url;
}
return $url;
}

/**
* Builds the story node for sending to api.example.com
*
* There are other values that can be added to a story node but
* the buildNode function provides the minimal set for a story with a location.
*
* Location can be provided via full address or long/lat values. When a full address is provided,
* the system attempts to calculate a valid long/lat.
*
* @param string $title
* The title of the story. Title field in database is varchar(255)
* but it should probably be a shorter string than that.
* @param string $body
* The Body of the story is a text field.
* @param struct $location
* $location is an array containing location parameters. Location can be
* defined using either longitude/latitude or address.
* If address is provided, system will attempted to validate and
* calculate long/lat.
* - $location = array(
* 'longitude' => '37.533',
* 'latitude' => '77.467',
* );
* - $location = array(
* 'street' => '100 North 17th St',
* 'city' => 'richmond',
* 'province' => 'virginia',
* 'postal_code' => '23219',
* );
*
* @return struct
* A node struct for sending to the service.
*/
function buildNode($title, $body, $location){

// Construct the node object.
$data = (object) array
(
'type' => 'story', // DO NOT CHANGE THIS. Although other node types can be created via the service, default type for api.example.com is story and changing this will produce unexpected results.
'title' => $title,
'field_body' => array(array('value' => $body)), // CCK fields require a nested array.
'locations' => array(0 => $location), // Location field requires an enumerated array position.
);

return $data;
}

/**
* Processes the xmlrpc request.
*
* @param string $op
* The operation we want to perform. Default is node.save. Others could be
* node.get, node.delete, etc.
* @param mixed $data
* The data we are passing. Format will vary for each $op.
* A node.save op requires a stdClass Node object. A node.get could take a
* int or a string.
*
* @return mixed
* Returns the results of the xmlrpc operation. Upon success of a node.save,
* we return the new NID. Upon failure, a struct with the error message;
*/
function xmlrpcRequest($url, $op = 'node.save', $data, $domain, $api_key){

// Prep the values we'll need for the Hash.
$timestamp = (string) time();
$nonce = getUniqueCode("20");
$hash = hash_hmac('sha256', $timestamp .';'.$domain .';'. $nonce .';'.$op, $api_key);

// Make the xmlrpc call and capture the result.
$xmlrpc_result = xmlrpc($url . '/services/xmlrpc', $op, $hash, $domain, $timestamp, $nonce, $data);

// Return either the output from the xmlrpc call or any errors.
if ($xmlrpc_result === FALSE) {
return print_r(xmlrpc_error(), TRUE);
}
else {
return print_r($xmlrpc_result, TRUE);
}
}

/**
* Function for generating a random string, used for
* generating a nonce token for the XML-RPC session
*
* Code source http://groups.drupal.org/node/57483
*
* @param string $length
* The desired length of our nonce
*
* @return string
* Returns a nonce to be used in generating a hash.
*
*/
function getUniqueCode($length = "") {
$code = md5(uniqid(rand(), true));
if ($length != "") return substr($code, 0, $length);
else return $code;
}

*** saveNodeTest.php ***

// Author: David Hazel dave@hazelconsulting.com 6/1/2010

require_once './api.inc';

/**
* Set domain that corresponds to the API key.
* This is the domain value we configured in the services key.
* http://richmond.thegopage.com/admin/build/services/keys
*/
$domain = 'api.example.com';
$api_key = '7e0246f3dd05ab2cde860a46c06ffc16';

/**
* Set the $url of the service.
* For this example $domain and $url are the same but they don't have to be
* $domain could be foo.example.com
* $url could be bar.example.com
* $domain is the value we set for the key
* $url is the url of the services we are trying to access
*/
$url = $domain;

// Add timestamp to test stories to differentiate the generated titles/bodies.
date_default_timezone_set('America/Los_Angeles');

$title = ' title + ' . date("F j, Y, g:i a");
$body = ' body text + ' . date("F j, Y, g:i a");

$location_long_lat = array(
'longitude' => '37.533',
'latitude' => '77.467'
);

$location_addr = array(
'street' => '100 North 17th St',
'city' => 'richmond',
'province' => 'virginia',
'postal_code' => '23219',
);

// Submit a test using long/lat for a location.
echo submitStory($url, 'long/lat location test ' . $title, $body, $location_long_lat, $domain, $api_key);
// Submit a test using address for the location.
echo submitStory($url, 'addr location test ' . $title, $body, $location_addr, $domain, $api_key);

*** getNodeTest.php ***

// Author: David Hazel dave@hazelconsulting.com 6/1/2010
require_once './api.inc';

/**
* Set domain that corresponds to the API key.
* This is the domain value we configured in the services key.
* http://richmond.thegopage.com/admin/build/services/keys
*/
$domain = 'api.example.com';
$api_key = '7e0246f3dd05ab2cde860a46c06ffc16';

/**
* Set the $url of the service.
* For this example $domain and $url are the same but they don't have to be
* $domain could be foo.example.com
* $url could be bar.example.com
* $domain is the value we set for the key
* $url is the url of the services we are trying to access
*/
$url = $domain;
$nid = '3353';

echo getStory($url, $nid, $domain, $api_key);

AttachmentSize
services_api.tar_.gz55.02 KB