Files
easystream-main/f_core/f_functions/functions.api.php
SamiAhmed7777 f0f346deb9
Some checks failed
EasyStream Test Suite / test (pull_request) Has been cancelled
EasyStream Test Suite / code-quality (pull_request) Has been cancelled
EasyStream Test Suite / integration-test (pull_request) Has been cancelled
Sync current dev state
2025-12-15 17:28:21 -08:00

394 lines
11 KiB
PHP

<?php
/**
* API Response Helper Functions
* Provides standardized API responses across EasyStream
*/
if (!defined('_ISVALID')) {
die('Direct access is not allowed');
}
/**
* Send success response
* Standardized success response format for all API endpoints
*
* @param mixed $data Data to return (can be array, object, string, etc.)
* @param int $statusCode HTTP status code (default: 200)
* @param array $meta Optional metadata (pagination, etc.)
* @return void (exits after sending response)
*/
function sendApiSuccess($data = null, $statusCode = 200, $meta = null) {
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
$response = [
'success' => true,
'data' => $data,
'error' => null
];
// Add metadata if provided (pagination, etc.)
if ($meta !== null) {
$response['meta'] = $meta;
}
// Add timestamp
$response['timestamp'] = date('Y-m-d H:i:s');
echo json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Send error response
* Standardized error response format for all API endpoints
*
* @param string $message Error message
* @param int $statusCode HTTP status code (default: 400)
* @param array $details Additional error details (validation errors, etc.)
* @return void (exits after sending response)
*/
function sendApiError($message, $statusCode = 400, $details = null) {
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
$response = [
'success' => false,
'data' => null,
'error' => $message
];
// Add error details if provided
if ($details !== null) {
$response['details'] = $details;
}
// Add timestamp
$response['timestamp'] = date('Y-m-d H:i:s');
// Log error for debugging
VLogger::log('error', 'API Error: ' . $message, [
'status_code' => $statusCode,
'details' => $details,
'endpoint' => $_SERVER['REQUEST_URI'] ?? 'unknown',
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown'
]);
echo json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Validate API request method
* Ensures only allowed HTTP methods are used
*
* @param string|array $allowedMethods Single method or array of allowed methods
* @throws Exception if method not allowed (sends 405 response)
* @return void
*/
function validateApiMethod($allowedMethods) {
$allowedMethods = (array) $allowedMethods;
$currentMethod = $_SERVER['REQUEST_METHOD'];
if (!in_array($currentMethod, $allowedMethods)) {
sendApiError(
'Method not allowed. Allowed methods: ' . implode(', ', $allowedMethods),
405,
['allowed_methods' => $allowedMethods, 'current_method' => $currentMethod]
);
}
}
/**
* Get JSON input from request body
* Handles both JSON and form-encoded data
*
* @param bool $assoc Return as associative array (default: true)
* @return array|object Decoded JSON data
* @throws Exception if JSON is invalid
*/
function getJsonInput($assoc = true) {
$input = file_get_contents('php://input');
if (empty($input)) {
return $assoc ? [] : (object) [];
}
$data = json_decode($input, $assoc);
if (json_last_error() !== JSON_ERROR_NONE) {
sendApiError(
'Invalid JSON input: ' . json_last_error_msg(),
400,
['json_error' => json_last_error_msg()]
);
}
return $data;
}
/**
* Validate required fields in request data
* Checks if all required fields are present and not empty
*
* @param array $data Request data
* @param array $requiredFields Array of required field names
* @return void (sends error response if validation fails)
*/
function validateRequiredFields($data, $requiredFields) {
$missingFields = [];
foreach ($requiredFields as $field) {
if (!isset($data[$field]) || (is_string($data[$field]) && trim($data[$field]) === '')) {
$missingFields[] = $field;
}
}
if (!empty($missingFields)) {
sendApiError(
'Missing required fields',
400,
['missing_fields' => $missingFields, 'required_fields' => $requiredFields]
);
}
}
/**
* Get pagination parameters from request
* Standardized pagination handling
*
* @param int $defaultLimit Default items per page (default: 20)
* @param int $maxLimit Maximum items per page (default: 100)
* @return array ['page', 'limit', 'offset']
*/
function getPaginationParams($defaultLimit = 20, $maxLimit = 100) {
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min($maxLimit, max(1, (int) $_GET['limit'])) : $defaultLimit;
$offset = ($page - 1) * $limit;
return [
'page' => $page,
'limit' => $limit,
'offset' => $offset
];
}
/**
* Create pagination metadata
* Returns standardized pagination info for responses
*
* @param int $page Current page
* @param int $limit Items per page
* @param int $total Total items
* @return array Pagination metadata
*/
function createPaginationMeta($page, $limit, $total) {
return [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit),
'has_more' => $page < ceil($total / $limit)
];
}
/**
* Require authentication for API endpoint
* Checks if user is authenticated via JWT or session
*
* @return int User ID
* @throws Exception if not authenticated (sends 401 response)
*/
function requireAuth() {
$userId = null;
// Try JWT authentication first
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? null;
if ($authHeader && preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
$token = $matches[1];
$tokenData = VAuth::verifyToken($token);
if ($tokenData && isset($tokenData['user_id'])) {
$userId = $tokenData['user_id'];
}
}
// Fall back to session authentication
if (!$userId) {
$userId = getCurrentUserId();
}
if (!$userId) {
sendApiError('Authentication required', 401);
}
return $userId;
}
/**
* Check user permission for API endpoint
* Requires authentication and checks RBAC permission
*
* @param string $permission Permission to check (e.g., 'videos.delete')
* @return int User ID if authorized
* @throws Exception if not authorized (sends 403 response)
*/
function requirePermission($permission) {
$userId = requireAuth();
if (!VAuth::hasPermission($permission)) {
sendApiError(
'Permission denied',
403,
['required_permission' => $permission, 'user_id' => $userId]
);
}
return $userId;
}
/**
* Rate limit API requests
* Simple in-memory rate limiting (consider Redis for production)
*
* @param string $key Rate limit key (e.g., 'api:' . $userId)
* @param int $maxRequests Maximum requests allowed
* @param int $timeWindow Time window in seconds
* @return void (sends 429 response if limit exceeded)
*/
function rateLimitApiRequest($key, $maxRequests = 60, $timeWindow = 60) {
// Use APCu if available, otherwise session
if (function_exists('apcu_fetch')) {
$requests = apcu_fetch($key, $success);
if (!$success) {
$requests = ['count' => 0, 'start' => time()];
}
// Reset if time window expired
if (time() - $requests['start'] > $timeWindow) {
$requests = ['count' => 0, 'start' => time()];
}
$requests['count']++;
if ($requests['count'] > $maxRequests) {
$retryAfter = $timeWindow - (time() - $requests['start']);
header('Retry-After: ' . $retryAfter);
header('X-RateLimit-Limit: ' . $maxRequests);
header('X-RateLimit-Remaining: 0');
header('X-RateLimit-Reset: ' . ($requests['start'] + $timeWindow));
sendApiError(
'Rate limit exceeded',
429,
[
'max_requests' => $maxRequests,
'time_window' => $timeWindow,
'retry_after' => $retryAfter
]
);
}
apcu_store($key, $requests, $timeWindow);
// Add rate limit headers
header('X-RateLimit-Limit: ' . $maxRequests);
header('X-RateLimit-Remaining: ' . ($maxRequests - $requests['count']));
header('X-RateLimit-Reset: ' . ($requests['start'] + $timeWindow));
} else {
// Fallback: Use session storage (not ideal for distributed systems)
if (!isset($_SESSION['rate_limit'])) {
$_SESSION['rate_limit'] = [];
}
if (!isset($_SESSION['rate_limit'][$key])) {
$_SESSION['rate_limit'][$key] = ['count' => 0, 'start' => time()];
}
$requests = $_SESSION['rate_limit'][$key];
// Reset if time window expired
if (time() - $requests['start'] > $timeWindow) {
$_SESSION['rate_limit'][$key] = ['count' => 0, 'start' => time()];
$requests = $_SESSION['rate_limit'][$key];
}
$requests['count']++;
$_SESSION['rate_limit'][$key] = $requests;
if ($requests['count'] > $maxRequests) {
$retryAfter = $timeWindow - (time() - $requests['start']);
header('Retry-After: ' . $retryAfter);
sendApiError(
'Rate limit exceeded',
429,
[
'max_requests' => $maxRequests,
'time_window' => $timeWindow,
'retry_after' => $retryAfter
]
);
}
}
}
/**
* Sanitize API input
* Wrapper around VSecurity::sanitize with logging
*
* @param mixed $input Input to sanitize
* @param string $type Type of sanitization (string, email, url, int, etc.)
* @return mixed Sanitized input
*/
function sanitizeApiInput($input, $type = 'string') {
if ($input === null) {
return null;
}
switch ($type) {
case 'int':
return (int) $input;
case 'float':
return (float) $input;
case 'bool':
return (bool) $input;
case 'email':
return filter_var($input, FILTER_SANITIZE_EMAIL);
case 'url':
return filter_var($input, FILTER_SANITIZE_URL);
case 'string':
default:
return VSecurity::sanitize($input);
}
}
/**
* Log API request for debugging/analytics
*
* @param string $endpoint Endpoint name
* @param array $data Additional data to log
* @return void
*/
function logApiRequest($endpoint, $data = []) {
VLogger::log('info', 'API Request', array_merge([
'endpoint' => $endpoint,
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
'user_id' => getCurrentUserId(),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 200)
], $data));
}