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)); }