Files
resourcespace/include/api_functions.php
2025-07-18 16:20:14 +07:00

309 lines
9.8 KiB
PHP

<?php
/*
* API v2 functions
*
* Montala Limited, July 2016
*
* For documentation please see: http://www.resourcespace.com/knowledge-base/api/
*
*/
global $iiif_enabled;
if ($iiif_enabled) {
include_once __DIR__ . '/iiif_functions.php';
}
/**
* Return a private scramble key for this user.
*
* @param integer $user The user ID
* @return string|false
*/
function get_api_key($user)
{
global $api_scramble_key;
return hash("sha256", $user . $api_scramble_key);
}
/**
* Check a query is signed correctly.
*
* @param string $username The username of the calling user
* @param string $querystring The query being passed to the API
* @param string $sign The signature to check
* @param string $authmode The type of key being provided (user key or session key)
*/
function check_api_key($username, $querystring, $sign, $authmode = "userkey"): bool
{
// Fetch user ID and API key
$user = get_user_by_username($username);
if ($user === false) {
return false;
}
$aj = strpos($querystring, "&ajax=");
if ($aj !== false) {
$querystring = substr($querystring, 0, $aj);
}
if ($authmode == "sessionkey") {
$userkey = get_session_api_key($user);
} else {
$userkey = get_api_key($user);
}
# Calculate the expected signature and check it matches
$expected = hash("sha256", $userkey . $querystring);
if ($expected === $sign) {
return true;
}
# Also try matching against the username - allows remote API use without knowing the user ID, e.g. in the event of managing multiple systems each with a common username but different ID.
if (hash("sha256", get_api_key($username) . $querystring) === $sign) {
return true;
}
return false;
}
/**
* Execute the specified API function.
*
* @param string $query The query string passed to the API
* @param boolean $pretty Should the JSON encoded result be 'pretty' i.e. formatted for reading?
* @return bool|string
*/
function execute_api_call($query, $pretty = false)
{
$params = [];
parse_str($query, $params);
if (!array_key_exists("function", $params)) {
return false;
}
$function = $params["function"];
if (!function_exists("api_" . $function)) {
return false;
}
global $lang;
// Check if this is a whitelisted function for browser use (native mode bypasses $enable_remote_apis=false;)
if (
defined("API_AUTHMODE_NATIVE")
&& !in_array($function, API_NATIVE_WHITELIST)
) {
ajax_unauthorized();
}
// Construct an array of the real params, setting default values as necessary
$setparams = [];
$n = 0;
$fct = new ReflectionFunction("api_" . $function);
foreach ($fct->getParameters() as $fparam) {
$paramkey = $n + 1;
$param_name = $fparam->getName();
debug("API: Checking for parameter " . $param_name . " (param" . $paramkey . ")");
if (array_key_exists("param" . $paramkey, $params)) {
debug("API: " . $param_name . " - value has been passed : '" . $params["param" . $paramkey] . "'");
$setparams[$n] = $params["param" . $paramkey];
} elseif (array_key_exists($param_name, $params)) {
debug("API: {$param_name} - value has been passed (by name): '" . json_encode($params[$param_name]) . "'");
// Check if array;
$type = $fparam->getType();
if (gettype($type) == "object") {
// type is an object
$type = $type->getName();
}
if ($fparam->hasType() && gettype($type) == "string" && $type == "array" && !is_array($params[$param_name])) {
// Decode as must be json encoded if array
$GLOBALS["use_error_exception"] = true;
try {
$decoded = json_decode($params[$param_name], JSON_OBJECT_AS_ARRAY);
} catch (Exception $e) {
$error = str_replace(
array("%arg", "%expected-type", "%type"),
array($param_name, "array (json encoded)",$lang['unknown']),
$lang["error-type-mismatch"]
);
return json_encode($error);
}
unset($GLOBALS["use_error_exception"]);
// Check passed data type after decode
if (gettype($decoded) != "array") {
$error = str_replace(
array("%arg", "%expected-type", "%type"),
array($param_name, "array (json encoded)", $lang['unknown']),
$lang["error-type-mismatch"]
);
return json_encode($error);
}
$params[$param_name] = $decoded;
}
$setparams[$n] = $params[$param_name];
} elseif ($fparam->isOptional()) {
// Set default value if nothing passed e.g. from API test tool
debug("API: " . $param_name . " - setting default value = '" . $fparam->getDefaultValue() . "'");
$setparams[$n] = $fparam->getDefaultValue();
} else {
// Set as empty
debug("API: {$param_name} - setting empty value");
$setparams[$n] = "";
}
$n++;
}
debug("API: calling api_" . $function);
$result = call_user_func_array("api_" . $function, $setparams);
if ($pretty) {
debug("API: json_encode() using JSON_PRETTY_PRINT");
$json_encoded_result = json_encode($result, (defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0));
} else {
debug("API: json_encode()");
$json_encoded_result = json_encode($result);
}
if (json_last_error() !== JSON_ERROR_NONE) {
debug("API: JSON error: " . json_last_error_msg());
debug("API: JSON error when \$result = " . print_r($result, true));
$json_encoded_result = json_encode($result, JSON_UNESCAPED_UNICODE);
}
return $json_encoded_result;
}
/**
* Return the session specific key for the given user.
*
* @param integer $user The user ID
* @return string
*/
function get_session_api_key($user)
{
global $scramble_key;
$private_key = get_api_key($user);
$usersession = ps_value("SELECT session value FROM user where ref = ?", array("i",$user), "");
return hash_hmac("sha256", "{$usersession}{$private_key}", $scramble_key);
}
/**
* API login function
*
* @param string $username Username
* @param string $password Password to validate
* @return string|false FALSE if invalid, session API key if valid
*/
function api_login($username, $password)
{
global $session_hash, $scramble_key;
$user = get_user_by_username($username);
if ($user === false) {
return false;
}
$result = perform_login($username, $password);
$private_key = get_api_key($user);
if ((bool)$result['valid']) {
return hash_hmac("sha256", "{$session_hash}{$private_key}", $scramble_key);
}
return false;
}
/**
* Validate URL supplied in APIs create resource or upload by URL. Requires the URL hostname to be added in config $api_upload_urls
*
* @param string $url The full URL.
*
* @return bool Returns true if a valid URL is found.
*/
function api_validate_upload_url($url)
{
$url = filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
if ($url === false) {
return false;
}
$url_parts = parse_url($url);
if (in_array($url_parts['scheme'], BLOCKED_STREAM_WRAPPERS)) {
return false;
}
global $api_upload_urls;
if (!isset($api_upload_urls)) {
return true; // For systems prior to this config.
}
if (in_array($url_parts['host'], $api_upload_urls)) {
return true;
}
return false;
}
/**
* Assert API request is using POST method.
*
* @param bool $force Force the assertion
*
* @return array Returns JSend data back {@see ajax_functions.php} if not POST method
*/
function assert_post_request(bool $force): array
{
// Legacy use cases we don't want to break backwards compatibility for (e.g JS api() makes only POST requests but
// other clients might only use GET because it was allowed if not authenticating with native mode)
if (!$force) {
return [];
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
return [];
} else {
http_response_code(405);
return ajax_response_fail(ajax_build_message($GLOBALS['lang']['error-method-not_allowed']));
}
}
/**
* Assert API sent the expected content type.
*
* @param string $expected MIME type
* @param string $received_raw MIME type
*
* @return array Returns JSend data back {@see ajax_functions.php} if received Content-Type is unexpected
*/
function assert_content_type(string $expected, string $received_raw): array
{
$expected = trim($expected);
if ($expected === '') {
trigger_error('Expected MIME type MUST not be a blank string', E_USER_ERROR);
}
$encoding = 'UTF-8';
$received = mb_strcut($received_raw, 0, mb_strlen($expected, $encoding), $encoding);
if ($expected === $received) {
return [];
}
http_response_code(415);
header("Accept: {$expected}");
return ajax_response_fail([]);
}
/**
* Return a summary of daily statistics
*
* @param int $days The number of days - note max 365 days as only the current and previous year's data is accessed.
*/
function api_get_daily_stat_summary(int $days = 30)
{
if (!checkperm("a")) {
return false;
} // Admin only
return ps_query("SELECT activity_type,sum(count) `count`
FROM daily_stat
WHERE
(`year`=year(NOW()) OR `year`=year(NOW())-1)
AND
concat(`year`,'-',`month`,'-',`day`,'-')>date_sub(NOW(), interval ? DAY)
GROUP BY activity_type
", ["i",$days]);
}