You are here

services.module in Services 6.3

Provides a generic but powerful API for web services.

File

services.module
View source
<?php

/**
 * @file
 *  Provides a generic but powerful API for web services.
 */

/**
 * Minimum CTools version needed.
 */
define('SERVICES_REQUIRED_CTOOLS_API', '1.7');

/**
 * Implementation of hook_help().
 */
function services_help($path, $arg) {
  $output = NULL;
  switch ($path) {
    case 'admin/help#services':
      $output = '<p>' . t('Visit the <a href="@handbook_url">Services Handbook</a> for help and information.', array(
        '@handbook_url' => 'http://drupal.org/node/109782',
      )) . '</p>';
      break;
    case 'admin/build/services':
      $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 are shown. Click on any method to view information or test.') . '</p>';
      break;
  }
  return $output;
}

/**
 * Implementation of hook_perm().
 */
function services_perm() {
  return array(
    'administer services',
    'get taxonomy tree',
    // File resource permissions
    'get any binary files',
    'get own binary files',
    'save file information',
    // System resource permissions
    'get a system variable',
    'set a system variable',
    // Query permissions
    'perform unlimited index queries',
  );
}

/**
 * Implementation of hook_menu().
 */
function services_menu() {
  $items = array();

  // Add menu items for the different endpoints
  if (module_exists('ctools')) {
    $endpoints = services_endpoint_load_all();
    foreach ($endpoints as $endpoint) {
      if (empty($endpoint->disabled)) {
        $items[$endpoint->path] = array(
          'title' => 'Services endpoint',
          'access callback' => 'services_access_menu',
          'page callback' => 'services_endpoint_callback',
          'page arguments' => array(
            $endpoint->name,
          ),
          'type' => MENU_CALLBACK,
        );
      }
    }
  }
  return $items;
}

/**
 * Implementation of hook_ctools_plugin_api().
 */
function services_ctools_plugin_api($module, $api) {
  if ($module == 'services' && $api == 'plugins') {
    return array(
      'version' => 3,
    );
  }
}

/**
 * Implementation of hook_ctools_plugin_directory().
 */
function services_ctools_plugin_directory($module, $type) {

  // Safety: go away if CTools is not at an appropriate version.
  if (!module_invoke('ctools', 'api_version', SERVICES_REQUIRED_CTOOLS_API)) {
    return;
  }
  if ($type == 'export_ui') {
    return 'plugins/export_ui';
  }
}

/**
 * Access callback that always returns TRUE.
 *
 * This callback is necessary for services like login and logout that should
 * always be wide open and accessible.
 *
 * *** USE THIS WITH GREAT CAUTION ***
 *
 * If you think you need it you are almost certainly wrong.
 */
function services_access_menu() {
  return TRUE;
}

/**
 * Implementation of hook_theme().
 */
function services_theme() {
  return array(
    'services_endpoint_index' => array(
      'template' => 'services_endpoint_index',
      'arguments' => array(
        'endpoints' => NULL,
      ),
    ),
    'services_resource_table' => array(
      'render element' => 'table',
      'file' => 'services.admin.inc',
    ),
  );
}

/**
 * Returns information about the installed server modules on the system.
 *
 * @return array
 *  An associative array keyed after module name containing information about
 *  the installed server implementations.
 */
function services_get_servers() {
  static $servers;
  if (!$servers) {
    $servers = array();
    foreach (module_implements('server_info') as $module) {
      $servers[$module] = call_user_func($module . '_server_info');
    }
  }
  return $servers;
}

/**
 * Menu system page callback for server endpoints.
 *
 * @param string $endpoint
 *  The endpoint name.
 * @return void
 */
function services_endpoint_callback($endpoint_name) {
  module_load_include('runtime.inc', 'services');
  $endpoint = services_endpoint_load($endpoint_name);
  $server = $endpoint->server;
  if (function_exists($server . '_server')) {

    // call the server
    if ($endpoint->debug) {
      watchdog('services', 'Calling server: %server', array(
        '%server' => $server . '_server',
      ), WATCHDOG_DEBUG);
    }
    services_set_server_info_from_array(array(
      'module' => $server,
      'endpoint' => $endpoint_name,
      'endpoint_path' => $endpoint->path,
      'debug' => $endpoint->debug,
      'settings' => $endpoint->server_settings,
    ));
    if ($endpoint->debug) {
      watchdog('services', 'Server info main object: <pre>@info</pre>', array(
        '@info' => print_r(services_server_info_object(), TRUE),
      ), WATCHDOG_DEBUG);
    }
    print call_user_func($server . '_server');

    // Do not let this output
    drupal_page_footer();
    exit;
  }

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

/**
 * Create a new endpoint with defaults appropriately set from schema.
 *
 * @return stdClass
 *  An endpoint initialized with the default values.
 */
function services_endpoint_new() {
  ctools_include('export');
  return ctools_export_new_object('services_endpoint');
}

/**
 * Load a single endpoint.
 *
 * @param string $name
 *  The name of the endpoint.
 * @return stdClass
 *  The endpoint configuration.
 */
function services_endpoint_load($name) {
  ctools_include('export');
  $result = ctools_export_load_object('services_endpoint', 'names', array(
    $name,
  ));
  if (isset($result[$name])) {
    return $result[$name];
  }
  return FALSE;
}

/**
 * Load all endpoints.
 *
 * @return array
 *  Array of endpoint objects keyed by endpoint names.
 */
function services_endpoint_load_all() {
  ctools_include('export');
  return ctools_export_load_object('services_endpoint');
}

/**
 * Saves an endpoint in the database.
 *
 * @return void
 */
function services_endpoint_save($endpoint) {
  if (is_array($endpoint) && isset($endpoint['build_info'])) {
    $endpoint = $endpoint['build_info']['args'][0];
  }

  // Set a default of an array if the value is not present.
  foreach (array(
    'server_settings',
    'resources',
    'authentication',
  ) as $endpoint_field) {
    if (empty($endpoint->{$endpoint_field})) {
      $endpoint->{$endpoint_field} = array();
    }
  }
  ctools_export_crud_save('services_endpoint', $endpoint);
  ctools_export_load_object_reset('services_endpoint');
  menu_rebuild();
  cache_clear_all('services:' . $endpoint->name . ':', 'cache', TRUE);
}

/**
 * Remove an endpoint.
 *
 * @return void
 */
function services_endpoint_delete($endpoint) {
  ctools_export_crud_delete('services_endpoint', $endpoint);
  ctools_export_load_object_reset('services_endpoint');
  menu_rebuild();
  cache_clear_all('services:' . $endpoint->name . ':', 'cache', TRUE);
}

/**
 * Export an endpoint.
 *
 * @return string
 */
function services_endpoint_export($endpoint, $indent = '') {
  ctools_include('export');
  return ctools_export_object('services_endpoint', $endpoint, $indent);
}

/**
 * Gets all resource definitions.
 *
 * @param string $endpoint_name
 *   Optional. The endpoint endpoint that's being used.
 * @return array
 *  An array containing all resources.
 */
function services_get_resources($endpoint_name = '') {
  $cache_key = 'services:' . $endpoint_name . ':resources';
  $resources = array();
  if (($cache = cache_get($cache_key)) && isset($cache->data)) {
    $resources = $cache->data;
  }
  else {
    module_load_include('resource_build.inc', 'services');
    $resources = _services_build_resources($endpoint_name);
    cache_set($cache_key, $resources);
  }
  return $resources;
}

/**
 * Returns information about resource API version information.
 * The resource API is the way modules expose resources to services,
 * not the API that is exposed to the consumers of your services.
 *
 * @return array
 *  API version information. 'default_version' is the version that's assumed
 *  if the module doesn't declare an API version. 'versions' is an array
 *  containing the known API versions. 'current_version' is the current
 *  version number.
 */
function services_resource_api_version_info() {
  $info = array(
    'default_version' => 3001,
    'versions' => array(
      3002,
    ),
  );
  $info['current_version'] = max($info['versions']);
  return $info;
}

/**
 * Implementation of hook_services_resources().
 */
function services_services_resources() {
  module_load_include('resource_build.inc', 'services');

  // Return resources representing legacy services
  return _services_core_resources();
}

/**
 * Implementation of hook_services_authentication().
 */
function services_services_authentication_info() {
  return array(
    'title' => t('Session authentication'),
    'description' => t("Uses Drupal's built in sessions to authenticate."),
    'authenticate_call' => '_services_sessions_authenticate_call',
  );
}

/**
 * Authenticates a call using Drupal's built in sessions
 *
 * @return void
 */
function _services_sessions_authenticate_call() {
  $arg = func_get_args();
  global $user;
  $original_user = services_get_server_info('original_user');

  //If the user is logged in already and the callback contains the string login

  //we need to set it to the original user so that the already logged in as @user message

  //is displayed
  if (strpos($arg[1]['callback'], 'login') !== FALSE) {
    if ($original_user->uid != 0) {
      $user = $original_user;
    }
  }

  //For every callback that has nothing to do with login we need to

  //set it to the original user so that they are no longer anonymous
  if (strpos($arg[1]['callback'], 'login') === FALSE) {

    // The account should be restored to the session's user.
    $user = $original_user;
  }
}

/**
 * Get operation class information.
 *
 * @return array An array with operation class information keyed by operation machine name.
 */
function services_operation_class_info() {
  return array(
    'operations' => array(
      'title' => t('CRUD operations'),
      'name' => t('CRUD operation'),
      'class_singular' => 'operation',
    ),
    'actions' => array(
      'title' => t('Actions'),
      'name' => t('action'),
      'class_singular' => 'action',
    ),
    'relationships' => array(
      'title' => t('Relationships'),
      'name' => t('relationship'),
      'class_singular' => 'relationship',
    ),
    'targeted_actions' => array(
      'title' => t('Targeted actions'),
      'name' => t('targeted action'),
      'class_singular' => 'targeted_action',
    ),
  );
}

/**
 * Returns all the controller names for a endpoint.
 *
 * @param string $endpoint
 *  The endpoint that should be used.
 * @return array
 *   an array containing all controller names
 */
function services_controllers_list($endpoint) {
  $controllers = array();
  $class_info = services_operation_class_info();
  $resources = services_get_resources($endpoint);
  foreach ($resources as $resource_name => $resource) {
    foreach ($class_info as $class_name => $class) {
      if (empty($resource[$class_name])) {
        continue;
      }
      foreach ($resource[$class_name] as $op_name => $op) {
        $method = "{$resource_name}.{$op_name}";
        if (empty($controllers[$method])) {
          $controllers[$method] = $method;
        }
        else {
          watchdog('services', 'Naming collision when listing controllers as methods. The %class %operation is not included in the listing.', array(
            '%class' => $class['name'],
            '%operation' => $op_name,
          ), WATCHDOG_WARNING);
        }
      }
    }
  }
  return $controllers;
}

/**
 * Returns the requested controller.
 *
 * @param string $name
 *  The name of the controller in the format: {resource}.{name} or
 *  {resource}.{operation}. Examples: "node.retrieve", "system.getVariable".
 * @param string $endpoint
 *  The endpoint that should be used.
 */
function services_controller_get($name, $endpoint) {
  list($resource_name, $method) = explode('.', $name);
  $resources = services_get_resources($endpoint);
  if (isset($resources[$resource_name])) {
    $res = $resources[$resource_name];
    if (isset($res[$method])) {
      return $res[$method];
    }
    else {
      $class_info = services_operation_class_info();

      // Handle extended operatios
      foreach ($class_info as $class => $info) {
        if (isset($res[$class]) && isset($res[$class][$method])) {
          return $res[$class][$method];
        }
      }
    }
  }
}

/**
 * Convert a resource to RPC-style methods.
 *
 * @param array $resource
 *   A resource definition.
 * @param string $resource_name
 *   The resource name, ie: node.
 *
 * @return array
 *   An array of RPC method definitions
 */
function services_resources_as_procedures($resource, $resource_name) {
  $methods = array();
  $class_info = services_operation_class_info();
  foreach ($class_info as $class_name => $class) {
    if (empty($resource[$class_name])) {
      continue;
    }
    foreach ($resource[$class_name] as $op_name => $op) {
      $method_name = "{$resource_name}.{$op_name}";
      if (empty($methods[$method_name])) {
        $methods[$method_name] = array(
          'method' => $method_name,
        ) + $op;
      }
      else {
        watchdog('services', 'Naming collision when listing controllers as methods. The %class %operation wont be available for RPC-style servers.', array(
          '%class' => $class['name'],
          '%operation' => $op_name,
        ), WATCHDOG_WARNING);
      }
    }
  }
  return $methods;
}

/**
 * Helper function to build index queries.
 *
 * @param $query
 *   Object database query object.
 * @param $page
 *   Integer page number we are requesting.
 * @param $fields
 *   Array fields to return.
 * @param $service_params
 *   Array parameters to add to the index query.
 * @param $page_size
 *   Integer number of items to be returned.
 * @param $resource
 *   String name of the resource building the index query
 */
function services_resource_build_index_query($schema, $order, $page, $fields, $service_params = array(), $primary_table, $primary_field, $page_size, $resource) {
  $where = array();
  $query_params = array();
  $fields = db_escape_string($fields);

  // need to append table prefix
  if ($fields_array = explode(',', $fields)) {
    foreach ($fields_array as &$field) {
      $field = $primary_table . '.' . trim($field);
    }
    $fields = implode(',', $fields_array);
  }
  $schema = db_escape_string($schema);
  $table = $schema;
  $schema = drupal_get_schema($schema);

  // Build an array of fields with the appropriate placeholders for use in
  // db_query().
  if (is_array($service_params)) {
    foreach ($service_params as $param_field => $param_value) {
      if (!$schema['fields'][$param_field]) {
        services_error('Parameter "' . $param_field . '" is not valid.');
      }
      $in_placeholders = array();
      foreach (services_str_getcsv($param_value) as $single_value) {
        $in_placeholders[] = db_type_placeholder($schema['fields'][$param_field]['type']);
        $query_params[] = $single_value;
      }
      $where[] = $primary_table . '.' . $param_field . ' IN (' . implode(',', $in_placeholders) . ')';
    }
  }

  // Now implode that array into an actual WHERE clause.
  $where = !empty($where) ? ' WHERE ' . implode(' AND ', $where) : '';

  // Apply page size limits.
  $default_limit = variable_get("services_{$resource}_index_page_size", 20);
  if (!user_access('perform unlimited index queries') && $page_size > $default_limit) {
    $page_size = $default_limit;
  }

  // Run through db_rewrite_sql to make sure proper access checks are applied.
  $sql = "SELECT {$fields} FROM {{$table}} AS {$primary_table} {$where} ORDER BY {$order}";
  $sql = db_rewrite_sql($sql, $primary_table, $primary_field);
  $result = db_query_range($sql, $query_params, $page * $page_size, $page_size);
  return $result;
}

/**
 * Emulate str_getcsv on systems where it is not available.
 *
 * @ingroup php_wrappers
 */
function services_str_getcsv($input, $delimiter = ',', $enclosure = '"', $escape = '\\') {
  $ret = array();
  if (!function_exists('str_getcsv')) {
    $temp = fopen("php://memory", "rw");
    fwrite($temp, $input);
    fseek($temp, 0);
    $ret = fgetcsv($temp, 0, $delimiter, $enclosure);
    fclose($temp);
  }
  else {
    $ret = str_getcsv($input, $delimiter, $enclosure, $escape);
  }
  return $ret;
}

/**
 * Helper function to build a list of items satisfying the index query.
 *
 * @param $results
 *   Object database query results object.
 * @param $type
 *   String type of index that is being processed.
 * @param $field
 *   String field to use for looking up uri.
 */
function services_resource_build_index_list($results, $type, $field) {

  // Put together array of matching items to return.
  $items = array();
  foreach ($results as $result) {
    if ($uri = services_resource_uri(array(
      $type,
      $result->{$field},
    ))) {
      $result->uri = $uri;
      if ($type == 'user') {
        services_remove_user_data($result);
      }
    }
    $items[] = $result;
  }
  return $items;
}

/**
 *  Helper function to remove data from the user object.
 *
 *  @param $account
 *    Object user object.
 */
function services_remove_user_data(&$account) {

  // Remove the users password from the account object.
  unset($account->pass);
  if (!user_access('administer users')) {
    unset($account->mail);
    unset($account->init);
  }
  drupal_alter('services_account_object', $account);
}

Functions

Namesort descending Description
services_access_menu Access callback that always returns TRUE.
services_controllers_list Returns all the controller names for a endpoint.
services_controller_get Returns the requested controller.
services_ctools_plugin_api Implementation of hook_ctools_plugin_api().
services_ctools_plugin_directory Implementation of hook_ctools_plugin_directory().
services_endpoint_callback Menu system page callback for server endpoints.
services_endpoint_delete Remove an endpoint.
services_endpoint_export Export an endpoint.
services_endpoint_load Load a single endpoint.
services_endpoint_load_all Load all endpoints.
services_endpoint_new Create a new endpoint with defaults appropriately set from schema.
services_endpoint_save Saves an endpoint in the database.
services_get_resources Gets all resource definitions.
services_get_servers Returns information about the installed server modules on the system.
services_help Implementation of hook_help().
services_menu Implementation of hook_menu().
services_operation_class_info Get operation class information.
services_perm Implementation of hook_perm().
services_remove_user_data Helper function to remove data from the user object.
services_resources_as_procedures Convert a resource to RPC-style methods.
services_resource_api_version_info Returns information about resource API version information. The resource API is the way modules expose resources to services, not the API that is exposed to the consumers of your services.
services_resource_build_index_list Helper function to build a list of items satisfying the index query.
services_resource_build_index_query Helper function to build index queries.
services_services_authentication_info Implementation of hook_services_authentication().
services_services_resources Implementation of hook_services_resources().
services_str_getcsv Emulate str_getcsv on systems where it is not available.
services_theme Implementation of hook_theme().
_services_sessions_authenticate_call Authenticates a call using Drupal's built in sessions

Constants

Namesort descending Description
SERVICES_REQUIRED_CTOOLS_API Minimum CTools version needed.