Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

_php_api_error_handler()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 4
nop 5
dl 0
loc 24
ccs 0
cts 12
cp 0
crap 20
rs 8.6845
c 0
b 0
f 0
1
<?php
2
/**
3
 * Elgg web services API library
4
 * Functions and objects for exposing custom web services.
5
 *
6
 */
7
8
/**
9
 * Check that the method call has the proper API and user authentication
10
 *
11
 * @param string $method The api name that was exposed
12
 *
13
 * @return true or throws an exception
14
 * @throws APIException
15
 * @since 1.7.0
16
 * @access private
17
 */
18
function authenticate_method($method) {
19 4
	global $API_METHODS;
20
21
	// method must be exposed
22 4
	if (!isset($API_METHODS[$method])) {
23 1
		throw new APIException(elgg_echo('APIException:MethodCallNotImplemented', [$method]));
24
	}
25
26
	// check API authentication if required
27 3
	if ($API_METHODS[$method]["require_api_auth"] == true) {
28 1
		$api_pam = new ElggPAM('api');
29 1
		if ($api_pam->authenticate() !== true) {
30 1
			throw new APIException(elgg_echo('APIException:APIAuthenticationFailed'));
31
		}
32
	}
33
34 2
	$user_pam = new ElggPAM('user');
35 2
	$user_auth_result = $user_pam->authenticate([]);
36
37
	// check if user authentication is required
38 2
	if ($API_METHODS[$method]["require_user_auth"] == true) {
39 1
		if ($user_auth_result == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
40 1
			throw new APIException($user_pam->getFailureMessage(), ErrorResult::$RESULT_FAIL_AUTHTOKEN);
41
		}
42
	}
43
44 1
	return true;
45
}
46
47
/**
48
 * Executes a method.
49
 * A method is a function which you have previously exposed using expose_function.
50
 *
51
 * @param string $method Method, e.g. "foo.bar"
52
 *
53
 * @return GenericResult The result of the execution.
54
 * @throws APIException|CallException
55
 * @access private
56
 */
57
function execute_method($method) {
58 5
	global $API_METHODS;
59
60
	// method must be exposed
61 5
	if (!isset($API_METHODS[$method])) {
62 1
		$msg = elgg_echo('APIException:MethodCallNotImplemented', [$method]);
63 1
		throw new APIException($msg);
64
	}
65
66
	// function must be callable
67 4
	$function = elgg_extract('function', $API_METHODS[$method]);
68 4
	if (!$function || !is_callable($function)) {
69 1
		$msg = elgg_echo('APIException:FunctionDoesNotExist', [$method]);
70 1
		throw new APIException($msg);
71
	}
72
73
	// check http call method
74 3
	if (strcmp(get_call_method(), $API_METHODS[$method]["call_method"]) != 0) {
75 1
		$msg = elgg_echo('CallException:InvalidCallMethod', [$method,
76 1
		$API_METHODS[$method]["call_method"]]);
77 1
		throw new CallException($msg);
78
	}
79
80 2
	$parameters = get_parameters_for_method($method);
81
82
	// may throw exception, which is not caught here
83 2
	verify_parameters($method, $parameters);
84
85 2
	$serialised_parameters = serialise_parameters($method, $parameters);
86
87
	// Execute function: Construct function and calling parameters
88 2
	$serialised_parameters = trim($serialised_parameters, ", ");
89
90
	// Sadly we probably can't get rid of this eval() in 2.x. Doing so would involve
91
	// replacing serialise_parameters(), which does a bunch of weird stuff we need to
92
	// stay BC with 2.x. There are tests for a lot of these quirks in ElggCoreWebServicesApiTest
93
	// particularly in testSerialiseParametersCasting().
94 2
	$arguments = eval("return [$serialised_parameters];");
1 ignored issue
show
introduced by
The use of eval() is discouraged.
Loading history...
95
96 2
	if ($API_METHODS[$method]['assoc']) {
97 1
		$argument = array_combine(_elgg_ws_get_parameter_names($method), $arguments);
98 1
		$result = call_user_func($function, $argument);
99
	} else {
100 1
		$result = call_user_func_array($function, $arguments);
101
	}
102
103 2
	$result = elgg_trigger_plugin_hook('rest:output', $method, $parameters, $result);
104
	
105
	// Sanity check result
106
	// If this function returns an api result itself, just return it
107 2
	if ($result instanceof GenericResult) {
108
		return $result;
109
	}
110
111 2
	if ($result === false) {
112
		$msg = elgg_echo('APIException:FunctionParseError', [$function, $serialised_parameters]);
113
		throw new APIException($msg);
114
	}
115
116 2
	if ($result === null) {
117
		// If no value
118
		$msg = elgg_echo('APIException:FunctionNoReturn', [$function, $serialised_parameters]);
119
		throw new APIException($msg);
120
	}
121
122
	// Otherwise assume that the call was successful and return it as a success object.
123 2
	return SuccessResult::getInstance($result);
124
}
125
126
/**
127
 * Get the request method.
128
 *
129
 * @return string HTTP request method
130
 * @access private
131
 */
132
function get_call_method() {
133 23
	return _elgg_services()->request->server->get('REQUEST_METHOD');
134
}
135
136
/**
137
 * This function analyses all expected parameters for a given method
138
 *
139
 * This function sanitizes the input parameters and returns them in
140
 * an associated array.
141
 *
142
 * @param string $method The method
143
 *
144
 * @return array containing parameters as key => value
145
 * @access private
146
 */
147
function get_parameters_for_method($method) {
148 2
	global $API_METHODS;
149
150 2
	$sanitised = [];
151
152
	// if there are parameters, sanitize them
153 2
	if (isset($API_METHODS[$method]['parameters'])) {
154 2
		foreach ($API_METHODS[$method]['parameters'] as $k => $v) {
155 2
			$param = get_input($k); // Make things go through the sanitiser
156 2
			if ($param !== '' && $param !== null) {
157 2
				$sanitised[$k] = $param;
158
			} else {
159
				// parameter wasn't passed so check for default
160 2
				if (isset($v['default'])) {
161 2
					$sanitised[$k] = $v['default'];
162
				}
163
			}
164
		}
165
	}
166
167 2
	return $sanitised;
168
}
169
170
/**
171
 * Get POST data
172
 * Since this is called through a handler, we need to manually get the post data
173
 *
174
 * @return POST data as string encoded as multipart/form-data
0 ignored issues
show
Bug introduced by
The type POST was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
175
 * @access private
176
 */
177
function get_post_data() {
178
179
	$postdata = file_get_contents('php://input');
180
181
	return $postdata;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $postdata returns the type string which is incompatible with the documented return type POST.
Loading history...
182
}
183
184
/**
185
 * Verify that the required parameters are present
186
 *
187
 * @param string $method     Method name
188
 * @param array  $parameters List of expected parameters
189
 *
190
 * @return true on success or exception
191
 * @throws APIException
192
 * @since 1.7.0
193
 * @access private
194
 */
195
function verify_parameters($method, $parameters) {
196 5
	global $API_METHODS;
197
198
	// are there any parameters for this method
199 5
	if (!(isset($API_METHODS[$method]["parameters"]))) {
200
		return true; // no so return
201
	}
202
203
	// check that the parameters were registered correctly and all required ones are there
204 5
	foreach ($API_METHODS[$method]['parameters'] as $key => $value) {
205
		// this tests the expose structure: must be array to describe parameter and type must be defined
206 5
		if (!is_array($value) || !isset($value['type'])) {
207 1
			$msg = elgg_echo('APIException:InvalidParameter', [$key, $method]);
208 1
			throw new APIException($msg);
209
		}
210
211
		// Check that the variable is present in the request if required
212 4
		if ($value['required'] && !array_key_exists($key, $parameters)) {
213 1
			$msg = elgg_echo('APIException:MissingParameterInMethod', [$key, $method]);
214 4
			throw new APIException($msg);
215
		}
216
	}
217
218 3
	return true;
219
}
220
221
/**
222
 * Get the names of a method's parameters
223
 *
224
 * @param string $method the api method to get the params for
225
 * @return string[]
226
 * @access private
227
 */
228
function _elgg_ws_get_parameter_names($method) {
229 1
	global $API_METHODS;
230
231 1
	if (!isset($API_METHODS[$method]["parameters"])) {
232
		return [];
233
	}
234
235 1
	return array_keys($API_METHODS[$method]["parameters"]);
236
}
237
238
/**
239
 * Serialize an array of parameters for an API method call, applying transformations
240
 * to values depending on the declared parameter type, and returning a string of PHP
241
 * code representing the contents of a PHP array literal.
242
 *
243
 * A leading comma needs to be removed from the output.
244
 *
245
 * @see \ElggCoreWebServicesApiTest::testSerialiseParametersCasting
246
 *
247
 * @param string $method     API method name
248
 * @param array  $parameters Array of parameters
249
 *
250
 * @return string or exception E.g. ',"foo",2.1'
251
 * @throws APIException
252
 * @since 1.7.0
253
 * @access private
254
 *
255
 * @todo in 3.0 this should return an array of parameter values instead of a string of code.
256
 */
257
function serialise_parameters($method, $parameters) {
258 4
	global $API_METHODS;
259
260
	// are there any parameters for this method
261 4
	if (!(isset($API_METHODS[$method]["parameters"]))) {
262
		return ''; // if not, return
263
	}
264
265 4
	$serialised_parameters = "";
266 4
	foreach ($API_METHODS[$method]['parameters'] as $key => $value) {
267
		// avoid warning on parameters that are not required and not present
268 4
		if (!isset($parameters[$key])) {
269 3
			$serialised_parameters .= ',null';
270 3
			continue;
271
		}
272
273
		// Set variables casting to type.
274 4
		switch (strtolower($value['type'])) {
275
			case 'int':
276
			case 'integer' :
277 4
				$serialised_parameters .= "," . (int) trim($parameters[$key]);
278 4
				break;
279
			case 'bool':
280
			case 'boolean':
281
				// change word false to boolean false
282 4
				if (strcasecmp(trim($parameters[$key]), "false") == 0) {
283 1
					$serialised_parameters .= ',false';
284 4
				} else if ($parameters[$key] == 0) {
285 2
					$serialised_parameters .= ',false';
286
				} else {
287 3
					$serialised_parameters .= ',true';
288
				}
289
290 4
				break;
291
			case 'string':
292 2
				$serialised_parameters .= ',' . var_export(trim($parameters[$key]), true);
293 2
				break;
294
			case 'float':
295 2
				$serialised_parameters .= "," . (float) trim($parameters[$key]);
296 2
				break;
297
			case 'array':
298
				// we can handle an array of strings, maybe ints, definitely not booleans or other arrays
299 2
				if (!is_array($parameters[$key])) {
300
					$msg = elgg_echo('APIException:ParameterNotArray', [$key]);
301
					throw new APIException($msg);
302
				}
303
304 2
				$array = "array(";
305
306 2
				foreach ($parameters[$key] as $k => $v) {
307
					// This is using sanitise_string() to escape characters to be inside a
308
					// single-quoted string literal in PHP code. Not sure what we have to do
309
					// to keep this safe in 3.0...
310 2
					$k = sanitise_string($k);
1 ignored issue
show
Deprecated Code introduced by
The function sanitise_string() has been deprecated: Use query parameters where possible ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

310
					$k = /** @scrutinizer ignore-deprecated */ sanitise_string($k);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
311 2
					$v = sanitise_string($v);
1 ignored issue
show
Deprecated Code introduced by
The function sanitise_string() has been deprecated: Use query parameters where possible ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

311
					$v = /** @scrutinizer ignore-deprecated */ sanitise_string($v);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
312
313 2
					$array .= "'$k'=>'$v',";
314
				}
315
316 2
				$array = trim($array, ",");
317
318 2
				$array .= ")";
319 2
				$array = ",$array";
320
321 2
				$serialised_parameters .= $array;
322 2
				break;
323
			default:
324 1
				$msg = elgg_echo('APIException:UnrecognisedTypeCast', [$value['type'], $key, $method]);
325 4
				throw new APIException($msg);
326
		}
327
	}
328
329 4
	return $serialised_parameters;
330
}
331
332
// API authorization handlers /////////////////////////////////////////////////////////////////////
333
334
/**
335
 * PAM: Confirm that the call includes a valid API key
336
 *
337
 * @return bool true if good API key - otherwise throws exception
338
 * @throws APIException
339
 * @since 1.7.0
340
 * @access private
341
 */
342
function api_auth_key() {
343
	// check that an API key is present
344 2
	$api_key = get_input('api_key');
345 2
	if ($api_key == "") {
346 1
		throw new APIException(elgg_echo('APIException:MissingAPIKey'));
347
	}
348
349
	// check that it is active
350 1
	$api_user = get_api_user(elgg_get_site_entity()->guid, $api_key);
0 ignored issues
show
Unused Code introduced by
The call to get_api_user() has too many arguments starting with $api_key. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

350
	$api_user = /** @scrutinizer ignore-call */ get_api_user(elgg_get_site_entity()->guid, $api_key);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
351 1
	if (!$api_user) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $api_user of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
352
		// key is not active or does not exist
353 1
		throw new APIException(elgg_echo('APIException:BadAPIKey'));
354
	}
355
356
	// can be used for keeping stats
357
	// plugin can also return false to fail this authentication method
358
	return elgg_trigger_plugin_hook('api_key', 'use', $api_key, true);
359
}
360
361
/**
362
 * PAM: Confirm the HMAC signature
363
 *
364
 * @return true if success - otherwise throws exception
365
 *
366
 * @throws SecurityException
367
 * @since 1.7.0
368
 * @access private
369
 */
370
function api_auth_hmac() {
371
	// Get api header
372
	$api_header = get_and_validate_api_headers();
373
374
	// Pull API user details
375
	$api_user = get_api_user(elgg_get_site_entity()->guid, $api_header->api_key);
0 ignored issues
show
Unused Code introduced by
The call to get_api_user() has too many arguments starting with $api_header->api_key. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

375
	$api_user = /** @scrutinizer ignore-call */ get_api_user(elgg_get_site_entity()->guid, $api_header->api_key);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
376
377
	if (!$api_user) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $api_user of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
378
		throw new SecurityException(elgg_echo('SecurityException:InvalidAPIKey'),
379
		ErrorResult::$RESULT_FAIL_APIKEY_INVALID);
380
	}
381
382
	// Get the secret key
383
	$secret_key = $api_user->secret;
384
385
	// get the query string
386
	$query = _elgg_services()->request->server->get('REQUEST_URI');
387
	$query = substr($query, strpos($query, '?') + 1);
388
389
	// calculate expected HMAC
390
	$hmac = calculate_hmac(	$api_header->hmac_algo,
391
							$api_header->time,
392
							$api_header->nonce,
393
							$api_header->api_key,
394
							$secret_key,
395
							$query,
396
							$api_header->method == 'POST' ? $api_header->posthash : "");
397
398
399
	if ($api_header->hmac !== $hmac) {
400
		throw new SecurityException("HMAC is invalid.  {$api_header->hmac} != [calc]$hmac");
401
	}
402
403
	// Now make sure this is not a replay
404
	if (cache_hmac_check_replay($hmac)) {
405
		throw new SecurityException(elgg_echo('SecurityException:DupePacket'));
406
	}
407
408
	// Validate post data
409
	if ($api_header->method == "POST") {
410
		$postdata = get_post_data();
411
		$calculated_posthash = calculate_posthash($postdata, $api_header->posthash_algo);
412
413
		if (strcmp($api_header->posthash, $calculated_posthash) != 0) {
414
			$msg = elgg_echo('SecurityException:InvalidPostHash',
415
			[$calculated_posthash, $api_header->posthash]);
416
417
			throw new SecurityException($msg);
418
		}
419
	}
420
421
	return true;
422
}
423
424
// HMAC /////////////////////////////////////////////////////////////////////
425
426
/**
427
 * This function extracts the various header variables needed for the HMAC PAM
428
 *
429
 * @return stdClass Containing all the values.
430
 * @throws APIException Detailing any error.
431
 * @access private
432
 */
433
function get_and_validate_api_headers() {
434
	$result = new stdClass;
435
436
	$result->method = get_call_method();
437
	// Only allow these methods
438
	if (($result->method != "GET") && ($result->method != "POST")) {
439
		throw new APIException(elgg_echo('APIException:NotGetOrPost'));
440
	}
441
442
	$server = _elgg_services()->request->server;
443
444
	$result->api_key = $server->get('HTTP_X_ELGG_APIKEY');
445
	if ($result->api_key == "") {
446
		throw new APIException(elgg_echo('APIException:MissingAPIKey'));
447
	}
448
449
	$result->hmac = $server->get('HTTP_X_ELGG_HMAC');
450
	if ($result->hmac == "") {
451
		throw new APIException(elgg_echo('APIException:MissingHmac'));
452
	}
453
454
	$result->hmac_algo = $server->get('HTTP_X_ELGG_HMAC_ALGO');
455
	if ($result->hmac_algo == "") {
456
		throw new APIException(elgg_echo('APIException:MissingHmacAlgo'));
457
	}
458
459
	$result->time = $server->get('HTTP_X_ELGG_TIME');
460
	if ($result->time == "") {
461
		throw new APIException(elgg_echo('APIException:MissingTime'));
462
	}
463
464
	// Must have been sent within 25 hour period.
465
	// 25 hours is more than enough to handle server clock drift.
466
	// This values determines how long the HMAC cache needs to store previous
467
	// signatures. Heavy use of HMAC is better handled with a shorter sig lifetime.
468
	// See cache_hmac_check_replay()
469
	if (($result->time < (time() - 90000)) || ($result->time > (time() + 90000))) {
470
		throw new APIException(elgg_echo('APIException:TemporalDrift'));
471
	}
472
473
	$result->nonce = $server->get('HTTP_X_ELGG_NONCE');
474
	if ($result->nonce == "") {
475
		throw new APIException(elgg_echo('APIException:MissingNonce'));
476
	}
477
478
	if ($result->method == "POST") {
479
		$result->posthash = $server->get('HTTP_X_ELGG_POSTHASH');
480
		if ($result->posthash == "") {
481
			throw new APIException(elgg_echo('APIException:MissingPOSTHash'));
482
		}
483
484
		$result->posthash_algo = $server->get('HTTP_X_ELGG_POSTHASH_ALGO');
485
		if ($result->posthash_algo == "") {
486
			throw new APIException(elgg_echo('APIException:MissingPOSTAlgo'));
487
		}
488
489
		$result->content_type = $server->get('CONTENT_TYPE');
490
		if ($result->content_type == "") {
491
			throw new APIException(elgg_echo('APIException:MissingContentType'));
492
		}
493
	}
494
495
	return $result;
496
}
497
498
/**
499
 * Map various algorithms to their PHP equivs.
500
 * This also gives us an easy way to disable algorithms.
501
 *
502
 * @param string $algo The algorithm
503
 *
504
 * @return string The php algorithm
505
 * @throws APIException if an algorithm is not supported.
506
 * @access private
507
 */
508
function map_api_hash($algo) {
509
	$algo = strtolower(sanitise_string($algo));
0 ignored issues
show
Deprecated Code introduced by
The function sanitise_string() has been deprecated: Use query parameters where possible ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

509
	$algo = strtolower(/** @scrutinizer ignore-deprecated */ sanitise_string($algo));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
510
	$supported_algos = [
511
		"md5" => "md5",	// @todo Consider phasing this out
512
		"sha" => "sha1", // alias for sha1
513
		"sha1" => "sha1",
514
		"sha256" => "sha256"
515
	];
516
517
	if (array_key_exists($algo, $supported_algos)) {
518
		return $supported_algos[$algo];
519
	}
520
521
	throw new APIException(elgg_echo('APIException:AlgorithmNotSupported', [$algo]));
522
}
523
524
/**
525
 * Calculate the HMAC for the http request.
526
 * This function signs an api request using the information provided. The signature returned
527
 * has been base64 encoded and then url encoded.
528
 *
529
 * @param string $algo          The HMAC algorithm used
530
 * @param string $time          String representation of unix time
531
 * @param string $nonce         Nonce
532
 * @param string $api_key       Your api key
533
 * @param string $secret_key    Your private key
534
 * @param string $get_variables URLEncoded string representation of the get variable parameters,
535
 *                              eg "method=user&guid=2"
536
 * @param string $post_hash     Optional sha1 hash of the post data.
537
 *
538
 * @return string The HMAC signature
539
 * @access private
540
 */
541
function calculate_hmac($algo, $time, $nonce, $api_key, $secret_key,
542
$get_variables, $post_hash = "") {
543
544
	elgg_log("HMAC Parts: $algo, $time, $api_key, $secret_key, $get_variables, $post_hash");
545
546
	$ctx = hash_init(map_api_hash($algo), HASH_HMAC, $secret_key);
547
548
	hash_update($ctx, trim($time));
549
	hash_update($ctx, trim($nonce));
550
	hash_update($ctx, trim($api_key));
551
	hash_update($ctx, trim($get_variables));
552
	if (trim($post_hash) != "") {
553
		hash_update($ctx, trim($post_hash));
554
	}
555
556
	return urlencode(base64_encode(hash_final($ctx, true)));
557
}
558
559
/**
560
 * Calculate a hash for some post data.
561
 *
562
 * @todo Work out how to handle really large bits of data.
563
 *
564
 * @param string $postdata The post data.
565
 * @param string $algo     The algorithm used.
566
 *
567
 * @return string The hash.
568
 * @access private
569
 */
570
function calculate_posthash($postdata, $algo) {
571
	$ctx = hash_init(map_api_hash($algo));
572
573
	hash_update($ctx, $postdata);
574
575
	return hash_final($ctx);
576
}
577
578
/**
579
 * This function will do two things. Firstly it verifies that a HMAC signature
580
 * hasn't been seen before, and secondly it will add the given hmac to the cache.
581
 *
582
 * @param string $hmac The hmac string.
583
 *
584
 * @return bool True if replay detected, false if not.
585
 * @access private
586
 */
587
function cache_hmac_check_replay($hmac) {
588
	// cache lifetime is 25 hours (this should be related to the time drift
589
	// allowed in get_and_validate_headers
590
	$cache = new ElggHMACCache(90000);
591
592
	if (!$cache->load($hmac)) {
593
		$cache->save($hmac, $hmac);
594
595
		return false;
596
	}
597
598
	return true;
599
}
600
601
/**
602
 * Check the user token
603
 * This examines whether an authentication token is present and returns true if
604
 * it is present and is valid. The user gets logged in so with the current
605
 * session code of Elgg, that user will be logged out of all other sessions.
606
 *
607
 * @return bool
608
 * @access private
609
 */
610
function pam_auth_usertoken() {
611
	$token = get_input('auth_token');
612
	if (!$token) {
613
		return false;
614
	}
615
	
616
	$validated_userid = validate_user_token($token, elgg_get_site_entity()->guid);
0 ignored issues
show
Unused Code introduced by
The call to validate_user_token() has too many arguments starting with elgg_get_site_entity()->guid. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

616
	$validated_userid = /** @scrutinizer ignore-call */ validate_user_token($token, elgg_get_site_entity()->guid);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
617
618
	if ($validated_userid) {
619
		$u = get_entity($validated_userid);
620
621
		// Could we get the user?
622
		if (!$u) {
623
			return false;
624
		}
625
626
		// Not an elgg user
627
		if ((!$u instanceof ElggUser)) {
628
			return false;
629
		}
630
631
		// User is banned
632
		if ($u->isBanned()) {
633
			return false;
634
		}
635
636
		// Fail if we couldn't log the user in
637
		if (!login($u)) {
638
			return false;
639
		}
640
641
		return true;
642
	}
643
644
	return false;
645
}
646
647
/**
648
 * See if the user has a valid login sesson
649
 *
650
 * @return bool
651
 * @access private
652
 */
653
function pam_auth_session() {
654
	return elgg_is_logged_in();
655
}
656
657
/**
658
 * API PHP Error handler function.
659
 * This function acts as a wrapper to catch and report PHP error messages.
660
 *
661
 * @see http://uk3.php.net/set-error-handler
662
 *
663
 * @param int    $errno    Error number
664
 * @param string $errmsg   Human readable message
665
 * @param string $filename Filename
666
 * @param int    $linenum  Line number
667
 * @param array  $vars     Vars
668
 *
669
 * @return void
670
 * @access private
671
 *
672
 * @throws Exception
673
 */
674
function _php_api_error_handler($errno, $errmsg, $filename, $linenum, $vars) {
675
	global $ERRORS;
676
677
	$error = date("Y-m-d H:i:s (T)") . ": \"" . $errmsg . "\" in file "
678
	. $filename . " (line " . $linenum . ")";
679
680
	switch ($errno) {
681
		case E_USER_ERROR:
682
			error_log("ERROR: " . $error);
683
			$ERRORS[] = "ERROR: " . $error;
684
685
			// Since this is a fatal error, we want to stop any further execution but do so gracefully.
686
			throw new Exception("ERROR: " . $error);
687
			break;
688
689
		case E_WARNING :
690
		case E_USER_WARNING :
691
			error_log("WARNING: " . $error);
692
			$ERRORS[] = "WARNING: " . $error;
693
			break;
694
695
		default:
696
			error_log("DEBUG: " . $error);
697
			$ERRORS[] = "DEBUG: " . $error;
698
	}
699
}
700
701
/**
702
 * API PHP Exception handler.
703
 * This is a generic exception handler for PHP exceptions. This will catch any
704
 * uncaught exception, end API execution and return the result to the requestor
705
 * as an ErrorResult in the requested format.
706
 *
707
 * @param Exception $exception Exception
708
 *
709
 * @return void
710
 * @access private
711
 */
712
function _php_api_exception_handler($exception) {
713
714
	error_log("*** FATAL EXCEPTION (API) *** : " . $exception);
715
716
	$code   = $exception->getCode() == 0 ? ErrorResult::$RESULT_FAIL : $exception->getCode();
717
	$result = new ErrorResult($exception->getMessage(), $code, null);
718
719
	echo elgg_view_page($exception->getMessage(), elgg_view("api/output", ["result" => $result]));
720
}
721
722
723
/**
724
 * Services handler - turns request over to the registered handler
725
 * If no handler is found, this returns a 404 error
726
 *
727
 * @param string $handler Handler name
728
 * @param array  $request Request string
729
 *
730
 * @return void
731
 * @access private
732
 */
733
function service_handler($handler, $request) {
734
	elgg_set_context('api');
735
736
	$request = explode('/', $request);
0 ignored issues
show
Bug introduced by
$request of type array is incompatible with the type string expected by parameter $string of explode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

736
	$request = explode('/', /** @scrutinizer ignore-type */ $request);
Loading history...
737
738
	// after the handler, the first identifier is response format
739
	// ex) http://example.org/services/api/rest/json/?method=test
740
	$response_format = array_shift($request);
741
	if (!$response_format) {
742
		$response_format = 'json';
743
	}
744
745
	if (!ctype_alpha($response_format)) {
746
		header("HTTP/1.0 400 Bad Request");
747
		header("Content-type: text/plain");
748
		echo "Invalid format.";
749
		exit;
1 ignored issue
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
750
	}
751
752
	elgg_set_viewtype($response_format);
753
754
	$servicehandler = _elgg_config()->servicehandler;
755
756
	if (!isset($servicehandler) || empty($handler)) {
757
		// no handlers set or bad url
758
		header("HTTP/1.0 404 Not Found");
759
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
760
	} else if (isset($servicehandler[$handler]) && is_callable($servicehandler[$handler])) {
761
		$function = $servicehandler[$handler];
762
		call_user_func($function, $request, $handler);
763
	} else {
764
		// no handler for this web service
765
		header("HTTP/1.0 404 Not Found");
766
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
767
	}
768
}
769