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 ***

<?php

// 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 ***

<?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 ***
<code>
<?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

Comments

FreddieK’s picture

I'm trying to use this example on a test installation, but when calling getNodeTest.php the server response is:

stdClass Object ( [is_error] => 1 [code] => 1 [message] => Missing required arguments. )

kanani’s picture

If you post your code I'll take a look.

Also make sure it's not a permissions issue.

ericpai’s picture

Is the example using Keys "on" and sessions "off" ?
I'm trying a simple example of this in a php enabled textarea like this but I'm getting an error.
I know the key is correct and gave the key permissions for node.save.

$server = 'http://my.serversite.com/services/xmlrpc';

$api_key = 'a8701f38cca293b29850cae4408fa055';
$timestamp = (string) time();
$nonce = user_password();
$domain = 'myothersite.com';
// Create secure hash using your api key.
$op='node.save';
$hash = hash_hmac('sha256', $timestamp .';'.$domain .';'. $nonce .';'.$op, $api_key);
//------------------------->
// Create content on the server site
$data=array('type'=>'page', 'title'=>'Test from myothersite.com', 'body'=>'A test 1');

// create a node on the remote server.
$xmlrpc_result = xmlrpc($server, 'node.save', $hash, $domain, $timestamp, $nonce, $data);
print_r($xmlrpc_result);


     // 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);
      }

Outputs this error: stdClass Object ( [is_error] => 1 [code] => 1 [message] => 1 1 Invalid API key. )

kanani’s picture

Can you verify that all permissions have been granted for anonymous and authenticated users for the user_service module in admin/user/permissions?

Your API keys are handling the access control so it's ok to grant all permissions for that module to everyone. And actually if you don't, you'll get a permissions error. Although it might not be the invalide API key error your getting.

What happens when you use something like XDEBUG to debug the services request?

On trick that's a little different with that is you have to trigger the debug call in services request url, and not from the Browsers like you typically would with remote debugging.