services.module in Services 6
Same filename and directory in other branches
@author Services Dev Team
Provides a generic by powerful API for web services.
File
services.moduleView 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
Name | 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 |