services.module

Tracking 5.x-1.x branch
  1. drupal
    1. 5 contributions/services/services.module
    2. 6 contributions/services/services.module
    3. 7 contributions/services/services.module

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

Functions & methods

NameDescription
services_admin_jsUI enhancement for services page
services_admin_settings
services_cronImplementation of hook_cron().
services_crossdomain_xml
services_errorPrepare an error message for returning to the XMLRPC caller.
services_get_allThis should probably be cached in drupal cache.
services_get_key
services_get_keys
services_get_server_info
services_helpImplementation of hook_help().
services_menuImplementation of hook_menu.
services_method_callThis is the magic function through which all remote method calls must pass.
services_method_get
services_node_loadMake any changes we might want to make to node.
services_permImplementation of hook_perm().
services_serverCallback for server endpoint.
services_session_loadBackup current session data and import user session.
services_session_unloadRevert to previously backuped session.
services_set_server_info
services_validate_key
services_xml_output

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * The module which provides the core code for drupal services
  5. *
  6. * This module is responsible for calling the appropriate method,
  7. * expose servers and determining what data must be present in a service call
  8. */
  9. /**
  10. * Implementation of hook_help().
  11. */
  12. function services_help($section) {
  13. switch ($section) {
  14. case 'admin/help#services':
  15. 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>';
  16. case 'admin/build/services':
  17. case 'admin/build/services/browse':
  18. $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>';
  19. $output .= '<p>'. t('All enabled services and methods are shown. Click on any method to view information or test.') .'</p>';
  20. return $output;
  21. case 'admin/build/services/keys':
  22. return t('An API key is required to allow an application to access Drupal remotely.');
  23. }
  24. }
  25. /**
  26. * Implementation of hook_perm().
  27. */
  28. function services_perm() {
  29. return array('access services', 'administer services');
  30. }
  31. /**
  32. * Implementation of hook_menu.
  33. */
  34. function services_menu($may_cache) {
  35. $items = array();
  36. $access = user_access('access services');
  37. $admin_access = user_access('administer services');
  38. $path = drupal_get_path('module', 'services');
  39. if ($may_cache) {
  40. // admin
  41. $items[] = array(
  42. 'path' => 'admin/build/services',
  43. 'title' => t('Services'),
  44. 'access' => $admin_access,
  45. 'callback' => 'services_admin_browse_index',
  46. 'description' => t('Allows external applications to communicate with Drupal.'),
  47. );
  48. // browse
  49. $items[] = array(
  50. 'path' => 'admin/build/services/browse',
  51. 'title' => t('Browse'),
  52. 'access' => $admin_access,
  53. 'callback' => 'services_admin_browse_index',
  54. 'description' => t('Browse and test available remote services.'),
  55. 'type' => MENU_DEFAULT_LOCAL_TASK
  56. );
  57. // API Keys
  58. if (variable_get('services_use_key', TRUE)) {
  59. $items[] = array(
  60. 'path' => 'admin/build/services/keys',
  61. 'title' => t('Keys'),
  62. 'access' => $admin_access,
  63. 'callback' => 'services_admin_keys_list',
  64. 'description' => t('Manage application access to site services.'),
  65. 'type' => MENU_LOCAL_TASK,
  66. );
  67. $items[] = array(
  68. 'path' => 'admin/build/services/keys/list',
  69. 'title' => t('List'),
  70. 'access' => $admin_access,
  71. 'type' => MENU_DEFAULT_LOCAL_TASK,
  72. 'weight' => -10,
  73. );
  74. $items[] = array(
  75. 'path' => 'admin/build/services/keys/add',
  76. 'title' => t('Create key'),
  77. 'access' => $admin_access,
  78. 'callback' => 'drupal_get_form',
  79. 'callback arguments' => array('services_admin_keys_form'),
  80. 'type' => MENU_LOCAL_TASK,
  81. );
  82. }
  83. // Settings
  84. $items[] = array(
  85. 'path' => 'admin/build/services/settings',
  86. 'title' => t('Settings'),
  87. 'access' => $admin_access,
  88. 'callback' => 'drupal_get_form',
  89. 'callback arguments' => 'services_admin_settings',
  90. 'description' => t('Configure service settings.'),
  91. 'type' => MENU_LOCAL_TASK,
  92. );
  93. $items[] = array(
  94. 'path' => 'admin/build/services/settings/general',
  95. 'title' => t('General'),
  96. 'access' => $admin_access,
  97. 'callback' => 'drupal_get_form',
  98. 'callback arguments' => 'services_admin_settings',
  99. 'description' => t('Configure service settings.'),
  100. 'type' => MENU_DEFAULT_LOCAL_TASK,
  101. 'weight' => -10,
  102. );
  103. // crossdomain.xml
  104. $items[] = array(
  105. 'path' => 'crossdomain.xml',
  106. 'access' => $access,
  107. 'callback' => 'services_crossdomain_xml',
  108. 'type' => MENU_CALLBACK,
  109. );
  110. }
  111. else {
  112. if (arg(0) == 'services') {
  113. // server
  114. foreach (module_implements('server_info') as $module) {
  115. $info = module_invoke($module, 'server_info');
  116. if ($info['#path'] == arg(1)) {
  117. $items[] = array(
  118. 'path' => 'services/'. $info['#path'],
  119. 'title' => t('Services'),
  120. 'access' => $access,
  121. 'callback' => 'services_server',
  122. 'callback arguments' => array($module),
  123. 'type' => MENU_CALLBACK,
  124. );
  125. }
  126. }
  127. }
  128. // admin
  129. if (arg(0) == 'admin' && arg(1) == 'build' && arg(2) == 'services') {
  130. // browse
  131. if (arg(3) == 'browse' || !arg(3)) {
  132. require_once "$path/services_admin_browse.inc";
  133. if (arg(4)) {
  134. $items[] = array(
  135. 'path' => 'admin/build/services/browse/'. arg(4),
  136. 'title' => arg(4),
  137. 'access' => $admin_access,
  138. 'callback' => 'services_admin_browse_method',
  139. 'type' => MENU_LOCAL_TASK
  140. );
  141. }
  142. drupal_add_css("$path/services.css", 'module');
  143. }
  144. // keys
  145. if (arg(3) == 'keys' && variable_get('services_use_key', TRUE)) {
  146. require_once "$path/services_admin_keys.inc";
  147. if ($key = services_get_key(arg(4))) {
  148. if (!empty($key)) {
  149. $items[] = array(
  150. 'path' => 'admin/build/services/keys/'. $key->kid,
  151. 'title' => t('Edit key'),
  152. 'access' => $admin_access,
  153. 'callback' => 'drupal_get_form',
  154. 'callback arguments' => array('services_admin_keys_form', $key),
  155. 'type' => MENU_CALLBACK,
  156. );
  157. $items[] = array(
  158. 'path' => 'admin/build/services/keys/'. $key->kid .'/delete',
  159. 'title' => '',
  160. 'access' => $admin_access,
  161. 'callback' => 'drupal_get_form',
  162. 'callback arguments' => array('services_admin_keys_delete_confirm', $key),
  163. 'type' => MENU_CALLBACK,
  164. );
  165. }
  166. }
  167. }
  168. }
  169. }
  170. return $items;
  171. }
  172. /*
  173. * Callback for admin page.
  174. */
  175. function services_admin_settings() {
  176. $node_types = node_get_types('names');
  177. $defaults = isset($node_types['blog']) ? array('blog' => 1) : array();
  178. $form['security'] = array(
  179. '#title' => t('Security'),
  180. '#type' => 'fieldset',
  181. '#description' => t('Changing security settings will require you to adjust all method calls. This will affect all applications using site services.'),
  182. );
  183. $form['security']['services_use_key'] = array(
  184. '#type' => 'checkbox',
  185. '#title' => t('Use keys'),
  186. '#default_value' => variable_get('services_use_key', TRUE),
  187. '#description' => t('When enabled all method calls need to provide a validation token to autheciate themselves with the server.')
  188. );
  189. $form['security']['services_key_expiry'] = array(
  190. '#type' => 'textfield',
  191. '#prefix' => "<div id='services-key-expiry'>",
  192. '#suffix' => "</div>",
  193. '#title' => t('Token expiry time'),
  194. '#default_value' => variable_get('services_key_expiry', 30),
  195. '#description' => t('The time frame for which the token will be valid. Default is 30 secs')
  196. );
  197. $form['security']['services_use_sessid'] = array(
  198. '#type' => 'checkbox',
  199. '#title' => t('Use sessid'),
  200. '#default_value' => variable_get('services_use_sessid', TRUE),
  201. '#description' => t('When enabled, all method calls must include a valid sessid. Only disable this setting if the application will use browser-based cookies.')
  202. );
  203. $form['#pre_render'][] = 'services_admin_js';
  204. return system_settings_form($form);
  205. }
  206. /**
  207. * UI enhancement for services page
  208. */
  209. function services_admin_js($form_id, $form) {
  210. $out = <<<EOJS
  211. $(document).ready(function() {
  212. $("#services-key-expiry")[$("#edit-services-use-key").attr('checked') ? 'show' : 'hide']();
  213. $("#edit-services-use-key").click(function() {
  214. $("#services-key-expiry")[$(this).attr('checked') ? 'show' : 'hide']();
  215. });
  216. });
  217. EOJS;
  218. drupal_add_js($out, 'inline', 'footer');
  219. }
  220. /**
  221. * Callback for server endpoint.
  222. */
  223. function services_server($module = NULL) {
  224. services_set_server_info($module);
  225. print module_invoke($module, 'server');
  226. // Do not let this output.
  227. exit;
  228. }
  229. /*
  230. * Callback for crossdomain.xml.
  231. */
  232. function services_crossdomain_xml() {
  233. global $base_url;
  234. $output = '<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">'."\n";
  235. $output .= '<cross-domain-policy>'."\n";
  236. $output .= ' <allow-access-from domain="'. check_plain($_SERVER['HTTP_HOST']) .'" />'."\n";
  237. $output .= ' <allow-access-from domain="*.'. check_plain($_SERVER['HTTP_HOST']) .'" />'."\n";
  238. $keys = services_get_keys();
  239. foreach ($keys as $key) {
  240. if (!empty($key->domain)) {
  241. $output .= ' <allow-access-from domain="'. check_plain($key->domain) .'" />'."\n";
  242. $output .= ' <allow-access-from domain="*.'. check_plain($key->domain) .'" />'."\n";
  243. }
  244. }
  245. $output .= '</cross-domain-policy>';
  246. services_xml_output($output);
  247. }
  248. function services_xml_output($xml) {
  249. $xml = '<?xml version="1.0"?>'."\n". $xml;
  250. header('Connection: close');
  251. header('Content-Length: '. strlen($xml));
  252. header('Content-Type: text/xml');
  253. header('Date: '. date('r'));
  254. echo $xml;
  255. exit;
  256. }
  257. function services_set_server_info($module) {
  258. $server_info = new stdClass();
  259. $server_info->module = $module;
  260. $server_info->drupal_path = getcwd();
  261. return services_get_server_info($server_info);
  262. }
  263. function services_get_server_info($server_info = NULL) {
  264. static $info;
  265. if (!$info && $server_info) {
  266. $info = $server_info;
  267. }
  268. return $info;
  269. }
  270. /**
  271. * Prepare an error message for returning to the XMLRPC caller.
  272. */
  273. function services_error($message) {
  274. $server_info = services_get_server_info();
  275. // Look for custom error handling function.
  276. // Should be defined in each server module.
  277. if ($server_info && module_hook($server_info->module, 'server_error')) {
  278. return module_invoke($server_info->module, 'server_error', $message);
  279. }
  280. // No custom error handling function found.
  281. return $message;
  282. }
  283. /**
  284. * Implementation of hook_cron().
  285. *
  286. * Clear down old values from the nonce table.
  287. */
  288. function services_cron() {
  289. $expiry_time = time() - variable_get('services_key_expiry', 30);
  290. db_query("DELETE FROM {services_timestamp_nonce} WHERE timestamp < '%s'", $expiry_time);
  291. }
  292. /**
  293. * This is the magic function through which all remote method calls must pass.
  294. */
  295. function services_method_call($method_name, $args = array(), $ignore_hash = FALSE) {
  296. $method = services_method_get($method_name);
  297. // Check that method exists.
  298. if (empty($method)) {
  299. return services_error(t('Method %name does not exist.', array('%name' => $method_name)));
  300. }
  301. // Check for missing args and identify if arg is required in the hash.
  302. $hash_parameters = array();
  303. foreach ($method['#args'] as $key => $arg) {
  304. if (!$arg['#optional']) {
  305. if (!isset($args[$key]) && !is_array($args[$key]) && !is_bool($args[$key])) {
  306. if ($arg['#name'] == 'sessid' && session_id()) {
  307. $args[$key] = session_id();
  308. }
  309. else {
  310. return services_error(t('Missing required arguments.'));
  311. }
  312. }
  313. }
  314. // Key is part of the hash
  315. if ($arg['#signed'] == TRUE && variable_get('services_use_key', TRUE)) {
  316. if (is_numeric($args[$key]) || !empty($args[$key])) {
  317. if (is_array($args[$key]) || is_object($args[$key])){
  318. $hash_parameters[] = serialize($args[$key]);
  319. }
  320. else{
  321. $hash_parameters[] = $args[$key];
  322. }
  323. }
  324. else{
  325. $hash_parameters[] = '';
  326. }
  327. }
  328. }
  329. // Add additonal processing for methods requiring api key.
  330. if ($method['#key'] && variable_get('services_use_key', TRUE)) {
  331. $hash = array_shift($args);
  332. $domain = array_shift($args);
  333. $timestamp = array_shift($args);
  334. $nonce = array_shift($args);
  335. $expiry_time = $timestamp + variable_get('services_key_expiry', 30);
  336. if ($expiry_time < time()) {
  337. return services_error(t('Token has expired.'));
  338. }
  339. // Still in time but has it been used before
  340. if (db_result(db_query("SELECT count(*) FROM {services_timestamp_nonce} WHERE domain = '%s' AND timestamp = %d AND nonce = '%s'", $domain, $timestamp, $nonce))) {
  341. return services_error(t('Token has been used previously for a request.'));
  342. }
  343. else{
  344. db_query("INSERT INTO {services_timestamp_nonce} (domain, timestamp, nonce) VALUES ('%s', %d, '%s')", $domain, $timestamp, $nonce);
  345. }
  346. $api_key = db_result(db_query("SELECT kid FROM {services_keys} WHERE domain = '%s'", $domain));
  347. if (!services_validate_key($api_key, $timestamp, $domain, $nonce, $method_name, $hash_parameters, $hash)) {
  348. return services_error(t('Invalid API key.'));
  349. }
  350. }
  351. // Add additonal processing for methods requiring authentication.
  352. $session_backup = NULL;
  353. if ($method['#auth'] && variable_get('services_use_sessid', TRUE)) {
  354. $sessid = array_shift($args);
  355. if (empty($sessid)) {
  356. return services_error(t('Invalid sessid.'));
  357. }
  358. $session_backup = services_session_load($sessid);
  359. }
  360. // Check access
  361. $access_arguments = isset($method['#access arguments']) ? $method['#access arguments'] : $args;
  362. // Call default or custom access callback
  363. if (call_user_func_array($method['#access callback'], $access_arguments) != TRUE) {
  364. return services_error(t('Access denied.'));
  365. }
  366. // Change working directory to drupal root to call drupal function,
  367. // then change it back to server module root to handle return.
  368. $server_root = getcwd();
  369. $server_info = services_get_server_info();
  370. if ($server_info) {
  371. chdir($server_info->drupal_path);
  372. }
  373. $result = call_user_func_array($method['#callback'], $args);
  374. if ($server_info) {
  375. chdir($server_root);
  376. }
  377. // Add additonal processing for methods requiring authentication.
  378. if ($session_backup !== NULL) {
  379. services_session_unload($session_backup);
  380. }
  381. return $result;
  382. }
  383. /**
  384. * This should probably be cached in drupal cache.
  385. */
  386. function services_get_all() {
  387. static $methods_cache;
  388. if (!isset($methods_cache)) {
  389. $methods = module_invoke_all('service');
  390. // api_key arg
  391. $arg_api_key = array(
  392. '#name' => 'hash',
  393. '#type' => 'string',
  394. '#description' => t('A valid API key.'),
  395. );
  396. // sessid arg
  397. $arg_sessid = array(
  398. '#name' => 'sessid',
  399. '#type' => 'string',
  400. '#description' => t('A valid sessid.'),
  401. );
  402. // domain arg
  403. $arg_domain_name = array(
  404. '#name' => 'domain_name',
  405. '#type' => 'string',
  406. '#description' => t('A valid domain for the API key.'),
  407. );
  408. $arg_domain_time_stamp = array(
  409. '#name' => 'domain_time_stamp',
  410. '#type' => 'string',
  411. '#description' => t('Time stamp used to hash key.'),
  412. );
  413. $arg_nonce = array(
  414. '#name' => 'nonce',
  415. '#type' => 'string',
  416. '#description' => t('One time use nonce also used hash key.'),
  417. );
  418. foreach ($methods as $key => $method) {
  419. // set method defaults
  420. if (!isset($methods[$key]['#auth'])) {
  421. $methods[$key]['#auth'] = TRUE;
  422. }
  423. if (!isset($methods[$key]['#key'])) {
  424. $methods[$key]['#key'] = TRUE;
  425. }
  426. if (!isset($methods[$key]['#access callback'])) {
  427. $methods[$key]['#access callback'] = 'user_access';
  428. if (!isset($methods[$key]['#access arguments'])) {
  429. $methods[$key]['#access arguments'] = array('access services');
  430. }
  431. }
  432. if (!isset($methods[$key]['#args'])) {
  433. $methods[$key]['#args'] = array();
  434. }
  435. if ($methods[$key]['#auth'] && variable_get('services_use_sessid', TRUE)) {
  436. $methods[$key]['#args'] = array_merge(array($arg_sessid), $methods[$key]['#args']);
  437. }
  438. if ($methods[$key]['#key'] && variable_get('services_use_key', TRUE)) {
  439. $methods[$key]['#args'] = array_merge(array($arg_nonce), $methods[$key]['#args']);
  440. $methods[$key]['#args'] = array_merge(array($arg_domain_time_stamp), $methods[$key]['#args']);
  441. $methods[$key]['#args'] = array_merge(array($arg_domain_name), $methods[$key]['#args']);
  442. $methods[$key]['#args'] = array_merge(array($arg_api_key), $methods[$key]['#args']);
  443. }
  444. // set defaults for args
  445. foreach ($methods[$key]['#args'] as $arg_key => $arg) {
  446. if (is_array($arg)) {
  447. if (!isset($arg['#optional'])) {
  448. $methods[$key]['#args'][$arg_key]['#optional'] = FALSE;
  449. }
  450. }
  451. else {
  452. $arr_arg = array();
  453. $arr_arg['#name'] = t('unnamed');
  454. $arr_arg['#type'] = $arg;
  455. $arr_arg['#description'] = t('No description given.');
  456. $arr_arg['#optional'] = FALSE;
  457. $methods[$key]['#args'][$arg_key] = $arr_arg;
  458. }
  459. }
  460. reset($methods[$key]['#args']);
  461. }
  462. $methods_cache = $methods;
  463. }
  464. return $methods_cache;
  465. }
  466. function services_method_get($method_name) {
  467. static $method_cache;
  468. if (!isset($method_cache[$method_name])) {
  469. foreach (services_get_all() as $method) {
  470. if ($method_name == $method['#method']) {
  471. $method_cache[$method_name] = $method;
  472. break;
  473. }
  474. }
  475. }
  476. return $method_cache[$method_name];
  477. }
  478. function services_validate_key($kid, $timestamp, $domain, $nonce, $method_name, $hash_parameters, $hash) {
  479. $hash_parameters = array_merge(array($timestamp, $domain, $nonce, $method_name), $hash_parameters);
  480. $rehash = hash_hmac("sha256", implode(';', $hash_parameters), $kid);
  481. return $rehash == $hash;
  482. }
  483. function services_get_key($kid) {
  484. $keys = services_get_keys();
  485. foreach ($keys as $key) {
  486. if ($key->kid == $kid) {
  487. return $key;
  488. }
  489. }
  490. }
  491. function services_get_keys() {
  492. static $keys;
  493. if (!$keys) {
  494. $keys = array();
  495. $result = db_query("SELECT * FROM {services_keys}");
  496. while ($key = db_fetch_object($result)) {
  497. $keys[$key->kid] = $key;
  498. }
  499. }
  500. return $keys;
  501. }
  502. /**
  503. * Make any changes we might want to make to node.
  504. */
  505. function services_node_load($node, $fields = array()) {
  506. if (!$node->nid) {
  507. return NULL;
  508. }
  509. // Loop through and get only requested fields.
  510. if (count($fields) > 0) {
  511. foreach ($fields as $field) {
  512. $val->{$field} = $node->{$field};
  513. }
  514. }
  515. else {
  516. $val = $node;
  517. }
  518. return $val;
  519. }
  520. /**
  521. * Backup current session data and import user session.
  522. */
  523. function services_session_load($sessid) {
  524. global $user;
  525. // If user's session is already loaded, just return current user's data
  526. if ($user->sid == $sessid) {
  527. return $user;
  528. }
  529. // Make backup of current user and session data
  530. $backup = $user;
  531. $backup->session = session_encode();
  532. // Empty current session data
  533. foreach ($_SESSION as $key => $value) {
  534. unset($_SESSION[$key]);
  535. }
  536. // Some client/servers, like XMLRPC, do not handle cookies, so imitate it to make sess_read() function try to look for user,
  537. // instead of just loading anonymous user :).
  538. if (!isset($_COOKIE[session_name()])) $_COOKIE[session_name()] = $sessid;
  539. // Load session data
  540. session_id($sessid);
  541. sess_read($sessid);
  542. // Check if it really loaded user and, for additional security, if user was logged from the same IP. If not, then revert automatically.
  543. if ($user->sid != $sessid) {
  544. services_session_unload($backup);
  545. return NULL;
  546. }
  547. return $backup;
  548. }
  549. /**
  550. * Revert to previously backuped session.
  551. */
  552. function services_session_unload($backup) {
  553. global $user;
  554. // No point in reverting if it's the same user's data
  555. if ($user->sid == $backup->sid) {
  556. return;
  557. }
  558. // Some client/servers, like XMLRPC, do not handle cookies, so imitate it to make sess_read() function try to look for user,
  559. // instead of just loading anonymous user :).
  560. if (!isset($_COOKIE[session_name()])) $_COOKIE[session_name()] = $sessid;
  561. // Save current session data
  562. sess_write($user->sid, session_encode());
  563. // Empty current session data
  564. foreach ($_SESSION as $key => $value) {
  565. unset($_SESSION[$key]);
  566. }
  567. // Revert to previous user and session data
  568. $user = $backup;
  569. session_id($backup->sessid);
  570. session_decode($user->session);
  571. }