services.module in Services 5
Same filename and directory in other branches
The module which provides the core code for drupal services
This module is responsible for calling the appropriate method, expose servers and determining what data must be present in a service call
File
services.moduleView source
<?php
/**
* @file
* The module which provides the core code for drupal services
*
* This module is responsible for calling the appropriate method,
* expose servers and determining what data must be present in a service call
*/
/**
* Implementation of hook_help().
*/
function services_help($section) {
switch ($section) {
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($may_cache) {
$items = array();
$access = user_access('access services');
$admin_access = user_access('administer services');
$path = drupal_get_path('module', 'services');
if ($may_cache) {
// admin
$items[] = array(
'path' => 'admin/build/services',
'title' => t('Services'),
'access' => $admin_access,
'callback' => 'services_admin_browse_index',
'description' => t('Allows external applications to communicate with Drupal.'),
);
// browse
$items[] = array(
'path' => 'admin/build/services/browse',
'title' => t('Browse'),
'access' => $admin_access,
'callback' => 'services_admin_browse_index',
'description' => t('Browse and test available remote services.'),
'type' => MENU_DEFAULT_LOCAL_TASK,
);
// API Keys
if (variable_get('services_use_key', TRUE)) {
$items[] = array(
'path' => 'admin/build/services/keys',
'title' => t('Keys'),
'access' => $admin_access,
'callback' => 'services_admin_keys_list',
'description' => t('Manage application access to site services.'),
'type' => MENU_LOCAL_TASK,
);
$items[] = array(
'path' => 'admin/build/services/keys/list',
'title' => t('List'),
'access' => $admin_access,
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items[] = array(
'path' => 'admin/build/services/keys/add',
'title' => t('Create key'),
'access' => $admin_access,
'callback' => 'drupal_get_form',
'callback arguments' => array(
'services_admin_keys_form',
),
'type' => MENU_LOCAL_TASK,
);
}
// Settings
$items[] = array(
'path' => 'admin/build/services/settings',
'title' => t('Settings'),
'access' => $admin_access,
'callback' => 'drupal_get_form',
'callback arguments' => 'services_admin_settings',
'description' => t('Configure service settings.'),
'type' => MENU_LOCAL_TASK,
);
$items[] = array(
'path' => 'admin/build/services/settings/general',
'title' => t('General'),
'access' => $admin_access,
'callback' => 'drupal_get_form',
'callback arguments' => 'services_admin_settings',
'description' => t('Configure service settings.'),
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
// crossdomain.xml
$items[] = array(
'path' => 'crossdomain.xml',
'access' => $access,
'callback' => 'services_crossdomain_xml',
'type' => MENU_CALLBACK,
);
}
else {
if (arg(0) == 'services') {
// server
foreach (module_implements('server_info') as $module) {
$info = module_invoke($module, 'server_info');
if ($info['#path'] == arg(1)) {
$items[] = array(
'path' => 'services/' . $info['#path'],
'title' => t('Services'),
'access' => $access,
'callback' => 'services_server',
'callback arguments' => array(
$module,
),
'type' => MENU_CALLBACK,
);
}
}
}
// admin
if (arg(0) == 'admin' && arg(1) == 'build' && arg(2) == 'services') {
// browse
if (arg(3) == 'browse' || !arg(3)) {
require_once "{$path}/services_admin_browse.inc";
if (arg(4)) {
$items[] = array(
'path' => 'admin/build/services/browse/' . arg(4),
'title' => arg(4),
'access' => $admin_access,
'callback' => 'services_admin_browse_method',
'type' => MENU_LOCAL_TASK,
);
}
drupal_add_css("{$path}/services.css", 'module');
}
// keys
if (arg(3) == 'keys' && variable_get('services_use_key', TRUE)) {
require_once "{$path}/services_admin_keys.inc";
if ($key = services_get_key(arg(4))) {
if (!empty($key)) {
$items[] = array(
'path' => 'admin/build/services/keys/' . $key->kid,
'title' => t('Edit key'),
'access' => $admin_access,
'callback' => 'drupal_get_form',
'callback arguments' => array(
'services_admin_keys_form',
$key,
),
'type' => MENU_CALLBACK,
);
$items[] = array(
'path' => 'admin/build/services/keys/' . $key->kid . '/delete',
'title' => '',
'access' => $admin_access,
'callback' => 'drupal_get_form',
'callback arguments' => array(
'services_admin_keys_delete_confirm',
$key,
),
'type' => MENU_CALLBACK,
);
}
}
}
}
}
return $items;
}
/*
* Callback for admin page.
*/
function services_admin_settings() {
$node_types = node_get_types('names');
$defaults = isset($node_types['blog']) ? array(
'blog' => 1,
) : array();
$form['security'] = array(
'#title' => t('Security'),
'#type' => 'fieldset',
'#description' => t('Changing security settings will require you to adjust all method calls. This will affect all applications using site services.'),
);
$form['security']['services_use_key'] = array(
'#type' => 'checkbox',
'#title' => t('Use keys'),
'#default_value' => variable_get('services_use_key', TRUE),
'#description' => t('When enabled all method calls need to provide a validation token to autheciate themselves with the server.'),
);
$form['security']['services_key_expiry'] = array(
'#type' => 'textfield',
'#prefix' => "<div id='services-key-expiry'>",
'#suffix' => "</div>",
'#title' => t('Token expiry time'),
'#default_value' => variable_get('services_key_expiry', 30),
'#description' => t('The time frame for which the token will be valid. Default is 30 secs'),
);
$form['security']['services_use_sessid'] = array(
'#type' => 'checkbox',
'#title' => t('Use sessid'),
'#default_value' => variable_get('services_use_sessid', TRUE),
'#description' => t('When enabled, all method calls must include a valid sessid. Only disable this setting if the application will use browser-based cookies.'),
);
$form['#pre_render'][] = 'services_admin_js';
return system_settings_form($form);
}
/**
* UI enhancement for services page
*/
function services_admin_js($form_id, $form) {
$out = <<<EOJS
\$(document).ready(function() {
\$("#services-key-expiry")[\$("#edit-services-use-key").attr('checked') ? 'show' : 'hide']();
\$("#edit-services-use-key").click(function() {
\$("#services-key-expiry")[\$(this).attr('checked') ? 'show' : 'hide']();
});
});
EOJS;
drupal_add_js($out, 'inline', 'footer');
}
/**
* Callback for server endpoint.
*/
function services_server($module = NULL) {
services_set_server_info($module);
print module_invoke($module, 'server');
// Do not let this output.
exit;
}
/*
* 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: ' . 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 && $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;
}
/**
* 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} WHERE timestamp < '%s'", $expiry_time);
}
/**
* This is the magic function through which all remote method calls must pass.
*/
function services_method_call($method_name, $args = array(), $ignore_hash = FALSE) {
$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 (!isset($args[$key]) && !is_array($args[$key]) && !is_bool($args[$key])) {
if ($arg['#name'] == 'sessid' && session_id()) {
$args[$key] = session_id();
}
else {
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[] = '';
}
}
}
// Add additonal processing for methods requiring api key.
if ($method['#key'] && 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} 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) 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'] && 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);
}
// 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'] && variable_get('services_use_sessid', TRUE)) {
$methods[$key]['#args'] = array_merge(array(
$arg_sessid,
), $methods[$key]['#args']);
}
if ($methods[$key]['#key'] && 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;
}
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;
}
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 (!$node->nid) {
return NULL;
}
// 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
foreach ($_SESSION as $key => $value) {
unset($_SESSION[$key]);
}
// 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
session_id($sessid);
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) {
services_session_unload($backup);
return NULL;
}
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
foreach ($_SESSION as $key => $value) {
unset($_SESSION[$key]);
}
// Revert to previous user and session data
$user = $backup;
session_id($backup->sessid);
session_decode($user->session);
}
Functions
Name | Description |
---|---|
services_admin_js | UI enhancement for services page |
services_admin_settings | |
services_cron | Implementation of hook_cron(). |
services_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_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_validate_key | |
services_xml_output |