Here's some working logic that I cobbled together from various partial examples online and the deploy module. It's rough, but works. The key difference in this example from others is the fact that this version only uses Drupal API routines, rather than the 'experimental' PHP routines such as xmlrpc_encode_request() ( see http://php.net/manual/en/function.xmlrpc-encode-request.php ) and xmlrpc_decode(). Such routines may not exist in some environments and are also subject to change.

<?php
/***************************************************/
class DrupalXmlrpc {
 
  function __construct( $domain = '', $apiKey = '', $endPoint = '', $verbose = FALSE ) 
  {
    // set local domain or IP address
    // this needs to match the domain set when you created the API key
    $this->domain = $domain;
   
    // set API key
    $this->kid = $apiKey;
   
    // set target web service endpoint
    $this->endpoint = $endPoint;

	// extended debugging
	$this->verbose = $verbose;
   
	// call system.connect to get our required anonymous sessionId:
	$retVal = $this->send( 'system.connect', array() );
    $this->session_id = $retVal['sessid'];

	if ($this->verbose) {
	   $func = 'DrupalXmlrpc->__construct:';
	   if ($this->session_id)
	        error_log( $func.' got anonymous session id fine' );
	   else error_log( $func.' failed to get anonymous session id!' );
	}
  }
 
  /***********************************************************************
  * Function for sending xmlrpc requests
  */
  public function send( $methodName, $functionArgs = array() )
  {
    $protocolArgs = array();
	// only the system.connect method does not require a sessionId:
    if ($methodName == 'system.connect') {
	   $protocolArgs = array( $this->endpoint, $methodName );
	}
	else {
       $timestamp = (string)time();
       $nonce = $this->getUniqueCode("10");
   
       // prepare a hash
       $hash_parameters = array( $timestamp, $this->domain, $nonce, $methodName );
       $hash = hash_hmac("sha256", implode(';', $hash_parameters), $this->kid);
   
       // prepared the arguments for this service:
	   // note, the sessid needs to be the one returned by user.login
       $protocolArgs = array( $this->endpoint, $methodName, $hash, $this->domain, $timestamp, $nonce, $this->session_id );
	}

	$params = array_merge( $protocolArgs, $functionArgs );
	return call_user_func_array( 'xmlrpc', $params );
  }

  /***************************************************
   * login and return user object
   */
  public function userLogin( $userName = '', $userPass = '' ) 
  {
	if ($this->verbose)
         error_log( 'DrupalXmlrpc->userLogin() called with userName "'.$userName.'" and pass "'.$userPass.'"' );

	// clear out any lingering xmlrpc errors:
	xmlrpc_error( NULL, NULL, TRUE );

    $retVal = $this->send( 'user.login', array($userName, $userPass) );
	if (!$retVal && xmlrpc_errno()) {
	   if ($this->verbose)
	      error_log( 'userLogin() failed! errno "'.xmlrpc_errno().'" msg "'.xmlrpc_error_msg().'"' );
	   return FALSE;
	}
	else {
	   // remember our logged in session id:
       $this->session_id = $retVal['sessid'];

       // we might need the user object later, so save it:
       $user = new stdClass();
       $user = (object)$response['user'];
       $this->authenticated_user = $user;
       return $user;
	}
  }

  /***************************************************
   * logout, returns 0 for okay, or -1 for error.
   */
  public function userLogout()
  {
    $retVal = $this->send( 'user.logout', array() );
	if (!$retVal) {
	   if ($this->verbose)
	      error_log( 'userLogout() failed! errno "'.xmlrpc_errno().'" msg "'.xmlrpc_error_msg().'"' );
	   return -1;
	}

    return 0; // meaning okay
  }

  /***************************************************
  * Function for generating a random string, used for
  * generating a token for the XML-RPC session
  */
  private function getUniqueCode($length = "") 
  {
    $code = md5(uniqid(rand(), true));
    if ($length != "") 
         return substr($code, 0, $length);
    else return $code;
  }
}
?>

Use it like this:

<?php
       // create a drupal session:
       $localDomain   = 'whatever domain you used when you created this api key';
       $apiKey        = 'Services generated api key string';
       $endPoint      = 'url to your destination endpoint';
       $drupalSession = new DrupalXmlrpc( $localDomain, $apiKey, $endPoint );
       if ($drupalSession->session_id) {

          $userName   = 'username used to login to remote system';
          $userPass   = 'password of user on remote system';
          $drupalUser = $drupalSession->userLogin( $userName, $userPass );
          if ($drupalUser) {

             $result = $drupalSession->send( 'method.name-of-remote-function, array(remote-functions-parameters-in-an-array) );

             if ($result == -1) 
                $retVal = SOME_SYMBOL_MEANING_REMOTE_WORK_FAILED;
  
             $drupalSession->userLogout(); // all done communicating, so logout
		  }
		  else {
			 $retVal = SOME_SYMBOL_MEANING_LOGIN_FAILED;
		  }
       }
       else {
          $retVal = SOME_SYMBOL_MEANING_CONNECTION_FAILED;
       }
?>

I hope this is useful for others.

Comments

maxi’s picture

PHP Warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, 'xmlrpc' was given in ......./xmlrpc.php on line 67

below the lines 66-67 :

$params = array_merge( $protocolArgs, $functionArgs );
return call_user_func_array( 'xmlrpc', $params );

$params is an array similar this Array ( [0] => localhost/drupal-6.16/services/xmlrpc [1] => system.connect )

someone can help me ?
thanks

Maxi

bsenftner’s picture

You really should be asking at the Services.module issue queue.

When you post your issue over there, be sure to include your Drupal version & Services version. Your first error on line 67 should not happen, because 'xmlrpc' is a valid function name, it's part of the Drupal API: http://api.drupal.org/api/function/xmlrpc

The second part is simply an issue of you creating the $params array incorrectly. Do it just like you're sending the params to a function, as in $params = array( param1, param2, ... paramN ), without the associative array keys. And if they are strings, be sure to quote them...

Renee S’s picture

Yeah, I'm getting the same error here. Could it be a PHP5.3 thing? Did you ever get it solved?

PHP Warning: call_user_func_array() expects parameter 1 to be a valid callback, function 'xmlrpc' not found or invalid function name

@Blake: This question does belong under the example, since - if you take as a given that the user has set up and tested the Services module already, and it works (as, in my case, it does), it's related to a problem with this particular example code. If the example isn't correct, it should be removed. If there's a caveat or something related to the environment, it should be noted here for others (like me) who're trying to get it to work!

Renee S’s picture

Here is a revised example that does function. Note that the class requires includes from Drupal; you can bundle them or link toyour site install. Make sure permissions are set in THREE places:

1. In Site Permissions: For the user to access/update the content
2. In Site Permissions: For the user to use/access the services themselves
3. In the API Key Permissions: enable the services for each API client

main.php

<?php
require('service_class.php');

// create a drupal session:
$localDomain   = 'www.sendingdomain.com';
$apiKey        = 'a89sad9asd8asjda89jd9a8sjd98aj98jsad98j';
$endPoint      = 'http://www.drupalsite.com/services/xmlrpc';
$userName = '';
$userPass = '';
$title = '';
$description '';

$drupalSession = new DrupalXmlrpc( $localDomain, $apiKey, $endPoint );

if ( isset($drupalSession->session_id) ) {

	$request = (object)array(
  		'nid' => '3753',
	);
	
	// Add build node function
	$create = (object)array(
	    'type' => 'page',
	    'title' => $title,
	    'body' => $description,
	);
    
	$drupalUser = $drupalSession->userLogin( $userName, $userPass );

	if ( isset($drupalUser->uid ) ) {

      	//$result = $drupalSession->send( 'node_resource.retrieve', array('3749') );

			// Some variables go into the basic array, others are structs.
			// Send whatever the service is looking for: in the case of node.get, array.
			// In the case of user.load, struct.
			
         	$result = $drupalSession->send( 'node.get', $request, NULL );
         	//$result = $drupalSession->send( 'user.load', NULL, $user );
         	//$result = $drupalSession->send( 'node.save', NULL, $create );

             	if ($result == -1) 
                	$retVal = SOME_SYMBOL_MEANING_REMOTE_WORK_FAILED;
          	
		$drupalSession->userLogout(); // all done communicating, so logout
          }
          else {
             	$retVal = SOME_SYMBOL_MEANING_LOGIN_FAILED;
          }
} else { 
    $retVal = CONNECTION_FAILED;
}
?>

service_class.php


<?php
/***************************************************/
require_once('includes/bootstrap.inc');
require_once('includes/common.inc');
require_once('includes/xmlrpc.inc');

class DrupalXmlrpc {

  function __construct( $domain = '', $apiKey = '', $endPoint = '', $verbose = FALSE ) 
  {
    // set local domain or IP address
    // this needs to match the domain set when you created the API key
    $this->domain = $domain;
   
    // set API key
    $this->kid = $apiKey;
   
    // set target web service endpoint
    $this->endpoint = $endPoint;

    // extended debugging
    $this->verbose = $verbose;
   
    // call system.connect to get our required anonymous sessionId:
    $retVal = $this->send( 'system.connect', array() );
    $this->session_id = $retVal['sessid'];

    if ($this->verbose) {
       $func = 'DrupalXmlrpc->__construct:';
       if ($this->session_id)
            error_log( $func.' got anonymous session id fine' );
       else error_log( $func.' failed to get anonymous session id!' );
    }
  }

  /***********************************************************************
  * Function for sending xmlrpc requests
  */
  public function send( $methodName, $functionArgs = array(), $functionObjects = array() )
  {
    $protocolArgs = array();

    // only the system.connect method does not require a sessionId:
    if ($methodName == 'system.connect') {
       $protocolArgs = array( $this->endpoint, $methodName );
    }
    else {
       $timestamp = (string)time();
       $nonce = $this->getUniqueCode("10");
   
       // prepare a hash
       $hash_parameters = array( $timestamp, $this->domain, $nonce, $methodName );
       $hash = hash_hmac("sha256", implode(';', $hash_parameters), $this->kid);
   
       // prepared the arguments for this service:
       // note, the sessid needs to be the one returned by user.login
       $protocolArgs = array( $this->endpoint, $methodName, $hash, $this->domain, $timestamp, $nonce, $this->session_id );
	
	// this won't function in the case of structs, - in some cases, each one needs to be an arg, not 
	// have the whole passed array appended 
		
	if( isset($functionObjects) ) {
		array_push( $protocolArgs, $functionObjects );
   	}
	if( isset($functionArgs) ) { 
		array_splice( $protocolArgs, count($protocolArgs), 0, $functionArgs );
	}
  }
  return call_user_func_array( "_xmlrpc", $protocolArgs );

}

  /***************************************************
   * login and return user object
   */
  public function userLogin( $userName = '', $userPass = '' ) 
  {
    if ($this->verbose)
         error_log( 'DrupalXmlrpc->userLogin() called with userName "'.$userName.'" and pass "'.$userPass.'"' );

    // clear out any lingering xmlrpc errors:
    xmlrpc_error( NULL, NULL, TRUE );

    $retVal = $this->send( 'user.login', array( $userName, $userPass ), NULL );
    
    if (!$retVal && xmlrpc_errno()) {
       if ($this->verbose)
          error_log( 'userLogin() failed! errno "'.xmlrpc_errno().'" msg "'.xmlrpc_error_msg().'"' );
       return FALSE;
    }
    else {
       // remember our logged in session id:
       $this->session_id = $retVal['sessid'];

       // we might need the user object later, so save it:
       $user = new stdClass();
       $user = (object)$retVal['user'];
       $this->authenticated_user = $user;
       return $user;
    }
  }

  /***************************************************
   * logout, returns 0 for okay, or -1 for error.
   */
  public function userLogout()
  {
    $retVal = $this->send( 'user.logout' );
    if (!$retVal) {
       if ($this->verbose)
          error_log( 'userLogout() failed! errno "'.xmlrpc_errno().'" msg "'.xmlrpc_error_msg().'"' );
       return -1;
    }

    return 0; // meaning okay
  }

  /***************************************************
  * Function for generating a random string, used for
  * generating a token for the XML-RPC session
  */
  private function getUniqueCode($length = "") 
  {
    $code = md5(uniqid(rand(), true));
    if ($length != "") 
         return substr($code, 0, $length);
    else return $code;
  }
}
?>
Barnettech’s picture

thank you Reinette,

I had it working without sessions, but then suddenly my server stopped wanting to accept requests without sessions (yes I unchecked the box in settings and everything), go figure. Anyhow after scouring the net, this post saved the day. Your code sample worked for me.

Muchas Muchas Gracias,

James

dru_paul’s picture

The error I'm getting back is (Drupal 5)

userLogin() failed! errno "-32602" msg "Server error. Invalid method parameters.

anyone have some info on how to remedy this problem?

thank you

the problem was simply that I did not realize that system connect was not automatically supported as a service!

resolved.