'. t('Visit the Services Handbook for help and information.', array('@handbook_url' => 'http://drupal.org/node/109782')) .'

'; case 'admin/build/services': case 'admin/build/services/browse': $output = '

'. 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 Services Handbook for help and information.', array('@handbook_url' => 'http://drupal.org/node/109782')) .'

'; $output .= '

'. t('All enabled services and methods are shown. Click on any method to view information or test.') .'

'; 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' => "
", '#suffix' => "
", '#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 = <<'."\n"; $output .= ' '."\n"; $keys = services_get_keys(); foreach ($keys as $key) { if (!empty($key->domain)) { $output .= ' '."\n"; $output .= ' '."\n"; } } $output .= ''; services_xml_output($output); } function services_xml_output($xml) { $xml = ''."\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); }