You are here

services.module in Services 6

@author Services Dev Team

Provides a generic by powerful API for web services.

File

services.module
View source
<?php

/**
 * @author Services Dev Team
 * @file
 *  Provides a generic by powerful API for web services.
 */

/**
 * Implementation of hook_cron().
 *
 * Clear down old values from the nonce table.
 */
function services_cron() {
  $expiry_time = time() - variable_get('services_key_expiry', 30);
  db_query("DELETE FROM {services_timestamp_nonce}\n    WHERE timestamp < %d", $expiry_time);
}

/**
 * Implementation of hook_help().
 */
function services_help($path, $arg) {
  switch ($path) {
    case 'admin/help#services':
      return '<p>' . t('Visit the <a href="@handbook_url">Services Handbook</a> for help and information.', array(
        '@handbook_url' => 'http://drupal.org/node/109782',
      )) . '</p>';
    case 'admin/build/services':
    case 'admin/build/services/browse':
      $output = '<p>' . t('Services are collections of methods available to remote applications. They are defined in modules, and may be accessed in a number of ways through server modules. Visit the <a href="@handbook_url">Services Handbook</a> for help and information.', array(
        '@handbook_url' => 'http://drupal.org/node/109782',
      )) . '</p>';
      $output .= '<p>' . t('All enabled services and methods are shown. Click on any method to view information or test.') . '</p>';
      return $output;
    case 'admin/build/services/keys':
      return t('An API key is required to allow an application to access Drupal remotely.');
  }
}

/**
 * Implementation of hook_perm().
 */
function services_perm() {
  return array(
    'access services',
    'administer services',
  );
}

/**
 * Implementation of hook_menu().
 */
function services_menu() {
  $items['admin/build/services'] = array(
    'title' => 'Services',
    'description' => 'Allows external applications to communicate with Drupal.',
    'access arguments' => array(
      'administer services',
    ),
    'page callback' => 'services_admin_browse_index',
    'file' => 'services_admin_browse.inc',
  );
  $items['admin/build/services/browse'] = array(
    'title' => 'Browse',
    'description' => 'Browse and test available remote services.',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/build/services/browse/%services_method'] = array(
    'title' => 'Services',
    'description' => 'Calls a Services method.',
    'page callback' => 'services_admin_browse_method',
    'page arguments' => array(
      4,
    ),
    'access arguments' => array(
      'administer services',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'services_admin_browse.inc',
  );
  $items['admin/build/services/keys'] = array(
    'title' => 'Keys',
    'description' => 'Manage application access to site services.',
    'page callback' => 'services_admin_keys_list',
    'access callback' => 'services_auth_access',
    'type' => MENU_LOCAL_TASK,
    'file' => 'services_admin_keys.inc',
  );
  $items['admin/build/services/keys/%'] = array(
    'title' => 'Edit key',
    'access arguments' => array(
      'administer services',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'services_admin_keys_form',
    ),
    'file' => 'services_admin_keys.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/build/services/keys/%/delete'] = array(
    'title' => $type->name,
    'access arguments' => array(
      'administer services',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'services_admin_keys_delete_confirm',
    ),
    'file' => 'services_admin_keys.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/build/services/keys/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'access callback' => 'services_auth_access',
    'weight' => -10,
  );
  $items['admin/build/services/keys/add'] = array(
    'title' => 'Create key',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'services_admin_keys_form',
    ),
    'access callback' => 'services_auth_access',
    'type' => MENU_LOCAL_TASK,
    'file' => 'services_admin_keys.inc',
  );
  $items['admin/build/services/settings'] = array(
    'title' => 'Settings',
    'description' => 'Configure service settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'services_admin_settings',
    ),
    'access arguments' => array(
      'administer services',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'services_admin_browse.inc',
  );
  $items['admin/build/services/settings/general'] = array(
    'title' => 'General',
    'description' => 'Configure service settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'services_admin_settings',
    ),
    'access arguments' => array(
      'administer services',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['crossdomain.xml'] = array(
    'access arguments' => array(
      'access services',
    ),
    'page callback' => 'services_crossdomain_xml',
    'type' => MENU_CALLBACK,
  );
  $items['services/%'] = array(
    'title' => 'Services',
    'access arguments' => array(
      'access services',
    ),
    'page callback' => 'services_server',
    'page arguments' => array(
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * This function is called to determine whether the current user has 
 * access to a keys configuration.
 * 
 */
function services_auth_access() {
  return user_access('administer services') && variable_get('services_use_key', TRUE);
}

/**
 * Implementation of hook_theme().
 */
function services_theme() {
  return array(
    'services_admin_browse_test' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
  );
}

/**
 * Callback for server endpoint
 */
function services_server($server_path = NULL) {

  // Find which module the server is part of
  foreach (module_implements('server_info') as $module) {
    $info = module_invoke($module, 'server_info');
    if ($info['#path'] == $server_path) {

      // call the server
      services_set_server_info($module);
      print module_invoke($module, 'server');

      // Do not let this output
      module_invoke_all('exit');
      exit;
    }
  }

  // return 404 if the service doesn't exist
  drupal_not_found();
}

/**
 * Callback for crossdomain.xml
 */
function services_crossdomain_xml() {
  global $base_url;
  $output = '<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">' . "\n";
  $output .= '<cross-domain-policy>' . "\n";
  $output .= '  <allow-access-from domain="' . check_plain($_SERVER['HTTP_HOST']) . '" />' . "\n";
  $output .= '  <allow-access-from domain="*.' . check_plain($_SERVER['HTTP_HOST']) . '" />' . "\n";
  $keys = services_get_keys();
  foreach ($keys as $key) {
    if (!empty($key->domain)) {
      $output .= '  <allow-access-from domain="' . check_plain($key->domain) . '" />' . "\n";
      $output .= '  <allow-access-from domain="*.' . check_plain($key->domain) . '" />' . "\n";
    }
  }
  $output .= '</cross-domain-policy>';
  services_xml_output($output);
}
function services_xml_output($xml) {
  $xml = '<?xml version="1.0"?>' . "\n" . $xml;
  header('Connection: close');
  header('Content-Length: ' . drupal_strlen($xml));
  header('Content-Type: text/xml');
  header('Date: ' . date('r'));
  echo $xml;
  exit;
}
function services_set_server_info($module) {
  $server_info = new stdClass();
  $server_info->module = $module;
  $server_info->drupal_path = getcwd();
  return services_get_server_info($server_info);
}
function services_get_server_info($server_info = NULL) {
  static $info;
  if (!$info and $server_info) {
    $info = $server_info;
  }
  return $info;
}

/**
 * Prepare an error message for returning to the XMLRPC caller.
 */
function services_error($message) {
  $server_info = services_get_server_info();

  // Look for custom error handling function.
  // Should be defined in each server module.
  if ($server_info && module_hook($server_info->module, 'server_error')) {
    return module_invoke($server_info->module, 'server_error', $message);
  }

  // No custom error handling function found.
  return $message;
}

/**
 * This is the magic function through which all remote method calls must pass.
 */
function services_method_call($method_name, $args = array()) {
  $method = services_method_get($method_name);

  // Check that method exists.
  if (empty($method)) {
    return services_error(t('Method %name does not exist.', array(
      '%name' => $method_name,
    )));
  }

  // Check for missing args and identify if arg is required in the hash.
  $hash_parameters = array();
  foreach ($method['#args'] as $key => $arg) {
    if (!$arg['#optional']) {
      if (!is_numeric($args[$key]) and empty($args[$key])) {
        return services_error(t('Missing required arguments.'));
      }
    }

    // Key is part of the hash
    if ($arg['#signed'] == TRUE && variable_get('services_use_key', TRUE)) {
      if (is_numeric($args[$key]) || !empty($args[$key])) {
        if (is_array($args[$key]) || is_object($args[$key])) {
          $hash_parameters[] = serialize($args[$key]);
        }
        else {
          $hash_parameters[] = $args[$key];
        }
      }
      else {
        $hash_parameters[] = '';
      }
    }
  }
  if ($method['#key'] and variable_get('services_use_key', TRUE)) {
    $hash = array_shift($args);
    $domain = array_shift($args);
    $timestamp = array_shift($args);
    $nonce = array_shift($args);
    $expiry_time = $timestamp + variable_get('services_key_expiry', 30);
    if ($expiry_time < time()) {
      return services_error(t('Token has expired.'));
    }

    // Still in time but has it been used before
    if (db_result(db_query("SELECT count(*) FROM {services_timestamp_nonce}\n        WHERE domain = '%s' AND timestamp = %d AND nonce = '%s'", $domain, $timestamp, $nonce))) {
      return services_error(t('Token has been used previously for a request.'));
    }
    else {
      db_query("INSERT INTO {services_timestamp_nonce} (domain, timestamp, nonce)\n        VALUES ('%s', %d, '%s')", $domain, $timestamp, $nonce);
    }
    $api_key = db_result(db_query("SELECT kid FROM {services_keys} WHERE domain = '%s'", $domain));
    if (!services_validate_key($api_key, $timestamp, $domain, $nonce, $method_name, $hash_parameters, $hash)) {
      return services_error(t('Invalid API key.'));
    }
  }

  // Add additonal processing for methods requiring authentication
  $session_backup = NULL;
  if ($method['#auth'] and variable_get('services_use_sessid', TRUE)) {
    $sessid = array_shift($args);
    if (empty($sessid)) {
      return services_error(t('Invalid sessid.'));
    }
    $session_backup = services_session_load($sessid);
  }

  // Load the proper file
  if ($file = $method['#file']) {
    module_load_include($file['file'], $file['module']);
  }

  // Check access
  $access_arguments = isset($method['#access arguments']) ? $method['#access arguments'] : $args;

  // Call default or custom access callback
  if (call_user_func_array($method['#access callback'], $access_arguments) != TRUE) {
    return services_error(t('Access denied.'));
  }

  // Change working directory to drupal root to call drupal function,
  // then change it back to server module root to handle return.
  $server_root = getcwd();
  $server_info = services_get_server_info();
  if ($server_info) {
    chdir($server_info->drupal_path);
  }
  $result = call_user_func_array($method['#callback'], $args);
  if ($server_info) {
    chdir($server_root);
  }

  // Add additonal processing for methods requiring authentication.
  if ($session_backup !== NULL) {
    services_session_unload($session_backup);
  }
  return $result;
}

/**
 * This should probably be cached in drupal cache.
 */
function services_get_all() {
  static $methods_cache;
  if (!isset($methods_cache)) {
    $methods = module_invoke_all('service');

    // api_key arg
    $arg_api_key = array(
      '#name' => 'hash',
      '#type' => 'string',
      '#description' => t('A valid API key.'),
    );

    // sessid arg
    $arg_sessid = array(
      '#name' => 'sessid',
      '#type' => 'string',
      '#description' => t('A valid sessid.'),
    );

    // domain arg
    $arg_domain_name = array(
      '#name' => 'domain_name',
      '#type' => 'string',
      '#description' => t('A valid domain for the API key.'),
    );
    $arg_domain_time_stamp = array(
      '#name' => 'domain_time_stamp',
      '#type' => 'string',
      '#description' => t('Time stamp used to hash key.'),
    );
    $arg_nonce = array(
      '#name' => 'nonce',
      '#type' => 'string',
      '#description' => t('One time use nonce also used hash key.'),
    );
    foreach ($methods as $key => $method) {

      // set method defaults
      if (!isset($methods[$key]['#auth'])) {
        $methods[$key]['#auth'] = TRUE;
      }
      if (!isset($methods[$key]['#key'])) {
        $methods[$key]['#key'] = TRUE;
      }
      if (!isset($methods[$key]['#access callback'])) {
        $methods[$key]['#access callback'] = 'user_access';
        if (!isset($methods[$key]['#access arguments'])) {
          $methods[$key]['#access arguments'] = array(
            'access services',
          );
        }
      }
      if (!isset($methods[$key]['#args'])) {
        $methods[$key]['#args'] = array();
      }
      if ($methods[$key]['#auth'] and variable_get('services_use_sessid', TRUE)) {
        $methods[$key]['#args'] = array_merge(array(
          $arg_sessid,
        ), $methods[$key]['#args']);
      }
      if ($methods[$key]['#key'] and variable_get('services_use_key', TRUE)) {
        $methods[$key]['#args'] = array_merge(array(
          $arg_nonce,
        ), $methods[$key]['#args']);
        $methods[$key]['#args'] = array_merge(array(
          $arg_domain_time_stamp,
        ), $methods[$key]['#args']);
        $methods[$key]['#args'] = array_merge(array(
          $arg_domain_name,
        ), $methods[$key]['#args']);
        $methods[$key]['#args'] = array_merge(array(
          $arg_api_key,
        ), $methods[$key]['#args']);
      }

      // set defaults for args
      foreach ($methods[$key]['#args'] as $arg_key => $arg) {
        if (is_array($arg)) {
          if (!isset($arg['#optional'])) {
            $methods[$key]['#args'][$arg_key]['#optional'] = FALSE;
          }
        }
        else {
          $arr_arg = array();
          $arr_arg['#name'] = t('unnamed');
          $arr_arg['#type'] = $arg;
          $arr_arg['#description'] = t('No description given.');
          $arr_arg['#optional'] = FALSE;
          $methods[$key]['#args'][$arg_key] = $arr_arg;
        }
      }
      reset($methods[$key]['#args']);
    }
    $methods_cache = $methods;
  }
  return $methods_cache;
}

/**
 * Menu wildcard loader for browsing Service methods.
 */
function services_method_load($method) {
  $method = services_method_get($method);
  return isset($method) ? $method : FALSE;
}
function services_method_get($method_name) {
  static $method_cache;
  if (!isset($method_cache[$method_name])) {
    foreach (services_get_all() as $method) {
      if ($method_name == $method['#method']) {
        $method_cache[$method_name] = $method;
        break;
      }
    }
  }
  return $method_cache[$method_name];
}
function services_validate_key($kid, $timestamp, $domain, $nonce, $method_name, $hash_parameters, $hash) {
  $hash_parameters = array_merge(array(
    $timestamp,
    $domain,
    $nonce,
    $method_name,
  ), $hash_parameters);
  $rehash = hash_hmac("sha256", implode(';', $hash_parameters), $kid);
  return $rehash == $hash ? TRUE : FALSE;
}
function services_get_key($kid) {
  $keys = services_get_keys();
  foreach ($keys as $key) {
    if ($key->kid == $kid) {
      return $key;
    }
  }
}
function services_get_keys() {
  static $keys;
  if (!$keys) {
    $keys = array();
    $result = db_query("SELECT * FROM {services_keys}");
    while ($key = db_fetch_object($result)) {
      $keys[$key->kid] = $key;
    }
  }
  return $keys;
}

/**
 * Make any changes we might want to make to node.
 */
function services_node_load($node, $fields = array()) {
  if (!isset($node->nid)) {
    return NULL;
  }

  // Apply filters to fields
  $body = $node->body;
  $node->body = new stdClass();
  $node->body_value = $body;
  $node->body = check_markup($body, $node->format, FALSE);

  // Loop through and get only requested fields
  if (count($fields) > 0) {
    foreach ($fields as $field) {
      $val->{$field} = $node->{$field};
    }
  }
  else {
    $val = $node;
  }
  return $val;
}

/**
 * Backup current session data and import user session.
 */
function services_session_load($sessid) {
  global $user;

  // If user's session is already loaded, just return current user's data
  if ($user->sid == $sessid) {
    return $user;
  }

  // Make backup of current user and session data
  $backup = $user;
  $backup->session = session_encode();

  // Empty current session data
  $_SESSION = array();

  // Some client/servers, like XMLRPC, do not handle cookies, so imitate it to make sess_read() function try to look for user,
  // instead of just loading anonymous user :).
  if (!isset($_COOKIE[session_name()])) {
    $_COOKIE[session_name()] = $sessid;
  }

  // Load session data
  sess_read($sessid);

  // Check if it really loaded user and, for additional security, if user was logged from the same IP. If not, then revert automatically.
  if ($user->sid != $sessid or $user->hostname != $backup->hostname) {
    services_session_unload($backup);
    return NULL;
  }

  // Prevent saving of this impersonation in case of unexpected failure.
  session_save_session(FALSE);
  return $backup;
}

/**
 * Revert to previously backuped session.
 */
function services_session_unload($backup) {
  global $user;

  // No point in reverting if it's the same user's data
  if ($user->sid == $backup->sid) {
    return;
  }

  // Some client/servers, like XMLRPC, do not handle cookies, so imitate it to make sess_read() function try to look for user,
  // instead of just loading anonymous user :).
  if (!isset($_COOKIE[session_name()])) {
    $_COOKIE[session_name()] = $sessid;
  }

  // Save current session data
  sess_write($user->sid, session_encode());

  // Empty current session data
  $_SESSION = array();

  // Revert to previous user and session data
  $user = $backup;
  session_decode($user->session);
  session_save_session(TRUE);
}

Functions

Namesort descending Description
services_auth_access This function is called to determine whether the current user has access to a keys configuration.
services_cron Implementation of hook_cron().
services_crossdomain_xml Callback for crossdomain.xml
services_error Prepare an error message for returning to the XMLRPC caller.
services_get_all This should probably be cached in drupal cache.
services_get_key
services_get_keys
services_get_server_info
services_help Implementation of hook_help().
services_menu Implementation of hook_menu().
services_method_call This is the magic function through which all remote method calls must pass.
services_method_get
services_method_load Menu wildcard loader for browsing Service methods.
services_node_load Make any changes we might want to make to node.
services_perm Implementation of hook_perm().
services_server Callback for server endpoint
services_session_load Backup current session data and import user session.
services_session_unload Revert to previously backuped session.
services_set_server_info
services_theme Implementation of hook_theme().
services_validate_key
services_xml_output