5.x-1.x branch
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
| 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 |
- <?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);
- }
-
-