web_services.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. <?php
  2. /**
  3. * Elgg web services API library
  4. * Functions and objects for exposing custom web services.
  5. *
  6. */
  7. /**
  8. * Check that the method call has the proper API and user authentication
  9. *
  10. * @param string $method The api name that was exposed
  11. *
  12. * @return true or throws an exception
  13. * @throws APIException
  14. * @since 1.7.0
  15. * @access private
  16. */
  17. function authenticate_method($method) {
  18. global $API_METHODS;
  19. // method must be exposed
  20. if (!isset($API_METHODS[$method])) {
  21. throw new APIException(elgg_echo('APIException:MethodCallNotImplemented', array($method)));
  22. }
  23. // check API authentication if required
  24. if ($API_METHODS[$method]["require_api_auth"] == true) {
  25. $api_pam = new ElggPAM('api');
  26. if ($api_pam->authenticate() !== true) {
  27. throw new APIException(elgg_echo('APIException:APIAuthenticationFailed'));
  28. }
  29. }
  30. $user_pam = new ElggPAM('user');
  31. $user_auth_result = $user_pam->authenticate(array());
  32. // check if user authentication is required
  33. if ($API_METHODS[$method]["require_user_auth"] == true) {
  34. if ($user_auth_result == false) {
  35. throw new APIException($user_pam->getFailureMessage(), ErrorResult::$RESULT_FAIL_AUTHTOKEN);
  36. }
  37. }
  38. return true;
  39. }
  40. /**
  41. * Executes a method.
  42. * A method is a function which you have previously exposed using expose_function.
  43. *
  44. * @param string $method Method, e.g. "foo.bar"
  45. *
  46. * @return GenericResult The result of the execution.
  47. * @throws APIException|CallException
  48. * @access private
  49. */
  50. function execute_method($method) {
  51. global $API_METHODS;
  52. // method must be exposed
  53. if (!isset($API_METHODS[$method])) {
  54. $msg = elgg_echo('APIException:MethodCallNotImplemented', array($method));
  55. throw new APIException($msg);
  56. }
  57. // function must be callable
  58. $function = null;
  59. if (isset($API_METHODS[$method]["function"])) {
  60. $function = $API_METHODS[$method]["function"];
  61. // allow array version of static callback
  62. if (is_array($function)
  63. && isset($function[0], $function[1])
  64. && is_string($function[0])
  65. && is_string($function[1])) {
  66. $function = "{$function[0]}::{$function[1]}";
  67. }
  68. }
  69. if (!is_string($function) || !is_callable($function)) {
  70. $msg = elgg_echo('APIException:FunctionDoesNotExist', array($method));
  71. throw new APIException($msg);
  72. }
  73. // check http call method
  74. if (strcmp(get_call_method(), $API_METHODS[$method]["call_method"]) != 0) {
  75. $msg = elgg_echo('CallException:InvalidCallMethod', array($method,
  76. $API_METHODS[$method]["call_method"]));
  77. throw new CallException($msg);
  78. }
  79. $parameters = get_parameters_for_method($method);
  80. // may throw exception, which is not caught here
  81. verify_parameters($method, $parameters);
  82. $serialised_parameters = serialise_parameters($method, $parameters);
  83. // Execute function: Construct function and calling parameters
  84. $serialised_parameters = trim($serialised_parameters, ", ");
  85. // @todo remove the need for eval()
  86. $result = eval("return $function($serialised_parameters);");
  87. // Sanity check result
  88. // If this function returns an api result itself, just return it
  89. if ($result instanceof GenericResult) {
  90. return $result;
  91. }
  92. if ($result === false) {
  93. $msg = elgg_echo('APIException:FunctionParseError', array($function, $serialised_parameters));
  94. throw new APIException($msg);
  95. }
  96. if ($result === NULL) {
  97. // If no value
  98. $msg = elgg_echo('APIException:FunctionNoReturn', array($function, $serialised_parameters));
  99. throw new APIException($msg);
  100. }
  101. // Otherwise assume that the call was successful and return it as a success object.
  102. return SuccessResult::getInstance($result);
  103. }
  104. /**
  105. * Get the request method.
  106. *
  107. * @return string HTTP request method
  108. * @access private
  109. */
  110. function get_call_method() {
  111. return _elgg_services()->request->server->get('REQUEST_METHOD');
  112. }
  113. /**
  114. * This function analyses all expected parameters for a given method
  115. *
  116. * This function sanitizes the input parameters and returns them in
  117. * an associated array.
  118. *
  119. * @param string $method The method
  120. *
  121. * @return array containing parameters as key => value
  122. * @access private
  123. */
  124. function get_parameters_for_method($method) {
  125. global $API_METHODS;
  126. $sanitised = array();
  127. // if there are parameters, sanitize them
  128. if (isset($API_METHODS[$method]['parameters'])) {
  129. foreach ($API_METHODS[$method]['parameters'] as $k => $v) {
  130. $param = get_input($k); // Make things go through the sanitiser
  131. if ($param !== '' && $param !== null) {
  132. $sanitised[$k] = $param;
  133. } else {
  134. // parameter wasn't passed so check for default
  135. if (isset($v['default'])) {
  136. $sanitised[$k] = $v['default'];
  137. }
  138. }
  139. }
  140. }
  141. return $sanitised;
  142. }
  143. /**
  144. * Get POST data
  145. * Since this is called through a handler, we need to manually get the post data
  146. *
  147. * @return POST data as string encoded as multipart/form-data
  148. * @access private
  149. */
  150. function get_post_data() {
  151. $postdata = file_get_contents('php://input');
  152. return $postdata;
  153. }
  154. /**
  155. * Verify that the required parameters are present
  156. *
  157. * @param string $method Method name
  158. * @param array $parameters List of expected parameters
  159. *
  160. * @return true on success or exception
  161. * @throws APIException
  162. * @since 1.7.0
  163. * @access private
  164. */
  165. function verify_parameters($method, $parameters) {
  166. global $API_METHODS;
  167. // are there any parameters for this method
  168. if (!(isset($API_METHODS[$method]["parameters"]))) {
  169. return true; // no so return
  170. }
  171. // check that the parameters were registered correctly and all required ones are there
  172. foreach ($API_METHODS[$method]['parameters'] as $key => $value) {
  173. // this tests the expose structure: must be array to describe parameter and type must be defined
  174. if (!is_array($value) || !isset($value['type'])) {
  175. $msg = elgg_echo('APIException:InvalidParameter', array($key, $method));
  176. throw new APIException($msg);
  177. }
  178. // Check that the variable is present in the request if required
  179. if ($value['required'] && !array_key_exists($key, $parameters)) {
  180. $msg = elgg_echo('APIException:MissingParameterInMethod', array($key, $method));
  181. throw new APIException($msg);
  182. }
  183. }
  184. return true;
  185. }
  186. /**
  187. * Serialize an array of parameters for an API method call
  188. *
  189. * @param string $method API method name
  190. * @param array $parameters Array of parameters
  191. *
  192. * @return string or exception
  193. * @throws APIException
  194. * @since 1.7.0
  195. * @access private
  196. */
  197. function serialise_parameters($method, $parameters) {
  198. global $API_METHODS;
  199. // are there any parameters for this method
  200. if (!(isset($API_METHODS[$method]["parameters"]))) {
  201. return ''; // if not, return
  202. }
  203. $serialised_parameters = "";
  204. foreach ($API_METHODS[$method]['parameters'] as $key => $value) {
  205. // avoid warning on parameters that are not required and not present
  206. if (!isset($parameters[$key])) {
  207. continue;
  208. }
  209. // Set variables casting to type.
  210. switch (strtolower($value['type']))
  211. {
  212. case 'int':
  213. case 'integer' :
  214. $serialised_parameters .= "," . (int)trim($parameters[$key]);
  215. break;
  216. case 'bool':
  217. case 'boolean':
  218. // change word false to boolean false
  219. if (strcasecmp(trim($parameters[$key]), "false") == 0) {
  220. $serialised_parameters .= ',false';
  221. } else if ($parameters[$key] == 0) {
  222. $serialised_parameters .= ',false';
  223. } else {
  224. $serialised_parameters .= ',true';
  225. }
  226. break;
  227. case 'string':
  228. $serialised_parameters .= ",'" . addcslashes(trim($parameters[$key]), "'") . "'";
  229. break;
  230. case 'float':
  231. $serialised_parameters .= "," . (float)trim($parameters[$key]);
  232. break;
  233. case 'array':
  234. // we can handle an array of strings, maybe ints, definitely not booleans or other arrays
  235. if (!is_array($parameters[$key])) {
  236. $msg = elgg_echo('APIException:ParameterNotArray', array($key));
  237. throw new APIException($msg);
  238. }
  239. $array = "array(";
  240. foreach ($parameters[$key] as $k => $v) {
  241. $k = sanitise_string($k);
  242. $v = sanitise_string($v);
  243. $array .= "'$k'=>'$v',";
  244. }
  245. $array = trim($array, ",");
  246. $array .= ")";
  247. $array = ",$array";
  248. $serialised_parameters .= $array;
  249. break;
  250. default:
  251. $msg = elgg_echo('APIException:UnrecognisedTypeCast', array($value['type'], $key, $method));
  252. throw new APIException($msg);
  253. }
  254. }
  255. return $serialised_parameters;
  256. }
  257. // API authorization handlers /////////////////////////////////////////////////////////////////////
  258. /**
  259. * PAM: Confirm that the call includes a valid API key
  260. *
  261. * @return true if good API key - otherwise throws exception
  262. *
  263. * @return mixed
  264. * @throws APIException
  265. * @since 1.7.0
  266. * @access private
  267. */
  268. function api_auth_key() {
  269. global $CONFIG;
  270. // check that an API key is present
  271. $api_key = get_input('api_key');
  272. if ($api_key == "") {
  273. throw new APIException(elgg_echo('APIException:MissingAPIKey'));
  274. }
  275. // check that it is active
  276. $api_user = get_api_user($CONFIG->site_id, $api_key);
  277. if (!$api_user) {
  278. // key is not active or does not exist
  279. throw new APIException(elgg_echo('APIException:BadAPIKey'));
  280. }
  281. // can be used for keeping stats
  282. // plugin can also return false to fail this authentication method
  283. return elgg_trigger_plugin_hook('api_key', 'use', $api_key, true);
  284. }
  285. /**
  286. * PAM: Confirm the HMAC signature
  287. *
  288. * @return true if success - otherwise throws exception
  289. *
  290. * @throws SecurityException
  291. * @since 1.7.0
  292. * @access private
  293. */
  294. function api_auth_hmac() {
  295. global $CONFIG;
  296. // Get api header
  297. $api_header = get_and_validate_api_headers();
  298. // Pull API user details
  299. $api_user = get_api_user($CONFIG->site_id, $api_header->api_key);
  300. if (!$api_user) {
  301. throw new SecurityException(elgg_echo('SecurityException:InvalidAPIKey'),
  302. ErrorResult::$RESULT_FAIL_APIKEY_INVALID);
  303. }
  304. // Get the secret key
  305. $secret_key = $api_user->secret;
  306. // get the query string
  307. $query = _elgg_services()->request->server->get('REQUEST_URI');
  308. $query = substr($query, strpos($query, '?') + 1);
  309. // calculate expected HMAC
  310. $hmac = calculate_hmac( $api_header->hmac_algo,
  311. $api_header->time,
  312. $api_header->nonce,
  313. $api_header->api_key,
  314. $secret_key,
  315. $query,
  316. $api_header->method == 'POST' ? $api_header->posthash : "");
  317. if ($api_header->hmac !== $hmac) {
  318. throw new SecurityException("HMAC is invalid. {$api_header->hmac} != [calc]$hmac");
  319. }
  320. // Now make sure this is not a replay
  321. if (cache_hmac_check_replay($hmac)) {
  322. throw new SecurityException(elgg_echo('SecurityException:DupePacket'));
  323. }
  324. // Validate post data
  325. if ($api_header->method == "POST") {
  326. $postdata = get_post_data();
  327. $calculated_posthash = calculate_posthash($postdata, $api_header->posthash_algo);
  328. if (strcmp($api_header->posthash, $calculated_posthash) != 0) {
  329. $msg = elgg_echo('SecurityException:InvalidPostHash',
  330. array($calculated_posthash, $api_header->posthash));
  331. throw new SecurityException($msg);
  332. }
  333. }
  334. return true;
  335. }
  336. // HMAC /////////////////////////////////////////////////////////////////////
  337. /**
  338. * This function extracts the various header variables needed for the HMAC PAM
  339. *
  340. * @return stdClass Containing all the values.
  341. * @throws APIException Detailing any error.
  342. * @access private
  343. */
  344. function get_and_validate_api_headers() {
  345. $result = new stdClass;
  346. $result->method = get_call_method();
  347. // Only allow these methods
  348. if (($result->method != "GET") && ($result->method != "POST")) {
  349. throw new APIException(elgg_echo('APIException:NotGetOrPost'));
  350. }
  351. $server = _elgg_services()->request->server;
  352. $result->api_key = $server->get('HTTP_X_ELGG_APIKEY');
  353. if ($result->api_key == "") {
  354. throw new APIException(elgg_echo('APIException:MissingAPIKey'));
  355. }
  356. $result->hmac = $server->get('HTTP_X_ELGG_HMAC');
  357. if ($result->hmac == "") {
  358. throw new APIException(elgg_echo('APIException:MissingHmac'));
  359. }
  360. $result->hmac_algo = $server->get('HTTP_X_ELGG_HMAC_ALGO');
  361. if ($result->hmac_algo == "") {
  362. throw new APIException(elgg_echo('APIException:MissingHmacAlgo'));
  363. }
  364. $result->time = $server->get('HTTP_X_ELGG_TIME');
  365. if ($result->time == "") {
  366. throw new APIException(elgg_echo('APIException:MissingTime'));
  367. }
  368. // Must have been sent within 25 hour period.
  369. // 25 hours is more than enough to handle server clock drift.
  370. // This values determines how long the HMAC cache needs to store previous
  371. // signatures. Heavy use of HMAC is better handled with a shorter sig lifetime.
  372. // See cache_hmac_check_replay()
  373. if (($result->time < (time() - 90000)) || ($result->time > (time() + 90000))) {
  374. throw new APIException(elgg_echo('APIException:TemporalDrift'));
  375. }
  376. $result->nonce = $server->get('HTTP_X_ELGG_NONCE');
  377. if ($result->nonce == "") {
  378. throw new APIException(elgg_echo('APIException:MissingNonce'));
  379. }
  380. if ($result->method == "POST") {
  381. $result->posthash = $server->get('HTTP_X_ELGG_POSTHASH');
  382. if ($result->posthash == "") {
  383. throw new APIException(elgg_echo('APIException:MissingPOSTHash'));
  384. }
  385. $result->posthash_algo = $server->get('HTTP_X_ELGG_POSTHASH_ALGO');
  386. if ($result->posthash_algo == "") {
  387. throw new APIException(elgg_echo('APIException:MissingPOSTAlgo'));
  388. }
  389. $result->content_type = $server->get('CONTENT_TYPE');
  390. if ($result->content_type == "") {
  391. throw new APIException(elgg_echo('APIException:MissingContentType'));
  392. }
  393. }
  394. return $result;
  395. }
  396. /**
  397. * Map various algorithms to their PHP equivs.
  398. * This also gives us an easy way to disable algorithms.
  399. *
  400. * @param string $algo The algorithm
  401. *
  402. * @return string The php algorithm
  403. * @throws APIException if an algorithm is not supported.
  404. * @access private
  405. */
  406. function map_api_hash($algo) {
  407. $algo = strtolower(sanitise_string($algo));
  408. $supported_algos = array(
  409. "md5" => "md5", // @todo Consider phasing this out
  410. "sha" => "sha1", // alias for sha1
  411. "sha1" => "sha1",
  412. "sha256" => "sha256"
  413. );
  414. if (array_key_exists($algo, $supported_algos)) {
  415. return $supported_algos[$algo];
  416. }
  417. throw new APIException(elgg_echo('APIException:AlgorithmNotSupported', array($algo)));
  418. }
  419. /**
  420. * Calculate the HMAC for the http request.
  421. * This function signs an api request using the information provided. The signature returned
  422. * has been base64 encoded and then url encoded.
  423. *
  424. * @param string $algo The HMAC algorithm used
  425. * @param string $time String representation of unix time
  426. * @param string $nonce Nonce
  427. * @param string $api_key Your api key
  428. * @param string $secret_key Your private key
  429. * @param string $get_variables URLEncoded string representation of the get variable parameters,
  430. * eg "method=user&guid=2"
  431. * @param string $post_hash Optional sha1 hash of the post data.
  432. *
  433. * @return string The HMAC signature
  434. * @access private
  435. */
  436. function calculate_hmac($algo, $time, $nonce, $api_key, $secret_key,
  437. $get_variables, $post_hash = "") {
  438. global $CONFIG;
  439. elgg_log("HMAC Parts: $algo, $time, $api_key, $secret_key, $get_variables, $post_hash");
  440. $ctx = hash_init(map_api_hash($algo), HASH_HMAC, $secret_key);
  441. hash_update($ctx, trim($time));
  442. hash_update($ctx, trim($nonce));
  443. hash_update($ctx, trim($api_key));
  444. hash_update($ctx, trim($get_variables));
  445. if (trim($post_hash) != "") {
  446. hash_update($ctx, trim($post_hash));
  447. }
  448. return urlencode(base64_encode(hash_final($ctx, true)));
  449. }
  450. /**
  451. * Calculate a hash for some post data.
  452. *
  453. * @todo Work out how to handle really large bits of data.
  454. *
  455. * @param string $postdata The post data.
  456. * @param string $algo The algorithm used.
  457. *
  458. * @return string The hash.
  459. * @access private
  460. */
  461. function calculate_posthash($postdata, $algo) {
  462. $ctx = hash_init(map_api_hash($algo));
  463. hash_update($ctx, $postdata);
  464. return hash_final($ctx);
  465. }
  466. /**
  467. * This function will do two things. Firstly it verifies that a HMAC signature
  468. * hasn't been seen before, and secondly it will add the given hmac to the cache.
  469. *
  470. * @param string $hmac The hmac string.
  471. *
  472. * @return bool True if replay detected, false if not.
  473. * @access private
  474. */
  475. function cache_hmac_check_replay($hmac) {
  476. // cache lifetime is 25 hours (this should be related to the time drift
  477. // allowed in get_and_validate_headers
  478. $cache = new ElggHMACCache(90000);
  479. if (!$cache->load($hmac)) {
  480. $cache->save($hmac, $hmac);
  481. return false;
  482. }
  483. return true;
  484. }
  485. /**
  486. * Check the user token
  487. * This examines whether an authentication token is present and returns true if
  488. * it is present and is valid. The user gets logged in so with the current
  489. * session code of Elgg, that user will be logged out of all other sessions.
  490. *
  491. * @return bool
  492. * @access private
  493. */
  494. function pam_auth_usertoken() {
  495. global $CONFIG;
  496. $token = get_input('auth_token');
  497. if (!$token) {
  498. return false;
  499. }
  500. $validated_userid = validate_user_token($token, $CONFIG->site_id);
  501. if ($validated_userid) {
  502. $u = get_entity($validated_userid);
  503. // Could we get the user?
  504. if (!$u) {
  505. return false;
  506. }
  507. // Not an elgg user
  508. if ((!$u instanceof ElggUser)) {
  509. return false;
  510. }
  511. // User is banned
  512. if ($u->isBanned()) {
  513. return false;
  514. }
  515. // Fail if we couldn't log the user in
  516. if (!login($u)) {
  517. return false;
  518. }
  519. return true;
  520. }
  521. return false;
  522. }
  523. /**
  524. * See if the user has a valid login sesson
  525. *
  526. * @return bool
  527. * @access private
  528. */
  529. function pam_auth_session() {
  530. return elgg_is_logged_in();
  531. }
  532. /**
  533. * API PHP Error handler function.
  534. * This function acts as a wrapper to catch and report PHP error messages.
  535. *
  536. * @see http://uk3.php.net/set-error-handler
  537. *
  538. * @param int $errno Error number
  539. * @param string $errmsg Human readable message
  540. * @param string $filename Filename
  541. * @param int $linenum Line number
  542. * @param array $vars Vars
  543. *
  544. * @return void
  545. * @access private
  546. *
  547. * @throws Exception
  548. */
  549. function _php_api_error_handler($errno, $errmsg, $filename, $linenum, $vars) {
  550. global $ERRORS;
  551. $error = date("Y-m-d H:i:s (T)") . ": \"" . $errmsg . "\" in file "
  552. . $filename . " (line " . $linenum . ")";
  553. switch ($errno) {
  554. case E_USER_ERROR:
  555. error_log("ERROR: " . $error);
  556. $ERRORS[] = "ERROR: " . $error;
  557. // Since this is a fatal error, we want to stop any further execution but do so gracefully.
  558. throw new Exception("ERROR: " . $error);
  559. break;
  560. case E_WARNING :
  561. case E_USER_WARNING :
  562. error_log("WARNING: " . $error);
  563. $ERRORS[] = "WARNING: " . $error;
  564. break;
  565. default:
  566. error_log("DEBUG: " . $error);
  567. $ERRORS[] = "DEBUG: " . $error;
  568. }
  569. }
  570. /**
  571. * API PHP Exception handler.
  572. * This is a generic exception handler for PHP exceptions. This will catch any
  573. * uncaught exception, end API execution and return the result to the requestor
  574. * as an ErrorResult in the requested format.
  575. *
  576. * @param Exception $exception Exception
  577. *
  578. * @return void
  579. * @access private
  580. */
  581. function _php_api_exception_handler($exception) {
  582. error_log("*** FATAL EXCEPTION (API) *** : " . $exception);
  583. $code = $exception->getCode() == 0 ? ErrorResult::$RESULT_FAIL : $exception->getCode();
  584. $result = new ErrorResult($exception->getMessage(), $code, NULL);
  585. echo elgg_view_page($exception->getMessage(), elgg_view("api/output", array("result" => $result)));
  586. }
  587. /**
  588. * Services handler - turns request over to the registered handler
  589. * If no handler is found, this returns a 404 error
  590. *
  591. * @param string $handler Handler name
  592. * @param array $request Request string
  593. *
  594. * @return void
  595. * @access private
  596. */
  597. function service_handler($handler, $request) {
  598. global $CONFIG;
  599. elgg_set_context('api');
  600. $request = explode('/', $request);
  601. // after the handler, the first identifier is response format
  602. // ex) http://example.org/services/api/rest/json/?method=test
  603. $response_format = array_shift($request);
  604. if (!$response_format) {
  605. $response_format = 'json';
  606. }
  607. if (!ctype_alpha($response_format)) {
  608. header("HTTP/1.0 400 Bad Request");
  609. header("Content-type: text/plain");
  610. echo "Invalid format.";
  611. exit;
  612. }
  613. elgg_set_viewtype($response_format);
  614. if (!isset($CONFIG->servicehandler) || empty($handler)) {
  615. // no handlers set or bad url
  616. header("HTTP/1.0 404 Not Found");
  617. exit;
  618. } else if (isset($CONFIG->servicehandler[$handler]) && is_callable($CONFIG->servicehandler[$handler])) {
  619. $function = $CONFIG->servicehandler[$handler];
  620. call_user_func($function, $request, $handler);
  621. } else {
  622. // no handler for this web service
  623. header("HTTP/1.0 404 Not Found");
  624. exit;
  625. }
  626. }